Android插件化——資源加載

前言

資源,是APK包體積過(guò)大的病因之一。插件化技術(shù)將模塊解耦,通過(guò)插件的形式加載。插件化技術(shù)中,每個(gè)插件都能夠作為單獨(dú)的APK獨(dú)立運(yùn)行。宿主啟動(dòng)插件的類,難免要涉及插件類中的資源問(wèn)題。

那么,如何加載插件資源,就成為一個(gè)待解決的問(wèn)題。

原理

參考APK打包流程:Android插件化基礎(chǔ)-APK打包流程

Android工程在打包成apk時(shí),會(huì)使用aapt將工程中的資源名與id在R.java中一一映射起來(lái)。

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;

我們每次加載資源時(shí),先要獲取Resources。然后通過(guò):

Drawable drawable = resources.getDrawable(resId);

獲取對(duì)應(yīng)的資源。

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

實(shí)踐

那么我們?cè)撊绾潍@得插件的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());
    }

通過(guò)注釋我們可以清晰的看到,真正讓Resources與眾不同的是AssetManager。

我們注意到這個(gè)構(gòu)造方法在PackageParser#parseBaseApk方法中調(diào)用。在此我們可以想到,我們是不是可以仿照Apk的安裝過(guò)程,為一個(gè)未安裝的Apk創(chuàng)建一個(gè)Resources呢?

因此,我們繼續(xù)看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 {
...
}

通過(guò)注釋我們可以看到,這個(gè)類提供了我們?cè)L問(wèn)資源文件的方式。讀一下源碼,可以找到一個(gè)方法:

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

通過(guò)注釋我們可以看到,這個(gè)方法提供了包裝ZIP文件的方法。注釋中說(shuō)這不是用于Applications。但我們知道APK文件其實(shí)就是ZIP文件。

看到這里我們的思路就有了。通過(guò)這個(gè)方法,我們將插件APK的path傳入,包裝一個(gè)AssetManager。然后用AssetManager生成Resources,那么這個(gè)Resources就是插件的Resources。雖然插件APK并未安裝,但我們仿照了安裝的流程。

通過(guò)上面的分析,我們能夠得到一個(gè)獲取插件Resources的方法:

/**
 * 獲取對(duì)應(yīng)插件的Resource對(duì)象
 * @param context 宿主apk的上下文
 * @param pluginPath 插件apk的路徑,帶apk名
 * @return
 */
public static Resources getPluginResources(Context context, String pluginPath) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        // 反射調(diào)用方法addAssetPath(String path)
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        // 將插件Apk文件添加進(jìn)AssetManager
        addAssetPath.invoke(assetManager, pluginPath);
        // 獲取宿主apk的Resources對(duì)象
        Resources superRes = context.getResources();
        // 獲取插件apk的Resources對(duì)象
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

至此我們獲得了插件的Resources,我們通過(guò)獲取資源,如Drawable的方法是:

Drawable drawable = resources.getDrawable(resId);

因此,我們還缺一個(gè)resId,即插件資源在插件R.java中對(duì)應(yīng)的id。

我們可以通過(guò)反射的方式,獲取R.java中的id:

    /**
     * 加載apk獲得內(nèi)部資源id
     *
     * @param context 宿主上下文
     * @param pluginPath apk路徑
     */
    public static int getResId(Context context, String pluginPath, String apkPackageName, String resName) {
        try {
            //在應(yīng)用安裝目錄下創(chuàng)建一個(gè)名為app_dex文件夾目錄,如果已經(jīng)存在則不創(chuàng)建
            File optimizedDirectoryFile = context.getDir("dex", Context.MODE_PRIVATE);
            // 構(gòu)建插件的DexClassLoader類加載器,參數(shù):
            // 1、包含dex的apk文件或jar文件的路徑,
            // 2、apk、jar解壓縮生成dex存儲(chǔ)的目錄,
            // 3、本地library庫(kù)目錄,一般為null,
            // 4、父ClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
            //通過(guò)使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源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);

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

總結(jié)

  1. 明確思路,通過(guò)獲取插件的Resources和resId來(lái)加載資源
  2. 通過(guò)仿APK解析過(guò)程,獲取插件Resources
  3. 通過(guò)對(duì)插件的R.java的反射,獲取resId
  4. 完成加載
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 是時(shí)候來(lái)一波Android插件化了 是時(shí)候來(lái)一波Android插件化了前言Android開(kāi)發(fā)演進(jìn)模塊化介紹插件化介...
    流水不腐小夏閱讀 4,934評(píng)論 3 51
  • 插件化-資源處理 寫的比較長(zhǎng),可以選擇跳過(guò)前面2節(jié),直接從0x03實(shí)例分析開(kāi)始。如有錯(cuò)誤,請(qǐng)不吝指正。 0x00 ...
    唐一川閱讀 5,809評(píng)論 2 22
  • 動(dòng)態(tài)加載技術(shù) 介紹 在程序運(yùn)行的時(shí)候,加載一些程序自身原本不存在的可執(zhí)行文件并運(yùn)行這些文件里的代碼邏輯。 動(dòng)態(tài)調(diào)用...
    冰點(diǎn)k閱讀 4,219評(píng)論 1 11
  • 前段時(shí)間中開(kāi)發(fā)中遇到把美元轉(zhuǎn)成千分位顯示的需求,封裝了個(gè)方法解決這個(gè)問(wèn)題 用到的宏 轉(zhuǎn)換方法
    Exia_L閱讀 3,161評(píng)論 0 3

友情鏈接更多精彩內(nèi)容