Android 動態載入技術三個關鍵問題詳解
動態載入技術(也叫外掛化技術)在技術驅動型的公司中扮演著相當重要的角色,當專案越來越龐大的時候,需要通過外掛化來減輕應用的記憶體和CPU佔用,還可以實現熱插拔,即在不釋出新版本的情況下更新某些模組。動態載入是一項很複雜的技術,這裡主要介紹動態載入技術中的三個基礎性問題,至於完整的動態載入技術的實現請參考筆者發起的開源外掛化框架DL:。專案期間有多位開發人員一起貢獻程式碼。
不同的外掛化方案各有各的特色,但是它們都必須要解決三個基礎性問題:資源訪問、Activity生命週期的管理和ClassLoader的管理。在介紹它們之前,首先要明白宿主和外掛的概念,宿主是指普通的apk,而外掛一般是指經過處理的dex或者apk,在主流的外掛化框架中多采用經過特殊處理的apk來作為外掛,處理方式往往和編譯以及打包環節有關,另外很多外掛化框架都需要用到代理Activity的概念,外掛Activity的啟動大多數是藉助一個代理Activity來實現的。
1.資源訪問
我們知道,宿主程式調起未安裝的外掛apk,一個很大的問題就是資源如何訪問,具體來說就是外掛中凡是以R開頭的資源都不能訪問了。這是因為宿主程式中並沒有外掛的資源,所以通過R來載入外掛的資源是行不通的,程式會丟擲異常:無法找到某某id所對應的資源。
針對這個問題,有人提出了將外掛中的資源在宿主程式中也預置一份,這雖然能解決問題,但是這樣就會產生一些弊端。首先,這樣就需要宿主和外掛同時持有一份相同的資源,增加了宿主apk的大小;其次,在這種模式下,每次釋出一個外掛都需要將資源複製到宿主程式中,這意味著每釋出一個外掛都要更新一下宿主程式,這就和外掛化的思想相違背了。
因為外掛化的目的就是要減小宿主程式apk包的大小,同時降低宿主程式的更新頻率並做到自由裝載模組,所以這種方法不可取,它限制了外掛的線上更新這一重要特性。還有人提供了另一種方式,首先將外掛中的資源解壓出來,然後通過檔案流去讀取資源,這樣做理論上是可行的,但是實際操作起來還是有很大難度的。首先不同資源有不同的檔案流格式,比如圖片、XML等,其次針對不同裝置載入的資源可能是不一樣的,如何選擇合適的資源也是一個需要解決的問題,基於這兩點,這種方法也不建議使用,因為它實現起來有較大難度。為了方便地對外掛進行資源管理,下面給出一種合理的方式。
我們知道,Activity的工作主要是通過ContextImpl來完成的, Activity中有一個叫mBase的成員變數,它的型別就是ContextImpl。注意到Context中有如下兩個抽象方法,看起來是和資源有關的,實際上Context就是通過它們來獲取資源的。這兩個抽象方法的真正實現在ContextImpl中,也就是說,只要實現這兩個方法,就可以解決資源問題了。
/** Return an AssetManager instance for your application's package. */ public abstract AssetManager getAssets(); /** Return a Resources instance for your application's package. */ public abstract Resources getResources();
下面給出具體的實現方式,首先要載入apk中的資源,如下所示。
protected void loadResources() { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, mDexPath); mAssetManager = assetManager; } catch (Exception e) { e.printStackTrace(); } Resources superRes = super.getResources(); mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); mTheme = mResources.newTheme(); mTheme.setTo(super.getTheme()); }
從loadResources()的實現可以看出,載入資源的方法是通過反射,通過呼叫AssetManager中的addAssetPath方法,我們可以將一個apk中的資源載入到Resources物件中,由於addAssetPath是隱藏API我們無法直接呼叫,所以只能通過反射。下面是它的宣告,通過註釋我們可以看出,傳遞的路徑可以是zip檔案也可以是一個資源目錄,而apk就是一個zip,所以直接將apk的路徑傳給它,資源就載入到AssetManager中了。然後再通過AssetManager來建立一個新的Resources物件,通過這個物件我們就可以訪問外掛apk中的資源了,這樣一來問題就解決了。
/** * 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) { synchronized (this) { int res = addAssetPathNative(path); makeStringBlocks(mStringBlocks); return res; } }
接著在代理Activity中實現getAssets()和getResources(),如下所示。關於代理Activity的含義請參看DL開源外掛化框架的實現細節,這裡不再詳細描述了。
@Override public AssetManager getAssets() { return mAssetManager == null ? super.getAssets() : mAssetManager; } @Override public Resources getResources() { return mResources == null ? super.getResources() : mResources; }
通過上述這兩個步驟,就可以通過R來訪問外掛中的資源了。
2.Activity生命週期的管理
管理Activity生命週期的方式各種各樣,這裡只介紹兩種:反射方式和介面方式。反射的方式很好理解,首先通過Java的反射去獲取Activity的各種生命週期方法,比如onCreate、onStart、onResume等,然後在代理Activity中去呼叫外掛Activity對應的生命週期方法即可,如下所示。
@Override protected void onResume() { super.onResume(); Method onResume = mActivityLifecircleMethods.get("onResume"); if (onResume != null) { try { onResume.invoke(mRemoteActivity, new Object[] { }); } catch (Exception e) { e.printStackTrace(); } } } @Override protected void onPause() { Method onPause = mActivityLifecircleMethods.get("onPause"); if (onPause != null) { try { onPause.invoke(mRemoteActivity, new Object[] { }); } catch (Exception e) { e.printStackTrace(); } } super.onPause(); }
使用反射來管理外掛Activity的生命週期是有缺點的,一方面是反射程式碼寫起來比較複雜,另一方面是過多使用反射會有一定的效能開銷。下面介紹介面方式,介面方式很好地解決了反射方式的不足之處,這種方式將Activity的生命週期方法提取出來作為一個介面(比如叫DLPlugin),然後通過代理Activity去呼叫外掛Activity的生命週期方法,這樣就完成了外掛Activity的生命週期管理,並且沒有采用反射,這就解決了效能問題。同時介面的宣告也比較簡單,下面是DLPlugin的宣告:
public interface DLPlugin { public void onStart(); public void onRestart(); public void onActivityResult(int requestCode, int resultCode, Intent data); public void onResume(); public void onPause(); public void onStop(); public void onDestroy(); public void onCreate(Bundle savedInstanceState); public void setProxy(Activity proxyActivity, String dexPath); public void onSaveInstanceState(Bundle outState); public void onNewIntent(Intent intent); public void onRestoreInstanceState(Bundle savedInstanceState); public boolean onTouchEvent(MotionEvent event); public boolean onKeyUp(int keyCode, KeyEvent event); public void onWindowAttributesChanged(LayoutParams params); public void onWindowFocusChanged(boolean hasFocus); public void onBackPressed(); … }
在代理Activity中只需要按如下方式即可呼叫外掛Activity的生命週期方法,這就完成了外掛Activity的生命週期的管理。
... @Override protected void onStart() { mRemoteActivity.onStart(); super.onStart(); } @Override protected void onRestart() { mRemoteActivity.onRestart(); super.onRestart(); } @Override protected void onResume() { mRemoteActivity.onResume(); super.onResume(); } ...
通過上述程式碼應該不難理解介面方式對外掛Activity生命週期的管理思想,其中mRemoteActivity就是DLPlugin的實現。
3.外掛ClassLoader的管理
為了更好地對多外掛進行支援,需要合理地去管理各個外掛的DexClassLoader,這樣同一個外掛就可以採用同一個ClassLoader去載入類,從而避免了多個ClassLoader載入同一個類時所引發的型別轉換錯誤。在下面的程式碼中,通過將不同外掛的ClassLoader儲存在一個HashMap中,這樣就可以保證不同外掛中的類彼此互不干擾。
public class DLClassLoader extends DexClassLoader { private static final String TAG = "DLClassLoader"; private static final HashMap<String, DLClassLoader> mPluginClassLoaders = new HashMap<String, DLClassLoader>(); protected DLClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, optimizedDirectory, libraryPath, parent); } /** * return a available classloader which belongs to different apk */ public static DLClassLoader getClassLoader(String dexPath, Context context, ClassLoader parentLoader) { DLClassLoader dLClassLoader = mPluginClassLoaders.get(dexPath); if (dLClassLoader != null) return dLClassLoader; File dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE); final String dexOutputPath = dexOutputDir.getAbsolutePath(); dLClassLoader = new DLClassLoader(dexPath, dexOutputPath, null, parentLoader); mPluginClassLoaders.put(dexPath, dLClassLoader); return dLClassLoader; } }
事實上外掛化的技術細節非常多,這絕非一個章節的內容所能描述清楚的,另外外掛化作為一種核心技術,需要開發者有較深的開發功底才能夠很好地理解,因此本節的內容更多是讓讀者對外掛化開發有一個感性的瞭解,細節上還需要讀者自己去鑽研,也可以通過DL外掛化框架去深入地學習。
相關文章
- DL動態載入框架技術文件框架
- BIM,PIM接入GIS 需要解決的關鍵技術問題
- 【轉載】關於C#中動態載入AppDomain的問題C#APPAI
- 詳解.NET中的動態編譯技術編譯
- OpenAI Sora 關鍵技術詳解:揭秘時空碎片 (Spacetime Patches) 技術OpenAISora
- python動態載入(三)Python
- 實時雲渲染關鍵技術-低延遲詳解
- Ext4 checkbox 動態載入問題
- 大資料三個重要的技術問題大資料
- Android資源動態載入以及相關原理分析Android
- Android動態載入jar/dexAndroidJAR
- 動態載入程式集(三) (轉)
- 動態頁面資料載入不全的問題
- 美顏sdk動態貼紙開發技術詳解
- Android 軟鍵盤相關問題Android
- Qtum量子鏈關鍵技術解讀QT
- 一個非技術問題的問題
- QTP關鍵技術QT
- 最近的幾個技術問題總結和答疑(三)
- 關於顯示載入動態連結庫模組及解除安裝的問題
- 萬字詳解資料安全關鍵技術之資料脫敏
- 技術管理的6個關鍵原則
- 關於延遲載入,立即載入的問題
- 三-類的載入過程詳解
- 關於虛擬化技術的幾個問題薦
- drozer模組的編寫及模組動態載入問題研究
- 高德地圖fragment 動態載入地圖 巢狀問題地圖Fragment巢狀
- 問一個動態物件的問題物件
- Android遊戲開發案例與關鍵技術Android遊戲開發
- 關於iOS Webview 載入React 靜態資源的安全問題iOSWebViewReact
- 聊聊自動駕駛必須解決哪些感知問題?交通標誌識別技術詳解自動駕駛
- 詳解Android資料儲存技術Android
- 【技術向】Linux動態連結庫預載入型後門Linux
- SaaS無法解決“關鍵”問題
- 解決IP盜用問題的三種技術手段 (轉)
- “BIM關鍵技術研究”專題徵稿啟事
- Android之批量載入圖片OOM問題解決方案AndroidOOM
- Python爬蟲入門 | 7 分類爬取豆瓣電影,解決動態載入問題Python爬蟲