Android 4.x 正常,5.0+ (API 21+) 使用 DexClassLoader 動態加載插件/熱修復 Dex 報錯。核心原因:ART 取代 Dalvik,Dex 文件格式與驗證機制變更。
java.lang.ClassNotFoundException:類找不到java.lang.VerifyError:字節碼驗證失敗java.lang.IllegalAccessError:訪問權限檢查更嚴格dexopt failed:編譯優化失敗| 特性 | Dalvik (4.x) | ART (5.0+) |
|---|---|---|
| 編譯時機 | JIT (運行時即時編譯) | AOT (安裝時預編譯) + JIT 混合 |
| Dex 格式 | 標準 Dex | 支持 MultiDex、OAT (預編譯藍圖) |
| 字節碼驗證 | 寬鬆 | 嚴格:檢查訪問標誌、類型安全、方法簽名一致性 |
| 類加載器 | 單一 PathClassLoader | 支持 多 ClassLoader 並行、命名空間隔離 |
| 動態加載 | 直接 DexClassLoader 即可 | 需MultiDex.install()、類隔離、資源合併 |
// build.gradle
android {
defaultConfig {
multiDexEnabled true
}
}
dependencies {
implementation "androidx.multidex:multidex:2.0.1"
}
// Application 類
public class MyApp extends MultiDexApplication { // 或手動 install
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this); // 必須在 super 之前或之後調用
}
}
ART 要求所有 Dex 文件在加載前註冊到 BaseDexClassLoader 的 dexElements 數組中。
// 反射注入插件 Dex 到主 ClassLoader
public static void injectDex(Context context, String dexPath) throws Exception {
ClassLoader cl = context.getClassLoader();
// 1. 獲取 DexPathList
Field pathListField = findField(cl, "pathList");
Object dexPathList = pathListField.get(cl);
// 2. 獲取 dexElements 數組
Field dexElementsField = findField(dexPathList, "dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(dexPathList);
// 3. 創建新 DexFile
Class> dexFileClass = Class.forName("dalvik.system.DexFile");
Method loadDexMethod = dexFileClass.getDeclaredMethod("loadDex", String.class, String.class, int.class);
Object dexFile = loadDexMethod.invoke(null, dexPath, new File(context.getCacheDir(), "opt").getAbsolutePath(), 0);
// 4. 創建 Element 對象 (藉用構造函數)
Class> elementClass = dexElements.getClass().getComponentType();
Constructor> constructor = elementClass.getConstructor(dexFileClass, Boolean.TYPE, File.class, DexFile.class);
Object newElement = constructor.newInstance(null, false, new File(dexPath), dexFile);
// 5. 擴容數組並插入到最前(優先加載)
Object[] newElements = Arrays.copyOf(dexElements, dexElements.length + 1);
newElements[0] = newElement;
System.arraycopy(dexElements, 0, newElements, 1, dexElements.length);
// 6. 設置回去
dexElementsField.set(dexPathList, newElements);
}
| 框架 | 原理 | 支持版本 | 維護狀態 |
|---|---|---|---|
| Tinker (微信) | DexDiff + 反射注入 + 資源合併 | 4.0 - 14+ | 活躍 |
| AndFix (阿里) | Native 方法替換 (art::Method) | 4.0 - 9.0 | 停止維護 |
| Sophix (淘寶) | Dex 替換 + 反射 + 即時生效 | 4.0 - 14+ | 商業版 |
| HotFix (饿了么) | 類替換 + 反射注入 | 4.0 - 10+ | 較少更新 |
multiDexEnabled true 並 MultiDex.install()dexdump 對比宿主/插件類結構、方法簽名ClassLoader.toString() 確認 Dex 路徑已注入Class.forName("plugin.Class", true, pluginClassLoader) 手動加載驗證Android 5.0+ 動態加載不再是「new DexClassLoader」這麼簡單,MultiDex 注入 + 類隔離 + 簽名一致 是三大核心門檻。