直接在當前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_resource
Module生成的,你需要先生成,然后拷貝到項目的/data/data/files
目錄下,然后才可以調用加載。
作者:嘮嗑008
鏈接:https://www.jianshu.com/p/56b6665be502
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。