Android加載外部APK資源


直接在當前APK加載未安裝apk中的資源

我現在就是要在當前安裝的apk中去加載未安裝的apk的res目錄下的drawable、layout、string、color等。先來看一個簡單的實踐

首先去創建一個用於動態加載的項目dynamic_resource,我要做的事情很簡單,就是獲取res目錄下的一個string字符串和drawable下的一張圖片。我通過一個工具類來獲取。

class ResourceUtils { //直接返回文本字符串 fun getTextFromPlugin(): String { return "插件APK類里的文本內容" } //讀取資源文件里的文本字符串 fun getTextFromPluginRes(context: Context): String? { return context.resources.getString(R.string.plugin_text) } //讀取資源文件里的圖片 fun getDrawableFromPlugin(context: Context): Drawable? { return context.resources.getDrawable(R.drawable.kb) } } 

然后,編譯獲取到這個apk,我直接把它放到當前已安裝apk的私有目錄/data/data/下,然后在當前app中去加載這個未安裝apk的資源。想要實現這個不難吧,在之前文章Android類加載機制說過,可以通過類加載器中的DexClassLoader去加載外部的apk/dex文件,這樣的話,就可以加載到ResourceUtils類,然后通過反射去調用加載資源的方法。

private fun loadPluginResource() { try { apkPath = filesDir.path + "/dynamic_resource-debug.apk" var odexFile: File = getDir("odex", Context.MODE_PRIVATE) var dexClassLoader = DexClassLoader(apkPath, odexFile.path, null, classLoader) //加載插件中的資源獲取工具類 var clazz = dexClassLoader.loadClass("com.zx.dynamic_resource.ResourceUtils") var obj = clazz.newInstance() //加載插件里類中定義的字符串資源 var method: Method = clazz.getMethod("getTextFromPlugin") var text: String = method.invoke(obj) as String tv1.text = text method = clazz.getMethod("getTextFromPluginRes", Context::class.java) text = method.invoke(obj, this) as String tv2.text = text method = clazz.getMethod("getDrawableFromPlugin", Context::class.java) var drawable: Drawable = method.invoke(obj, this) as Drawable image.setImageDrawable(drawable) } catch (e: Exception) { Log.e("tag", "loadPluginResource---: $e") } } 

上面解釋過了,這段代碼大家也都看得懂。但是執行效果如何?大家可以思考一下。結果是:只有tv1加載成功了,另外2個都失敗了。

結果分析
第一個成功了,說明加載這個未安裝apk成功了,也確實加載到這個工具類ResourceUtils,但是后2個方法之所以失敗,是因為它們是通過當前apk傳入的Context去加載的資源,具體來說用的是當前app的Resources對象,但實際上這個Resources對象並不能訪問未安裝apk的資源。下面就通過簡單的源碼分析來看一下為什么不能?

訪問外部資源原理

這里只是介紹簡單的,需要了解Resource和AssetManager創建流程的參考:Android資源動態加載以及相關原理分析

context.getResources().getText()

##Resources @NonNull public CharSequence getText(@StringRes int id) throws NotFoundException { CharSequence res = mResourcesImpl.getAssets().getResourceText(id); if (res != null) { return res; } throw new NotFoundException("String resource ID #0x" + Integer.toHexString(id)); } ##ResourcesImpl public AssetManager getAssets() { return mAssets; } 

內部是調用了mResourcesImpl去訪問的,這個對象是ResourcesImpl類型,最后是通過AssetManager去訪問資源的。現在可以得出一個結論,AssetManager是真正加載資源的對象,而Resources是app層面API調用的類。

AssetManager

/** * 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 { /** * 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 */ 

這里非常的關鍵,需要解釋一下,首先AssetManager是資源管理器,專門負責加載資源的,它內部有個隱藏方法addAssetPath,是用於加載指定路徑下的資源文件,也就是說你把apk/zip的路徑傳給它,它就能吧資源數據讀到AssetManager,然后就可以訪問了。

但是有個問題,雖然實際加載資源的是AssetManager,但是我們通過API訪問的確是Resources對象,所以看下Resources對象的構造方法

ResourcesImpl的創建

/** * 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()); } 

看到這個構造方法,有點感覺了吧。可以通過AssetManager對象去構造mResourcesImpl對象,之前也分析過資源訪問是通過mResourcesImpl.getAssets().getXXX()方法來完成的,那現在就有辦法解決加載外部apk資源的問題了。

加載外部apk資源的解決思路

首先傳入apk路徑,通過AssetManager#addAssetPath()讓AssetManager加載指定路徑的資源,然后用用AssetManager創建Resources對象,這樣的話就可以了。但是需要注意一點,由於addAssetPath是個隱藏方法,所以可以只能通過反射去調用。

    /** * 加載外部apk中的資源,方法是通過反射調用AssetManager.addAssetPath()方法去 * 讓AssetManager加載指定路徑的資源 */ private fun initResources() { var clazz = AssetManager::class.java var assetManager = clazz.newInstance() var addAssetPathMethod = clazz.getMethod("addAssetPath", String::class.java) addAssetPathMethod.invoke(assetManager, apkPath) mResource = Resources(assetManager, resources?.displayMetrics, resources?.configuration) } 

到這一步,就獲得了加載外部apk資源的Resources對象。在剛才的工具類方法調用的時候是通過context.getResource().getXX調用的,當時的Resources對象是只能加載當前apk路徑的資源,現在我們已經創建了一個可以加載外部apk資源的Resource,所以,我們只需要重寫當前Activity的getResources()方法即可

    /** * 重寫當前apk中的Resources對象,這樣就可以加載指定路徑(外部apk)中的資源對象 */ override fun getResources(): Resources? { return if (mResource == null) super.getResources() else mResource } 

到這里,就可以成功加載外部apk的資源了。注意,這里只是提供了加載外部apk資源的一種思路,實際上在插件化和熱修復中的解決方式會比這個更佳復雜,但是有了這個基礎,后面再研究這2門技術等時候就會容易很多。關於動態加載資源在這2個技術中的應用,很快就會和大家見面,有所收獲的同學點個贊吧。

項目地址

https://github.com/zhouxu88/PluginSamples

注意:外部apk是dynamic_resourceModule生成的,你需要先生成,然后拷貝到項目的/data/data/files目錄下,然后才可以調用加載。



作者:嘮嗑008
鏈接:https://www.jianshu.com/p/56b6665be502
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM