看这个就够了啊,深入理解Android插件化技术
插件化技术核心
类的加载机制和反射机制。
https://www.cnblogs.com/muouren/p/11706519.html
插件化历史
想要实现插件化,主要是解决下面三个问题:
- 插件中代码的加载和与主工程的互相调用
- 插件中资源的加载和与主工程的互相访问
- 四大组件生命周期的管理
插件化原理
1. 类加载
Android中常用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。
区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件。
而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。我们使用DexClassLoader去加载外部的apk
1.1 双亲委托机制
ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载。
1.2 DexPathList
DexClassLoader重载了findClass方法,在加载类时会调用其内部的DexPathList去加载。DexPathList是在构造DexClassLoader时生成的,其内部包含了DexFile。
DexPathList的loadClass会去遍历DexFile直到找到需要加载的类。
(腾讯的qq空间热修复技术正是利用了DexClassLoader的加载机制,将需要替换的类添加到dexElements的前面,这样系统会使用先找到的修复过的类。)
单DexClassLoader与多DexClassLoader
通过给插件apk生成相应的DexClassLoader便可以访问其中的类分为单DexClassLoader和多DexClassLoader两种结构。
-
多DexClassLoader (RePlugin)
对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。
这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题。
-
单DexClassLoader(Small)
将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。
优点:可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。
2.插件通讯
插件调用主工程
在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。
主工程调用插件
多ClassLoader机制:主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。
插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。单ClassLoader机制:主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。
3.资源加载
Android系统通过Resource对象加载资源
因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问。
具体实现时,由于AssetManager并不是一个public的类,需要通过反射去创建,并且部分Rom对创建的Resource类进行了修改,所以需要考虑不同Rom的兼容性。
资源路径的处理
和代码加载相似,插件和主工程的资源关系也有两种处理方式:
- 合并式:addAssetPath时加入所有插件和主工程的路径;
- 独立式:各个插件只添加自己apk路径
合并式由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。
独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。
Context的处理
通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作。 对资源访问的不同实现方式也需要不同的额外工作。以VirtualAPK的处理方式为例。
第一步:创建Resource
第二步:hook主工程的Resource
对于合并式的资源访问方式,需要替换主工程的Resource,下面是具体替换的代码。
注意下上述代码hook了几个地方,包括以下几个hook点:
替换了主工程context中LoadedApk的mResource对象。
将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理。
第三步:关联resource和Activity
上述代码是在Activity创建时被调用的(后面会介绍如何hook Activity的创建过程),在activity被构造出来后,需要替换其中的mResources为插件的Resource。由于独立式时主工程的Resource不能访问插件的资源,所以如果不做替换,会产生资源访问错误。
做完以上工作后,则可以在插件的Activity中放心的使用setContentView,inflater等方法加载布局了。
解决资源冲突
合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id。
资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。如下表所示:
所以思路是修改资源ID的PP段,对于不同的插件使用不同的PP段,从而区分不同插件的资源。具体实现方式有两种:
- 修改aapt源码,编译期修改PP段。
- 修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。
四大组件支持
1. Activity
ProxyActivity代理
ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很简单,在主工程中放一个ProxyActivy,启动插件中的Activity时会先启动ProxyActivity,在ProxyActivity中创建插件Activity,并同步生命周期。下图展示了启动插件Activity的过程。
- 首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。
- ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。
- PluginActivty调用的setContentView被重写了,会去调用ProxyActivty的setContentView。由于ProxyActivity重写了getResource返回的是插件的Resource,所以setContentView能够访问到插件中的资源。同样findViewById也是调用ProxyActivity的。
ProxyActivity中的其他生命周期回调函数中调用相应PluginActivity的生命周期。
ProxyActivity代理方式主要注意两点:
- ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。
- PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。
缺点
- 插件中的Activity必须继承PluginActivity,开发侵入性强。
- 如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。
- 插件中需要小心处理Context,容易出错。
- 如果想把之前的模块改造成插件需要很多额外的工作。
该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,dynamic-load-apk之后的插件化方案很少继续使用该方式,而是通过hook系统启动Activity的过程,让启动插件中的Activity像启动主工程的Activity一样简单。
hook方式
- Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法。
Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。
- 通过跨进程的binder调用,进入ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。
- ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。
- 主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。
hook的方式启动插件中的Activity,需要解决以下两个问题:
- 插件中的Activity没有在AndroidManifest中注册,如何绕过检测。
- 如何构造Activity实例,同步生命周期
以VirtualAPK为例,核心思路如下:
- 先在Manifest中预埋StubActivity,启动时hook上图第1步,将Intent替换成StubActivity。
- hook第10步,通过插件的ClassLoader反射创建插件Activity
- 之后Activity的所有生命周期回调都会通知给插件Activity
替换系统Instrumentation
VirtualAPK在初始化时会调用hookInstrumentationAndHandler,该方法hook了系统的Instrumentaiton类,由上文可知该类和Activity的启动息息相关。
该段代码将主线程中的Instrumentation对象替换成了自定义的VAInstrumentation类。在启动和创建插件activity时,该类都会偷偷做一些手脚。
hook activity启动过程
VAInstrumentation类重写了execStartActivity方法,相关代码如下:
execStartActivity中会先去处理隐式intent,如果该隐式intent匹配到了插件中的Activity,将其转换成显式。之后通过markIntentIfNeeded将待启动的的插件Activity替换成了预先在AndroidManifest中占坑的StubActivity,并将插件Activity的信息保存到该intent中。其中有个dispatchStubActivity函数,会根据Activity的launchMode选择具体启动哪个StubActivity。VirtualAPK为了支持Activity的launchMode在主工程的AndroidManifest中对于每种启动模式的Activity都预埋了多个坑位。
hook Activity的创建过程
上一步欺骗了系统,让系统以为自己启动的是一个正常的Activity。当来到图 3.2的第10步时,再将插件的Activity换回来。此时调用的是VAInstrumentation类的newActivity方法。
由于AndroidManifest中预埋的StubActivity并没有具体的实现类,所以此时会发生ClassNotFoundException。之后在处理异常时取出插件Activity的信息,通过插件的ClassLoader反射构造插件的Activity。
其他操作
插件Activity构造出来后,为了能够保证其正常运行还要做些额外的工作。
这段代码主要是将Activity中的Resource,Context等对象替换成了插件的相应对象,保证插件Activity在调用涉及到Context的方法时能够正确运行。
经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。
其他组件
四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:
- Service:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。
- BroadCastReceiver:解析插件的manifest,将静态注册的广播转为动态注册。
- ContentProvider:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。