怎么将 Android 程序做成插件化的形式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了怎么将 Android 程序做成插件化的形式相关的知识,希望对你有一定的参考价值。
参考技术A 有个框架叫apkplug就是apk插件式的开发框架
其实原理都一样,因为android不支持动态的增加jar
因此插件需要做成一个单独的apk,框架APK去查找系统中的其它插件
然后结合一起调用即可
如何将Android程序做成插件化的形式?详解插件化实现原理
作者:ZYLAB
文章概览
- 发展历史
- 常用名词
- 使用Gradle简化插件开发流程
- 类加载器:ClassLoader
- 插件化实现难点
- 四大组件的插件化实现
一、发展历史
- 2012年:AndroidDynamicLoader 给予 Fragment 实现了插件化框架,可以动态加载插件中的 Fragment 实现页面的切换。
- 2013年:阿里技术沙龙上,伯奎做了 Atlas 插件化框架的分享,说明那时候阿里已经在做插件化的运用和开发了。
- 2014年:任玉刚开源了 dynamic-load-apk,通过代理分发的方式实现了动态化
- 2015年:张勇 发布了 DroidPlugin,使用 hook 系统方式实现插件化。
- 2017年:阿里推出 Atlas
- 2019年:腾讯推出了 Shadow,号称是零反射,并且框架自身也可实现动态化,看了代码以后发现,其实本质上还是使用了代理分发生命周期实现四大组件动态化,然后抽象接口来实现框架的动态化。后面有机会可以对其做一下分析。
从2012至今,可以说插件化技术基本成型了,主要是代理和 hook 系统两种方式
二、常用名词
在插件化中有一些专有名词,如果是第一次接触可能不太了解,这里解释一下。
- 宿主:负责加载插件的 apk,一般来说就是已经安装的应用本身。
- StubActivity:宿主中的占位 Activity,注册在宿主 Manifest 文件中,负责加载插件 Activity。
- PluginActivity:插件 Activity,在插件 apk 中,没有注册在 Manifest 文件中,需要 StubActivity 来加载。
三、使用 gradle 简化插件开发流程
在学习和开发插件化的时候,我们需要动态去加载插件 apk,所以开发过程中一般需要有两个 apk,一个是宿主 apk,一个是插件 apk,对应的就需要有宿主项目和插件项目。
在 CommonTec 这里创建了 app 作为宿主项目,plugin 为插件项目。为了方便,我们直接把生成的插件 apk 放到宿主 apk 中的 assets 中,apk 启动时直接放到内部存储空间中方便加载。这样的项目结构,我们调试问题时的流程就是下面这样:修改插件项目 -> 编译生成插件 apk -> 拷贝插件 apk 到宿主 assets -> 修改宿主项目 -> 编译生成宿主 apk -> 安装宿主 apk -> 验证问题 如果每次我们修改一个很小的问题,都经历这么长的流程,那么耐心很快就耗尽了。最好是可以**「直接编译宿主 apk 的时候自动打包插件 apk 并拷贝到宿主 assets 目录下」**,这样我们不管修改什么,都直接编译宿主项目就好了。如何实现呢?还记得我们之前讲解过的 gradle 系列么?现在就是学以致用的时候了。首先在 plugin 项目的 build.gradle 添加下面的代码:
project.afterEvaluate
project.tasks.each
if (it.name == "assembleDebug")
it.doLast
copy
from new File(project.getBuildDir(), 'outputs/apk/debug/plugin-debug.apk').absolutePath
into new File(project.getRootProject().getProjectDir(), 'app/src/main/assets')
rename 'plugin-debug.apk', 'plugin.apk'
这段代码是在 afterEvaluate 的时候,遍历项目的 task,找到打包 task 也就是 assembleDebug,然后在打包之后,把生成的 apk 拷贝到宿主项目的 assets 目录下,并且重命名为 plugin.apk。然后在 app 项目的 build.gradle 添加下面的代码:
project.afterEvaluate
project.tasks.each
if (it.name == 'mergeDebugAssets')
it.dependsOn ':plugin:assembleDebug'
找到宿主打包的 mergeDebugAssets 任务,依赖插件项目的打包,这样每次编译宿主项目的时候,会先编译插件项目,然后拷贝插件 apk 到宿主 apk 的 assets 目录下,以后每次修改,只要编译宿主项目就可以了。
四、ClassLoader
ClassLoader 是插件化中必须要掌握的,因为插件是未安装的 apk,系统不会处理其中的类,所以需要我们自己来处理。
4.1 java 中的 ClassLoader
BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等
ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包
AppClassLoader 负责加载 classpath 里的 jar 包和目录
4.2 android 中的 ClassLoader
在这里,我们统称 dex 文件,包含 dex 的 apk 文件以及 jar 文件为 dex 文件 PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件
DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。
我们在插件化中一般使用的是 DexClassLoader。
4.3 双亲委派机制
每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。下面是 ClassLoader 的 loadClass 方法的具体实现。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null)
try
if (parent != null)
// 先从父类加载器中进行加载
c = parent.loadClass(name, false);
else
c = findBootstrapClassOrNull(name);
catch (ClassNotFoundException e)
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
if (c == null)
// 没有找到,再自己加载
c = findClass(name);
return c;
4.4 如何加载插件中的类
要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要那些参数。
public class DexClassLoader extends BaseDexClassLoader
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
// ...
构造函数需要四个参数:「dexPath」 是需要加载的 dex / apk / jar 文件路径**「optimizedDirectory」** 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置**「librarySearchPath」** 是 native 依赖的位置**「parent」** 就是父类加载器,默认会先从 parent 加载对应的类
创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:
// 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin()
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
private fun init()
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用来加载插件类
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
五、插件化需要解决的难点
插件化,就是从插件中加载我们想要的类并运行,如果这个类是一个普通类,那么使用上面说到的 DexClassLoader 就可以直接加载了,如果这个类是特殊的类,比如说 Activity 等四大组件,那么就需要一些特殊的处理,因为四大组件是需要和系统进行交互的。插件化中,四大组件需要解决的难点如下:
- 「Activity」
- 生命周期如何调用
- 如何使用插件中的资源
- 「Service」
- 生命周期如何调用
- 「BroadcastReceiver」
- 静态广播和动态广播的注册
- 「ContentProvider」
- 如何注册插件 Provider 到系统
六、Activity 的插件化实现
6.1 难点分析
我们之前说到 Activity 插件化的难点,我们先来理顺一下为什么会有这两个问题。因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。可能有些同学会问,那为什么不能直接把插件的 Activity 注册到宿主 Manifest 里呢?这样是可以,不过就失去了插件化的动态特性,如果每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。**「我们再来说一下为什么没有注册的 Activity 不能和系统交互」**这里的不能直接交互的含义有两个
- 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:
android.content.ActivityNotFoundException: Unable to find explicit activity class com.zy.commontec/com.zy.plugin.PluginActivity; have you declared this activity in your AndroidManifest.xml?
这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:
public class Instrumentation
public static void checkStartActivityResult(int res, Object intent)
if (!ActivityManager.isStartResultFatalError(res))
return;
switch (res)
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
...
- Activity 的生命周期无法被调用 其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。
其实上面两个问题,最终都指向同一个难点,那就是插件中的 Activity 的生命周期如何被调用。解决问题之前我们先看一下正常系统是如何启动一个 Activity 的。这里对 Activity 的启动流程进行一些简单的介绍,具体的流程代码就不分析了,因为分析的话大概又能写一篇文章了,而且其实关于 Activity 的启动过程也有不少文章有分析了。这里放一张简图说明一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLgAchLZ-1644931575368)(https://upload-images.jianshu.io/upload_images/27552241-17dcb8e14a90bfd0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
整个调用路径如下:
Activity.startActivity -> Instrumentation.execStartActivity -> Binder -> AMS.startActivity -> ActivityStarter.startActivityMayWait -> startActivityLocked -> startActivityUnChecked -> ActivityStackSupervisor.resumeFocusedStackTopActivityLocked -> ActivityStatk.resumeTopAcitivityUncheckLocked -> resumeTopActivityInnerLocked -> ActivityStackSupervisor.startSpecificActivityLocked -> realStartActivityLocked -> Binder -> ApplictionThread.scheduleLauchActivity -> H -> ActivityThread.scheduleLauchActivity -> handleLaunchActivity -> performLaunchActivity -> Instrumentation.newActivity 创建 Activity -> callActivityOnCreate 一系列生命周期
其实我们可以把 AMS 理解为一个公司的背后**「大 Boss」,Activity 相当于「小职员」,没有权限直接和大 Boss 说话,想做什么事情都必须经过「秘书」**向上汇报,然后秘书再把大 Boss AMS 的命令传达下来。而且大 Boss 那里有所有职员的名单,如果想要混入非法职员时不可能的。而我们想让没有在大 Boss 那里注册的编外人员执行任务,只有两种方法,一种是正式职员领取任务,再分发给编外人员,另一种就是欺骗 Boss,让 Boss 以为这个职员是已经注册的。
对应到实际的解决方法就是:
- 我们手动去调用插件 Activity 的生命周期
- 欺骗系统,让系统以为 Activity 是注册在 Manifest 中的
**「说完生命周期的问题,再来看一下资源的问题」**在 Activity 中,基本上都会展示界面,而展示界面基本上都要用到资源。在 Activity 中,有一个 mResources 变量,是 Resources 类型。这个变量可以理解为代表了整个 apk 的资源。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mf872BIA-1644931575374)(https://upload-images.jianshu.io/upload_images/27552241-fae65953d5f0540c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
在宿主中调用的 Activity,mResources 自然代表了宿主的资源,所以需要我们对插件的资源进行特殊的处理。「我们先看一下如何生成代表插件资源的 Resources 类」。首先要生成一个 AssetManager 实例,然后通过其 addAssetPath 方法添加插件的路径,这样 AssetManager 中就包含了插件的资源。然后通过 Resources 构造函数生成插件资源。具体代码如下:
private fun handleResources()
try
// 首先通过反射生成 AssetManager 实例
pluginAssetManager = AssetManager::class.java.newInstance()
// 然后调用其 addAssetPath 把插件的路径添加进去。
val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
catch (e: Exception)
// 调用 Resources 构造函数生成实例
pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
前期准备的知识点差不多介绍完了,我们接着就看看具体的实现方法。
6.2 手动调用 Activity 生命周期
手动调用生命周期原理如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s4zVITYv-1644931575376)(https://upload-images.jianshu.io/upload_images/27552241-60cfa668cdb417ee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
我们手动调用插件 Activity 生命周期时,需要在正确的时机去调用,如何在正确的时机调用呢?那就是启动一个真正的 Activity,我们俗称占坑 Activity(StubActivity),然后在 StubActivity 的生命周期里调用插件 Activity 对应的生命周期,这样就间接的启动了插件 Activity。在 StubActivity 中调用 插件 Activity 生命周期的方法有两种,一种是直接反射其生命周期方法,粗暴简单,唯一的缺点就是反射的效率问题。另外一种方式就是生成一个接口,接口里对应的是生命周期方法,让插件 Activity 实现这个接口,在 StubActivity 里就能直接调用接口方法了,从而避免了反射的效率低下问题。
具体的代码实现在CommonTec项目里可以找到,这里贴一下主要的实现(这里的实现和 CommonTec 里的可能会有些区别,CommonTec 里有些代码做了一些封装,这里主要做原理的解释)。
6.2.1 通过反射调用 Activity 生命周期
具体的实现见 反射调用生命周期,下面列出了重点代码。
class StubReflectActivity : Activity()
protected var activityClassLoader: ClassLoader? = null
protected var activityName = ""
private var pluginPath = ""
private var nativeLibDir: String? = null
private var dexOutPath: String? = null
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = intent.getStringExtra("pluginPath")
activityName = intent.getStringExtra("activityName")
// 创建插件 ClassLoader
activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
// 以 onCreate 方法为例,其他 onStart 等生命周期方法类似
fun onCreate(savedInstanceState: Bundle?)
// 获取插件 Activity 的 onCreate 方法并调用
getMethod("onCreate", Bundle::class.java)?.invoke(activity, savedInstanceState)
fun getMethod(methodName: String, vararg params: Class<*>): Method?
return activityClassLoader?.loadClass(activity)?.getMethod(methodName, *params)
6.2.2 通过接口调用 Activity 生命周期
具体的实现见 接口调用生命周期,下面列出了重点代码。通过接口调用 Activity 生命周期的前提是要定义一个接口 IPluginActivity
interface IPluginActivity
fun attach(proxyActivity: Activity)
fun onCreate(savedInstanceState: Bundle?)
fun onStart()
fun onResume()
fun onPause()
fun onStop()
fun onDestroy()
然后在插件 Activity 中实现这个接口
open class BasePluginActivity : Activity(), IPluginActivity
var proxyActivity: Activity? = null
override fun attach(proxyActivity: Activity)
this.proxyActivity = proxyActivity
override fun onCreate(savedInstanceState: Bundle?)
if (proxyActivity == null)
super.onCreate(savedInstanceState)
// ...
在 StubActivity 通过接口调用插件 Activity 生命周期
class StubInterfaceActivity : StubBaseActivity()
protected var activityClassLoader: ClassLoader? = null
protected var activityName = ""
private var pluginPath = ""
private var activity: IPluginActivity? = null
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = intent.getStringExtra("pluginPath")
activityName = intent.getStringExtra("activityName")
// 生成插件 ClassLoader
activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
// 加载插件 Activity 类并转化成 IPluginActivity 接口
activity = activityClassLoader?.loadClass(activityName)?.newInstance() as IPluginActivity?
activity?.attach(this)
// 通过接口直接调用对应的生命周期方法
activity?.onCreate(savedInstanceState)
6.2.3 资源处理方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cT2Tl5Zt-1644931575379)(https://upload-images.jianshu.io/upload_images/27552241-21bbbb47681204e1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
由于手动调用生命周期的方式,会重写大量的 Activity 生命周期方法,所以我们只要重写 getResources 方法,返回插件的资源实例就可以了。下面是具体代码。
open class StubBaseActivity : Activity()
protected var activityClassLoader: ClassLoader? = null
protected var activityName = ""
private var pluginPath = ""
private var pluginAssetManager: AssetManager? = null
private var pluginResources: Resources? = null
private var pluginTheme: Resources.Theme? = null
private var nativeLibDir: String? = null
private var dexOutPath: String? = null
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = intent.getStringExtra("pluginPath")
activityName = intent.getStringExtra("activityName")
activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
handleResources()
override fun getResources(): Resources?
// 这里返回插件的资源,这样插件 Activity 中使用的就是插件资源了
return pluginResources ?: super.getResources()
override fun getAssets(): AssetManager
return pluginAssetManager ?: super.getAssets()
override fun getClassLoader(): ClassLoader
return activityClassLoader ?: super.getClassLoader()
private fun handleResources()
try
// 生成 AssetManager
pluginAssetManager = AssetManager::class.java.newInstance()
// 添加插件 apk 路径
val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
catch (e: Exception)
// 生成插件资源
pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
6.3 hook 系统相关实现的方式欺骗系统,让系统调用生命周期
6.3.1 hook Instrumentation
上面讲了如何通过手动调用插件 Activity 的生命周期方法来启动插件 Activity,现在来看一下欺骗系统的方法。
上面简单介绍了 Activity 的启动流程,我们可以看到,其实 Android 系统的运行是很巧妙的,AMS 是系统服务,应用通过 Binder 和 AMS 进行交互,其实和我们日常开发中客户端和服务端交互有些类似,只不过这里使用了 Binder 做为交互方式,关于 Binder,可以简单看看这篇文章。我们暂时只要知道通过 Binder 应用可以和 AMS 进行对话就行。这种架构的设计方式,也为我们提供了一些机会。理论上来说,我们只要在启动 Activity 的消息到达 AMS 之前把 Activity 的信息就行修改,然后再消息回来以后再把信息恢复,就可以达到欺骗系统的目的了。
在这个流程里,有很多 hook 点可以进行,而且不同的插件化框架对于 hook 点的选择也不同,这里我们选择 hook Instrumentation 的方式进行介绍(原因是个人感觉这种方式要简单一点)。简化以后的流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dU6x5vws-1644931575379)(https://upload-images.jianshu.io/upload_images/27552241-20c7568595342c97.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
Instrumentation 相当于 Activity 的管理者,Activity 的创建,以及生命周期的调用都是 AMS 通知以后通过 Instrumentation 来调用的。我们上面说到,AMS 相当于一个公司的背后**「大 Boss」,而 Instrumentation 相当于「秘书」,Activity 相当于「小职员」**,没有权限直接和大 Boss 说话,想做什么事情都必须经过秘书向上汇报,然后 Instrumentation 再把大 Boss AMS 的命令传达下来。而且大 Boss 那里有所有职员的名单,如果想要混入非法职员时不可能的。不过在整个过程中,由于 java 的语言特性,大 Boss 在和秘书 Instrumentation 对话时,不会管秘书到底是谁,只会确认这个人是不是秘书(是否是 Instrumentation 类型)。
我们加载插件中的 Activity,相当于让一个不在 Boss 名单上的编外职员去申请执行任务。在正常情况下,大 Boss 会检查职员的名单,确认职员的合法性,一定是通过不了的。但是上有政策,下有对策,我们悄悄的替换了秘书,在秘书和 Boss 汇报时,把职员名字改成大 Boss 名单中的职员,在 Boss 安排工作以后,秘书再把名字换回来,让编外职员去执行任务。而我们 hook 的方式就是替换调 Instrumentation,修改 Activity 类名,达到隐瞒 AMS 的效果。
hook 方式原理图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PdTRG5Cz-1644931575381)(https://upload-images.jianshu.io/upload_images/27552241-5fb3cd8de706b736.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
接下来看看具体的代码实现。具体的实现见 hook 实现插件化,下面主要讲解重点代码。替换 Instrumentation 之前,首先我们要实现一个我们自己的 Instrumentation,具体实现如下:
class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :
Instrumentation()
private val KEY_COMPONENT = "commontec_component"
companion object
fun inject(activity: Activity, pluginContext: PluginContext)
// hook 系统,替换 Instrumentation 为我们自己的 AppInstrumentation,Reflect 是从 VirtualApp 里拷贝的反射工具类,使用很流畅~
var reflect = Reflect.on(activity)
var activityThread = reflect.get<Any>("mMainThread")
var base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")
var appInstrumentation = AppInstrumentation(activity, base, pluginContext)
Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)
Reflect.on(activity).set("mInstrumentation", appInstrumentation)
override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity?
// 创建 Activity 的时候会调用这个方法,在这里需要返回插件 Activity 的实例
val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)
var clazz = pluginContext.classLoader.loadClass(componentName.className)
intent.component = componentName
return clazz.newInstance() as Activity?
private fun injectIntent(intent: Intent?)
var component: ComponentName? = null
var oldComponent = intent?.component
if (component == null || component.packageName == realContext.packageName)
// 替换 intent 中的类名为占位 Activity 的类名,这样系统在 Manifest 中查找的时候就可以找到 Activity
component = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")
intent?.component = component
intent?.putExtra(KEY_COMPONENT, oldComponent)
fun execStartActivity(
who: Context,
contextThread: IBinder,
token: IBinder,
target: Activity,
intent: Intent,
requestCode: Int
): Instrumentation.ActivityResult?
// 启动 activity 的时候会调用这个方法,在这个方法里替换 Intent 中的 ClassName 为已经注册的宿主 Activity
injectIntent(intent)
return Reflect.on(base)
.call("execStartActivity", who, contextThread, token, target, intent, requestCode).get()
// ...
在 AppInstrumentation 中有两个关键点,「execStartActivity 和 newActivity」。「execStartActivity」 是在启动 Activity 的时候必经的一个过程,这时还没有到达 AMS,所以,在这里把 Activity 替换成宿主中已经注册的 StubActivity,这样 AMS 在检测 Activity 的时候就认为已经注册过了。「newActivity」 是创建 Activity 实例,这里要返回真正需要运行的插件 Activity,这样后面系统就会基于这个 Activity 实例来进行对应的生命周期的调用。
6.3.2 hook 系统的资源处理方式
因为我们 hook 了 Instrumentation 的实现,还是把 Activity 生命周期的调用交给了系统,所以我们的资源处理方式和手动调用生命周期不太一样,这里我们生成 Resources 以后,直接反射替换掉 Activity 中的 mResource 变量即可。下面是具体代码。
class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) : Instrumentation()
private fun injectActivity(activity: Activity?)
val intent = activity?.intent
val base = activity?.baseContext
try
// 反射替换 mResources 资源
Reflect.on(base).set("mResources", pluginContext.resources)
Reflect.on(activity).set("mResources", pluginContext.resources)
Reflect.on(activity).set("mBase", pluginContext)
Reflect.on(activity).set("mApplication", pluginContext.applicationContext)
// for native activity
val componentName = intent!!.getParcelableExtra<ComponentName>(KEY_COMPONENT)
val wrapperIntent = Intent(intent)
wrapperIntent.setClassName(componentName.packageName, componentName.className)
activity.intent = wrapperIntent
catch (e: Exception)
override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?)
// 在这里进行资源的替换
injectActivity(activity)
super.callActivityOnCreate(activity, icicle)
public class PluginContext extends ContextWrapper
private void generateResources()
try
// 反射生成 AssetManager 实例
assetManager = AssetManager.class.newInstance();
// 调用 addAssetPath 添加插件路径
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.invoke(assetManager, pluginPath);
// 生成 Resources 实例
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
catch (Exception e)
e.printStackTrace();
讲完上面两种方法,我们这里对比一下这两种方法的优缺点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axwzbHhv-1644931575382)(https://upload-images.jianshu.io/upload_images/27552241-0137486d16408813.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
七、Service 的插件化实现
Service 比起 Activity 要简单不少,Service 没有太复杂的生命周期需要处理,类似的 onCreate 或者 onStartCommand 可以直接通过代理分发。可以直接在宿主 app 里添加一个占位 Service,然后在对应的生命周期里调用插件 Service 的生命周期方法即可。
class StubService : Service()
var serviceName: String? = null
var pluginService: Service? = null
companion object
var pluginClassLoader: ClassLoader? = null
fun startService(context: Context, classLoader: ClassLoader, serviceName: String)
pluginClassLoader = classLoader
val intent = Intent(context, StubService::class.java)
intent.putExtra("serviceName", serviceName)
context.startService(intent)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int
val res = super.onStartCommand(intent, flags, startId)
serviceName = intent?.getStringExtra("serviceName")
pluginService = pluginClassLoader?.loadClass(serviceName)?.newInstance() as Service
pluginService?.onCreate()
return pluginService?.onStartCommand(intent, flags, startId) ?: res
override fun onDestroy()
super.onDestroy()
pluginService?.onDestroy()
override fun onBind(intent: Intent?): IBinder?
return null
八、BroadcastReceiver 的插件化实现
动态广播的处理也比较简单,也没有复杂的生命周期,也不需要在 Manifest 中进行注册,使用的时候直接注册即可。所以只要通过 ClassLoader 加载插件 apk 中的广播类然后直接注册就好。
class BroadcastUtils
companion object
private val broadcastMap = HashMap<String, BroadcastReceiver>()
fun registerBroadcastReceiver(context: Context, classLoader: ClassLoader, action: String, broadcastName: String)
val receiver = classLoader.loadClass(broadcastName).newInstance() as BroadcastReceiver
val intentFilter = IntentFilter(action)
context.registerReceiver(receiver, intentFilter)
broadcastMap[action] = receiver
fun unregisterBroadcastReceiver(context: Context, action: String)
val receiver = broadcastMap.remove(action)
context.unregisterReceiver(receiver)
静态广播稍微麻烦一点,这里可以解析 Manifest 文件找到其中静态注册的 Broadcast 并进行动态注册,这里就不对 Manifest 进行解析了,知道其原理即可。
九、ContentProvider 的插件化实现
其实在日常开发中对于插件化中的 ContentProvider 使用还是比较少的,这里只介绍一种比较简单的 ContentProvider 插件化实现方法,就是类似 Service,在宿主 app 中注册占位 ContentProvider,然后转发相应的操作到插件 ContentProvider 中。代码如下:
class StubContentProvider : ContentProvider()
private var pluginProvider: ContentProvider? = null
private var uriMatcher: UriMatcher? = UriMatcher(UriMatcher.NO_MATCH)
override fun insert(uri: Uri?, values: ContentValues?): Uri?
loadPluginProvider()
return pluginProvider?.insert(uri, values)
override fun query(uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor?
loadPluginProvider()
if (isPlugin1(uri))
return pluginProvider?.query(uri, projection, selection, selectionArgs, sortOrder)
return null
override fun onCreate(): Boolean
uriMatcher?.addURI("com.zy.stubprovider", "plugin1", 0)
uriMatcher?.addURI("com.zy.stubprovider", "plugin2", 0)
return true
override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int
loadPluginProvider()
return pluginProvider?.update(uri, values, selection, selectionArgs) ?: 0
override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int
loadPluginProvider()
return pluginProvider?.delete(uri, selection, selectionArgs) ?: 0
override fun getType(uri: Uri?): String
loadPluginProvider()
return pluginProvider?.getType(uri) ?: ""
private fun loadPluginProvider()
if (pluginProvider == null)
pluginProvider = PluginUtils.classLoader?.loadClass("com.zy.plugin.PluginContentProvider")?.newInstance() as ContentProvider?
private fun isPlugin1(uri: Uri?): Boolean
if (uriMatcher?.match(uri) == 0)
return true
return false
这里面需要处理的就是,如何转发对应的 Uri 到正确的插件 Provider 中呢,解决方案是在 Uri 中定义不同的插件路径,比如 plugin1 的 Uri 对应就是 content://com.zy.stubprovider/plugin1,plugin2 对应的 uri 就是 content://com.zy.stubprovider/plugin2,然后在 StubContentProvider 中根据对应的 plugin 分发不同的插件 Provider。
十、总结
本文介绍了插件化的相关实现,主要集中在 Activity 的实现上。重点如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZZcnTZEY-1644931575383)(https://upload-images.jianshu.io/upload_images/27552241-4f6e55b7e110534f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
最后推荐大家在学习插件化的同时,也去学习一些四大组件以及 Binder 的系统实现。
前段时间收集整理了Android高工必备技能知识脑图和核心知识点笔记文档!既能够夯实底层原理核心技术点,又能够掌握普通开发者,难以触及的架构设计方法论。那你在工作中、团队里、面试时,也就拥有了同行难以复制的核心竞争力。
相关的一些知识点解析都已经做了收录整理上传至公号中:Android开发之家,大家可以自行访问查阅。
以上是关于怎么将 Android 程序做成插件化的形式的主要内容,如果未能解决你的问题,请参考以下文章