(4.6.29.4)插件化之资源加载:使用插件中的R资源

文章目录

  • 一、主app中使用插件中资源
    • 1.1 创建新的Resource对象方式
      • 1.1.1 背景知识
        • 资源链
        • Resources对象
      • 1.1.3 实现思路
        • addAssetPath的反射调用
      • 1.1.4 具体实现
      • 1.1.5 加载离线apk中的字符串和Drawable资源
      • 1.1.6 加载离线apk中的layout资源
        • 直接替换当前context的mResources私有成员变量
      • 1.1.7 Application中Resources的加载
  • 二、Resource分发:处理插件资源与宿主资源的处突
    • 2.1 插件和宿主的id冲突
  • 三、Resource全局置换:确保插件和宿主使用到的是被修改过的资源
    • 3.1 [补丁法全局生效]hook ResourceManager
      • 3.1.1 Small框架的实现
    • 3.2 [替换法调用处生效]hook 基础组件的生命周期函数
      • 3.2.1 Activity Context对象内的Resource实例置换
        • 置换时机
        • 置换Instrumentation的callActivityOnCreate方法
  • 四、开源框架的资源加载
    • 4.1 携程DynamicAPK
    • 4.2 Small框架
  • 参考文献

  1. 宿主如何加载插件资源
    开发中宿主程序调起未安装的插件apk,一个很大的问题就是资源如何访问,这些资源文件的ID都映射在gen文件夹下的R.java中,而插件中凡是以R开头的资源都不能访问。究其原因是因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源

  2. 如何处理插件资源与宿主资源的处突
    插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找。这样能做到动态更新的效果。

  3. 如何确保插件和宿主使用到的是被修改过的资源。

一、主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全局唯一性

  1. mResources通过一系列的缓存或者成员实例引用,实现了全局唯一。
  2. 如果通过打补丁的方式,会全局生效(small替换了resourcemanager中瓜缓存的resource的assetmanager,实现全局替换)。
  3. 但是在插件化方案中,为了避免id冲突,有的使用的是替换法,那就是所有有缓存和成员实例的地方都要替换为我们自己的resource;

1.1 创建新的Resource对象方式

要想获得资源文件必须得到一个Resource对象,想要获得插件的资源文件,必须得到一个插件的Resource对象,好在android.content.res.AssetManager.java中包含一个私有方法addAssetPath。

只需要将apk的路径作为参数传入,就可以获得对应的AssetsManager对象,从而创建一个Resources对象,然后就可以从Resource对象中访问apk中的资源了

通过以上方法却是可以在宿主中获取到插件的资源文件,只是宿需要用到相关资源的时候需跟插件约定好对应名称,以防出现找不到的情况

1.1.1 背景知识

资源链

(4.6.29.4)插件化之资源加载:使用插件中的R资源_第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实例存储
  1. Resource的实例保存在ContextImpl中,每次构建ContextImpl时,会从LoadedApk中拿到对应的Resource
  2. Loadedapk中的getResource会有成员实例mResource,命中直接返回,没命中委托Activitythread—ResourceManager生成
  3. 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;
  • ResourcesManagergetTopLevelResources方法中创建的
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对象,那么自然就可以借助该对象获取插件中的资源了

  1. 新建一个AssetManager对象
  2. 通过反射调用addAssetPath方法
  3. 以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 具体实现

  1. 新建一个AssetManager对象
  2. 通过反射调用addAssetPath方法
  3. 以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());
    }
	
  • nuptboyzhb/AndroidPluginFramework-- github

述代码是加载离线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个:

    1. 创建一个自己的ContextImpl,Override其方法。
    1. 通过反射,直接替换当前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的方式

  1. 替换Application的Resource对象
  2. 插件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来说。

this is a
this is b

//则会在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并不是我们想去替换的资源

要解决资源冲突,目前有很多插件化框架都提出了自己的解决方案:

  1. 隔离使用
    • 我们在加载插件Activity时,只在当前上下文注入插件的资源,这样宿主和插件之间是完全隔离的,也就无所谓资源id冲突了
  2. 修改aapt,在插件中如果有添加新的资源,则其命名要安装字典排序在原有的资源下递增
    • 在编译宿主时,手动指定宿主中所有资源的id,然后在编译插件时,通过在public.xml中设定padding,避免分配到宿主的资源id
  3. 修改aapt,插件和宿主的R资源的生成规则要不同
    • 资源id是通过0xPPTTEEEE的形式指定的,如果在编译插件资源时,指定插件的packageId不是0x7f,而是指定的值,那么即使TTEEEE重复,也能保证整个资源id不重复
    • 譬如,携程和small的前缀PP标示不同类型,后文会讲到
  4. 修改resource.arsc
    • 和第三种是同样的原理,都是修改packageId,只是是从resources.arsc文件出发

