Android插件化——资源加载

前言

资源,是APK包体积过大的病因之一。插件化技术将模块解耦,通过插件的形式加载。插件化技术中,每个插件都能够作为单独的APK独立运行。宿主启动插件的类,难免要涉及插件类中的资源问题。

那么,如何加载插件资源,就成为一个待解决的问题。

原理

参考APK打包流程:Android插件化基础-APK打包流程

Android工程在打包成apk时,会使用aapt将工程中的资源名与id在R.java中一一映射起来。

R.java

    public static final int ic_launcher=0x7f060054;
    public static final int ic_launcher_background=0x7f060055;
    public static final int ic_launcher_foreground=0x7f060056;
    public static final int notification_action_background=0x7f060057;

我们每次加载资源时,先要获取Resources。然后通过:

Drawable drawable = resources.getDrawable(resId);

获取对应的资源。

因此,我们的核心思路就是:获取插件的Resources和插件的resId

实践

那么我们该如何获得插件的Resources呢?

ContextThemeWrapper.java

    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

Resources.java是App资源的管理类。

    /**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

通过注释我们可以清晰的看到,真正让Resources与众不同的是AssetManager

我们注意到这个构造方法在PackageParser#parseBaseApk方法中调用。在此我们可以想到,我们是不是可以仿照Apk的安装过程,为一个未安装的Apk创建一个Resources呢?

因此,我们继续看AssetManager.java

/**
 * Provides access to an application's raw asset files; see {@link Resources}
 * for the way most applications will want to retrieve their resource data.
 * This class presents a lower-level API that allows you to open and read raw
 * files that have been bundled with the application as a simple stream of
 * bytes.
 */
public final class AssetManager implements AutoCloseable {
...
}

通过注释我们可以看到,这个类提供了我们访问资源文件的方式。读一下源码,可以找到一个方法:

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);
    }

通过注释我们可以看到,这个方法提供了包装ZIP文件的方法。注释中说这不是用于Applications。但我们知道APK文件其实就是ZIP文件。

看到这里我们的思路就有了。通过这个方法,我们将插件APK的path传入,包装一个AssetManager。然后用AssetManager生成Resources,那么这个Resources就是插件的Resources。虽然插件APK并未安装,但我们仿照了安装的流程。

通过上面的分析,我们能够得到一个获取插件Resources的方法:

/**
 * 获取对应插件的Resource对象
 * @param context 宿主apk的上下文
 * @param pluginPath 插件apk的路径,带apk名
 * @return
 */
public static Resources getPluginResources(Context context, String pluginPath) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        // 反射调用方法addAssetPath(String path)
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        // 将插件Apk文件添加进AssetManager
        addAssetPath.invoke(assetManager, pluginPath);
        // 获取宿主apk的Resources对象
        Resources superRes = context.getResources();
        // 获取插件apk的Resources对象
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

至此我们获得了插件的Resources,我们通过获取资源,如Drawable的方法是:

Drawable drawable = resources.getDrawable(resId);

因此,我们还缺一个resId,即插件资源在插件R.java中对应的id。

我们可以通过反射的方式,获取R.java中的id:

    /**
     * 加载apk获得内部资源id
     *
     * @param context 宿主上下文
     * @param pluginPath apk路径
     */
    public static int getResId(Context context, String pluginPath, String apkPackageName, String resName) {
        try {
            //在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建
            File optimizedDirectoryFile = context.getDir("dex", Context.MODE_PRIVATE);
            // 构建插件的DexClassLoader类加载器,参数:
            // 1、包含dex的apk文件或jar文件的路径,
            // 2、apk、jar解压缩生成dex存储的目录,
            // 3、本地library库目录,一般为null,
            // 4、父ClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
            //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
            Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
            Field field = clazz.getDeclaredField(resName);//得到名为resName的这张图片字段
            return field.getInt(R.id.class);//得到图片id
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

完成Resources和resId的获取后,我们就可以获取插件的资源了:

                    int resId = getResId(MainActivity.this.getApplication(), PATH, PLUGIN_PACKAGE_NAME, "ic_launcher");
                    Resources resources = getPluginResources(MainActivity.this.getApplication(), PATH);
                    Drawable drawable = resources.getDrawable(resId);
                    mIvTest.setImageDrawable(drawable);

至此,就是插件化加载的资源的基本思路和原理。

总结

  1. 明确思路,通过获取插件的Resources和resId来加载资源
  2. 通过仿APK解析过程,获取插件Resources
  3. 通过对插件的R.java的反射,获取resId
  4. 完成加载

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android机动车

用代码手把手教你使用MVVM

网上关于MVVM框架的搭建和使用的文章很少,大多提到MVVM框架,就是在介绍DataBinding的使用。对于MVVM中各模块之间如何划分,如何定义,又是如何配...

1112
来自专栏KK的小酒馆

Service的跨进程开发Android开发高级进阶

Service的跨进程通信主要由两种Android提供的方法进行,一个是AIDL,通过创建一个AIDL文件来完成,另一个是利用Messenger,发送Messa...

1072
来自专栏静默虚空的博客

NSIS 打包脚本基础

简介 NSIS(Nullsoft Scriptable Install System)是一个开源的 Windows 系统下安装程序制作程序。它提供了安装、卸载、...

4176
来自专栏7号代码

Android网络与数据存储——File存储(实现SD卡文件浏览器)

AndroidManifest.xml中manifest标签下有一个属性android:installLocation,用于指定应用程序安装在什么地方,该属性有...

2273
来自专栏码农笔录

Android全能开源项目xUtils3开发教程、简单封装

1422
来自专栏yang0range

MVVM框架的搭建(二)——项目搭建

介绍完背景以及初衷之后,我们开始搭建MVVM的框架,这一部分我们进行简单的搭建,了解MVVM架构的基本结构。

1102
来自专栏酷玩时刻

统一管理项目中的接口回调

无论是 Java Web 开发还是 Android 开发我们都避免不了异步处理业务的逻辑。有异步的出现一定就有异步结果(接口)的回调。你之前是不是有为每一个异步...

874
来自专栏Android机动车

Retrofit实现带进度下载

Retrofit是目前最主流的网络框架了,它对网络请求几近完美的封装,大大降低了我们开发者的研发难度,缩短了研发周期。最近项目中遇到了下载视频和图片文件的需求(...

992
来自专栏增长技术

Android文件存储使用

一般地,通过 Context 和 Environment 相关的方法获取文件存取的路径。

1182
来自专栏图像识别与深度学习

《Android》Lesson02-第1个Project,Log

18812

扫码关注云+社区