InfoQ 開(kāi)設(shè)新欄目“品味書香”,精選技術(shù)書籍的精彩章節(jié),以及分享看完書留下的思考和收獲,歡迎大家關(guān)注。本文節(jié)選自任玉剛著《Android 開(kāi)發(fā)藝術(shù)探索》中的章節(jié)“Android 的動(dòng)態(tài)加載技術(shù)”,探討了 Android 動(dòng)態(tài)加載的三個(gè)關(guān)鍵問(wèn)題。
動(dòng)態(tài)加載技術(shù)(也叫插件化技術(shù))在技術(shù)驅(qū)動(dòng)型的公司中扮演著相當(dāng)重要的角色,當(dāng)項(xiàng)目越來(lái)越龐大的時(shí)候,需要通過(guò)插件化來(lái)減輕應(yīng)用的內(nèi)存和 CPU 占用,還可以實(shí)現(xiàn)熱插拔,即在不發(fā)布新版本的情況下更新某些模塊。動(dòng)態(tài)加載是一項(xiàng)很復(fù)雜的技術(shù),這里主要介紹動(dòng)態(tài)加載技術(shù)中的三個(gè)基礎(chǔ)性問(wèn)題,至于完整的動(dòng)態(tài)加載技術(shù)的實(shí)現(xiàn)請(qǐng)參考筆者發(fā)起的開(kāi)源插件化框架 DL:。項(xiàng)目期間有多位開(kāi)發(fā)人員一起貢獻(xiàn)代碼。
不同的插件化方案各有各的特色,但是它們都必須要解決三個(gè)基礎(chǔ)性問(wèn)題:資源訪問(wèn)、Activity 生命周期的管理和 ClassLoader 的管理。在介紹它們之前,首先要明白宿主和插件的概念,宿主是指普通的 apk,而插件一般是指經(jīng)過(guò)處理的 dex 或者 apk,在主流的插件化框架中多采用經(jīng)過(guò)特殊處理的 apk 來(lái)作為插件,處理方式往往和編譯以及打包環(huán)節(jié)有關(guān),另外很多插件化框架都需要用到代理 Activity 的概念,插件 Activity 的啟動(dòng)大多數(shù)是借助一個(gè)代理 Activity 來(lái)實(shí)現(xiàn)的。
1.資源訪問(wèn)
我們知道,宿主程序調(diào)起未安裝的插件 apk,一個(gè)很大的問(wèn)題就是資源如何訪問(wèn),具體來(lái)說(shuō)就是插件中凡是以 R 開(kāi)頭的資源都不能訪問(wèn)了。這是因?yàn)樗拗鞒绦蛑胁](méi)有插件的資源,所以通過(guò) R 來(lái)加載插件的資源是行不通的,程序會(huì)拋出異常:無(wú)法找到某某 id 所對(duì)應(yīng)的資源。
針對(duì)這個(gè)問(wèn)題,有人提出了將插件中的資源在宿主程序中也預(yù)置一份,這雖然能解決問(wèn)題,但是這樣就會(huì)產(chǎn)生一些弊端。首先,這樣就需要宿主和插件同時(shí)持有一份相同的資源,增加了宿主 apk 的大小;其次,在這種模式下,每次發(fā)布一個(gè)插件都需要將資源復(fù)制到宿主程序中,這意味著每發(fā)布一個(gè)插件都要更新一下宿主程序,這就和插件化的思想相違背了。
因?yàn)椴寮哪康木褪且獪p小宿主程序 apk 包的大小,同時(shí)降低宿主程序的更新頻率并做到自由裝載模塊,所以這種方法不可取,它限制了插件的線上更新這一重要特性。還有人提供了另一種方式,首先將插件中的資源解壓出來(lái),然后通過(guò)文件流去讀取資源,這樣做理論上是可行的,但是實(shí)際操作起來(lái)還是有很大難度的。首先不同資源有不同的文件流格式,比如圖片、XML 等,其次針對(duì)不同設(shè)備加載的資源可能是不一樣的,如何選擇合適的資源也是一個(gè)需要解決的問(wèn)題,基于這兩點(diǎn),這種方法也不建議使用,因?yàn)樗鼘?shí)現(xiàn)起來(lái)有較大難度。為了方便地對(duì)插件進(jìn)行資源管理,下面給出一種合理的方式。
我們知道,Activity 的工作主要是通過(guò) ContextImpl 來(lái)完成的, Activity 中有一個(gè)叫 mBase 的成員變量,它的類型就是 ContextImpl。注意到 Context 中有如下兩個(gè)抽象方法,看起來(lái)是和資源有關(guān)的,實(shí)際上 Context 就是通過(guò)它們來(lái)獲取資源的。這兩個(gè)抽象方法的真正實(shí)現(xiàn)在 ContextImpl 中,也就是說(shuō),只要實(shí)現(xiàn)這兩個(gè)方法,就可以解決資源問(wèn)題。
? | /** 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(); |
下面給出具體的實(shí)現(xiàn)方式,首先要加載 apk 中的資源,如下所示。
復(fù)制代碼
? | 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() 的實(shí)現(xiàn)可以看出,加載資源的方法是通過(guò)反射,通過(guò)調(diào)用 AssetManager 中的 addAssetPath 方法,我們可以將一個(gè) apk 中的資源加載到 Resources 對(duì)象中,由于 addAssetPath 是隱藏 API 我們無(wú)法直接調(diào)用,所以只能通過(guò)反射。下面是它的聲明,通過(guò)注釋我們可以看出,傳遞的路徑可以是 zip 文件也可以是一個(gè)資源目錄,而 apk 就是一個(gè) zip,所以直接將 apk 的路徑傳給它,資源就加載到 AssetManager 中了。然后再通過(guò) AssetManager 來(lái)創(chuàng)建一個(gè)新的 Resources 對(duì)象,通過(guò)這個(gè)對(duì)象我們就可以訪問(wèn)插件 apk 中的資源了,這樣一來(lái)問(wèn)題就解決了。
復(fù)制代碼
? | /** |
? | {1} |
? | * Add an additional set of assets to the asset manager. This can be |
? | {1} |
? | * either a directory or ZIP file. Not for use by applications. Returns |
? | {1} |
? | * the cookie of the added asset, or 0 on failure. |
? | {1} |
? | * {@hide} |
? | {1} |
? | */ |
? | ? |
? | public final int addAssetPath(String path) { |
? | ? |
? | synchronized (this) { |
? | ? |
? | int res = addAssetPathNative(path); |
? | ? |
? | makeStringBlocks(mStringBlocks); |
? | ? |
? | return res; |
? | ? |
? | } |
? | ? |
? | } |
接著在代理 Activity 中實(shí)現(xiàn) getAssets() 和 getResources(),如下所示。關(guān)于代理 Activity 的含義請(qǐng)參看 DL 開(kāi)源插件化框架的實(shí)現(xiàn)細(xì)節(jié),這里不再詳細(xì)描述了。
復(fù)制代碼
? | @Override |
? | ? |
? | public AssetManager getAssets() { |
? | ? |
? | return mAssetManager == null ? super.getAssets() : mAssetManager; |
? | ? |
? | } |
? | ? |
? | ? |
? | @Override |
? | ? |
? | public Resources getResources() { |
? | ? |
? | return mResources == null ? super.getResources() : mResources; |
? | ? |
? | } |
通過(guò)上述這兩個(gè)步驟,就可以通過(guò) R 來(lái)訪問(wèn)插件中的資源了。
2.Activity 生命周期的管理
管理 Activity 生命周期的方式各種各樣,這里只介紹兩種:反射方式和接口方式。反射的方式很好理解,首先通過(guò) Java 的反射去獲取 Activity 的各種生命周期方法,比如 onCreate、onStart、onResume 等,然后在代理 Activity 中去調(diào)用插件 Activity 對(duì)應(yīng)的生命周期方法即可,如下所示。
復(fù)制代碼
? | @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(); |
? | ? |
? | } |
使用反射來(lái)管理插件 Activity 的生命周期是有缺點(diǎn)的,一方面是反射代碼寫起來(lái)比較復(fù)雜,另一方面是過(guò)多使用反射會(huì)有一定的性能開(kāi)銷。下面介紹接口方式,接口方式很好地解決了反射方式的不足之處,這種方式將 Activity 的生命周期方法提取出來(lái)作為一個(gè)接口(比如叫 DLPlugin),然后通過(guò)代理 Activity 去調(diào)用插件 Activity 的生命周期方法,這樣就完成了插件 Activity 的生命周期管理,并且沒(méi)有采用反射,這就解決了性能問(wèn)題。同時(shí)接口的聲明也比較簡(jiǎn)單,下面是 DLPlugin 的聲明:
復(fù)制代碼
? | public interface DLPlugin { |
? | ? |
? | public void onStart(); |
? | ? |
? | public void onRestart(); |
? | ? |
? | public void onActivityResult(int requestCode, int resultCode, Intent |
? | {1} |
? | 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 中只需要按如下方式即可調(diào)用插件 Activity 的生命周期方法,這就完成了插件 Activity 的生命周期的管理。
復(fù)制代碼
? | ... |
? | ? |
? | @Override |
? | ? |
? | protected void onStart() { |
? | ? |
? | mRemoteActivity.onStart(); |
? | ? |
? | super.onStart(); |
? | ? |
? | } |
? | ? |
? | ? |
? | @Override |
? | ? |
? | protected void onRestart() { |
? | ? |
? | mRemoteActivity.onRestart(); |
? | ? |
? | super.onRestart(); |
? | ? |
? | } |
? | ? |
? | ? |
? | @Override |
? | ? |
? | protected void onResume() { |
? | ? |
? | mRemoteActivity.onResume(); |
? | ? |
? | super.onResume(); |
? | ? |
? | } |
? | ? |
? | ... |
通過(guò)上述代碼應(yīng)該不難理解接口方式對(duì)插件 Activity 生命周期的管理思想,其中 mRemoteActivity 就是 DLPlugin 的實(shí)現(xiàn)。
3.插件 ClassLoader 的管理
為了更好地對(duì)多插件進(jìn)行支持,需要合理地去管理各個(gè)插件的 DexClassLoader,這樣同一個(gè)插件就可以采用同一個(gè) ClassLoader 去加載類,從而避免了多個(gè) ClassLoader 加載同一個(gè)類時(shí)所引發(fā)的類型轉(zhuǎn)換錯(cuò)誤。在下面的代碼中,通過(guò)將不同插件的 ClassLoader 存儲(chǔ)在一個(gè) HashMap 中,這樣就可以保證不同插件中的類彼此互不干擾。
復(fù)制代碼
? | public class DLClassLoader extends DexClassLoader { |
? | ? |
? | private static final String TAG = "DLClassLoader"; |
? | ? |
? | ? |
? |
private static final HashMap |
? | ? |
? |
= new HashMap |
? | ? |
? | ? |
? | protected DLClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { |
? | ? |
? | super(dexPath, optimizedDirectory, libraryPath, parent); |
? | ? |
? | } |
? | ? |
? | ? |
? | /** |
? | {1} |
? | * return a available classloader which belongs to different apk |
? | {1} |
? | */ |
? | ? |
? | 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; |
? | ? |
? | } |
? | ? |
? | } |
事實(shí)上插件化的技術(shù)細(xì)節(jié)非常多,這絕非一個(gè)章節(jié)的內(nèi)容所能描述清楚的,另外插件化作為一種核心技術(shù),需要開(kāi)發(fā)者有較深的開(kāi)發(fā)功底才能夠很好地理解,因此本節(jié)的內(nèi)容更多是讓讀者對(duì)插件化開(kāi)發(fā)有一個(gè)感性的了解,細(xì)節(jié)上還需要讀者自己去鉆研,也可以通過(guò) DL 插件化框架去深入地學(xué)習(xí)。
評(píng)論