浅谈Android插件化

插件化,一个陌生有熟悉的名词,从我们学习Android伊始,总能隐约听到关于它的消息,从360的RePlugin,到DiDi的VirtualAPK更新换代,再到Tencent Shadow横空出世,可以说插件化已经从一个剑走偏锋的黑科技,蜕变成了独步天下的高级技能,其中蕴含的各种思想和变化,也成了考核高级开发工程师无法避开的++障碍++,今天我们就来简单的说一下这些『高深』的技术。

1. 什么是插件化

插件化,通俗来说就是一种动态加载技术。我们可以通过一个已安装的Apk来加载本地的apk文件,通过这种动态的加载技术来实现应用功能的拓展、动态更新、灰度发布、A/B Test等功能,本质上宿主以及所有的插件都是Apk,只不过宿主可以把其他的插件Apk加载并且运行起来。

插件化

2. 插件化的原理

这里我们不去把四大组件的插件化都介绍一遍,而是把Activity的原理讲明白,其实原理都是大同小异的,大家能理解一个其他的也不在话下。

2.1 Apk是什么?

Apk作为Android系统中的安装文件,其本质就是一个压缩包,你可以直接把它当做一个zip包,里面有各种二进制和资源文件,只是Android系统通过有规律的加载并运行这些文件,并把这些在手机屏幕上显示出来。

在继续往下看之前,我们要抛开原先的一些『概念』,插件化里面的Activity不是一个界面,而是一个class文件,Service也不是服务,也是一个Class文件,Receiver、Provider以及各种资源都是这样,只有以文件的角度去看待,才能以系统的角度去思考。简单的说Apk里的文件分为两类 Class文件 和 资源文件。加载这两种文件也需要不同的方式,Class文件用ClassLoader加载,资源文件用AssetManager加载。

2.2 Activity启动

我们明明说的是插件化,为什么先扯到了Activity的启动上了呢?其实这里才是插件化里的最核心的地方,因为所有的插件化框架都是依托源码才做出来的,我们要做的相当于一个『小系统』,如果我们都不能明白源码时如何实现的,我们又怎么能在其基础上构建我们的框架呢?所以我们在继续往下之前,必须要把启动的过程都缕清楚,这样在面对众多的插件化框架时才能有条不紊发去分析。

2.2.1 入门

Activity的启动流程可以简单的理解为一个进程间的通信过程,只不过一端是我们的App,另一端是Android系统。试想我们现有的逻辑,如果想要打开一个界面,就是调用startActivity()方法,我们就要把这个Class交给系统,系统验证通过,实例化这个界面并且交还给我们,我们才可以使用。这就是一个进程间通讯的过程,由于Android IPC使用Binder作为进程间通讯的主要手段,我们甚至可以直接把它看作是一个C/S的模型,我们的App就是客户端而系统是服务端,我们所做的也只是一个请求,而系统也为我们做了大部分的事情,比如Class的加载、实例化、Activity生命周期的控制、权限的管理等等。


简易流程
2.2.1 进阶

上述的流程只是让大家有一个大致的印象,想要了解更多就的去分析源码了。

Activity启动流程

上图是基于Android 7.1 Activity启动流程的整理,要用文字的方式去讲清楚这么一件事,其实并不容易,何况是这么一件挺麻烦的事,这幅图也只是一个参考,方便我们的讲解,请大家一定要对照图和下面的方法引用图去看一下源码,只有先把源码看明白,才能对插件化有一个自己的认识。

对于Activity的启动来说,其实每个版本的差别并不大,但是低版本的源码封装的没那么复杂,更便于我们阅读。


Activity启动方法调用图
2.2.3 思考

在看完源码之后,就要开始真正的思考了,如果让你去实现一个插件化的框架你要从哪里开始呢?

自然是先打一个插件Apk的包,试着去打开并加载。那么,用什么去打开?用什么去加载呢?

ClassLoader

其实在Android中,Activity也是通过ClassLoader来实例化的,只不过和我们认为的Java中不太一样。Android里并没有完全使用Java的加载模型但是借鉴了相似的思路。在Android中ClassLoader分为3种,每一种ClassLoader分别加载不同的文件。

  1. BootClassLoader:为系统预加载使用
  2. PathClassLoader:给程序、系统程序、应用程序 加载class
  3. DexClassLoader:加载apk、zip 文件

一般而言,Boot是系统用的,Path是App用的,Dex是用户用的。有了DexClassLoader,我们就可以把Apk加载到内存中使用了。但是这种方法过于简单粗暴,并且在实例化之后丢失了Context的环境,而丢失上下文的后果就是,我们所熟知的大部分方法都无法使用了,比如:findViewById()、startActivity()、startService()等等,要解决这个问题,我们就得从ClassLoader的底层加载去入手了。

我们看Activity启动流程图中的方法9和10,可以看出在performLaunchActivity中获取ClassLoader,Class的其实是在Instrumentation中通过newInstance()创建的,这里我们具体了解一下这一段的调用流程。

我们从ActivityThread.performLaunchActivity() 方法出发,找到ClassLoader,逐步向上找去,发现其实这个ClassLoader其实就是PathClassLoader。

在PathClassLoader中,真正的加载其实都是通过BaseDexClassLoader来进行的,而BaseDexClassLoader中有一个pathList字段,这个变量相等于一个Dex数组,各种Dex的信息都在里面,而DexClassLoader的父类也是BaseDexClassLoader,这里就是一个完美的Hook点,既然可以获取相同的Dex数据,那也能把类似的数据拼接到一起。


ClassLoader UML图

