文章目录

  • 1. 前言
  • 2. 分析
  • 3. 加载外部资源文件代码
  • 4. References


1. 前言

在上篇Android插件化开发指南——Hook技术(一)【长文】中提到最终的效果其实在插件中的MainActivity加载的资源文件activity_main.xml其实加载的还是宿主appactivity_main.xml文件。所以在这篇中将解决如何从插件apk中加载资源文件的问题。首先我们需要知道资源存储在apk包的什么位置,不妨在AS中打开插件的apk文件,可以看见其文件结构为:


android butterknife studio 插件 android插件化开发指南_Hook技术

也就是在resources.arse文件中。不妨来看看在Android中是如何加载资源的。

2. 分析

比如下面从strings.xml文件中获取值:

// MainActivity.java
String string = getString(R.string.app_name);

就来看看这个方法的背后是怎么实现的。追踪可以看到:

// Context.java
public final String getString(@StringRes int resId) {
    return getResources().getString(resId);
}

也就是说是通过context上下文对象的getResources方法,然后再通过getString来得到的。换句话说,在上下文context调用getResources方法后,就持有了资源本身,所以才可以通过getString来得到。为了验证,这里不妨先追踪下getString()方法:

// Resources.java
private ResourcesImpl mResourcesImpl;

public String getString(@StringRes int id) throws Resources.NotFoundException {
    return getText(id).toString();
}

public CharSequence getText(@StringRes int id) throws Resources.NotFoundException {
    CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
    if (res != null) {
        return res;
    }
    throw new Resources.NotFoundException("String resource ID #0x"
            + Integer.toHexString(id));
}

最终通过mResourcesImpl.getAssets()来得到一个AssetManager对象,然后再通过AssetManager对象来获取到资源。所以说这里其实流程为:

通过context来获取到资源对象Resources,然后在资源对象Resources中,通过ResourcesImpl类的实例对象来获取到AssetManager对象,然后再获取到资源对象。

所以如果我们可以仿写一个得到我们自己资源的Resources,并把他赋值给当前contextgetResources()方法,那么就可以做到资源的替换。那么思路为在App中定义一个继承自Application的父类,在这个方法中重写getResources()方法,如果调用getResources()方法能够获取到我们自定义的插件的资源,就直接返回;如果获取不到那么就使用当前应用自己的getResources()方法。也就是这里的重点在于如何仿写一个获取到插件Resources对象的包装类。

在前面提到了,插件资源文件位于resources.arse文件中。所以说如果我们需要加载插件中的资源文件,类似的还是需要从apk文件中读取。我们知道要得到Resources对象,首先需要封装一个AssetManager对象,所以这里看看AssetManager.java的实现。当然首先需要解决的问题是如何通过反射来获取到这个对象,在这个类中提供了一个加载外部资源文件的方法:

/**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    return  addAssetPathInternal(path, false);
}

所以这里也是通过反射这个方法来加载外部资源对象。比如:

private static String pluginPath = "/sdcard/plugin-debug.apk";

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, pluginPath);

因为在getResources()方法返回的是一个Resources对象,所以这里继续查看Resources.java类中的和资源文件关联的方法。可以看到这么一个方法:

@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
    this(null);
    mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}

故而尝试使用下面的代码:

resources = new Resources(assetManager, context.getResources().getDisplayMetrics(),
        context.getResources().getConfiguration());

3. 加载外部资源文件代码

public static Resources loadPluginResource(Context context){
    Resources resources = null;
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
        addAssetPath.setAccessible(true);
        addAssetPath.invoke(assetManager, pluginPath);

        resources = new Resources(assetManager, context.getResources().getDisplayMetrics(),
                context.getResources().getConfiguration());
    }catch (Exception e){
        e.printStackTrace();
    }

    return resources;
}

为了能满足资源文件要么找自己应用程序的资源文件,要么找外部插件中的资源文件的逻辑,这里构建一个BaseApplication

public class BaseApplication extends Application {

    private static final String pluginPath = "/sdcard/plugin-debug.apk";
    private Resources pluginResources;

    @Override
    public void onCreate() {
        super.onCreate();

        LoadUtils.loadClass(this, pluginPath); // 原init方法,修改了名字
        HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);
        HookAMSUtils.hookActivityThreadToLaunchActivity();
        pluginResources = LoadUtils.loadPluginResource(this, pluginPath);
    }

    @Override
    public Resources getResources() {
        if (pluginResources != null) return pluginResources;
        return super.getResources();
    }
}

然后配置一下清单文件:

android:name="BaseApplication"

当然因为写代码是在Activity中,所以这里还需要定义一个BaseActivity,在这个类的getResources方法中调用BaseApplicationgetResources方法,即:

public class BaseActivity extends AppCompatActivity {

    @Override
    public Resources getResources() {
        if(getApplication() != null && getApplication().getResources() != null)
            return getApplication().getResources();
        return super.getResources();
    }
}

案例地址:https://github.com/baiyazi/pluginLearn/tree/main/demo1


4. References

References

  • Android插件化开发指南——Hook技术(一)【长文】
  • AssetManager.java
  • 29讲玩转插件化:深入底层分析Android插件化原理,从0到1手写实现360插件化项目架构