Android插件化之资源动态加载
一.概述
Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题
1.如何加载插件资源
2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找。这样能做到动态更新的效果。
3.如何确保插件和宿主使用到的是被修改过的资源。
二.原理分析
在做一件事之前必须先弄清楚原理,所以,这里先要弄清楚Android的资源体系原理。
1.资源链
Context:一个apk里面其context的个数为application+Activity+service的总和,因为他们都是继承context的,然而context只是一个抽象类,其真正的实现类是ContextImpl,那。拿Activity来说,在Activity的启动流程中,会在ActivityThread的performLaunchActivity()方法中调用Activity的attach方法把ContextImp实例传给Activity(即赋值给Activity内的成员变量mBase)。
Resources:ContextImpl内有一个Resources的成员变量mResources,代表的是应用的资源,我们平时在调用getResources()方法获取到的是该Resources。
AssetManager:Resources内部的一个重要成员是AssetManager(mAssets),其指向的是apk的资源路径,资源的获取最终都是通过它来得到的。这里需要注意的是AssetManager并不是Resources独立持有的,也就是说系统在获取资源的时候不一定是通过Resources获取的,有时候是直接通过AssetManager来获取,比如TypedArray,之前就踩过这个坑。
2.Android是如何构造一个应用的资源的,并且是如何传递给我们使用的,这个要讲的东西非常的多,可以看另一篇文章,这里主要讲资源插件化。
三.问题的解决方案
1.加载插件资源
资源的加载最后是通过AssetManager内的一个方法addAssetPath(String path)
该方法接收的参数是插件apk的路径,内部会调用native方法把插件apk对应的资源加载进来。然而该方法是hide的,我们不能直接调用,所有只能通过反射。
这样就成功构造出一个指向插件资源的AssetManager。当然这时候还不能使用,还要调用AssetManager的ensureStringBlocks()方法来初始化其内部参数,同样得使用反射。
2.如何解决插件资源与宿主资源的处突
如果使用到的资源,插件和宿主都同时存在,则使用插件的资源;如果使用到的资源只有插件有,则使用插件的;如果使用到的资源只有宿主有的,则使用宿主的。
AssetManager的addAssetPath()方法调用native层AssetManager对象的addAssetPath()方法,通过查看c++代码可以知道,该方法可以被调用多次,每次调用都会把对应资源添加起来,而后来添加的在使用资源是会被首先搜索到。可以怎么理解,C++层的AssetManager有一个存放资源的栈,每次调用addAssetPath()方法都会把资源对象压如栈,而在读取搜索资源时是从栈顶开始搜索,找不到就往下查。所以我们可以这样来处理AssetManager并得到Resources
其中dexPath2为宿主apk路径,dexPath为插件apk路径,superRes为宿主资源,resources为融合插件与宿主的资源。
3. 如何确保插件和宿主使用到的是被修改过的资源:
这是很重要的一步,之前我们已经成功获取资源并对其进行修饰,现在要做的是用它替换掉Android为我们生成的那个资源,这就是hook的思想。
使用到资源的地方归纳起来有两处,一处是在Java代码中通过Context.getResources获取,一处是在xml文件(如布局文件)里指定资源,其实xml文件里最终也是通过Context来获取资源的只不过是他一般获取的是Resources里的AssetManager。所以,我们可以在Context对象被创建后且还未使用时把它里面的Resources(mResources)替换掉。之前说过,整个应用的Context数目等于Application+Activity+Service的数目,Context会在这几个类创建对象的时候创建并添加进去。而这些行为都是在ActivityTHread和Instrumentation里做的。
以Activity为例,步骤如下:
a: Activity对象的创建是在ActivityThread里调用Instrumentation的newActivity方法
ActivityThread:
Instrumentation:
b: Context对象的创建是在ActivityThread里调用createBaseContextForActivity方法
ActivityThread:
c: Activity绑定Context是在ActivityThread里调用Activity对象的attach方法,其中appContext就是上面创建的Context对象
ActivityThread:
d: Activity的onCreate()方法的回调是在ActivityThread里调用Instrumentation的callActivityOnCreate()方法
ActivityThread:
替换掉Activity里Context里的Resources最好要早,基于上面的观察,我们可以在调用Instrumentation的callActivityOnCreate()方法时把Resources替换掉。那么问题又来了,我们如何控制callActivityOnCreate()方法的执行,这里又得使用hook的思想了,即把ActivityThread里面的Instrumentation对象(mInstrumentation)给替换掉,同样得使用反射。步骤如下
a: 获取ActivityThread对象
ActivityThread里面有一个静态方法,该方法返回的是ActivityThread对象本身,所以我们可以调用该方法来获取ActivityTHread对象
然而ActivityThread是被hide的,所以得通过反射来处理,处理如下:
b: 获取ActivityThread里的Instrumentation对象
c: 构建我们自己的Instrumentation对象,并从写callActivityOnCreate方法
在callActivityOnCreate方法里要先获取当前Activity对象里的Context(mBase),再获取Context对象里的Resources(mResources)变量,在把mResources变量指向我们构造的Resources对象,做到移花接木。
MyInstrumentation:
d: 最后,使ActivityThread里面的mInstrumentation变量指向我们构建的MyInstrumentation对象。
代码
四.应用
资源动态加载的一个应用当然就是Android插件化方面的使用。还有一个应用就是换肤功能,只需要在在工程里添加这些代码(当然还要处理一些逻辑),然后用户想要给应用换皮肤,主题等,即可从后台下载插件apk,放在指定文件夹就可以关系应用的资源,起到换肤的效果。当然,资源动态加载还有其他应用方法,自己琢磨咯!!!
五.存在问题
1.兼容性问题,因为hook要使用反射,从而来获取系统hide或类的私有属性。把它们隐藏是因为它们的不稳定性,如果哪天Google觉得那个变量的名称起的不吉利给改了,那就报错了。当然解决方法还是有的,就是为不同的API写不同的代码。
2.R方面的问题。当我们添加了一个资源(如在String.xml里添加了一个String),则系统会为我们在R里面为该资源生成一个int型的id与之对应,使用的时候是根据该id找到对应的资源。资源id是按照资源名称的字典顺序来递增的。拿String来说。
假如我们的String.xml里声明了名称为za,zb的资源
则会在R里面生成相应的id
基于上面的观察,我们会发现一个问题:举个例子
宿主资源情况为:存在za(id=0x7f060004) zb(id=0x7f060005)
插件资源情况为:存在za(id=0x7f060004) zab(id=0x7f060005) ab(0x7f060006)
这时候在宿主里获取资源zb,则根据上面所说,会根据id=0x7f060005先存在插件资源,这时候得到的是zab而不是zb,这就出错了。
解决方案有,在插件中如果有添加新的资源,则其命名要安装字典排序在原有的资源下递增。当然也有其他方案,自己琢磨吧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。