分析PathClassLoader的加载流程,我们通过Hook,从而把插件的Class直接『挂载』到上面,从而让插件里的Class无缝接入到主App中。


Dex加载流程
资源文件的加载

AssetManager本就是个十分强大的资源管理器,只是有些功能没有对我们开放,我们需要做的只是通过一些方法(反射)把Apk里的资源加进去就好了。
在AssetManager的源码中,mStringBlocks就是用来保存资源文件的变量,我们通过addAssetPath()方法把插件的路径加载进来,之后反射调用ensureStringBlocks()确保文件都加载进来,最后构造一个Resources在工程中使用即可。

    // 执行此 public final int addAssetPath(String path) 方法,能把插件的路径添加进去
    Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
    method.setAccessible(true);
    method.invoke(assetManager, file.getAbsolutePath());

    // 实例化 ensureStringBlocks()
    Method ensureStringBlocksMethod = assetManager.getClass().getDeclaredMethod("ensureStringBlocks");
    ensureStringBlocksMethod.setAccessible(true);
    // 执行了ensureStringBlocks  初始化 string.xml  color.xml  anim.xml 等文件
    ensureStringBlocksMethod.invoke(assetManager); 

    // 加载插件资源
    Resources r = getResources(); // 拿到宿主的配置信息
    resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
启动

现在Class和Recourse都已经准备好了,可以试着去启动了。

但是当你启动了之后你会遇到一个不可回避的问题,这就是我们可能时常忘记的Manifest声明错误。原因很显然,我们加载的是插件中的Activity,这个类甚至都不在系统里,更不要说在Manifest里面声明了。


ActivityNotFoundException

如果没有源码的话,我们的插件化之路可能已经要以失败告终了,好在Android是开源的,我们可以去研究为什么会报这个错误,或许可以解决这个限制。

你可以直接使用打印的错误在源码里面搜索,最后会在Instrumentation的 checkStartActivityResult方法中找到这段话,如果我们向上寻找的话就会发现,这个方法的调用者就是execStartActivity()。

ActivityNotFoundException

也许你会想到一个方案,就是事先在Manifest中声明好呀,这样不就很容易的解决了?我想在插件化发展中一定有这样的过程,这是一个简单且行之有效的方法,但是我们所熟知的框架里面却没有一个这样实现的,因为这是不实用的。试想,如果要用个插件化,这个App必然是有众多逻辑与界面,有多个业务的航母级应用,开发人员几十上百个,如果要这样去开发就违背了我们插件化的初衷,在开发难度上也并没有降低。

于是,大家就开始思考解决方案,最先出现的就是——占位式插件化。

预先在Manifest中声明一个ProxyActivity,所有的界面都以这个Activity作为跳板启动(把目标Activity的class路径放在extra里面),在打开ProxyActivity之后再通过ClassLoader加载并实例化目标Activity,这个Activity就启动成功了。

至此,我们的插件化框架就完成了,这个思路可以让这个『原始』的框架在Android9.0上运行,这是其他很多利用反射框架不可企及的,但是慢慢的你也会发现它的很多缺点,就是上文说提及的『侵入性』太强。因为这个Activity是完全不受系统管理的(这个Activity是我们自己实例化的),我们需要在ProxyActivity中接管Activity的生命周期,我们无法去管理Activity的启动栈了,我们甚至无法使用Context了。

显然,开发者和使用者对这样的实现方式并不满意,随后便有了更加便于开发的Hook式。

我们知道ActivityNotFoundException这个错误是在哪里抛出来的,也知道原本的代码,那么我们为什么不去绕过它?或者让系统不去执行这个方法呢?

Hook式的插件化实现起来稍显复杂,说复杂也只是因为要看的源码有点多。
这里我们不去深究代码实现,大家讲起来都差不多,我们的目标是把思路搞明白,如果想深入研究的话可以去看下深入理解Android插件化技术

插件化Activity启动流程

可以看到,Hook的方式也是需要ProxyActivity的,只不过使用的地方不一样,我们原来是直接启动ProxyActivity,但是现在这部分工作被Hook做了,既然有替换的过程,必然也有还原的地方,这个点就是在newActivity的Handler中。通过这个『神不知鬼不觉�』的过程,我们用另一种方式实现了插件化。

但是这样的方式也是有不足的地方的,Hook本就是『不安全』的,源码中更改了一个字段,删除了某个方法,都会造成不可预料的后果,事实也确实是这样,每个版本的启动过程都会有更改,而我们只能提前去适配新版本,避免出现问题。

3. 总结

这里只是说明了插件化的基本原理,其实完整的插件化框架还有很多东西,四大组件、Activity栈、插件中的组件相互启动、各个版本适配……如果你有兴趣不如去自己试一试,相信对你的成长有很大帮助。

一个好的插件化框架是需求足够且充足的前置知识,比如ClassLoader的加载,Hook、动态代理、Activity的启动流程等等。如果大家想学习FrameWork这是一个很好的切入点,因为大多数Hook的代码,都得你去阅读源码之后才能下手,而这无论是对于四大组件的启动流程,还是个版本之间的差异,你都需要把这些做到了如指掌。

原始的插件化还是需要借助各种反射的逻辑,寻找我们可能去着手的Hook点去做,但是随着Google从9.0开始对于『危险代码』的紧缩和Android版本之间的兼容性问题,Hook的方式也慢慢显露出各种问题,而腾讯基于无反射的实现,相信是未来插件化的发展方向。

源码地址:https://github.com/devilsen/PluginTest

参考 深入理解Android插件化技术

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339

推荐阅读更多精彩内容