三、Resource全局置换:确保插件和宿主使用到的是被修改过的资源

需要注意的是

在上边的使用中,我们都是显式的使用插件中的资源,主动的调用getBundleResource(Context context, String apkPath).getxxx(resId)来获取相关资源

也就是说,当我们需要加载插件中的资源时,替换掉当前Context的ContextImpl中的Resource对象

那么一些系统的隐式调用,怎么去保证能去插件中寻找资源呢?

  1. 一些系统或自定义的Java代码中,通过Context.getResources获取
  2. 在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 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 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(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,最终达到加载插件中资源的目的

(4.6.29.4)插件化之资源加载:使用插件中的R资源_第2张图片
【图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(sMergedResourcesImpl));
        }
    }
}
 
  

3.2 [替换法调用处生效]hook 基础组件的生命周期函数

由于隐式调用都是依赖于Context对象内的Resource实例,我们可以在Context对象被创建后且还未使用时把它里面的Resources(mResources)替换为具备分发功能的Resource

现在问题就转化为:

  1. 需要hook哪些类?
  2. 需要hook这些类的那个方法,以保证在在Context对象被创建后且还未使用时替换

我们已知,整个应用的Context数目等于Application+Activity+Service的数目,Context会在这几个类创建对象的时候创建并添加进去。

而这些行为都是在ActivityTHread和Instrumentation里做的,我们以Activity为例,看下如何使用hook来替换Resource实例

3.2.1 Activity Context对象内的Resource实例置换

置换时机

  • ActivityThread
  • Instrumentation

(4.6.29.4)插件化之资源加载:使用插件中的R资源_第3张图片
【图android-resources2.png】

ActivityThread在接收到LAUNCH_ACTIVITY消息以后,在 performLaunchActivity方法中:

  1. Context实例的创建

    • 在ActivityThread里调用createBaseContextForActivity方法。并在创建Context过程中实例化AssetManger和Resources。
  2. Activity实例的创建

    • 在 ActivityThread 中 使用 Instrumentation#newActivity 通过反射的方式
  3. Activity绑定Context

    • 在ActivityThread里调用Activity对象的attach方法

ActivityThread在LAUNCH_ACTIVITY消息中,完成了Activity生命周期中的三个回调,分别是onCreate onStart onRestoreInstanceStat

  1. Activity的onCreate()方法的回调
    • 在ActivityThread里调用Instrumentation#callActivityOnCreate()方法
	//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文件按照如下规则:

  1. R文件为int型,前8位代表插件的Id,其中两个特殊的Id:Host是0x7f,android系统自带的是以0x01开头.
  2. 紧跟着的8位是区分资源类型的,比如layout,id,string,dimen等
  3. 后面16位是资源的编号

在运行时,我们可以写一个ResourceManager类(相当于一个分发类),它继承自Resource对象,然后所有的Activity,都将其context的mResource成员变量修改为ResourceManager类,然后Override其方法

在加载资源时,ResourceManager类会根据不同的id的前缀,查找对应插件的Resource或者主app资源id

4.2 Small框架

  • Android插件化之资源加载

参考文献

  • 宿主访问插件资源
    • Android插件化学习之路(四)之使用插件中的R资源
    • Android插件化(三)加载插件apk中的Resource资源
      • nuptboyzhb/AndroidPluginFramework-- github
    • Android插件化开发 第三篇 [加载插件资源]
    • 插件化技术:宿主访问插件资源
    • 详解Android插件化开发-资源访问
    • 简书H3c:Android插件化开发 第四篇 [加载插件Activity]
  • Android插件化之资源动态加载
  • 插件化-资源处理
  • Android插件化之资源加载
  • DL拥有者csdnsingwhatiwanna
    • Android apk动态加载机制的研究(二):资源加载和activity生命周期管理

你可能感兴趣的:(4.6-android进阶)