0x00 前言:插件化的介绍
阅读须知:阅读本文的童鞋最好是有过插件化框架使用经历或者对插件化框架有过了解的。前方高能,大牛绕道。
最近一直在关注 Android 插件化方面,所以今天的主题就确定是 Android 中比较热门的“插件化”了。所谓的插件化就是下载 apk 到指定目录,不需要安装该 apk ,就能利用某个已安装的 apk (即“宿主”)调用起该未安装 apk 中的 Activity 、Service 等组件(即“插件”)。
Android 插件化的发展到目前为止也有一段时间了,从一开始任主席的 dynamic-load-apk 到今天要分析的 android-pluginmgr 再到360的 DroidPlugin ,也代表着插件化的思想从顶部的应用层向下到 Framework 层渗入。最早插件化的思想是 dynamic-load-apk 实现的, dynamic-load-apk 在“宿主” ProxyActivity 的生命周期中利用接口回调了“插件” PluginActivity 的“生命周期”,以此来间接实现 PluginActivity 的“生命周期”。也就是说,其实插件中的 “PluginActivity” 并不具有真正 Activity 的性质,实质就是一个普通类,只是利用接口回调了类中的生命周期方法而已。比接口回调更好的方案就是利用 ActivityThread 、Instrumentation 等去动态地 Hook 即将创建的 ProxyActivity ,也就是说表面上创建的是 ProxyActivity ,其实实际上是创建了 PluginActivity 。这种思想相比于 dynamic-load-apk 而言,插件中 Activity 已经是实质上的 Activity ,具备了生命周期方法。今天我们要解析的 android-pluginmgr 插件化框架就是基于这种思想的。最后就是像 DroidPlugin 这种插件化框架,改动了 ActivityManagerService 、 PackageManagerService 等 Android 源码,以此来实现插件化。总之,并没有哪种插件化框架是最好的,一切都是要根据自身实际情况而决定的。
熟悉插件化的童鞋都知道,插件化要解决的有三个基本难题:
- 插件中 ClassLoader 的问题;
- 插件中的资源文件访问问题;
- 插件中 Activity 组件的生命周期问题。
基本上,解决了上面三个问题,就可以算是一个合格的插件化框架了。但是要注意的是,插件化远远不止这三个问题,比如还有插件中 .so 文件加载,支持 Service 插件化等问题。
好了,讲了这么多废话,接下来我们就来分析 android-pluginmgr 的源码吧。
0x01 PluginManager.init
注:本文分析的 android-pluginmgr 为 master 分支,版本为0.2.2;
android-pluginmgr的简单用法
我们先简单地来看一下 android-pluginmgr 框架的用法(来自于 android-pluginmgr 的 README.md ):
declare permission in your
AndroidManifest.xml
:<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
regist an activity:
<activity android:name="androidx.pluginmgr.DynamicActivity" />
init PluginMgr in your application:
@Override public void onCreate(){ PluginManager.init(this); //... }
load plugin from plug apk:
PluginManager pluginMgr = PluginManager.getSingleton(); File myPlug = new File("/mnt/sdcard/Download/myplug.apk"); PlugInfo plug = pluginMgr.loadPlugin(myPlug).iterator().next();
start activity:
mgr.startMainActivity(context, plug);
基本的用法就像以上这五步,另外需要注意的是,“插件”中所需要的权限都要在“宿主”的 AndroidManifest.xml 中进行申明。
PluginManager.init(this)源码
下面我们来分析下 PluginManager.init(this);
的源码:
1 | /** |
可以看到在 init(Context context)
中主要创建了一个 SINGLETON
单例,所以我们就要追踪 PluginManager
构造器的源码了:
1 | /** |
在构造器中做的事情有点多,我们一步步来看下。一开始得到插件 dex opt 输出路径 dexOutputPath
和私有目录中存储插件的路径 dexInternalStoragePath
。这些路径都是在 Global
类中事先定义好的:
1 | /** |
但是根据常量定义的名称来看,总感觉作者在 context.getDir()
时把这两个路径搞反了 \(╯-╰)/。
之后在构造器中创建了 DelegateActivityThread
类的单例:
1 | public final class DelegateActivityThread { |
DelegateActivityThread 类的主要作用就是使用反射包装了当前的 ActivityThread ,并且一开始在 DelegateActivityThread 中使用 PluginInstrumentation 替换原始的 Instrumentation 。其实 Activity 的生命周期调用都是通过 Instrumentation 来完成的。我们来看看 PluginInstrumentation 的构造器相关代码:
1 | public class PluginInstrumentation extends DelegateInstrumentation |
可以看到 PluginInstrumentation 是继承自 DelegateInstrumentation 类的,而 DelegateInstrumentation 本质上就是 Instrumentation 。 DelegateInstrumentation 类中的方法都是直接调用 Instrumentation 类的:
1 | public class DelegateInstrumentation extends Instrumentation { |
好了,在 PluginManager.init()
方法中大概做的就是这些逻辑了。
0x02 PluginManager.loadPlugin
看完了上面的 PluginManager.init()
之后,下一步就是调用 pluginManager.loadPlugin
去加载插件。一起来看看相关源码:
1 | /** |
在 loadPlugin
代码的注释中,我们可以知道加载的插件可以是一个也可以是一个文件夹下的多个。因为会根据传入的 pluginSrcDirFile
参数去判断是文件还是文件夹,其实道理都是一样的,无非就是多了一个 for 循环而已。在这里要注意一下,PluginManager 是实现了 FileFilter 接口的,因此在加载多个插件时,调用 listFiles(this)
会过滤当前文件夹下非 apk 文件:
1 | @Override |
好了,我们在 loadPlugin()
的代码中会注意到,无论是加载单个插件还是多个插件都会调用 buildPlugInfo()
方法。顾名思义,就是根据传入的插件文件去加载:
1 | private PlugInfo buildPlugInfo(File pluginApk, String pluginId, |
从上面的代码中看到, buildPlugInfo()
方法中做的大致有四步:
- 复制插件 apk 到指定目录;
- 加载插件 apk 的 AndroidManifest.xml 文件;
- 加载插件 apk 中的资源文件;
- 为插件 apk 设置 ClassLoader。
复制插件 apk 到指定目录
下面我们慢慢来分析,第一步,会把传入的插件 apk 复制到 dexInternalStoragePath
路径下,也就是之前在 PluginManager 的构造器中所指定的目录。这部分的代码很简单,就省略了。
加载插件 apk 的 AndroidManifest.xml 文件
第二步,根据代码可知,会使用 PluginManifestUtil.setManifestInfo()
去加载 AndroidManifest 里的信息,那就去看下相关的代码实现:
1 | public static void setManifestInfo(Context context, String apkPath, PlugInfo info) |
在代码中,一开始会通过 apk 得到 AndroidManifest.xml 文件。然后使用 XmlManifestReader
去读取 AndroidManifest 中的信息。在 XmlManifestReader
中会使用 XmlPullParser
去解析 xml , XmlManifestReader
相关的源码就不贴出来了,想要进一步了解的童鞋可以自己去看,点击这里查看 XmlManifestReader 源码。接下来根据 apkPath
得到相应的 pkgInfo
,并且若有 libDir 会去加载相应的 .so 文件。最后会调用 setAttrs(info, manifestXML)
这个方法:
1 | private static void setAttrs(PlugInfo info, String manifestXML) |
在 setAttrs(PlugInfo info, String manifestXML)
方法中,使用了 pull 方式去解析 manifest ,并且根据 activity 、 recevicer 、 service 等调用不同的 addXxxx()
方法。这些方法其实本质上是一样的,我们就挑 addActivity()
方法来看一下:
1 | private static void addActivity(PlugInfo info, String namespace, |
addActivity()
代码中的逻辑比较简单,就是创建一个 ResolveInfo
类的对象 act
,把 Activity 相关的信息全部装进去,比如有 ActivityInfo 、 intent-filter 等。最后把 act
添加到 info
中。其他的 addReceiver
和 addService
也是同一个逻辑。而 parseApplicationInfo
也是把 Application 的相关信息封装到 info
中。感兴趣的同学可以看一下相关的源码,点击这里查看。到这里,就把加载插件中 AndroidManifest.xml 的代码分析完了。
加载插件 apk 中的资源文件
再回到 buildPlugInfo()
的代码中去,接下来就是第三步,加载插件中的资源文件了。
为了方便,我们把相关的代码复制到这里来:
1 | try { |
首先通过反射得到 AssetManager
的对象 am
,然后通过反射其 addAssetPath
方法传入 dexPath
参数来加载插件的资源文件,接下来就得到相应插件的 Resource
对象 res
了。这样就实现了访问插件中的资源文件了。那么到底 addAssetPath
这个方法有什么魔力呢?我们查看一下 Android 相关的源代码(android/content/res/AssetManager.java):
1 | /** |
查看方法的注释我们知道,这个 addAssetPath()
方法就是用来添加额外的资源文件到 AssetManager 中去的,但是已经被 hide 了。所以我们只能通过反射的方式来执行了。这样就解决了加载插件中的资源文件的问题了。
其实,大多数插件化框架都是通过反射 addAssetPath()
的方式来解决加载插件资源问题,基本上已经成为了标准方案了。
为插件 apk 设置 ClassLoader
终于到了最后一个步骤了,如何为插件设置 ClassLoader 呢?其实解决的方案就是通过 DexClassLoader
。我们先来看 buildPlugInfo()
中的代码:
1 | PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath |
在代码中创建了 pluginClassLoader
对象,而 PluginClassLoader
正是继承自 DexClassLoader
的,将 dexPath
、 dexOutputPath
等参数传入后,就可以去加载插件中的类了。 基本上所有的插件化框架都是通过 DexClassLoder
来作为插件 apk 的 ClassLoader 的。
之后在 makeApplication(info, appInfo)
就使用 PluginClassLoader
利用反射去创建插件的 Application 了:
1 | /** |
创建完插件的 Application 之后, 再调用 attachBaseContext(info, app)
方法把 Application 的 mBase 属性替换成 PluginContext
对象,PluginContext
类继承自 LayoutInflaterProxyContext ,里面封装了一些插件的信息,比如有插件资源、插件 ClassLoader 等。值得一提的是,在插件中 PluginContext 可以得到“宿主”的 Context ,也就是所谓的“破壳”。具体可查看 PluginContext 的源码。
1 | private void attachBaseContext(PlugInfo info, Application app) { |
讲到这里基本上把 buildPlugInfo()
中的逻辑讲完了, pluginManager.loadPlugin
剩下的代码都比较简单,相信大家一看就懂了。
0x03 PluginManager.startActivity
startActivity
在加载好插件 apk 之后,就可以使用插件了。和平常无异,我们使用 PluginManager.startActivity
来启动插件中的 Activity 。其实 PluginManager
有很多 startActivity 的方法:
但是终于都会调用 startActivity(Context from, PlugInfo plugInfo, ActivityInfo activityInfo, Intent intent)
这个方法:
1 | private DynamicActivitySelector activitySelector = DefaultActivitySelector.getDefault(); |
我们先来看代码, CreateActivityData
类是用来存储一个将要创建的插件 Activity 的数据,实现了 Serializable
接口,因此可以被序列化。总之, CreateActivityData
会存储将要创建的插件 Activity 的类名和包名,再把它放入 intent
中。之后, intent
设置要创建的 Activity 为 activitySelector.selectDynamicActivity(activityInfo)
,activitySelector
是 DefaultActivitySelector
类的对象,那么这 DefaultActivitySelector
到底是什么东西呢?一起来看看 DefaultActivitySelector
的源码:
1 | public class DefaultActivitySelector implements DynamicActivitySelector { |
其实很简单,不管传入的 pluginActivityInfo
参数是什么,返回的都是 DynamicActivity.class
。也就是我们在介绍 android-pluginmgr 简单用法时,第二步在 AndroidManifest 中注册的那个 DynamicActivity
。
看到这里的代码,我们一定可以猜到什么。因为这里的 intent
中设置即将启动的 Activity 仍然为 DynamicActivity
,所以在后面的代码中肯定会去动态地替换掉 DynamicActivity
。
动态Hook
之前在 PluginManager.init(this) 源码这一小节中介绍了,当前 ActivityThread
的 Instrumentation
已经被替换成了 PluginInstrumentation
。所以在创建 Activity 的时候会去调用 PluginInstrumentation
里面的方法。这样就可以在里面“做手脚”,实现了动态去替换 Activity 的思路。我们先来看一下 PluginInstrumentation
中部分方法的源码:
1 | private void replaceIntentTargetIfNeed(Context from, Intent intent) |
我们发现,在所有的 execStartActivity()
方法执行前,都加上了 replaceIntentTargetIfNeed(Context from, Intent intent)
这个方法,在方法里面 intent.setClass
中设置的还是 DynamicActivity.class
,把插件信息都检查了一遍。
在这之后,会去执行 PluginInstrumentation.newActivity
方法来创建即将要启动的Activity 。也正是在这里,对之前的 DynamicActivity
进行 Hook ,达到启动插件 Activity 的目的。
1 | @Override |
在 newActivity()
方法中,先拿到了插件信息 plugInfo
,然后会确保插件的 Application
已经创建。然后在第25行会去替换掉 className
和 cl
。这样,原本要创建的是 DynamicActivity
就变成了插件的 Activity
了,从而实现了创建插件 Activity 的目的,并且这个 Activity 是真实的 Activity 组件,具备生命周期的。
也许有童鞋会有疑问,如果直接在 startActivity
中设置要启动的 Activity 为插件 Activity ,这样不行吗?答案是肯定的,因为这样就会抛出一个异常:ActivityNotFoundException:...have you declared this activity in your AndroidManifest.xml?
我相信这个异常大家很熟悉的吧,在刚开始学习 Android 时,大家都会犯的一个错误。所以,我想我们也明白了为什么要花这么大的一个功夫去动态地替换要创建的 Activity ,就是为了绕过这个 ActivityNotFoundException
异常,达到去“欺骗” Android 系统的效果。
既然创建好了,那么就来看看 PluginInstrumentation
里调用相关生命周期的方法:
1 | @Override |
在 callActivityOnCreate()
中先去检查了创建的 Activity 是否来自于插件。如果是,那么会给 Activity 设置 Context 、 设置主题等;如果不是,则直接执行父类方法。在 super.callActivityOnCreate(activity, icicle)
中会去调用 Activity.onCreate()
方法。其他的生命周期方法作者没有特殊处理,这里就不讲了。
分析到这,我们终于把 android-pluginmgr 插件化实现的方案完整地梳理了一遍。当然,不同的插件化框架会有不同的实现方案,具体的仍然需要自己专心研究。另外我们发现该框架还没有实现启动插件 Service 的功能,如果想要了解,可以参考下其他插件化框架。
0x04 总结
上面乱七八糟的流程讲了一遍,可能还有一些童鞋不太懂,所以在这里给出一张 android-pluginmgr 的流程图。不懂的童鞋可以根据这张图再好好看一下源码,相信你会恍然大悟的。
最后,如果对本文哪里有疑问的童鞋,欢迎留言,一起交流。