Android dex动态加载后,启动Activity解决方案

前言

Android dex 动态加载(Kotlin 版)一文中我们讲解了如何 dex 动态加载,以及如何通过反射执行 dex 中的方法。
在进一步的研究中发现,直接调用 dex 中的方法startActivity会报错。
今天我们来一步步分析一下原因,并给出启动 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的地方,证明我们的推断没错。

扫描二维码关注公众号,回复: 16052941 查看本文章
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(),看一下源码。可以得知这个ClassLoaderLoadedApk提供的。

@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

LoadApk.java 源码

LoadedApk 是 apk 安装文件在内存中的数据,可以在 LoadedApk 中得到所有封装在 apk 文件中的信息如:

  1. 代码
  2. 资源文件
  3. Activity, Service 等组件
  4. Manifest 配置文件

4.原因

其实这里我们已经可以知道崩溃的原因:

虽然我们在主工程的Manifest中配置了测试 Activity,由于我们是在 App 启动后动态加载启动 dex 中的 Activity。此时的LoadedApk只包含了 app 自己class.dexassets/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,正是LoadedApkClassLoader
此时调用loadDexClass生成的 ClassLoader 既包含 app 的 dex,也包含assets/dexlib_dex.jar中的 dex。
也就是说如果我们把这个 ClassLoader 替换成LoadedApkClassLoader,再调用 startActivity 便不会再报错。

替换LoadedApkClassLoader

利用反射,替换 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 调试

再次编译启动测试,可以正常打开,问题解决。

源码

文中涉及源码如下:
https://github.com/DeMonDemoSpace/DexDynamicLoad

参考

Activity 的启动流程

【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( 替换 LoadedApk 中的类加载器 | 加载 DEX 文件中的 Activity 类并启动成功 )

猜你喜欢

转载自blog.csdn.net/DeMonliuhui/article/details/128567647