Android熱修復技術(shù)初探(三):動態(tài)加載外部資源

前面已經(jīng)介紹了Android平臺上的幾種ClassLoader椎瘟,這幾種ClassLoader都有各自的使用場景,有了這些基礎(chǔ)知識之后濒持,才能更好地理解以及探究Android熱修復技術(shù)旭旭。首先我們來探究怎么動態(tài)加載外部資源。

1. 動態(tài)加載外部資源

在Android中纹烹,資源文件一般指定義在res資源文件夾中的各種文件,常用到的有字符串資源strings.xml召边、顏色資源colors.xml铺呵、drawable文件等。動態(tài)加載外部資源的目標隧熙,是從一個外部的apk文件中加載資源文件片挂,該apk文件可以是從網(wǎng)絡(luò)下載的,可以是存在于手機存儲目錄中的等等贞盯。

可以想象這樣一種使用場景音念,當你的APP需要具有換膚功能,用戶只需要下載符合你規(guī)范的apk文件(包含皮膚的資源圖片文件等)躏敢,使用動態(tài)加載資源的方式闷愤,加載你下載的apk文件中的資源圖片文件,就能輕松實現(xiàn)換膚功能件余,這樣用戶不需要升級APP讥脐,只需要下載他喜歡的皮膚apk文件就可以了,極大地提高了應用的靈活性啼器。

2. 實現(xiàn)思路

PathClassLoader只能加載手機里已經(jīng)安裝的apk文件旬渠,只有DexClassLoader能加載任意目錄(有讀寫權(quán)限)的apk文件。所以我們考慮先使用DexClassLoader來加載外部的apk文件镀首,再通過該ClassLoader去加載特定的類坟漱,最后通過反射來調(diào)用類里的方法,從而獲取外部資源

3. 實現(xiàn)案例

首先更哄,我們需要有2個工程:一個是宿主工程芋齿,用來加載外部資源;另一個是插件工程成翩,用來提供外部資源觅捆。

3.1 插件工程

我們定義一個字符串資源、一個顏色資源麻敌、一個圖片資源栅炒,然后創(chuàng)建一個類來讀取這些資源。

  1. 字符串資源定義
<string name="content_plugin">插件APK資源里的文本內(nèi)容</string>
  1. 顏色資源定義
<color name="color_from_plugin">#66</color>
  1. 在圖片文件夾里放一個名為test.png的圖片
  2. 創(chuàng)建讀取資源文件的類及方法
package com.hjy.plugin;
import android.content.Context;
import android.graphics.drawable.Drawable;

public class Utils {

    /**
     * 直接返回文本字符串
     *
     * @return
     */
    public static String getTextFromPlugin() {
        return "插件APK類里的文本內(nèi)容";
    }

    /**
     * 讀取資源文件里的文本字符串
     *
     * @param context
     * @return
     */
    public static String getTextFromPluginRes(Context context) {
        return context.getResources().getString(R.string.content_plugin);
    }

    public static Drawable getDrawableFromPlugin(Context context) {
        return context.getResources().getDrawable(R.mipmap.test);
    }

    public static int getColorFromPlugin(Context context) {
        return context.getResources().getColor(R.color.color_from_plugin);
    }

}

該類提供了幾個靜態(tài)方法,分別來讀取包里的字符串赢赊、顏色乙漓、圖片。

編譯好該插件工程后释移,我們將生成的apk文件命名為plugin-debug.apk叭披,將該apk文件復制到手機SD卡根目錄,可使用命令"adb push plugin-debug.apk /mnt/sdcard/plugin-debug.apk
"
玩讳。不一定要放到SD卡根目錄涩蜘,可以是手機上的任何存儲目錄,只要具有讀寫權(quán)限即可熏纯,我這里只是為了演示方便而已同诫,下面都將以該目錄為準。

3.2 宿主工程

我們創(chuàng)建一個宿主工程樟澜,加載插件工程生成的apk文件误窖,并顯示出插件里的資源。

public class MainActivity extends AppCompatActivity {

    private Button mBtnTest;
    private TextView mTvText1;
    private TextView mTvText2;
    private ImageView mIvImg;

    private DexClassLoader mCustomClassLoader;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnTest = findViewById(R.id.btn_test);
        mTvText1 = findViewById(R.id.tv_text1);
        mTvText2 = findViewById(R.id.tv_text2);
        mIvImg = findViewById(R.id.iv_image);

        //優(yōu)化后的dex文件輸出目錄秩贰,應用必須具備讀寫權(quán)限
        String optimizedDirectory = getDir("dex", MODE_PRIVATE).getAbsolutePath();
        mCustomClassLoader = new DexClassLoader("/mnt/sdcard/plugin-debug.apk", optimizedDirectory, null, getClassLoader());

        mBtnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadResFromPluginApk();
            }
        });
    }

    private void loadResFromPluginApk() {
        try {
            Class clazz = mCustomClassLoader.loadClass("com.hjy.plugin.Utils");

            //加載插件里類中定義的字符串資源
            Method method = clazz.getMethod("getTextFromPlugin", new Class[]{});
            String text = (String) method.invoke(null);
            mTvText1.setText(text);

            //加載插件里的字符串資源
            method = clazz.getMethod("getTextFromPluginRes", Context.class);
            text = (String) method.invoke(null, MainActivity.this);
            mTvText2.setText(text);

            //加載插件里的顏色資源
            method = clazz.getMethod("getColorFromPlugin", Context.class);
            int color = (int) method.invoke(null, MainActivity.this);
            mTvText2.setTextColor(color);

            //加載插件里的圖片資源
            method = clazz.getMethod("getDrawableFromPlugin", Context.class);
            Drawable drawable = (Drawable) method.invoke(null, MainActivity.this);
            mIvImg.setImageDrawable(drawable);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

代碼很簡單贩猎,就是自己構(gòu)造了一個DexClassLoader對象,通過該ClassLoader去加載插件里Utils類萍膛,然后通過反射調(diào)用Utils類里的各個方法。其中/mnt/sdcard/plugin-debug.apk對應的就是插件apk在手機中的存儲地址嚷堡,根據(jù)實際情況而定蝗罗。

3.3 執(zhí)行效果

我們先運行插件工程,將插件apk傳入手機里面蝌戒。然后再運行宿主工程串塑,點擊測試按鈕開始動態(tài)加載資源。很遺憾的是北苟,這并沒有達到我們的預期效果桩匪,你只會看到第一個TextView有文本顯示,其內(nèi)容為"插件APK類里的文本內(nèi)容"友鼻,第二個TextView顯示的文本并不是插件工程里定義的傻昙,第三個ImageView的內(nèi)容為空,并且控制臺可以看到拋出了android.content.res.Resources$NotFoundException異常彩扔,也就是資源未找到妆档。

3.4 異常分析

從執(zhí)行結(jié)果中可以看到,在宿主工程中反射調(diào)用Utils類的方法時虫碉,只有第一個方法返回成功贾惦,后面幾個方法執(zhí)行都出現(xiàn)異常,到這里是不是有點沮喪了,第一個方法能正確返回內(nèi)容须板,這說明插件apk已經(jīng)被正確的加載了碰镜,但是為什么后面的幾個都失敗了呢?

別急习瑰,我們來看看第一個方法與其他的有什么差別绪颖。第一個方法為getTextFromPlugin(),沒帶任何參數(shù)杰刽,直接返回的是一個固定的字符串菠发,第二個方法為getTextFromPluginRes(Context context),帶有一個參數(shù)Context贺嫂,通過Context去獲取資源滓鸠,由此我們斷定問題是不是就出在這里。

在Android中第喳,apk中的資源都是通過Resources對象來獲取的糜俗,我們在反射調(diào)用后面幾個方法時,Context參數(shù)傳入的是MainActivity.this曲饱,這個是宿主工程的Context悠抹,因此加載插件apk資源用的實際是宿主的Resources對象,但是宿主的Resources對象目前并不能訪問插件apk的資源扩淀,所以會出現(xiàn)資源找不到的異常楔敌。

4. 訪問外部資源的正確姿勢

上面這個例子中可以分析出,從宿主工程中的Context對象獲取到的Resources對象驻谆,無法加載插件apk中的資源文件卵凑,只需要解決該問題,那么我們的動態(tài)加載資源就大功告成了胜臊。

通過Context.getResources()方法勺卢,可以獲取到Resources對象,所以我們需要重寫宿主工程的getResources()方法象对,重新創(chuàng)建一個能讀取插件apk資源的Resources對象黑忱,在宿主工程的MainActivity類中,需要完善的代碼如下:

    /**
     * 1.重新創(chuàng)建一個AssetManager資源管理器勒魔,通過反射調(diào)用addAssetPath()方法甫煞,可以加載插件apk中的資源。
     * <br/>
     * 2.依賴第一步創(chuàng)建的AssetManager冠绢,重新創(chuàng)建一個Resources對象危虱,該Resources對象包含了插件apk中的資源。
     * <br/>
     * 3.插件apk中的資源是通過Context.getResources()來獲取的唐全,因此需要重寫Context的getResources()方法埃跷,返回前面創(chuàng)建的Resources對象蕊玷。
     * 
     * @param dexPath 插件路徑
     */
    protected void loadPluginResource(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
            method.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }

        Resources resource = getResources();
        mResources = new Resources(mAssetManager, resource.getDisplayMetrics(), resource.getConfiguration());

        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        return mAssetManager != null ? mAssetManager : super.getAssets();
    }

    @Override
    public Resources getResources() {
        return mResources != null ? mResources : super.getResources();
    }

    @Override
    public Resources.Theme getTheme() {
        return mTheme != null ? mTheme : super.getTheme();
    }

在onCreate()中加入初始化代碼:

loadPluginResource("/mnt/sdcard/plugin-debug.apk");

這里的關(guān)鍵代碼是用了AssetManager的addAssetPath()方法,這是一個隱藏的方法弥雹,所以需要采用反射來調(diào)用垃帅。重新運行宿主工程,一切OK剪勿,插件apk中的字符串贸诚、顏色、圖片都能正確加載了厕吉,動態(tài)加載資源到此就初步完成了酱固。

5. 其他問題

5.1 宿主工程能正確加載自己工程里的資源嗎?

答案是否定的,原因是宿主工程MainActivity類中的Resources對象是我們新建的头朱,它只綁定了插件apk中的資源运悲,可以寫段測試代碼試試看:

System.out.println(getString(R.string.app_name));

你會發(fā)現(xiàn)打印出來的是插件apk的app_name,訪問本工程其他的資源文件也會出現(xiàn)異常项钮。到這里是不是很頭疼班眯,本來以為能動態(tài)加載外部apk的資源文件了,結(jié)果發(fā)現(xiàn)本工程的資源文件無法正常加載烁巫,本末倒置了署隘,那怎么解決這個問題呢?既然我們知道資源文件是通過Resources對象來加載亚隙,那我們只需要在插件工程里磁餐,將Context參數(shù)改成Resources,然后在宿主工程反射調(diào)用插件apk的方法時阿弃,只傳入自己構(gòu)造的Resources參數(shù)即可崖媚,完全沒必要重寫宿主工程MainActivity類里的getResources()方法,這樣避免了宿主工程原本的Resources被污染破壞恤浪。

5.2 通過反射獲取插件工程的資源id

我們這個例子中,插件工程的幾個方法是獲取固定的資源文件肴楷,如果有很多資源文件水由,那豈不是要寫很多對應的方法,這顯然不是我們想要的赛蔫,同樣我們可以通過反射來獲取資源的id砂客,這要宿主工程調(diào)用插件工程的方法時,只需要傳入資源名稱即可呵恢。

    /**
     * 通過資源名反射獲取資源id
     * 
     * @param pkgName 包名
     * @param type 資源類型鞠值,如:string, mipmap, drawable等
     * @param resName 資源名稱
     * @return 資源id
     */
    private int getResId(String pkgName, String type, String resName) {
        //構(gòu)造R文件內(nèi)部類的類名
        String className = pkgName + ".R$" + type;
        try {
            Class clazz = mCustomClassLoader.loadClass(className);
            Field field = clazz.getField(resName);
            field.setAccessible(true);
            Integer id = (Integer) field.get(null);
            return id;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return 0;
    }

通過反射獲取插件apk里的字符串資源content_plugin,代碼如下:

int resId = getResId("com.hjy.plugin","string", "content_plugin");
System.out.println(mResources.getString(resId));

這樣是不是靈活了很多渗钉。

6. 小結(jié)

本文只是初步探究了怎么去動態(tài)加載外部資源彤恶,但這是管中窺豹钞钙,有很多問題還沒有解決,不過當了解這些之后声离,談到這些話題的時候就不會覺得那么高深莫測了芒炼。

參考文章

Android應用程序資源管理器(Asset Manager)的創(chuàng)建過程分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市术徊,隨后出現(xiàn)的幾起案子本刽,更是在濱河造成了極大的恐慌,老刑警劉巖赠涮,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件子寓,死亡現(xiàn)場離奇詭異,居然都是意外死亡笋除,警方通過查閱死者的電腦和手機斜友,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來株憾,“玉大人蝙寨,你說我怎么就攤上這事∴拖梗” “怎么了墙歪?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贝奇。 經(jīng)常有香客問我虹菲,道長,這世上最難降的妖魔是什么掉瞳? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任毕源,我火速辦了婚禮,結(jié)果婚禮上陕习,老公的妹妹穿的比我還像新娘霎褐。我一直安慰自己,他們只是感情好该镣,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布冻璃。 她就那樣靜靜地躺著,像睡著了一般损合。 火紅的嫁衣襯著肌膚如雪省艳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天嫁审,我揣著相機與錄音跋炕,去河邊找鬼。 笑死律适,一個胖子當著我的面吹牛辐烂,可吹牛的內(nèi)容都是我干的遏插。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼棉圈,長吁一口氣:“原來是場噩夢啊……” “哼涩堤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起分瘾,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤胎围,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后德召,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體白魂,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年上岗,在試婚紗的時候發(fā)現(xiàn)自己被綠了福荸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡肴掷,死狀恐怖敬锐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呆瞻,我是刑警寧澤台夺,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站痴脾,受9級特大地震影響颤介,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赞赖,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一滚朵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧前域,春花似錦辕近、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至年堆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盏浇,已是汗流浹背变丧。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绢掰,地道東北人痒蓬。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓童擎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親攻晒。 傳聞我的和親對象是個殘疾皇子顾复,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容