-
宿主如何加载插件资源
开发中宿主程序调起未安装的插件apk,一个很大的问题就是资源如何访问,这些资源文件的ID都映射在gen文件夹下的R.java中,而插件中凡是以R开头的资源都不能访问。究其原因是因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源 -
如何处理插件资源与宿主资源的处突
插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找。这样能做到动态更新的效果。 -
如何确保插件和宿主使用到的是被修改过的资源。
一、主app中使用插件中资源
- res里的每一个资源都会在R.java里生成一个对应的Integer类型的id:
- Application module生成的R文件会包含所有Lib module的资源文件应用id;Lib module可以有自己的R文件,仅包含当前module中的资源;
- 两个都是R文件,但是包名不同,具体资源的id也不一样;
anim、array、attr、bool、color、dimen、drawable、id、integer、layout、menu、mipmap、raw、string、style、xml、styleable
package com.sangfor.pocket;
public final class R {
public static final class id{
public static final int Extra_layout=0x7f0f12dc;
...
}
public static final class string {
public static final int M=0x7f080019;
...
}
...
}
- APP启动时会先把R.java注册到当前的上下文环境,我们在代码里以R文件的方式使用资源时正是通过使用这些id访问res资源
getResources().getXXX(resid)
其中getResources()
获取的就是当前应用的全局Resource对象,然而,插件无论是apk还是so格式的,插件的R.java并没有注册到当前主app的上下文环境
那么我们getResources()
所获得全局Resource对象的getXXX(resid)
自然就找不到对应的资源路径
这也就是插件的res资源也就无法在主app中通过id使用了.
如果在主app中使用hook newActivity创建的Activity对象,是无法访问到插件res的。这是由于每个apk只能访问自己的res,所以这时候,虽然这个Activity确实是插件中的Activity,但是实际上是加载在宿主里的resource,所以也就是有个隔离,因此必须替换resource
那么我们到底应该如何在主App中使用插件apk的资源呢?
具体的解决思路有以下几种:
- 一种解决方式是插件里需要用到的新资源都通过纯Java代码的方式创建(包括XML布局、动画、点九图等),麻烦但是有效,不多过描述;
- 一种是是重写Context的getAsset、getResource之类的方法,偷换概念,让插件读取插件里的资源,但缺点就是宿主和插件的资源id会冲突,需要重写AAPT。
- 另一种是重写AMS中保存的插件列表,从而让宿主和插件分别去加载各自的资源而不会冲突。
- 第三种方法,就是打包后,执行一个脚本,修改生成包中资源id
Resource全局唯一性
- mResources通过一系列的缓存或者成员实例引用,实现了全局唯一。
- 如果通过打补丁的方式,会全局生效(small替换了resourcemanager中瓜缓存的resource的assetmanager,实现全局替换)。
- 但是在插件化方案中,为了避免id冲突,有的使用的是替换法,那就是所有有缓存和成员实例的地方都要替换为我们自己的resource;
1.1 创建新的Resource对象方式
要想获得资源文件必须得到一个Resource对象,想要获得插件的资源文件,必须得到一个插件的Resource对象,好在android.content.res.AssetManager.java中包含一个私有方法addAssetPath。
只需要将apk的路径作为参数传入,就可以获得对应的AssetsManager对象,从而创建一个Resources对象,然后就可以从Resource对象中访问apk中的资源了
通过以上方法却是可以在宿主中获取到插件的资源文件,只是宿需要用到相关资源的时候需跟插件约定好对应名称,以防出现找不到的情况
1.1.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来获取
Resources对象
- Resource实例存储
- Resource的实例保存在ContextImpl中,每次构建ContextImpl时,会从LoadedApk中拿到对应的Resource
- Loadedapk中的getResource会有成员实例mResource,命中直接返回,没命中委托Activitythread—ResourceManager生成
- ResourceMamager里有map缓存,命中直接返回,没命中则先新建Assetmanager,再构建Resource实例
全局唯一性
我们平时怎么使用res资源的吗,主要就是依赖于getResources()
的Resources对象, 也就是getResources().getXXX(resid)
- activity的
getResources
调用的是android.view.ContextThemeWrapper
的方法
@Override
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}
- 上面的代码发现是使用了super类
ContextWrapper#getResources()
方法:
android.content.ContextWrapper
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
@Override
public Resources getResources() {
return mBase.getResources();
}
- 样子又调用了Context的“getResources()”方法,看到这里,我们知道Context只是个抽象类
android.content.Context
public abstract Resources getResources();
- 其实际工作都是在ContextImpl完成的,赶紧去ContextImpl里看看“getResources()”方法吧
@Override
public Resources getResources() {
return mResources;
}
- 并没有mResources的创建过程啊!mResources是ContextImpl的成员变量,可能是在构造方法中创建的,于是我们看一下构造方法
resources=mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(),packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,displayId,
overrideConfiguration,compatInfo);
mResources=resources;
- 在
ResourcesManager
的getTopLevelResources
方法中创建的
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
Resources r;
AssetManager assets = new AssetManager();
if (libDirs != null) {
for (String libDir : libDirs) {
if (libDir.endsWith(".apk")) {
if (assets.addAssetPath(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
Configuration config;
……
r = new Resources(assets, dm, config, compatInfo);
……
return r;
}
- 通过这些代码从一个APK文件加载res资源并创建Resources实例,经过这些逻辑后就可以使用R文件访问资源了。具体过程是,获取一个AssetManager实例,使用其“addAssetPath”方法加载APK(里的资源),再使用DisplayMetrics、Configuration、CompatibilityInfo实例一起创建我们想要的Resources实例
1.1.3 实现思路
我们可以通过以下代码 实例化插件APK里res资源的Resources对象,那么自然就可以借助该对象获取插件中的资源了
- 新建一个AssetManager对象
- 通过反射调用addAssetPath方法
- 以AssetsManager对象为参数,创建Resources对象即可
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
注意,有的人担心从插件APK加载进来的res资源的ID可能与主项目里现有的资源ID冲突,其实这种方式加载进来的res资源并不是融入到主项目里面来
主项目里的res资源是保存在ContextImpl里面的Resources实例,整个项目共有,而新加进来的res资源是保存在新创建的Resources实例的,也就是说ProxyActivity其实有两套res资源,并不是把新的res资源和原有的res资源合并了(所以不怕R.id重复)
对两个res资源的访问都需要用对应的Resources实例,这也是开发时要处理的问题。(其实应该有3套,Android系统会加载一套framework-res.apk资源,里面存放系统默认Theme等资源)
addAssetPath的反射调用
这里你可能注意到了我们采用了反射的方法调用AssetManager的“addAssetPath”方法,而在上面ResourcesManager中调用AssetManager的“addAssetPath”方法是直接调用的
而且看看SDK里AssetManager的“addAssetPath”方法的源码(这里也能看到具体APK资源的提取过程是在Native里完成的),发现它也是public类型的,外部可以直接调用。
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
为什么还要用反射呢?
这里有个误区,SDK的源码只是给我们参考用的,APP实际上运行的代码逻辑在android.jar里面(位于android-sdk\platforms\android-XX),反编译android.jar并找到ResourcesManager类就可以发现这些接口都是对应用层隐藏的
1.1.4 具体实现
- 新建一个AssetManager对象
- 通过反射调用addAssetPath方法
- 以AssetsManager对象为参数,创建Resources对象即可
private static AssetManager createAssetManager(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
assetManager, apkPath);
return assetManager;
} catch (Throwable th) {
th.printStackTrace();
}
return null;
}
public static Resources getBundleResource(Context context, String apkPath){
AssetManager assetManager = createAssetManager(apkPath);
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
1.1.5 加载离线apk中的字符串和Drawable资源
我们使用插件的Resources对象,获取资源时,传递的ID必须是离线apk中R文件对应的资源的ID
- 由于没有ID引用,我们可以使用
resources#getIdentifier()
- 使用getIdentifier方法:根据资源名称,来获取资源id
- 第一个参数是资源名称,第二个参数是资源类型,第三个参数是离线apk的包名,切记第三个参数
- 也可以直接根据插件R中的id数值
Resources resources = BundlerResourceLoader.getBundleResource(getApplicationContext());
imageView = (ImageView)findViewById(R.id.image_view_iv);
if(resources != null){
String str = resources.getString(resources.getIdentifier("test_str", "string", "net.mobctrl.normal.apk"));
String strById = resources.getString(0x7f050001);//注意,id参照Bundle apk中的R文件
System.out.println("debug:"+str);
Toast.makeText(getApplicationContext(),strById, Toast.LENGTH_SHORT).show();
Drawable drawable = resources.getDrawable(0x7f020000);//注意,id参照Bundle apk中的R文件
imageView.setImageDrawable(drawable);
}
public static Resources getBundleResource(Context context){
AssetsManager.copyAllAssetsApk(context);//将apk拷贝到系统data/目录下
File dir = context.getDir(AssetsManager.APK_DIR, Context.MODE_PRIVATE);
String apkPath = dir.getAbsolutePath()+"/BundleApk.apk";
System.out.println("debug:apkPath = "+apkPath+",exists="+(new File(apkPath).exists()));
AssetManager assetManager = createAssetManager(apkPath);
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
述代码是加载离线apk中的字符串和Drawable资源,那么layout资源呢?
1.1.6 加载离线apk中的layout资源
我们使用LayoutInflate对象,一般使用方法如下:
View view = LayoutInflater.from(context).inflate(R.layout.main_fragment, null);
总结起来就是,传入当前上下文环境和资源id给LayoutInflater,LayoutInflater会借用上下文环境中的Resource去查找该id的资源并加载
其中,R.layout.main_fragment我们可以通过getIdentifier获取插件中的id,那么关键的一步就是如何生成一个context?
直接传入当前的context是不行的,因为这是两个不同的上下文对象,当前app的context中是找不到这个插件layout的id的
解决方案有2个:
-
- 创建一个自己的ContextImpl,Override其方法。
-
- 通过反射,直接替换当前context的mResources私有成员变量
直接替换当前context的mResources私有成员变量
我们在Activity的attachBaseContext方法中,对Context的mResources进行替换,就可以把当前的Context对象传入LayoutInflater函数中
但是需要注意的是,一旦替换,则当前Acitivity获取的Resouces都是基于插件资源的。
@Override
protected void attachBaseContext(Context context) {
replaceContextResources(context);
super.attachBaseContext(context);
}
/**
* 使用反射的方式,使用Bundle的Resource对象,替换Context的mResources对象
* @param context
*/
public void replaceContextResources(Context context){
try {
Field field = context.getClass().getDeclaredField("mResources");
field.setAccessible(true);
field.set(context, mBundleResources);
System.out.println("debug:repalceResources succ");
} catch (Exception e) {
System.out.println("debug:repalceResources error");
e.printStackTrace();
}
}
1.1.7 Application中Resources的加载
简书H3c:Android插件化开发 第四篇 [加载插件Activity]中,提供了一种 替换Application中Resources的方式
- 替换Application的Resource对象
- 插件Activity替换Resource对象
- 重写getResources()方法,使用Application的Resource替换当前的Resource方法
public class HostApplication extends Application {
private Resources mAppResources = null;
private Resources mOldResources = null;
@Override
public void onCreate() {
super.onCreate();
mOldResources = super.getResources();
AssetsMultiDexLoader.install(this);// 加载assets中的apk
installResource();
}
@Override
public Resources getResources() {
if(mAppResources == null){
return mOldResources;
}
return this.mAppResources;
}
private void installResource() {
if (mAppResources == null) {
mAppResources = BundlerResourceLoader.getAppResource(this);// 加载assets中的资源对象
}
}
@Override
public AssetManager getAssets() {
if (this.mAppResources == null) {
return super.getAssets();
}
return this.mAppResources.getAssets();
}
}
public class BundleActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.bundle_layout);
findViewById(R.id.text_view).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_LONG).show();
}
});
}
@Override
public Resources getResources() {
return getApplication().getResources();
}
}
二、Resource分发:处理插件资源与宿主资源的处突
- 如果使用到的资源,插件和宿主都同时存在,则使用插件的资源;
- 如果使用到的资源只有插件有,则使用插件的;
- 如果使用到的资源只有宿主有的,则使用宿主的
AssetManager的addAssetPath()方法调用native层AssetManager对象的addAssetPath()方法,通过查看c++代码可以知道,该方法可以被调用多次,每次调用都会把对应资源添加起来,而后来添加的在使用资源是会被首先搜索到
可以怎么理解,C++层的AssetManager有一个存放资源的栈,每次调用addAssetPath()方法都会把资源对象压如栈,而在读取搜索资源时是从栈顶开始搜索,找不到就往下查
这样我们就可以通过修改 getBundleResource(Context context, String apkPath)
方法实现:
public static Resources getBundleResource(Context context){
String plugDexPath = Environment.getExternalStorageState() + "/myplug/plug1.apk";
String appDexPath = mContext.getApplicationContext().getPackageResourcePath();
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, appDexPath);//【先入栈】
addAssetPathMethod.invoke(assetManager, plugDexPath);//【后入栈】
Resources superRes = mContext.getResources();
return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
appDexPath为宿主apk路径,plugDexPath为插件apk路径,superRes为宿主资源,resources为融合插件与宿主的资源
2.1 插件和宿主的id冲突
当我们添加了一个资源(如在String.xml里添加了一个String),则系统会为我们在R里面为该资源生成一个int型的id与之对应,使用的时候是根据该id找到对应的资源。
**资源id是按照资源名称的字典顺序来递增的。**拿String来说。
<string name = "za">this is a</string>
<string name = "zb">this is b</string>
//则会在R里面生成相应的id,按照资源名称的字典顺序来递增
public static final int za=0x7f06004
public static final int za=0x7f06005
基于上面的观察,我们会发现一个问题:
宿主资源情况为:存在
za(id=0x7f060004)
zb(id=0x7f060005)
插件资源情况为:存在
za(id=0x7f060004)
zab(id=0x7f060005)
ab(0x7f060006)
这时候在宿主里获取资源zb,则根据上面所说,无论是插件R中的id数值
还是resources#getIdentifier()
, 最终都是会根据id=0x7f060005去查找资源
但是由于,插件的plugDexPath处于栈顶,获取到的就是插件中的zab,而不是宿主中的zb
然而实际上,此处zab和zb并不是我们想去替换的资源
要解决资源冲突,目前有很多插件化框架都提出了自己的解决方案:
- 隔离使用
- 我们在加载插件Activity时,只在当前上下文注入插件的资源,这样宿主和插件之间是完全隔离的,也就无所谓资源id冲突了
- 修改aapt,在插件中如果有添加新的资源,则其命名要安装字典排序在原有的资源下递增
- 在编译宿主时,手动指定宿主中所有资源的id,然后在编译插件时,通过在public.xml中设定padding,避免分配到宿主的资源id
- 修改aapt,插件和宿主的R资源的生成规则要不同
- 资源id是通过0xPPTTEEEE的形式指定的,如果在编译插件资源时,指定插件的packageId不是0x7f,而是指定的值,那么即使TTEEEE重复,也能保证整个资源id不重复
- 譬如,携程和small的前缀PP标示不同类型,后文会讲到
- 修改resource.arsc
- 和第三种是同样的原理,都是修改packageId,只是是从resources.arsc文件出发
三、Resource全局置换:确保插件和宿主使用到的是被修改过的资源
需要注意的是
在上边的使用中,我们都是显式的使用插件中的资源,主动的调用getBundleResource(Context context, String apkPath).getxxx(resId)
来获取相关资源
也就是说,当我们需要加载插件中的资源时,替换掉当前Context的ContextImpl中的Resource对象
那么一些系统的隐式调用,怎么去保证能去插件中寻找资源呢?
- 一些系统或自定义的Java代码中,通过Context.getResources获取
- 在xml文件(如布局文件)里指定资源
- 其实xml文件里最终也是通过Context来获取资源的只不过是他一般获取的是Resources里的AssetManager
这就需要运用到Resource全局置换。 目前来看,做法也分为以下两种:
3.1 [补丁法全局生效]hook ResourceManager
Small框架就是采用该方式
我们看获取到Resources后如何找到对应id的资源,在Resources中定位到getString(int id)方法:
@NonNull
public String getString(@StringRes int id) throws NotFoundException {
final CharSequence res = getText(id);
if (res != null) {
return res.toString();
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mAssets.getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
最终寻找资源的调用是有AssetManager来执行的,这个AssetManager是 ResourceManager#getTopLevelResources()
创建Resources时来的
然而,需要注意的是:getTopLevelResources()
中具备缓存逻辑
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
Resources r;
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
if (r != null && r.getAssets().isUpToDate()) {
return r;
}
AssetManager assets = new AssetManager();
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
r = new Resources(assets, dm, config, compatInfo, token);
WeakReference<Resources> wr = mActiveResources.get(key);
Resources existing = wr != null ? wr.get() : null;
if (existing != null && existing.getAssets().isUpToDate()) {
r.getAssets().close();
return existing;
}
mActiveResources.put(key, new WeakReference<Resources>(r));
return r;
}
也就是说,由于ResourceManager是一个单例类,并且持有了当前App的Resource缓存,那么我们直接在App启动时手动替换掉ResourceManager中的Resource缓存,就可以在当前App中添加插件的资源,并且全局有效
3.1.1 Small框架的实现
Small框架的资源加载流程在ApkBundleLauncher中完成,setup流程获取到所有插件so的信息,在postSetUp中获取所有插件包的资源路径,通过反射调用AssetManager的addAssetPaths方法,构造一个包含宿主包资源、系统资源和插件包资源的AssetManger。最后还是通过反射,使用包含所有资源的AssetManager替换掉ResourcesManager中Resources的AssetManger,最终达到加载插件中资源的目的
【图small】
其实关注Small的源码中ReflectAccelerator.ensureCacheResources,这个方法想要的达到的作用是当每次启动Activity时遍历系统缓存的ResourceImpl,将它的AssetManager替换成包含插件资源的AssetManager。当然这个机制只在SDK>=24时生效
- sMergedResourcesImpl 自己修改了的Resource
- sResourceImpls 原来的Resource
public static void ensureCacheResources() {
if (Build.VERSION.SDK_INT < 24) return;
if (sResourceImpls == null || sMergedResourcesImpl == null) return;
Set<?> resourceKeys = sResourceImpls.keySet();
for (Object resourceKey : resourceKeys) {
WeakReference resourceImpl = (WeakReference)sResourceImpls.get(resourceKey);
if (resourceImpl != null && resourceImpl.get() != sMergedResourcesImpl) {
// Sometimes? the weak reference for the key was released by what
// we can not find the cache resources we had merged before.
// And the system will recreate a new one which only build with host resources.
// So we needs to restore the cache. Fix #429.
// FIXME: we'd better to find the way to KEEP the weak reference.
sResourceImpls.put(resourceKey, new WeakReference<Object>(sMergedResourcesImpl));
}
}
}
3.2 [替换法调用处生效]hook 基础组件的生命周期函数
由于隐式调用都是依赖于Context对象内的Resource实例,我们可以在Context对象被创建后且还未使用时把它里面的Resources(mResources)替换为具备分发功能的Resource
现在问题就转化为:
- 需要hook哪些类?
- 需要hook这些类的那个方法,以保证在在Context对象被创建后且还未使用时替换
我们已知,整个应用的Context数目等于Application+Activity+Service的数目,Context会在这几个类创建对象的时候创建并添加进去。
而这些行为都是在ActivityTHread和Instrumentation里做的,我们以Activity为例,看下如何使用hook来替换Resource实例
3.2.1 Activity Context对象内的Resource实例置换
置换时机
【图android-resources2.png】
ActivityThread在接收到LAUNCH_ACTIVITY消息以后,在 performLaunchActivity方法中:
-
Context实例的创建
- 在ActivityThread里调用createBaseContextForActivity方法。并在创建Context过程中实例化AssetManger和Resources。
-
Activity实例的创建
- 在 ActivityThread 中 使用 Instrumentation#newActivity 通过反射的方式
-
Activity绑定Context
- 在ActivityThread里调用Activity对象的attach方法
ActivityThread在LAUNCH_ACTIVITY消息中,完成了Activity生命周期中的三个回调,分别是onCreate onStart onRestoreInstanceStat
- Activity的onCreate()方法的回调
- 在ActivityThread里调用
Instrumentation#callActivityOnCreate()
方法
- 在ActivityThread里调用
//ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//1. Context实例的创建
ContextImpl appContext = createBaseContextForActivity(r);
...
//2. Activity实例的创建
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
...
//3. Activity绑定Context
appContext.setOuterContext(activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
//4. Activity的onCreate()方法的回调
activity.mCalled = false;
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
}
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
}
//Instrumentation
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
替换掉Activity里Context里的Resources最好要早,基于上面的观察,我们可以在调用Instrumentation的callActivityOnCreate()方法时把Resources替换掉
我们如何控制callActivityOnCreate()方法的执行,这里又得使用hook的思想了,即把ActivityThread里面的Instrumentation对象(mInstrumentation)给替换掉,同样得使用反射
置换Instrumentation的callActivityOnCreate方法
- 获取ActivityThread对象
- ActivityThread里面有一个静态方法,该方法返回的是ActivityThread对象本身,所以我们可以调用该方法来获取ActivityTHread对象
- ActivityThread是被hide的,所以得通过反射来处理
- 获取ActivityThread里的Instrumentation对象
- 构建我们自己的Instrumentation对象,重写callActivityOnCreate方法,并设置回去
- 在callActivityOnCreate方法里要先获取当前Activity对象里的Context(mBase),再获取Context对象里的Resources(mResources)变量,在把mResources变量指向我们构造的Resources对象,做到移花接木
//ActivityThread
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}
//实现过程
public static void hookResource(Context context){
try{
//反射获取 ActivityThread
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object oldActivityThread = currentActivityThreadMethod.invoke(null);
//反射获取Instrumentation
Field mInstrumentationFiled = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationFiled.setAccessible(true);
Instrumentation oldInstrumentation= (Instrumentation) mInstrumentationFiled.get(oldActivityThread);
//替换Instrumentation
Instrumentation proxy = new MyInstrumentation(oldInstrumentation, context);
mInstrumentationFiled.set(oldActivityThread, proxy);
}catch(Exception e){
}
}
//替换 Context.Resouce
public class MyInstrumentation extends Instrumentation{
@override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
chageResource(activity, icicle);
super.callActivityOnCreate(activity, icicle);
}
private void chageResource(Activity activity, Bundle icicle){
Filed mBaseFiled = Activity.class.getSuperclass().getSuperclass().getDeclaredField("mBase");
mBaseFiled.setAccessible(true);
Context mbase = (Context) mBaseFiled.get(activity);
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Filed mResourcesFiled = contextImplClass.getDeclaredField("mResources");
mResourcesFiled.setAccessible(true);
//自己的
Resources myResources = MyPluginsUtils.getBundleResource(mbase);
//反射注入
mResourcesFiled.set(mbase, myResources);
}
}
四、开源框架的资源加载
如果想要做到插件化,需要了解Android资源文件的打包过程aapt,这样可以为每一个插件进行编号,然后按照规则生成R文件
4.1 携程DynamicAPK
按照下述规则生成对应的插件apk,插件的R文件按照如下规则:
- R文件为int型,前8位代表插件的Id,其中两个特殊的Id:Host是0x7f,android系统自带的是以0x01开头.
- 紧跟着的8位是区分资源类型的,比如layout,id,string,dimen等
- 后面16位是资源的编号
在运行时,我们可以写一个ResourceManager类(相当于一个分发类),它继承自Resource对象,然后所有的Activity,都将其context的mResource成员变量修改为ResourceManager类,然后Override其方法
在加载资源时,ResourceManager类会根据不同的id的前缀,查找对应插件的Resource或者主app资源id
4.2 Small框架
参考文献
- 宿主访问插件资源
- Android插件化之资源动态加载
- 插件化-资源处理
- Android插件化之资源加载
- DL拥有者csdnsingwhatiwanna