在前一篇博客Android插件化(一):OpenAtlas架构以及实现原理概要中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。
插件的安装分为3种:宿主启动时立即安装,宿主启动时延时安装,使用时安装,其中使用时安装采用的是一种类似懒加载的机制。
这3种方式只是前面的处理有所不同,最后安装逻辑都是一样的。限于篇幅,本文只分析宿主启动时安装,使用时安装在下一篇分析。
由于宿主启动时安装和宿主启动时延时安装的逻辑大体相同,所以放在一起讲解,它们的流程如下:
关键流程分析如下:
1.初始化分析
需要实现插件化,自定义的宿主Application就需要继承AtlasApp,而在AtlasApp的attachBaseContext()中完成json文件的解析等初始化工作。在AtlasApp的onCreate()中调用OpenAtlasInitializer进行插件安装等初始化工作,代码如下:
|
|
2.OpenAtlasInitizlizer.startup()分析
进入OpenAtlasInitializer.startup()方法中,在这个方法中有非常多的内容,先看代码:
|
|
这个方法主要做了以下事情:
- 首先,调用isMatchVersion()检查版本号是否匹配,如果版本号匹配则init为true,此时会检查包名,如果包名不匹配则直接杀死进程;
- 如果版本号匹配,进行平台属性的设置;
- 利用反射将Globals中的sApplication替换为当前的Application对象,将Globals中的sClassLoader替换为DelegateClassLoader对象;
- 初始化BundlesInstaller,OptDexProcess对象,然后调用Atlas.getInstance().startup(properties);进行初始话工作,主要是属性的设置和获取;
- 最后调用installBundles(mBundlesInstaller,mOptDexProcess);开始插件的安装;
3.条件判断与设置
OpenAtlassInitializer中的installBundles()方法比较简单,就是如果InstallSolutionConfig.install_when_oncreate_auto为true,则发布异步任务进行插件的安装,其中InstallSolutionConfig中的各个属性可以由开发者进行配置; 如果InstallSolutionConfig.install_when_oncreate_auto为true,则会在启动时遍历AtlasConfig中的AUTO数组,安装AUTO数组中的所有插件;
4.安装条件检查与真正进入安装流程
进入BundleInstaller.process()方法中,这个方法其实很简单:先是从zipFile(路径类似/data/app/XX-1.apk)中获取所有lib/armeabi/下以libcom_为前缀,.so为后缀的插件文件路径。之后检查空间是否足够,如果足够则进入安装阶段,否则弹出Toast提示.另外,就是在这里区分立即安装和延时安装。代码如下:
|
|
由于这里bunnyblue对于延时安装实现的并不好,而且到了ACDD的时候没有这个功能了,所以这里就不再分析延时安装,直接进入到后面的安装过程。
5.安装过程
进入BundlesInstaller.processAutoStartBundles()方法中,代码如下:
|
|
显然是先安装插件再启动。而安装插件的代码如下:
|
|
注意zipFile是类似/data/app/cn.edu.zafu.atlasdemo-1.apk这样的压缩文件,而file则是类似/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so这样的文件,其实是由于/data/app/下存放第三方软件;而/data/data存放所有软件(包括/system/app和/data/app以及/mnt/asec中的软件)的一些lib和xml文件等数据信息. 也就是说安装完宿主APK之后,lib会解压到/data/data/pckageName/lib下面.但是,如果这个文件不存在(例如不小心被删除了),那么就需要从zipFile这个文件中读出我们需要的插件文件了,如根据”lib/armeabi/libcom_lizhangqu_test.so”就可以读取到libcom_lizhangqu_test.so这个文件。
一般file是存在的,进入Atlas.installBundle()进行分析。
6.Framework.installNewBundle()分析
Atlas.installBundle(String,File)直接调用Framework进行安装工作,可见Atlas其实是使用了装饰模式,真正完成工作的是Framework.进入Framework.installNewBundle(String,File)中分析,代码如下:
|
|
显然,这里利用了线程锁,首次安装时Framework.getBundle(location);的结果为空,所以进入到else分支,之后会先判断mBundleArchiveFile这个插件档案文件是否存在(路径类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test”),如果存在,就可以由这个存档文件直接生成BundleImpl对象.
不过这里有一个小bug,就是调用restoreFromExistedBundle()生成BundleImpl对象之后,其实到了BundleLock.WriteLock(location);之后,可以直接返回的,现在的逻辑是到了下面还会创建一个BundleImpl对象,显然不对。
那么这个mBundleArchiveFile到底是什么呢?其实它是一个类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test”这样的目录,在这个目录下面保存着与插件相关的数据。那么到底有哪些数据呢?
我们只要先看一下mBundleArchiveFile不存在时安装插件的情形,就可以发现在这个过程中新建了哪些文件。
此时会调用BundleImpl(File,String,BundleContextImpl,InputStream,File,boolean)这个构造方法:
|
|
这里由于archiveInputStream为null,故调用this.archive=new BundleArchive(location,bundleDir,archiveFile);创建BundleArchive对象,而对应的构造方法如下:
|
|
其中的location其实是包名,类似”com.lizhangqu.test”,而bundleDir类似指向/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test这样的目录,archiveFile其实是插件文件,类似/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so这样的,可见在BundleArchive()中会创建revisions这个TreeMap对象,并将版本号以及新建的BundleArchiveRevision对象保存到revisions中。下面看一下BundleArchiveRevision对应的构造方法:
|
|
首先是记录版本号和当前插件版本的目录,revisionDir类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/version.1”这样,显然,这个其实就是bundleDir+”/version.”+revisionNum生成的,archiveFile仍然是类似/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so这样的文件。
BundleInfoList是在AtlasApp的attachBaseContext()中解析assets中的json文件获得的插件信息,所以可以通过包名来获取对应的插件信息。
之后是对一些特殊ROM等做兼容,比如对于第一种情况,会判断是否为同一个路径(有的ROM可能在安装时解压路径比较奇怪),如果是则直接rename即可;否则将解压后的插件文件(如/data/data/cn.edu.zafu.atlasdemo/lib/libcom_lizhangqu_test.so)复制到bundleFile中即可,而bundleFile的路径类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/version.1/bundle.zip”;
第二种情况则是对于mt6592这个奇葩ROM,需要建立软链接;
大多数情况会走到第三个分支,此时不需要复制插件文件,只是revisionLocation变为REFERENCE_PROTOCOL+archiveFile.getAbsolutePath(),之后安装插件中的so库(如果有的话).
最后一种情况则是对YunOS进行兼容(YunOS使用的是阿里自己的虚拟机,运行的文件为.lex文件而非.dex文件)也是复制插件文件;
最后,调用updateMetadata()更新元数据:
|
|
其中的注释已经写得很清楚,metaFile就是类似/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/version.1/meta这样的文件,updateMetadata()就是往其中写入revisionLocation这个字符串,显然metaFile这个文件就是用于记录文件协议和插件文件位置的。
BundleArchive和BundleArchiveRevision的对象创建都分析完之后,发现其实到这里为止就创建了一个类似/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/version.1/meta 这样的元数据文件。
再回到BundleImpl()中,看建立BundleArchive对象之后的部分:
|
|
主要就是状态变为BundleEvent.STARTED,之后调用updateMetadata()更新数据,由于是安装,isInstall为true,故会将当前对象插入到Framework.bundles这个Map中,key是包名;
下面就看一下这里的updateMetadata()做了什么:
|
|
其中location为插件的包名,如”com.lizhangqu.test”,currentStartlevel为Framework.initStartlevel,这个值一般为1;而persistently表示当前插件的对应的BundleImpl对象是否已经启动,启动后则为true,否则为false;所以在每次persistently变动之后,都需要调用updateMetadata()进行数据更新;
而这个file对应的路径类似”/data/data/cn.edu.zafu.atlasdemo/files/storage/com.lizhangqu.test/meta”,显然和BundleArchiveRevision中的metaFile的路径是不同的,作用也是不同的,这个是与插件的版本无关的,直接在插件数据目录下的;
但是文件的I/O是非常低效的,如果改用数据库来进行数据的持久化,效率要高很多,这也是OpenAtlas的一个不足之处.可能也是手淘启动时卡顿的原因!
再回到前面那个问题,其实对于大部分ROM来说,建立BundleImpl过程中新建了两个meta文件,一个直接在插件目录下,里面保存了插件的包名,启动level和当前启动状态;另一个在插件对应版本的目录下,里面保存了文件协议和插件文件的位置。
那么Framework中restoreFromExistedBundle()是如何依据这个就建立插件对象(BundleImpl对象)的呢?
这个放到下一篇博客分析.