前言
今天我们来一步步分析一下原因,并给出启动 dex 中的 Activity 的解决方案。
问题分析
1.添加测试相关代码
测试 Activity
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()
}
}
测试启动 Activity 方法
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)
}
}
注册 AndroidManifest
由于 jar 包无法包含AndroidManifest.xml
因此我们需要在主工程的AndroidManifest.xml
中注册测试 Activity。
<!--注册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)
}
}
debug 调试
点击按钮启动 Activity,发现崩溃并报错。核心报错原因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)
...
熟悉Activity 启动流程的知道,最后启动 Activity 的核心方法就是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());
}
LoadedApk 在createApplicationContext
中被赋值,即 App 启动时就生成了 LoadedApk。
3.什么是 LoadedApk
LoadedApk 是 apk 安装文件在内存中的数据,可以在 LoadedApk 中得到所有封装在 apk 文件中的信息如:
- 代码
- 资源文件
- Activity, Service 等组件
- Manifest 配置文件
4.原因
其实这里我们已经可以知道崩溃的原因:
虽然我们在主工程的Manifest
中配置了测试 Activity,由于我们是在 App 启动后动态加载启动 dex 中的 Activity。此时的LoadedApk
只包含了 app 自己class.dex
,assets/dexlib_dex.jar
对于它来说只是一个资源文件,根本不可能找到测试 Activity 的。
解决方案
上面通过一系列分析,我们知道了问题的原因。
解决思路
我们看一下常规动态加载 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 既包含 app 的 dex,也包含assets/dexlib_dex.jar
中的 dex。
也就是说如果我们把这个 ClassLoader 替换成LoadedApk
的ClassLoader
,再调用 startActivity 便不会再报错。
替换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")
}
debug 调试
再次编译启动测试,可以正常打开,问题解决。
源码
文中涉及源码如下:
github.com/DeMonDemoSp…
参考
【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( 替换 LoadedApk 中的类加载器 | 加载 DEX 文件中的 Activity 类并启动成功 )
本文转自 blog.csdn.net/DeMonliuhui…,如有侵权,请联系删除。