在上一篇博客Android插件化(二):OpenAtlas插件安装过程分析中深入分析了OpenAtlas的随宿主启动的插件安装过程,本文将对插件的重建,”使用时插件的安装”过程,以及插件安装后类的加载过程进行分析,其中”使用时安装”这是我自己的定义,它类似懒加载机制,比如在需要用到插件中的某个组件时,会先check一下,如果发现有插件存在该组件,并且插件还未安装,就会先安装该插件。
1.插件的重建
Framework.restoreFromExistedBundle()方法如下:
|
|
其中的file其实是插件数据目录,类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test”,进入到对应的BundleImpl(File,BundleContextImpl)构造方法中:
|
|
显然,通过读取/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test目录下的meta文件,获取了包名,启动级别,启动状态信息。之后也是要创建BundleArchive对象,但是调用的构造方法和前面的不同,代码如下:
|
|
代码有点长,但是其实不难。
- 同样的,新建了revisions这个TreeMap对象;
- 获取插件目录bundleDir(类似”/data/data/cn.edu.zafu.atalasdemo/files/storage/com.lizhangqu.test”)下所有文件,如果是以”version”开头,则进行判断,如果这个版本的目录下有DEPRECATED_MARK文件存在,则说明是不推荐的版本,为了节约空间,直接将这个版本的插件目录以及其中的文件删除;否则将对应版本号的值置为null,即this.revisions.put(Long.valueOf(parseLong),null);这样做的原因是不能有两个版本的插件共存,需要安装(或重建)当前插件的话,就需要先把其他版本的插件从内存中消除;
- 如果revisions为空,则对进行这个操作的进程进行检查,保证是宿主进程在进行插件的重建工作,这样是为了防止Hacker将自己的插件安装到宿主中进行破坏活动;
- 注意longValue=this.revisions.lastKey().longValue();这里,会返回键值最大的那个,也就是版本号最大的那个;可以保证获取到最后一次安装时的版本号,而这个版本号就是我们要重建的插件版本号;
- 之后类似的,也要创建BundleArchiveRevision对象,但是调用的是不同的构造方法:
|
|
这个构造方法就是记录包名,版本号和版本目录;
之后从安装时创建的meta file中读出插件文件的位置,并且用bundleFile记录下来。这个bundleFile非常重要,在之后加载类,读取Manifest中的信息,以及加载插件中的资源都会用到。
到这里,记录插件信息的BundleImpl对象中的属性都已经初始化完毕,所以重建完毕。
到这里,插件的安装过程就完成了。之后,在BundleImpl中会调用Framework.bundles.put(location,this);将插件对象插入到Framework.bundles这个map中;之后调用resolveBundle(false);主要是新建了classLoader对象和将state该为4(BundleEvent.STOPPED),并通知BundleListeners,不过这里状态是BundleEvent.LOADED,表示当前插件已经加载完毕;最后调用Framework.notifyBundleListeners通知BundleListener,通知的状态为BundleEvent.INSTALLED,表示当前插件已经安装完毕。
再回到Framework中的installNewBundle(String,File)方法,之后会调用storeMetadata(),该方法如下:
|
|
这个方法非常简单,就是将startLevel和writeAheads(首个插件安装时startlevel为-1,writeAheads为空),写入到类似/data/data/cn.edu.atlasdemo/files/storage/meta这样的文件中。
需要注意的是,到这里并没有完成全部工作,回到BundleInstaller.processAutoStartBundles()中,发现如果是随宿主启动的话,就会立即开启各个BundleImpl对象,BundleImpl.start()的代码如下:
|
|
可见这里主要就是更改persistently的状态,而且当当前插件的启动level比总体的启动level低时,就需要startBundle(),不过首次安装插件时this.currentStartlevel>Framework.startlevel,所以不会执行startBundle();
再一路回到OpenAtlasInitializer中的installBundles()方法中,安装完插件之后调用的是mOptDexProcess.processPackage(false,false);方法:
|
|
这里的逻辑其实有点混乱,到了ACDD就修改好了.
由于optAuto和notifyResult都为false,故执行optStoreDex()方法:
|
|
这两个综合起来就是对已经安装或者安装方式为STORE的插件进行dex优化工作,BundleImpl的optDexFile()方法如下:
|
|
显然,就是调用BundleArchive的optDexFile()方法,而BundleArchive的optDexFile()又是调用BundleArchiveRevision的optDexFile()方法:
|
|
显然,这里就是进行dex优化工作,优化后的目标文件位置类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/version.1/bundle.dex”,先是调用了InitExecutor.optDexFile()进行了优化工作:
|
|
由于ART虚拟机和Dalvik虚拟机的优化方式不同,所以需要区分,而dexopt是个native方法:
|
|
它对应的C++方法如下:
|
|
显然,对于ART虚拟机,调用的是run_dex2oat()进行优化,而对于Dalvik虚拟机,则调用run_dexopt()进行优化工作。
优化完了之后,调用loadDex()方法:
|
|
这个其实就是将优化过的odex或oat文件中的数据加载到内存中,调用的是DexFile.loadDex()方法进行解析,由于这里涉及到较多的dex文件格式,虚拟机以及优化的问题,展开的话内容太多,会在后面专门写博客分析。这里先跳过,只要记住:在调用DexFile.loadDex()生成DexFile对象之后,就可以利用它来查找插件中定义的类了。DexFile本来也是有public Class loadClass(String,ClassLoader)方法的。
到这里,可以总结一下安装插件过程中主要做了哪些事:
- 新建了BundleImpl对象,在新建这个对象的过程中,新建了BundleArchive和BundleArchiveRevision,BundleClassLoader对象;
- 对一些特殊的ROM进行了兼容,方法是复制插件文件到目标位置或者建立软链接;
- 新建了插件版本元数据文件metaFile,就是类似/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/version.1/meta这样的文件,用于记录文件协议和插件文件位置;
- 新建(或打开)了类似/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/meta这样的插件元数据文件,往其中写入了包名,启动级别和启动状态;
- 新建(或打开)了类似/data/data/cn.edu.atlasdemo/files/storage/meta这样的记录总体插件状态的元数据文件,其中记录了当前Framework中的startLevel;
- 对于dex文件进行了优化
- 将BundleEvent.LOADED和BundleEvent.STARTED事件通知BundleListener
2.使用时安装流程
前面说过,使用时安装类似懒加载机制,比如在需要用到插件中的某个组件时,会先check一下,如果发现有插件存在该组件,并且插件还未安装,就会先安装该插件。
使用时安装的流程如下:
从以上两图中可以看出,宿主启动时安装和使用时安装只是前面部分不同,从判断插件存档文件是否存在开始,流程就一样了。
而会引起使用时的情况有很多,如ContextImplHook.bindService(),ContextImplHook.startActivity(),ContextImplHook.startService(),
ContextImplHook.findClass(),InstrumentationHook.execStartActivityInternal(),由于其他几种在后面会专门分析,这里就只分析ContextImpl.findClass()这种情况:
|
|
ClassLoadFromBundle.checkInstallBundleIfNeed()方法如下:
|
|
首次查找时sInternalBundles==null,故进入resolveInternalBundles();
|
|
利用的是寻找目录下ZipFile(其实确切地说是解压缩文件)的方法,其实效率非常低,因为打log发现sZipFile的entries有AndroidManifest.xml,META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF,classes.dex,
lib/armeabi/libcom_lizhangqu_test.so,lib/armeabi/libcom_lizhangqu_zxing.so,lib/armeabi/libdexopt.so,
lib/x86/libdexopt.so,res/anim/…,res/color/..,res/color-v11/..,res/drawable/..,res/drawable-../..,res/layout-../..所有这些文件,其实就是apk解压后的文件,如果宿主apk比较大,其中的资源和so文件较多,那么这样的查找就很费时。其实完全可以利用之前BundleParser的解析结果,然后按照约定去检查指定文件是否存在即可。
经过resolveInternalBundles()之后,sInternalBundles就包含了所有的插件名称,如{“com.lizhangqu.test”,”com.lizhangqu.zxing”},之后从解析的json文件结果中获取传入的组件对应的插件名称,如果sInternalBundles中含有该插件名称的话,就安装该插件。
其实这里的逻辑有冗余的地方,因为其实只要利用json文件解析的结果进行判断就行了,如果某个插件中含有该组件,就直接安装即可(如果之前没有安装的话),而且其实后面的checkInstallBundleAndDependency()方法中还会对插件文件是否存在进行判断.
之后进入checkInstallBundleAndDependency()中:
|
|
这个方法其实很简单:
- 先检查当前插件对其他插件的依赖情况,如果依赖其他的插件,则需要先安装其他的插件,其他的插件如果仍然有依赖,则还需要先安装依赖,所以这是一个递归方法;
- 检查插件文件是否存在,如果存在而且磁盘空间充足,就安装插件;之后插件的安装过程就跟前面随宿主启动时安装的过程一样了,不再赘述。
3.安装完插件后类的加载过程
在回到DelegateClassLoader的findClass()方法中,在调用ClassLoadFromBundle.checkInstallBundleIfNeed(className);安装完插件之后,接着就需要加载类了。
这里是通过调用ClassLoadFromBundle.loadFromInstalledBundles()来进行加载:
|
|
这个方法其实比较简单,主要是以下部分:
- 遍历Framework中注册的所有插件,如果该插件的组件中包含该组件名,则进入下一步;
- 调用bundleImpl.getArchive().optDexFile();而BundleImpl.getArchive().optDexFile();在前面mOptDexProcess.processPackage(false,false)分析过,这个就是先对dex进行优化(如果之前没有优化的话),然后利用DexFile.loadDex(odexFile);加载优化后的odex文件,并生成DexFile对象,之后bundleImpl.getClassLoader()返回BundleClassLoader对象,而BundleClassLoader其实是利用装饰模式,加载插件中的类时调用的是findOwnClass(),之后的调用流程为BundleClassLoader.findOwnClass()–>BundleArchive.findClass(String,ClassLoader)–>BundleArchiveRevision.findClass(String,ClassLoader),而该方法代码如下:
|
|
显然,对应普通的ROM(非YunOS),在loadDex()之后,利用dexFile就可以加载到我们需要的类。
但是,对应YunOS(即lexFile!=null),这里先生成BundleArchiveRevisionClassLoader对象(BundleArchiveRevisionClassLoader继承自DexClassLoader),之后利用反射调用DexClassLoader的findClass()方法来加载类(其实findClass()方法是BaseDexClassLoader中的,不过DexClassLoader继承自BaseDexClassLoader).
如下是BundleArchiveRevision的内部类BundleArchiveRevisionClassLoader的定义:
|
|
所以我们可以总结出来,对于普通的ROM,插件中的类是通过dexFile加载出来的;而对应YunOS系统,则是先生成BundleArchiveRevisionClassLoader(它是DexClassLoader的子类)对象,之后通过反射调用它的findClass()方法来加载类。