版權歸作者所有,如有轉發,請注明文章出處:https://cyrus-studio.github.io/blog/
DexClassLoader
DexClassLoader 可以加載任意路徑下的 dex,或者 jar、apk、zip 文件(包含classes.dex)。常用于插件化、熱修復以及 dex 加殼。
源碼如下:
public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}
}
http://aospxref.com/android-10.0.0_r47/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
參數說明
參數 | 類型 | 說明 |
---|---|---|
dexPath | String | 需要加載的 dex、apk、jar、zip 文件的路徑,多個路徑用 : 分隔。支持任意目錄下的文件。 |
optimizedDirectory | String | 用于存放優化后的 dex 文件(即 .odex),在 API level 26 已棄用(Android 8.0 Oreo)。 |
librarySearchPath | String | 指定本地庫(native library,.so 文件)搜索路徑,多個路徑用 : 分隔。 |
parent | ClassLoader | 父類加載器,用于實現類加載的委托機制。 |
動態加載
動態加載 = 運行時按需加載代碼或資源
動態加載 是實現 dex加殼、插件化、熱更新、熱修復 的基礎。比如阿里的 AndFix 、騰訊 tinker、美團 Robust 等熱修復框架的基礎。
使用 DexClassLoader 加載一個外部 .dex 或 .apk 文件,然后反射調用里面的類和方法。
步驟如下:
-
準備好外部的 dex / apk / jar;
-
將它放在你 app 可以訪問的路徑(如 /data/data/包名/files/);
-
用 DexClassLoader 加載它;
-
使用反射調用其中的類和方法。
1. 插件工程示例
插件工程主要包含這兩個類:
PluginActivity 源碼:使用 Jetpack Compose 創建一個白色背景、居中顯示文本的界面,文本內容為 “PluginActivity from plugin” 加上 ClassLoader 信息
package com.cyrus.example.pluginimport android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.spclass PluginActivity : ComponentActivity() {private val TAG = "PluginActivity"override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)Log.d(TAG, "onCreate")val classLoaderInfo = this.javaClass.classLoader.toString()setContent {PluginActivityContent(classLoaderInfo)}}@Composablefun PluginActivityContent(classLoaderInfo: String) {Box(modifier = Modifier.fillMaxSize().background(Color.White).padding(horizontal = 16.dp),contentAlignment = Alignment.Center) {Text(text = "PluginActivity from plugin\n\n$classLoaderInfo",fontSize = 18.sp,color = Color.Black)}}override fun onStart() {super.onStart()Log.d(TAG, "onStart")}override fun onResume() {super.onResume()Log.d(TAG, "onResume")}override fun onPause() {super.onPause()Log.d(TAG, "onPause")}override fun onStop() {super.onStop()Log.d(TAG, "onStop")}override fun onRestart() {super.onRestart()Log.d(TAG, "onRestart")}override fun onDestroy() {super.onDestroy()Log.d(TAG, "onDestroy")}}
PluginClass 源碼:
package com.cyrus.example.pluginclass PluginClass {fun getString(): String {return "String from plugin."}}
編譯 apk
把 apk 推送到設備 sdcard
adb push plugin-debug.apk /sdcard/Android/data/com.cyrus.example/files
2. 動態加載示例
創建 DexClassLoader 實例,加載指定路徑下的 APK/DEX 文件
val apkPath = "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk"// 創建 DexClassLoader 加載 sdcard 上的 apk
val classLoader = DexClassLoader(apkPath,null,this@ClassLoaderActivity.packageResourcePath,context.classLoader // parent 設為當前 context 的類加載器
)
調用示例:
// classLoader 加載 com.cyrus.example.plugin.PluginClass 類并通過反射調用 getString 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.PluginClass")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("getString")
method.isAccessible = true
val result = method.invoke(instance) as? Stringoutput = "動態加載:${pluginPath}\n\ncall ${method}\n\nreuslt=${result}"
效果如下,可以看到正常調用了 apk 中 PluginClass 的 getString 方法并拿到了返回值
組件類 ClassNotFoundException
在 AndroidManifest.xml 中聲明 PluginActivity
<activityandroid:name="com.cyrus.example.plugin.PluginActivity"android:exported="true">
</activity>
通過自定義 ClassLoader 加載 PluginActivity 類并啟動
// 通過 classLoader 加載 PluginActivity 類并啟動
val pluginActivityClass = classLoader.loadClass("com.cyrus.example.plugin.PluginActivity")
val intent = Intent(this@ClassLoaderActivity, pluginActivityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this@ClassLoaderActivity.startActivity(intent)
報錯如下:
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.cyrus.example/com.cyrus.example.plugin.PluginActivity}: java.lang.ClassNotFoundException: Didn't find class "com.cyrus.example.plugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/lib/arm64, /data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3194)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.cyrus.example.plugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/lib/arm64, /data/app/com.cyrus.example-MZoMs5LmgjwUZ_FiJ-u0fQ==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:44)
at android.app.Instrumentation.newActivity(Instrumentation.java:1250)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3182)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)?
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)?
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)?
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)?
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)?
at android.os.Handler.dispatchMessage(Handler.java:107)?
at android.os.Looper.loop(Looper.java:214)?
at android.app.ActivityThread.main(ActivityThread.java:7356)?
at java.lang.reflect.Method.invoke(Native Method)?
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)?
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)?
斷點調試看看,PluginActivity 類是正常加載了的
原因是:
-
動態加載的 dex 不具有生命周期特征,APP 中的 Activity 、Service 等組件無法正常工作,只能完成一般函數的調用;
-
需要對 ClassLoader 進行修正,APP 才能夠正常運行。
生命周期類處理
DexClassLoader 加載的類是沒有組件生命周期的,也就是說即使 DexClassLoader 通過對 APK 動態加載完成了對組件類的加載,當系統啟動該組件時,依然會出現加載類失敗的異常。
為什么組件類被動態加載入虛擬機,但系統卻出現加載類失敗呢?
因為 系統在啟動組件(如 Activity、Service)時,是通過 AMS → ActivityThread → Instrumentation 最終調用 Class.forName(組件類名) 來加載組件類的,而這個過程默認使用的是系統的 ClassLoader(通常是 PathClassLoader),而不是我們自定義的 DexClassLoader。
從 ClassLoader 來看,兩種解決方案:
-
替換系統組件類加載器為我們的 DexClassLoader,同時設置 DexClassLoader 的 parent 為系統組件類加載器
-
打破原有的雙親關系,在系統組件類加載器和 BootClassLoader 的中間插入我們自己的 DexClassLoader 即可
或者可以對 PathClassLoader 中的 Elements 進行合并(常用于熱修復框架(如 Tinker、Robust))。
相關文章:Android 下的 ClassLoader 與 雙親委派機制
方案 1:替換 ClassLoader 為 自定義ClassLoader
變化前結構(系統默認)
[BootClassLoader]↑
[PathClassLoader] ← 原來的 ClassLoader↑Activity、Application 加載組件類
變化后結構(方案1)
[BootClassLoader]↑
[PathClassLoader] ← 原來的 ClassLoader↑
[DexClassLoader] ← 反射替換 LoadedApk.mClassLoader↑Activity、Application 加載組件類
通過 反射替換掉 系統的 ClassLoader 為自定義的 ClassLoader,同時設置 parent 為 系統的 ClassLoader
示例代碼:
private fun replaceClassLoader(context: Context): ClassLoader? {try {// 1. 創建自定義 ClassLoader 實例,加載 sdcard 上的 apkval classLoader = DexClassLoader(apkPath,null,this@ClassLoaderActivity.packageResourcePath,context.classLoader // 設置 parent 為 系統的 ClassLoader)// 2. 拿到 ActivityThreadval activityThreadClass = Class.forName("android.app.ActivityThread")val currentActivityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null)// 3. 拿到 mPackages 字段: Map<String, WeakReference<LoadedApk>>val mPackagesField = activityThreadClass.getDeclaredField("mPackages")mPackagesField.isAccessible = trueval mPackages = mPackagesField.get(currentActivityThread) as Map<*, *>// 4. 拿到當前包名對應的 LoadedApk 實例val loadedApkRef = mPackages[context.packageName] as? WeakReference<*>?: throw IllegalStateException("LoadedApk not found for package: ${context.packageName}")val loadedApk = loadedApkRef.get()?: throw IllegalStateException("LoadedApk is null")// 5. 替換 LoadedApk.mClassLoaderval loadedApkClass = loadedApk.javaClassval mClassLoaderField = loadedApkClass.getDeclaredField("mClassLoader")mClassLoaderField.isAccessible = truemClassLoaderField.set(loadedApk, classLoader)// ? 替換成功Log.d(TAG, "? ClassLoader has been replaced successfully!")return classLoader} catch (e: Exception) {e.printStackTrace()Log.d(TAG, "? Failed to replace ClassLoader: ${e.message}")}return null
}
-
Android 每個 App 都有一個 LoadedApk 對象負責管理 dex 加載;
-
mClassLoader 是決定類加載的實際執行者;
-
修改它就等于修改了整個 App 的“類查找邏輯”。
調用示例:
// 方案 1:替換 ClassLoader 為 自定義ClassLoader
val classLoader = replaceClassLoader(context)if (classLoader == null) {Log.d(TAG, "? Failed to replace ClassLoader")return@Button
}// classLoader 加載 com.cyrus.example.plugin.PluginClass 類并通過反射調用 getString 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.PluginClass")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("getString")
method.isAccessible = true
val result = method.invoke(instance) as? String// 通過 classLoader 加載 PluginActivity 類并啟動
val pluginActivityClass = classLoader.loadClass("com.cyrus.example.plugin.PluginActivity")
val intent = Intent(this@ClassLoaderActivity, pluginActivityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this@ClassLoaderActivity.startActivity(intent)output = "動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}"
成功啟動 PluginActivity,而且可以看到 ClassLoader 是自定義的 DexClassLoader
日志輸出如下,生命周期相關方法也是正常執行
方案 2:插入中間 ClassLoader / 打破雙親委派
變化前結構
[BootClassLoader]↑
[PathClassLoader] ← 原始 ClassLoader
變化后結構(方案2)
[BootClassLoader]↑
[DexClassLoader] ← 反射插入自定義的 DexClassLoader↑
[PathClassLoader] ← 原始 ClassLoader
在 BootClassLoader 和 PathClassLoader 中間 通過反射插入 自定義的 DexClassLoader
通過反射修改 PathClassLoader 的 parent 為 自定義ClassLoader,并設置 自定義ClassLoader 的 parent 為 BootClassLoader
目標結構如下:
-
PathClassLoader.parent = 自定義ClassLoader
-
自定義ClassLoader.parent = BootClassLoader
通過反射獲取并修改 PathClassLoader 的 parent 字段
private fun injectClassLoader(context: Context): DexClassLoader? {try {// 拿到當前 PathClassLoaderval appClassLoader = context.classLoaderval pathClassLoaderClass = ClassLoader::class.java// 反射訪問 parent 字段val parentField = pathClassLoaderClass.getDeclaredField("parent")parentField.isAccessible = trueval bootClassLoader = ClassLoader.getSystemClassLoader().parent// 自定義ClassLoader.parent = BootClassLoaderval classLoader = DexClassLoader(apkPath,null,this@ClassLoaderActivity.packageResourcePath,bootClassLoader // 設置 parent 為 BootClassLoader)// PathClassLoader.parent = 自定義ClassLoaderparentField.set(appClassLoader, classLoader)Log.d(TAG, "? 成功將 ${classLoader} 注入到 PathClassLoader.parent")return classLoader} catch (e: Exception) {e.printStackTrace()Log.d(TAG, "? 注入失敗:${e.message}")}return null
}
調用示例:
// 方案 2:插入中間 ClassLoader / 打破雙親委派
val classLoader = injectClassLoader(context)if (classLoader == null) {Log.d(TAG, "? Failed to replace ClassLoader")return@Button
}// classLoader 加載 com.cyrus.example.plugin.PluginClass 類并通過反射調用 getString 方法
val pluginClass = classLoader.loadClass("com.cyrus.example.plugin.PluginClass")
val constructor = pluginClass.getDeclaredConstructor()
constructor.isAccessible = true
val instance = constructor.newInstance()
val method = pluginClass.getDeclaredMethod("getString")
method.isAccessible = true
val result = method.invoke(instance) as? String// 通過 classLoader 加載 PluginActivity 類并啟動
val pluginActivityClass = classLoader.loadClass("com.cyrus.example.plugin.PluginActivity")
val intent = Intent(this@ClassLoaderActivity, pluginActivityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this@ClassLoaderActivity.startActivity(intent)output = "動態加載:${apkPath}\n\ncall ${method}\n\nreuslt=${result}"
成功啟動 PluginActivity,而且可以看到 ClassLoader 是自定義的 DexClassLoader
日志輸出如下,生命周期相關方法也是正常執行
這樣你就“無縫攔截”了類加載流程,同時也保留系統 PathClassLoader 的全部能力,比完全替換更可靠 💯
加殼應用的運行流程
參考文章:
-
詳解 Android APP 啟動流程
-
FART:ART環境下基于主動調用的自動化脫殼方案
殼執行的時機必須比 app 原來的邏輯早。
在 APP啟動流程中我們最終可以得出結論,app 最先獲得執行權限的是 app 中聲明的 Application 類中的 attachBaseContext 和 onCreate 函數。因此,殼要想完成應用中加固代碼的解密以及應用執行權的交付就都是在這兩個函數上做文章。
加殼應用運行流程:
→ AMS 發起啟動
→ Zygote fork → ActivityThread.main
→ ActivityThread.handleBindApplication()
→ 殼Application.attachBaseContext(base)? 解密 dex? 自定義 ClassLoader 加載解密 dex? 反射設置 LoadedApk.mClassLoader 為自定義 ClassLoader
→ 殼Application.onCreate()? 反射生成真實的 Application 對象? 反射替換 ActivityThread.mInitialApplication 為真實 Application 對象? 已進入原始 app 世界
完整源碼
開源地址:https://github.com/CYRUS-STUDIO/AndroidExample