ClassLoader和动态加载

xiaoeryu Lv5

本章是加壳的一些前置原理,主要是基于Android8.0中ClassLoader的双亲委派模式原理和代码验证以及动态加载的代码验证

类加载器ClassLoader

Android的dalvik和art虚拟机都是继承于JVM的一种实现,是基于寄存器来实现的,这是和JVM不同的点

  • JVM的类加载器包括三种:每一个作用都是不一样的

    1. Bootstrap ClassLoader(引导类加载器):C/C++代码实现的加载器,用于加载指定的JDK的核心类库,比如java.lang、java.uti。等这些系统类。java虚拟机的启动就是通过Bootstrap,该ClassLoader在java里无法获取,负责加载*/lib*下的类,这些类在java中是没有办法获取到的。

    2. Extensions ClassLoader(扩展类加载器):Java中的实现类为ExtClassLoader,提供了除了系统类之外的额外功能,可以在java里获取,负责加载/lib/ext下的类。

    3. Application ClassLoader(应用程序类加载器):java开发人员接触最多的。Java中的实现类为AppClassLoader是与我们接触最多的类加载器,开发人员写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。

    4. 也可以自定义类加载器,只需要通过继承java.lang.ClassLoadr类的方式来实现自己的类加载器即可。

      • 加载顺序:
        1. Bootstrap ClassLoader
        2. Extension ClassLoader
        3. Application ClassLoader

双亲委派:

  • 双亲委派模式的工作原理是;如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个委托给父类的加载器去执行,如果父类加载器还存在其自己的父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这个就是双亲委派。
  • 为什么要有双亲委派?
    1. 避免重复加载,如果已经加载过一次Class,可以直接读取已经加载的Class
    2. 更加安全,无法自定义类来替代系统的核心类,可以防止核心API库被随意篡改

类加载:

  • 隐式加载:

    创建类的实例

    访问类的静态变量,或者为静态变量赋值

    调用类的静态方法

    使用反射方式来强制创建某个类或接口对应的java.lang.Class对象

    初始化某个类的子类

  • 显示加载:

    使用LoadClass()加载

    使用ForName()加载

  • 在JVM中加载类的步骤

    1. 装载:查找和导入Class文件
    2. 链接:其中解析步骤是可以选择的
      1. 检查:检查载入的class文件数据的正确性
      2. 准备:给类的静态变量分配存储空间
      3. 解析:将符号引用转成直接引用
    3. 初始化:即调用函数,对静态变量、静态代码块执行初始化工作

ClassLoader的继承关系:InMemoryDexClassLoader为Android8.0新引入的ClassLoader

  • Android系统中与ClassLoader相关的一共有8个:

    ClassLoader为抽象类;

    BootClassLoader预加载常用类,单例模式。与Java中的BootClassLoader不同,他并不是由C/C++代码实现,而是由Java实现的;

    BaseDexClassLoader是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。

    SecureClassLoader继承了抽象类ClassLoader,扩展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源。

    其中重点关注的是PathClassLoader和DexClassLoader。

    PathClassLoader是Android默认使用的类加载器,一个apk中的Activity等类便是在其中加载。

    DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现插件化、热修复以及dex加壳的重点。

    Android8.0新引入InMemroyDexClassLoader,从名字就可以看出是用于直接从内存中加载dex。

用代码验证一下是否能获取到父类加载器:编码使用kotlin

package com.example.classloadertest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        testClassLoader()
    }
    public fun testClassLoader() {
        var thisClassloader = MainActivity::class.java.classLoader // 获取当前类的类加载器
        Log.i("kanxue","thisClassLoader: " + thisClassloader)
        var parentClassloader = thisClassloader.parent // 获取父类加载器
        while (parentClassloader != null){
            Log.i("kanxue","this: " + thisClassloader + "..." + parentClassloader)
            val tmpClassloader = parentClassloader.parent // 获取父类加载器的父类加载器
            thisClassloader = parentClassloader // 更新当前类加载器为父类加载器
            parentClassloader = tmpClassloader // 更新父类加载器为父类加载器的父类加载器
        }
        Log.i("kanxue","root: " + thisClassloader) // 输出根类加载器
    }


}

打印结果:

thisClassLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.classloadertest-xAcs1S3DFPHFAwYBO7fLbA==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.classloadertest-xAcs1S3DFPHFAwYBO7fLbA==/lib/arm64, /system/lib64, /vendor/lib64, /system/product/lib64]]]
this: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.classloadertest-xAcs1S3DFPHFAwYBO7fLbA==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.classloadertest-xAcs1S3DFPHFAwYBO7fLbA==/lib/arm64, /system/lib64, /vendor/lib64, /system/product/lib64]]]...java.lang.BootClassLoader@29144af
root: java.lang.BootClassLoader@29144af

小结:

  • 这里我们简单的验证了一下双亲验证的关系,这是非常重要的一个点。如果双亲委派没有搞好,开发插件的时候当中的类会出现notfoundClass的问题,或者我们使用四大组件中的activity、service出现系统没有管理,不能正常工作,可能都是这个ClassLoader这个环节的问题。
  • 如果写xposed插件的话会经常用到这个
  • frida框架的话会自动帮我们处理好这个问题(会通过反射帮我们找到最终app所在的classloader)

接下来再写代码验证一下动态加载dex

  • loaddex01

    package com.kanxue.loaddex01
    
    import android.content.Context
    import android.content.pm.PackageManager
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import dalvik.system.DexClassLoader
    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // 获取外部存储的读取权限状态
            val readPermission = checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
            if (readPermission != PackageManager.PERMISSION_GRANTED) {
                // 请求外部存储的读取权限
                requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE)
            } else {
                // 已有读取权限,可以加载 DEX 文件
                val appContext = this.applicationContext
                val dexFilePath = "/sdcard/1.dex" // 这里替换为你的 DEX 文件路径
                testDexClassLoader(appContext, dexFilePath)
            }
        }
    
        // 权限请求回调
        override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults) // 调用父类的方法
    
            if (requestCode == PERMISSION_REQUEST_CODE) {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    val appContext = this.applicationContext
                    val dexFilePath = "/sdcard/1.dex" // 这里替换为你的 DEX 文件路径
                    testDexClassLoader(appContext, dexFilePath)
                } else {
                    // 权限被拒绝,你可以在这里处理相应的操作,如给用户一个提示等
                    Log.i("TAG", "权限请求被拒绝")
                }
            }
        }
    
    
        private fun testDexClassLoader(context: Context, dexFilePath: String) {
            val optFile = context.getDir("opt_dex", 0)
            val libFile = context.getDir("lib_path", 0)
    
            var dexClassLoader: DexClassLoader? = null
    
            try {
                // 创建 DexClassLoader,用于从 DEX 文件加载类和资源
                dexClassLoader = DexClassLoader(
                    dexFilePath,
                    optFile.absolutePath,
                    libFile.absolutePath,
                    context.classLoader
                )
    
                // 加载类并调用方法
                val loadedClass = dexClassLoader.loadClass("com.kanxue.test.TestClass")
                val instance = loadedClass.getDeclaredConstructor().newInstance()
                val method = loadedClass.getDeclaredMethod("testFunc")
                method.invoke(instance)
            } catch (e: ClassNotFoundException) {
                e.printStackTrace()
                // 处理类未找到异常
            } catch (e: Exception) {
                e.printStackTrace()
                // 处理其他异常
            }
        }
    
        companion object {
            private const val PERMISSION_REQUEST_CODE = 1
        }
    }
    
  • TestClass:构建一个apk,把它里面的dex拿出来用作动态加载

    package com.kanxue.test
    
    import android.util.Log
    
    public class TestClass {
        public fun testFunc() {
            Log.i("kanxue","i an from com.example.test.TestClass.testFunc")
        }
    }
    
  • 执行结果

    • 执行的时候第一次加载dex是失败的因为dex没有加载进去,重新运行第二次就好了。
  • 小结:

    • 这个项目有一些需要注意的点

      1. 需要在loaddex01项目的AndroidManifest.xml中配置外部存储读写权限

        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
        
      2. 需要修改build.gradle文件配置

        android {
            ...
            compileSdk 33	// 新生成的项目是32需要修改到33或33以上
            }
        
      3. 然后就是权限问题了,需要在loaddex01的代码中实时获取权限,不然在Android8.0中还是读取不到我们放在外部存储卡的dex文件。

  • 标题: ClassLoader和动态加载
  • 作者: xiaoeryu
  • 创建于 : 2023-08-19 10:32:29
  • 更新于 : 2023-08-26 21:50:42
  • 链接: https://github.com/xiaoeryu/2023/08/19/ClassLoader和动态加载/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
ClassLoader和动态加载