当前位置:   article > 正文

【Android 修炼手册】常用技术篇 -- Android 插件化解析(2)_android 四大组件的插件化

android 四大组件的插件化

2015 年 张勇 发布了 DroidPlugin,使用 hook 系统方式实现插件化。
2015 年 携程发布 DynamicApk
2015 - 2016 之间(这块时间不太确定),Lody 发布了 VirtualApp,可以直接运行未安装的 apk,基本上还是使用 hook 系统的方式实现的,不过里面的实现要精致很多,实现了自己的一套 AMS 来管理插件 Activity 等等。
2017 年阿里推出 Atlas
2017 年 360 推出 RePlugin
2017 年滴滴推出 VirtualApk
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
  1. 生命周期如何调用
  2. 如何使用插件中的资源
  • Service
  1. 生命周期如何调用
  • BroadcastReceiver
  1. 静态广播和动态广播的注册
  • ContentProvider
  1. 如何注册插件 Provider 到系统

六、Activity 的插件化实现

6.1 难点分析

我们之前说到 Activity 插件化的难点,我们先来理顺一下为什么会有这两个问题。
因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。
可能有些同学会问,那为什么不能直接把插件的 Activity 注册到宿主 Manifest 里呢?这样是可以,不过就失去了插件化的动态特性,如果每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。
我们再来说一下为什么没有注册的 Activity 不能和系统交互
这里的不能直接交互的含义有两个

  1. 系统会检测 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);

    }
    }
    }
  1. Activity 的生命周期无法被调用 其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。

其实上面两个问题,最终都指向同一个难点,那就是插件中的 Activity 的生命周期如何被调用。 解决问题之前我们先看一下正常系统是如何启动一个 Activity 的。
这里对 Activity 的启动流程进行一些简单的介绍,具体的流程代码就不分析了,因为分析的话大概又能写一篇文章了,而且其实关于 Activity 的启动过程也有不少文章有分析了。这里放一张简图说明一下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

整个调用路径如下:

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 以为这个职员是已经注册的。

对应到实际的解决方法就是:

  1. 我们手动去调用插件 Activity 的生命周期
  2. 欺骗系统,让系统以为 Activity 是注册在 Manifest 中的

说完生命周期的问题,再来看一下资源的问题
在 Activity 中,基本上都会展示界面,而展示界面基本上都要用到资源。
在 Activity 中,有一个 mResources 变量,是 Resources 类型。这个变量可以理解为代表了整个 apk 的资源。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在宿主中调用的 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 生命周期

手动调用生命周期原理如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们手动调用插件 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 资源处理方式

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于手动调用生命周期的方式,会重写大量的 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 的方式进行介绍(原因是个人感觉这种方式要简单一点)。
简化以后的流程如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

题外话

我们见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

注意:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

不断奔跑,你就知道学习的意义所在!

注意:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-lPXBF3lJ-1714529919504)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/861113
推荐阅读
相关标签
  

闽ICP备14008679号