序文
Android dex の動的読み込み (Kotlin 版)の記事では、 dex を動的に読み込む方法と、リフレクションを通じて dex 内のメソッドを実行する方法について説明しました。
さらに調査を進めると、dex でメソッドを直接呼び出すとstartActivity
エラーが報告されることがわかりました。
今日はその理由を段階的に分析し、dex でアクティビティを開始するための解決策を示します。
問題分析
1. テスト関連のコードを追加する
テスト活動
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val text = TextView(this)
setContentView(text)
val sb: StringBuilder = StringBuilder()
sb.append("TestActivity\n")
intent.extras?.run {
keySet().forEach {
sb.append("$it=${
get(it)}\n")
}
}
text.text = sb.toString()
}
}
テスト開始 アクティビティ方法
object DexInit {
//...
@JvmStatic
fun start() {
val intent = Intent(mContext, TestActivity::class.java)
if (mContext !is Activity) {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
mContext.startActivity(intent)
}
}
Androidマニフェストの登録
jarパッケージはインクルードできないため、AndroidManifest.xml
メインプロジェクトにテスト用のActivityを登録する必要がありますAndroidManifest.xml
。
<!--注册dex中的Activity,全类名注册
不影响编译&打包,无视报错即可-->
<activity android:name="com.demon.dexlib.TestActivity"/>
テストコードを呼び出す
dex が動的にロードされ、リフレクションを通じて初期化され、呼び出した後DexInit.start()
Utils.copyDex(this, dexName)
Utils.loader = Utils.loadDexClass(this, dexName)
/**
* 初始化
*/
val cla = Utils.loader?.loadClass("com.demon.dexlib.DexInit")
cla?.run {
getDeclaredMethod("init", Context::class.java, String::class.java, String::class.java).invoke(
null, this@MainActivity.applicationContext,
"https://idemon.oss-cn-guangzhou.aliyuncs.com/luffy.jpg",
"https://www.baidu.com/"
)
findViewById<Button>(R.id.btn7).setOnClickListener {
getDeclaredMethod("start").invoke(null)
}
}
デバッグ デバッグ
ボタンをクリックしてアクティビティを開始し、クラッシュを見つけてエラーを報告します。コアエラーの原因java.lang.ClassNotFoundException: Didn't find class "com.demon.dexlib.TestActivity"
2. 原因分析
AndroidStudio を使用してコンパイルされた APK を開くと、dex パッケージにそれが含まれていることがわかりTestActivity
、他のメソッド呼び出しが正常であるという前提で通常どおり使用できますTestActivity
。
次のように、コンソールを開いてエラー メッセージを表示します (スペースの都合上、一部の重要でない情報は削除されています)。
Process: com.demon.dexdynamicload, PID: 12977
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{
com.demon.dexdynamicload/com.demon.dexlib.TestActivity}: java.lang.ClassNotFoundException: Didn't find class "com.demon.dexlib.TestActivity"
...
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4048)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4312)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
...
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.demon.dexlib.TestActivity"
...
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:259)
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:45)
at android.app.Instrumentation.newActivity(Instrumentation.java:1328)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4035)
...
アクティビティの起動プロセスに精通している人は、アクティビティを最終的に起動するための中心的なメソッドが であることを知っていますActivityThread.performLaunchActivity
。
簡略化されたperformLaunchActivity
コアのソース コードは次のとおりです。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//获取ActivityInfo类
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
//获取APK文件的描述类LoadedApk
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
// 启动的Activity的ComponentName类
ComponentName component = r.intent.getComponent();
//创建要启动Activity的上下文环境
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
//获取类加载器
java.lang.ClassLoader cl = appContext.getClassLoader();
//用类加载器来创建该Activity的实例
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
// ...
} catch (Exception e) {
// ...
}
try {
// 创建Application
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
// 初始化Activity
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);
...
// 调用Instrumentation的callActivityOnCreate方法来启动Activity
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
}
...
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
...
}
return activity;
}
performLaunchActivity
ソース コードを見ると、この中心的なプロセスは次のとおりであることがわかります。
获取LoadedApk-->获取类加载器ClassLoader-->获取Activity实例-->初始化Activity-->执行Activity的onCreate方法
エラー メッセージと組み合わせるとClassLoader.loadClass
、このステップでエラーが報告されたと大まかに推測できます获取Activity实例
。
//用类加载器来创建该Activity的实例
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
私たちはコードを入力するためにこの場所を探しnewActivity
、それを見つけinstantiateActivity
、loadClass
電話がかけられた場所を見つけて、私たちの推論が正しかったことを証明しました。
public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();
}
この ClassLoader を取得する方法は、getClassLoader()
ソース コードを確認することです。ClassLoader
が提供されていることが分かりますLoadedApk
。
@UnsupportedAppUsage
final @NonNull LoadedApk mPackageInfo;
@Override
public ClassLoader getClassLoader() {
return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
LoadedApkcreateApplicationContext
は で割り当てられます。つまり、アプリの起動時に LoadedApk が生成されます。
3.LoadedApkとは何ですか
LoadedApk はメモリ内の APK インストール ファイルのデータです。LoadedApk では、APK ファイルにカプセル化されている次のようなすべての情報を取得できます。
- コード
- リソース
- アクティビティ、サービス、その他のコンポーネント
- マニフェスト構成ファイル
4. 理由
実際、ここでクラッシュの原因をすでに知ることができます。
メインプロジェクトManifest
でテストアクティビティを設定しましたが、アプリの起動後にdexでアクティビティを動的にロードして開始するためです。現時点では、LoadedApk
アプリ自体だけが含まれておりclass.dex
、assets/dexlib_dex.jar
それはそのリソース ファイルにすぎず、テスト アクティビティを見つけることはできません。
解決
上記の一連の分析を通じて、問題の原因がわかりました。
ソリューション
従来の dex の動的ロードのコードを見てみましょう。
/**
* 加载dex
*/
fun loadDexClass(context: Context, dexName: String): DexClassLoader? {
try {
//下面开始加载dex class
//1.待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限,
//2.解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写
//3.指向包含本地库(so)的文件夹路径,可以设为null
//4.父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
val cacheFile = File(context.filesDir, "dex")
val internalPath = cacheFile.absolutePath + File.separator + dexName
return DexClassLoader(internalPath, cacheFile.absolutePath, null, context.classLoader)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
DexClassLoader
インスタンス化の最後のパラメータが親クラス ローダーに渡されていることがわかりますcontext.classLoader
。これはLoadedApk
正しいですClassLoader
。
このとき、loadDexClass
呼び出しによって生成された ClassLoader には、アプリの dex とassets/dexlib_dex.jar
アプリの dex の両方が含まれます。
つまり、この ClassLoader を に置き換えて startActivity を再度呼び出してもLoadedApk
、ClassLoader
エラーは報告されません。
交換されましたLoadedApk
_ClassLoader
リフレクションを使用して、LoadedApk のクラスローダー ClassLoader を置き換えます。
/**
* 替换 LoadedApk 中的 类加载器 ClassLoader
*
* @param context
* @param loader 动态加载dex的ClassLoader
*/
fun replaceLoadedApkClassLoader(context: Context, loader: DexClassLoader) {
// I. 获取 ActivityThread 实例对象
// 获取 ActivityThread 字节码类 , 这里可以使用自定义的类加载器加载
// 原因是 基于 双亲委派机制 , 自定义的 DexClassLoader 无法加载 , 但是其父类可以加载
// 即使父类不可加载 , 父类的父类也可以加载
var activityThreadClass: Class<*>? = null
try {
activityThreadClass = loader.loadClass("android.app.ActivityThread")
} catch (e: ClassNotFoundException) {
e.printStackTrace()
}
// 获取 ActivityThread 中的 sCurrentActivityThread 成员
// 获取的字段如下 :
// private static volatile ActivityThread sCurrentActivityThread;
// 获取字段的方法如下 :
// public static ActivityThread currentActivityThread() {return sCurrentActivityThread;}
var currentActivityThreadMethod: Method? = null
try {
currentActivityThreadMethod = activityThreadClass?.getDeclaredMethod("currentActivityThread")
// 设置可访问性 , 所有的 方法 , 字段 反射 , 都要设置可访问性
currentActivityThreadMethod?.isAccessible = true
} catch (e: NoSuchMethodException) {
e.printStackTrace()
}
// 执行 ActivityThread 的 currentActivityThread() 方法 , 传入参数 null
var activityThreadObject: Any? = null
try {
activityThreadObject = currentActivityThreadMethod?.invoke(null)
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: InvocationTargetException) {
e.printStackTrace()
}
// II. 获取 LoadedApk 实例对象
// 获取 ActivityThread 实例对象的 mPackages 成员
// final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
var mPackagesField: Field? = null
try {
mPackagesField = activityThreadClass?.getDeclaredField("mPackages")
// 设置可访问性 , 所有的 方法 , 字段 反射 , 都要设置可访问性
mPackagesField?.isAccessible = true
} catch (e: NoSuchFieldException) {
e.printStackTrace()
}
// 从 ActivityThread 实例对象 activityThreadObject 中
// 获取 mPackages 成员
var mPackagesObject: ArrayMap<String, WeakReference<Any>>? = null
try {
mPackagesObject = mPackagesField?.get(activityThreadObject) as ArrayMap<String, WeakReference<Any>>?
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
// 获取 WeakReference<LoadedApk> 弱引用对象
val weakReference: WeakReference<Any>? = mPackagesObject?.get(context.packageName)
// 获取 LoadedApk 实例对象
val loadedApkObject = weakReference?.get()
// III. 替换 LoadedApk 实例对象中的 mClassLoader 类加载器
// 加载 android.app.LoadedApk 类
var loadedApkClass: Class<*>? = null
try {
loadedApkClass = loader.loadClass("android.app.LoadedApk")
} catch (e: ClassNotFoundException) {
e.printStackTrace()
}
// 通过反射获取 private ClassLoader mClassLoader; 类加载器对象
var mClassLoaderField: Field? = null
try {
mClassLoaderField = loadedApkClass?.getDeclaredField("mClassLoader")
// 设置可访问性
mClassLoaderField?.isAccessible = true
} catch (e: NoSuchFieldException) {
e.printStackTrace()
}
// 替换 mClassLoader 成员
try {
mClassLoaderField?.set(loadedApkObject, loader)
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
移行:
Utils.loader?.run {
Utils.replaceLoadedApkClassLoader(this@MainActivity, this)
}
テストコードを追加する
/**
* 启动Dex中的Activity
* @param context
* @param actClas Activity全绝对路径类名,如com.demon.dexlib.TestActivity
*/
fun startDexActivity(context: Context, actClas: String) {
// 加载Activity类
// 该类中有可执行方法 test()
var clazz: Class<*>? = null
try {
clazz = loader?.loadClass(actClas)
} catch (e: ClassNotFoundException) {
e.printStackTrace()
}
// 启动Activity组件
if (clazz != null) {
val intent = Intent(context, clazz)
intent.putExtra("string", "hello dex~")
intent.putExtra("number", "1024")
context.startActivity(intent)
}
}
findViewById<Button>(R.id.btn6).setOnClickListener {
Utils.startDexActivity(this, "com.demon.dexlib.TestActivity")
}
デバッグ デバッグ
コンパイルしてテストを再度開始すると、正常に開くことができ、問題は解決します。
ソースコード
この記事に含まれるソース コードは次のとおりです:
https://github.com/DeMonDemoSpace/DexDynamicLoad