動(dòng)態(tài)加載插件apk中的資源

場(chǎng)景

前段時(shí)間,產(chǎn)品又提出了新的需求,要把a(bǔ)pp的主題換成圣誕節(jié)的主題,過(guò)后再換回來(lái)。一種思路就是跟夜間模式那樣,準(zhǔn)備多套主題資源放在app內(nèi)的資源文件夾內(nèi),切換時(shí)調(diào)用不用的主題即可,但這樣無(wú)疑增加了app的包體積,而且如果有新的主題資源包要加進(jìn)來(lái),用戶又得更新整個(gè)app,這樣的更新方式肯定是不好的,這種情況下我們可以考慮另外一種思路,動(dòng)態(tài)加載資源主題包的apk文件。

先看看最終實(shí)現(xiàn)的效果對(duì)比:

1701036.png

原理

動(dòng)態(tài)加載apk有兩種方式:

  • 一種是將資源主題包的apk安裝到手機(jī)上再讀取apk內(nèi)的資源,這種方式的原理是將宿主app和插件app設(shè)置相同的sharedUserId,這樣兩個(gè)app將會(huì)在同一個(gè)進(jìn)程中運(yùn)行,并可以相互訪問(wèn)內(nèi)部資源了。
  • 一種是不用安裝資源apk的方式。其原理是通過(guò)DexClassLoader類(lèi)加載器去加載指定路徑下的apk、dex或者jar文件,反射出R類(lèi)中相應(yīng)的內(nèi)部類(lèi)然后根據(jù)資源名來(lái)獲取我們需要的資源id,然后根據(jù)資源id得到對(duì)應(yīng)的圖片或者xml文件。

實(shí)現(xiàn)

無(wú)論是哪種方式,我們都需要新的資源包,我們新建一個(gè)android工程,把需要更換的新圖片和xml資源文件放在這個(gè)工程對(duì)應(yīng)的目錄下,注意,文件名必需和宿主app內(nèi)對(duì)應(yīng)的文件名相同,因?yàn)楹竺娣瓷涫歉鶕?jù)資源名去找資源id。然后將這個(gè)工程打包成apk并使用跟宿主app相同的簽名文件簽名,在app啟動(dòng)的Activity中需要加一個(gè)檢查是否有新的資源包和是否需要?jiǎng)h掉資源包的接口(需要后臺(tái)人員配合寫(xiě)接口),如果有就下載apk,至于安裝apk和不安裝apk這兩種方式哪種更好,我覺(jué)得安裝apk這種方式不太友好,即使我們可以做到安裝后在桌面上沒(méi)有啟動(dòng)圖標(biāo),但還是有一個(gè)安裝的過(guò)程,對(duì)用戶來(lái)說(shuō),可能不知道這是什么東西,以為又安裝了什么新應(yīng)用,所以我會(huì)使用不安裝apk來(lái)更新這種方式,這里也還是要記錄下安裝apk方式是怎么做的。

準(zhǔn)備資源包

新建工程Skin-Plugin,將要更換的圖片或者xml文件放在對(duì)應(yīng)的drawable文件夾內(nèi),在AndroidManifest.xml中增加shareUserId,然后打包成apk文件。如果是不需要安裝apk的,就不用設(shè)置shareUserId了。

shareUserId這個(gè)值可以隨意設(shè)定,但是必須和宿主app里面的設(shè)置為相同才行。

12081.png

我這里只更新幾個(gè)icon圖標(biāo)和底部tab的selector資源。

12082.png

AndroidManifest配置如上圖所示,需要注意的是,讓app不在桌面上生成應(yīng)用圖標(biāo),需要將啟動(dòng)activity去掉下面的過(guò)濾配置:

<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

去掉上述配置后這個(gè)工程是無(wú)法執(zhí)行Run操作了,但是不要緊,不影響打包成apk。

加載安裝的apk

前面說(shuō)過(guò)要提供一個(gè)接口下載新的資源包,下載后自動(dòng)安裝,我們?cè)谑褂眠@些資源的地方去檢查資源apk有沒(méi)有安裝,如果有,就加載資源包中的資源,將檢查apk是否安裝的方法寫(xiě)到工具類(lèi)中,這里需要傳入資源app的包名。

/**
 * apk是否已安裝
 * @param packageName
 * @return true已經(jīng)安裝,false未安裝或者已經(jīng)卸載。
 */
public static boolean checkApkInstalled(Context context, String packageName) {
    if (packageName == null || "".equals(packageName)) {
        return false;
    }
    try {
        ApplicationInfo info = context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
        return true;
    } catch (PackageManager.NameNotFoundException e) {
        return false;
    }
}

檢查到安裝了插件apk后,需要?jiǎng)?chuàng)建一個(gè)插件apk內(nèi)的上下文對(duì)象,因?yàn)橹挥胁寮pk的上下文對(duì)象才能獲取到它的Resourece對(duì)象,從而通過(guò)插件上下文獲取資源id。

//獲取對(duì)應(yīng)插件中的上下文,通過(guò)它可得到插件的Resource  
Context pluginContext = this.createPackageContext(packageName, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);  
//獲取資源id
int resId = pluginContext.getResources().getIdentifier(......);

