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 条评论
登录 后参与评论

相关文章

来自专栏AhDung

【C#】分享基于Win32 API的服务操作类(解决ManagedInstallerClass.InstallHelper不能带参数安装的问题)

------------------201508250915更新------------------

602
来自专栏机器学习算法与Python学习

Python:爬虫系列笔记(8) -- 爬去MM图片

转载于:静觅 » Python爬虫实战四之抓取淘宝MM照片 链接:http://cuiqingcai.com/1001.html 1.抓取淘宝MM的姓名,头像,...

3956
来自专栏散尽浮华

Linux服务器性能压力测试

对于新采购的服务器,需要进行有必要的性能测试。这里选择UnixBench工具进行性能测试。记录如下: 1)安装使用 下面的脚本使用了最新版UnixBench5....

2897
来自专栏微信终端开发团队的专栏

XCode基本使用及调试技巧

对于初学iOS开发的同学,了解了Objective-C的基本使用后,最关心的应该是如何把OC程序运行起来。由于Xcode的基本使用比较简单,所以本文着重介绍一些...

4627
来自专栏ml

C/C++ 如何来自动优雅的涮别银家的贴子

  被涮屏涮烦了,就分享一下如何用低调的c/c++来涮别人家的屏吧! 此处埋下三颗雷! 这不是啥新知识,也不是什么浅显的代码。下面,来淘淘这份经验,呼呼 我们要...

3415
来自专栏Flutter入门到实战

开发工具总结(5)之Markdown语法图文全面详解及其工具介绍

版权声明:本文为博主原创文章(少量文字参考他人博文,已加上引用说明),未经博主允许不得转载。https://www.jianshu.com/p/c0a2897a...

904
来自专栏非著名程序员

Android面试系列之应用内多语言切换

昨天组里的同事遇到一些切换多语言的细节问题,发现在Android N版本上配置应用内多语言没有生效,正好拿出来做为一个面试题讲解一下。 面试题:如何实现应用内多...

29010
来自专栏高性能服务器开发

windows完成端口(五)

系列目录 windows完成端口(一) windows完成端口(二) windows完成端口(三) windows完成端口(四) windows完成端口(五) ...

3537
来自专栏landv

实现用VB.Net/(C#)开发K/3 BOS 插件的真正可行方法

781
来自专栏不想当开发的产品不是好测试

jenkins 多选框

背景 jenkins自带的参数化不支持多选框,不过有插件支持:Extended Choice Parameter Plug-In 插件地址: https://p...

2219

扫码关注云+社区