DynamicLoadApk 應該算是 Android 外掛化諸多框架中資歷比較老的一個了。它的專案地址在:dynamic-load-apk。該專案執行之後的效果是,使用 Gradle 編譯出外掛包和宿主包,都是以 APK 的形式。安裝宿主包之後,通過 ADB 將外掛包 push 到手機中。啟動宿主包時,它會自動進行掃描將外掛載入到應用中。點選外掛之後,進入到外掛的應用介面。
印象中最初接觸的外掛都是以單獨安裝的形式存在的,比如我可以做一個基礎的應用,然後在該應用的基礎上開發外掛。使用者可以對外掛進行選擇,然後下載並安裝,以讓自己的應用具有更豐富的功能。外掛化也算是一種比較實用的技術,畢竟我們使用 Chrome 和 AS 的時候不是一樣要載入外掛。只是比較反感的是去修改底層的程式碼,容易給系統帶來不穩定因素不說,技術到了一些人手裡,你知道他用來幹什麼。外掛化挺好,但真的要去推廣這項技術,還是看好 Google 官方去進行規範。
技術要服務於產品,好的產品不一定要高超的技術,技術並不是最重要的,重要的是你究竟想要表達什麼。這就像國內很多人只注重數理化,不注重人文學科。相比於國內的技術精英,我還是比較贊同 Google 站在整個生態的角度去考慮技術演進。前些日子社群裡對外掛化的討論:移動開發的羅曼蒂克消亡史。好吧,我自己的理解是,這從來就不是什麼羅曼蒂克。
DynamicLoadApk 外掛化的實現方式還是挺有意思的,它使用純 Java 實現,沒有涉及 Native 層的程式碼,下面我理了下 DynamicLoadApk 的 Demo 程式的整個執行過程。後續的文章我們就圍繞這張圖進行,
首先是掃描檔案路徑並載入 APK,這裡需要解析 APK 檔案的資訊,它是本質上是通過 PMS 實現的;
public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
mFrom = DLConstants.FROM_EXTERNAL;
// 通過 PMS 獲取包資訊,這裡獲取了 Activity 和 Service 的資訊
PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
if (packageInfo == null) {
return null;
}
DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
if (hasSoLib) {
copySoLib(dexPath);
}
return pluginPackage;
}
複製程式碼
然後,它通過呼叫 preparePluginEnv()
方法來建立 AssetManager, DexClassLoader 和 Resource 等。我們的外掛類載入各種資源和類的時候使用的就是這哥仨:
private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {
DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);
if (pluginPackage != null) {
return pluginPackage;
}
DexClassLoader dexClassLoader = createDexClassLoader(dexPath);
AssetManager assetManager = createAssetManager(dexPath);
Resources resources = createResources(assetManager);
// create pluginPackage
pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);
mPackagesHolder.put(packageInfo.packageName, pluginPackage);
return pluginPackage;
}
private DexClassLoader createDexClassLoader(String dexPath) {
File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
dexOutputPath = dexOutputDir.getAbsolutePath();
DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader());
return loader;
}
private AssetManager createAssetManager(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private Resources createResources(AssetManager assetManager) {
Resources superRes = mContext.getResources();
Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
return resources;
}
複製程式碼
然後點選外掛的時候要啟動外掛的 Activity,
PluginItem item = mPluginItems.get(position);
DLPluginManager pluginManager = DLPluginManager.getInstance(this);
pluginManager.startPluginActivity(this, new DLIntent(item.packageInfo.packageName, item.launcherActivityName));
複製程式碼
這裡會把要啟動的包名和啟動類名包裝到 DLIntent 中。DLIntent 是 Intent 的子類。啟動外掛的進一步的邏輯在 DLPluginManager 的 startPluginActivity()
方法中。按照上文的描述,這裡主要做了以下四件事情:
- 判斷要啟動的 Activity 是否是外掛 Activity:因為要啟動的類也可能不是外掛類,所以我們需要分成兩種情況來進行處理,普通的 Activity 直接呼叫
Context.startActivity()
外掛 Activity 需要呼叫代理 Activity 來執行。 - 判斷包名,獲取外掛相關資訊:這裡就算是一個安全的校驗吧,主要是從之前解析的 APK 資訊中進行校驗。
- 使用外掛的 DexClassLoader 載入啟動類:先要使用類載入器載入外掛的 Activity 到記憶體中,外掛 Activity 的資訊會作為 Intent 的引數一起傳遞給代理 Activity。
- 使用
DLIntent.setClass()
啟動代理類:要啟動的代理類可能是 DLProxyFragmentActivity 和 DLProxyActivity,所以這裡我們先使用getProxyActivityClass()
得到代理類。該方法中使用了 Class 的isAssignableFrom()
方法來判斷某個例項是否是指定型別的。比如DLBasePluginActivity.class.isAssignableFrom(clazz)
表示 clazz 是否是 DLBasePluginActivity 型別的。
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
// 1.判斷要啟動的 Activity 是否是外掛 Activity
if (mFrom == DLConstants.FROM_INTERNAL) {
dlIntent.setClassName(context, dlIntent.getPluginClass());
performStartActivityForResult(context, dlIntent, requestCode);
return DLPluginManager.START_RESULT_SUCCESS;
}
// 2.判斷包名,獲取外掛相關資訊
String packageName = dlIntent.getPluginPackage();
if (TextUtils.isEmpty(packageName)) {
throw new NullPointerException("disallow null packageName.");
}
DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
if (pluginPackage == null) {
return START_RESULT_NO_PKG;
}
// 3.使用外掛的 DexClassLoader 載入啟動類
final String className = getPluginActivityFullPath(dlIntent, pluginPackage);
Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className);
if (clazz == null) {
return START_RESULT_NO_CLASS;
}
Class<? extends Activity> activityClass = getProxyActivityClass(clazz);
if (activityClass == null) {
return START_RESULT_TYPE_ERROR;
}
// 4.使用 DLIntent.setClass() 啟動代理類,並傳入外掛類和包資訊
dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
dlIntent.setClass(mContext, activityClass);
performStartActivityForResult(context, dlIntent, requestCode);
return START_RESULT_SUCCESS;
}
private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) {
Class<? extends Activity> activityClass = null;
if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {
activityClass = DLProxyActivity.class;
} else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {
activityClass = DLProxyFragmentActivity.class;
}
return activityClass;
}
private void performStartActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
if (context instanceof Activity) {
((Activity) context).startActivityForResult(dlIntent, requestCode);
} else {
context.startActivity(dlIntent);
}
}
複製程式碼
這裡需要注意下,我們的外掛 Activity 是需要繼承 DLBasePluginActivity 或者 DLProxyFragmentActivity。這兩個類中重寫了 Activity 的許多生命週期方法。在代理 Activity 啟動之後,代理 Activity 會被傳遞到前面兩個基類中。比如,當外掛類想要獲取 AssetsManager 的時候,會呼叫到這兩個基類的 getAssetsManager()
,然後基類通過代理類得到之前我們建立的 AssetsManager.
按照上述流程,代理類被正常啟動。啟動之後它會建立 DLProxyImpl 例項,並在 onCreate()
方法中呼叫 DLProxyImpl 的 onCreate()
方法:
public void onCreate(Intent intent) {
intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);
mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
mPluginManager = DLPluginManager.getInstance(mProxyActivity);
mPluginPackage = mPluginManager.getPackage(mPackageName);
mAssetManager = mPluginPackage.assetManager;
mResources = mPluginPackage.resources;
initializeActivityInfo();
handleActivityInfo();
launchTargetActivity();
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected void launchTargetActivity() {
try {
Class<?> localClass = getClassLoader().loadClass(mClass);
Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
mPluginActivity = (DLPlugin) instance;
((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
mPluginActivity.attach(mProxyActivity, mPluginPackage);
Bundle bundle = new Bundle();
bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
mPluginActivity.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
這裡的主要邏輯在上述兩個方法中。第一個方法中會根據包名從 DLPluginManager 中獲取包的類載入器,然後使用該載入其載入器載入外掛類,反射觸發其構造方法,獲取例項。然後呼叫代理 Activity 的 attach()
方法將該外掛類複製給代理類。然後當 AMS 回撥代理類的各個生命週期的時候,代理類呼叫外掛類的各個生命週期。(這裡會使用類載入器再次載入外掛類,其實這是沒必要的,我們可以直接使用 Intent 將外掛類的 Class 通過序列化的方式傳遞過來,然後直接觸發其構造方法即可,無需再次執行類載入邏輯。)
好了,以上就是 DynamicLoadApk 的原理,其實本質就是:外掛類作為一個普通的類被呼叫,它不歸 AMS 負責。當我們啟動外掛的時候,實際啟動的是代理類,當 AMS 回撥代理類的生命週期的時候,代理類再呼叫外掛類的各個生命週期方法。只是,對資源和類載入的部分需要注意下,因為我們需要進行自定義配置來把它們的路徑指向我們的外掛包。