加載未安裝的apk

同樣的這種方式也要提供一個(gè)資源包,用戶啟動(dòng)app時(shí)在后臺(tái)靜默下載插件apk文件,保存到指定的路徑下。我們要加載這個(gè)插件,就需要一個(gè)插件的類(lèi)加載器,而不是宿主app的類(lèi)加載器,這時(shí)候只能去手動(dòng)構(gòu)建DexClassLoader,再通過(guò)類(lèi)加載器,反射出R類(lèi)中相應(yīng)的內(nèi)部類(lèi)進(jìn)而獲取我們需要的資源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類(lèi)加載器,參數(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自己的類(lèi)加載器,反射出R類(lèi)中相應(yīng)的內(nèi)部類(lè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;
}

其中第二個(gè)參數(shù)是插件apk的全路徑,文件名必需是帶.apk,第三個(gè)參數(shù)是插件apk的包名,第四個(gè)參數(shù)是資源名。

/**
 * 獲取插件apk的包名
 * @param context
 * @param pluginPath 插件apk的絕對(duì)路徑
 * @return
 */
public static String getPluginPackagename(Context context, String pluginPath) {
    PackageManager pm = context.getPackageManager();
    PackageInfo pkgInfo = pm.getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);
    if (pkgInfo != null) {
        ApplicationInfo appInfo = pkgInfo.applicationInfo;
        String pkgName = appInfo.packageName;//包名
        return pkgName;
    }
    return null;
}

只有資源id還不夠,還需要插件apk的Resources對(duì)象,因?yàn)橹挥兴拍芨鶕?jù)資源id獲取到對(duì)應(yīng)的資源。

/**
 * 獲取對(duì)應(yīng)插件的Resource對(duì)象
 * @param context
 * @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對(duì)象其實(shí)就是用反射的方式調(diào)用了AssetManager類(lèi)的addAssetPath方法,這個(gè)方法的目的是將插件apk里的資源都加載到AssetManager對(duì)象中進(jìn)行管理,然后來(lái)構(gòu)建插件apk的Resources。至于為什么要用反射,看看addAssetPath的源碼:

/** 
 * 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) {  
    int res = addAssetPathNative(path);  
    return res;  
}  

這里有個(gè)注解@hide,表示即使它是public的,但是外界仍然無(wú)法訪問(wèn)它的,因?yàn)閍ndroid sdk導(dǎo)出的時(shí)候會(huì)自動(dòng)忽略隱藏的api,因此只能通過(guò)反射來(lái)調(diào)用。

// 根據(jù)資源名去加載新的資源
String pluginPath = Environment.getExternalStorageDirectory().toString() + "/dynamicload/download/skin-plugin.apk";
if (item.getResName() != null) {
    Drawable drawable = Util.getPluginResources(mContext, pluginPath).getDrawable(Util.getResId(mContext, pluginPath, Util.getPluginPackagename(mContext, pluginPath), item.getResName()));
    imageView.setImageDrawable(drawable);
}

至此就完成了動(dòng)態(tài)加載插件apk資源,當(dāng)我們需要切換回原來(lái)的資源時(shí),只需要將資源包刪除即可,或者重新構(gòu)建一個(gè)資源包,讓用戶去下載,由于我們是運(yùn)行時(shí)加載,所以當(dāng)更換了資源包時(shí),第一次打開(kāi)只是去下載這個(gè)插件資源包,再次打開(kāi)時(shí)才會(huì)去加載。

代碼下載地址:
https://github.com/shenhuniurou/BlogDemos/tree/master/DynamicLoadDemo

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,192評(píng)論 25 708
  • 動(dòng)態(tài)加載技術(shù) 介紹 在程序運(yùn)行的時(shí)候,加載一些程序自身原本不存在的可執(zhí)行文件并運(yùn)行這些文件里的代碼邏輯。 動(dòng)態(tài)調(diào)用...
    冰點(diǎn)k閱讀 4,212評(píng)論 1 11
  • 首先引入一個(gè)概念,動(dòng)態(tài)加載技術(shù)是什么?為什么要引入動(dòng)態(tài)加載?它有什么好處呢?首先要明白這幾個(gè)問(wèn)題,我們先從應(yīng)用程序...
    CHSmile閱讀 1,870評(píng)論 0 10
  • 最近幾年移動(dòng)開(kāi)發(fā)業(yè)界興起了「 插件化技術(shù) 」的旋風(fēng),各個(gè)大廠都推出了自己的插件化框架,各種開(kāi)源框架都評(píng)價(jià)自身功能優(yōu)...
    斜杠時(shí)光閱讀 4,113評(píng)論 1 36
  • 啟動(dòng)服務(wù)器時(shí)加載過(guò)濾器的實(shí)例調(diào)用init()方法初始化實(shí)例,在項(xiàng)目啟動(dòng)時(shí)候調(diào)用一次;每一請(qǐng)求只調(diào)用方法的doFil...
    小孩真笨閱讀 1,904評(píng)論 0 0

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