在 Android 插件化、熱修復、模組化解耦場景中,宿主 App 需要在運行時加載外部 APK(插件)中的 Activity。由於 Android 系統只識別安裝時註冊在 AndroidManifest.xml 中的組件,直接 startActivity 插件類會拋出 ClassNotFoundException。本文深入剖析兩種主流繞過方案:**替換系統類加載器** 與 **代理 Activity 模式**。
Android 啟動 Activity 時,最終在 ActivityThread.performLaunchActivity() 中通過 LoadedApk.getClassLoader() 獲取類加載器(預設為 PathClassLoader,僅能加載已安裝 APK)。若將宿主 LoadedApk 中的 mClassLoader 替換為可加載插件 APK 的 DexClassLoader,系統即可正常實例化插件 Activity。
mPackages Map → 宿主包名對應的 LoadedApk。startActivity(new Intent(this, PluginActivity.class)) 正常啟動。private void replaceClassLoader(DexClassLoader pluginLoader) {
try {
Class> atClass = Class.forName("android.app.ActivityThread");
Class> laClass = Class.forName("android.app.LoadedApk");
Object currentAT = atClass.getMethod("currentActivityThread").invoke(null);
Field pkgsField = atClass.getDeclaredField("mPackages");
pkgsField.setAccessible(true);
@SuppressWarnings("unchecked")
Map> mPackages = (Map) pkgsField.get(currentAT);
WeakReference> ref = mPackages.get(getPackageName());
Object loadedApk = ref.get();
Field clField = laClass.getDeclaredField("mClassLoader");
clField.setAccessible(true);
clField.set(loadedApk, pluginLoader); // 關鍵替換
} catch (Exception e) { throw new RuntimeException(e); }
}
替換類加載器後,R.layout.xxx 仍指向宿主資源 ID。插件佈局需單獨加載:
// 宿主中預先創建包含插件資源的 Resources
AssetManager assetManager = AssetManager.class.newInstance();
assetManager.getClass().getMethod("addAssetPath", String.class).invoke(assetManager, pluginApkPath);
Resources pluginRes = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());
// 插件 Activity 中通過反射設置佈局
View view = LayoutInflater.from(hostContext).inflate(pluginRes.getLayout(R.layout.plugin_main), null);
hostContext.setContentView(view);
並重寫宿主 Activity 的 getResources()、getAssets() 返回插件資源對象。
| 優點 | 缺點 |
|---|---|
| 系統完整管理生命週期,無需手動轉發 | 依賴反射私有 API(mPackages、mClassLoader),Android 版本升級易失效 |
| 插件 Activity 可直接用系統服務、主題、返回棧 | 需在宿主 Manifest 靜態註冊所有插件 Activity(或動態註冊更複雜) |
| 代碼侵入性小,插件寫法與普通 Activity 一致 | 資源隔離需額外處理,多插件類加載器隔離較麻煩 |
宿主預先在 Manifest 註冊一個通用 ProxyActivity。啟動時傳遞插件 Activity 類名作為 Intent Extra。ProxyActivity 在自身生命週期中,通過 DexClassLoader 加載插件類,反射實例化並轉發 onCreate/Start/Resume/Pause/Stop/Destroy 等回調。插件類不繼承 Activity,而是實現一個自定義介面(如 IPluginActivity)。
public interface IPluginActivity {
void onCreate(Bundle savedInstanceState);
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
void setProxyContext(Context proxy); // 提供 Context 能力
}
public class ProxyActivity extends AppCompatActivity {
private IPluginActivity plugin;
private DexClassLoader loader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String className = getIntent().getStringExtra("plugin_class");
loader = new DexClassLoader(pluginApkPath, getDir("dex", MODE_PRIVATE).getAbsolutePath(), null, getClassLoader());
Class> clazz = loader.loadClass(className);
plugin = (IPluginActivity) clazz.newInstance();
plugin.setProxyContext(this); // 關鍵:提供 Context
plugin.onCreate(savedInstanceState);
}
@Override protected void onStart() { super.onStart(); plugin.onStart(); }
@Override protected void onResume() { super.onResume(); plugin.onResume(); }
@Override protected void onPause() { super.onPause(); plugin.onPause(); }
@Override protected void onStop() { super.onStop(); plugin.onStop(); }
@Override protected void onDestroy() { super.onDestroy(); plugin.onDestroy(); }
}
public class PluginMain implements IPluginActivity {
private Context proxyContext;
@Override public void setProxyContext(Context ctx) { this.proxyContext = ctx; }
@Override public void onCreate(Bundle s) {
TextView tv = new TextView(proxyContext);
tv.setText("來自插件的 UI");
((Activity)proxyContext).setContentView(tv);
}
// 其他生命週期...
@Override public void onStart() { Toast.makeText(proxyContext, "插件 onStart", 0).show(); }
}
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra("plugin_class", "com.plugin.PluginMain");
startActivity(intent);
| 優點 | 缺點 |
|---|---|
| 不依賴系統私有 API,相容性極強 | 需手動轉發所有生命週期、觸摸事件、權限回調、ActivityResult 等,樣板代碼多 |
| 無需在宿主 Manifest 註冊具體插件 Activity | 插件無法直接使用 this 作為 Context,需持有代理引用 |
| 天然支援多插件隔離(每插件一個 ClassLoader) | 系統服務(如 ActionBar、FragmentManager)綁定在 Proxy 上,插件使用受限 |
成熟框架(VirtualApk、RePlugin、Atlas)多採用混合策略:核心組件用代理,關鍵頁面通過 Hook AMS/PMS 實現真組件啟動。
動態加載 Activity 的本質是打破「安裝時靜態註冊」與「運行時動態代碼」的矛盾。替換類加載器從「類加載入口」切入,代理模式從「組件生命週期入口」切入。理解兩者的核心流程——performLaunchActivity → ClassLoader → newInstance 與 ProxyActivity → 反射轉發,是掌握 Android 插件化技術的關鍵基石。