ClassLoader和动态加载
本章是加壳的一些前置原理,主要是基于Android8.0中ClassLoader的双亲委派模式原理和代码验证以及动态加载的代码验证
类加载器ClassLoader
Android的dalvik和art虚拟机都是继承于JVM的一种实现,是基于寄存器来实现的,这是和JVM不同的点
JVM的类加载器包括三种:每一个作用都是不一样的
Bootstrap ClassLoader(引导类加载器):C/C++代码实现的加载器,用于加载指定的JDK的核心类库,比如java.lang、java.uti。等这些系统类。java虚拟机的启动就是通过Bootstrap,该ClassLoader在java里无法获取,负责加载*/lib*下的类,这些类在java中是没有办法获取到的。
Extensions ClassLoader(扩展类加载器):Java中的实现类为ExtClassLoader,提供了除了系统类之外的额外功能,可以在java里获取,负责加载/lib/ext下的类。
Application ClassLoader(应用程序类加载器):java开发人员接触最多的。Java中的实现类为AppClassLoader是与我们接触最多的类加载器,开发人员写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
也可以自定义类加载器,只需要通过继承java.lang.ClassLoadr类的方式来实现自己的类加载器即可。
- 加载顺序:
- Bootstrap ClassLoader
- Extension ClassLoader
- Application ClassLoader
- 加载顺序:
双亲委派:
- 双亲委派模式的工作原理是;如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个委托给父类的加载器去执行,如果父类加载器还存在其自己的父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这个就是双亲委派。
- 为什么要有双亲委派?
- 避免重复加载,如果已经加载过一次Class,可以直接读取已经加载的Class
- 更加安全,无法自定义类来替代系统的核心类,可以防止核心API库被随意篡改
类加载:
隐式加载:
创建类的实例
访问类的静态变量,或者为静态变量赋值
调用类的静态方法
使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
初始化某个类的子类
显示加载:
使用LoadClass()加载
使用ForName()加载
在JVM中加载类的步骤
- 装载:查找和导入Class文件
- 链接:其中解析步骤是可以选择的
- 检查:检查载入的class文件数据的正确性
- 准备:给类的静态变量分配存储空间
- 解析:将符号引用转成直接引用
- 初始化:即调用
函数,对静态变量、静态代码块执行初始化工作
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没有加载进去,重新运行第二次就好了。
小结:
这个项目有一些需要注意的点
需要在loaddex01项目的AndroidManifest.xml中配置外部存储读写权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
需要修改build.gradle文件配置
android { ... compileSdk 33 // 新生成的项目是32需要修改到33或33以上 }
然后就是权限问题了,需要在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 进行许可。