首先借助一张网图了解apk的编译流程
我们的字节码插桩就发生在javac编译生成class文件后,修改内容,合成.dex文件
ScanAllFileTransform 文件解读----- 1.其实主要看 transform 方法 ,在该方法中我们可以扫描所有javac编译后生成的class文件。 2.主要分为两个循环,一个是当前模块下的内容,一个是它所依赖的所有模块文件,或jar包等 3.根据匹配规则,找寻我们需要被插桩使用的class文件
AppAroutCodeInjector 文件解读----- 1.获取将要进行修改的文件,在我这个例子里是:ARouterUtils.class 2.通过ASM动态注入字节码,自定义AppAroutClassVisitor类继承ClassVisitor,找寻插桩位置 3.找到插桩点后 定义LoadAppLikeMethodAdapter类继承AdviceAdapter方法,进行Asm的字节码规则操作
我们的难点在于ASM如何使用,对于这一点官方提供了jar包工具,帮助我们将java文件转化为asm的字节码,这时候就根据规则编写就行
Asm字节码插桩工具的使用:
通过编写代码然后生成class文件 ,利用jar包将class文件生成所需的asm文件代码:
//Asm的使用:https://www.jianshu.com/p/905be2a9a700
* 使用asm提供的通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载 asm-all.jar 库,我下载的是最新的 asm-all-5.2.jar,然后使用如下命令,即可生成
* 命令:
* java -classpath E:\googleDowmload\asm-all-5.1.jar org.objectweb.asm.util.ASMifier E:\MyLearing\MyAptUseLearing\base-arouter\build\tmp\kotlin-classes\debug\com\example\base_arouter\ARouterUtils.class
*
* jar包下载地址:
* http://nexus.neeveresearch.com/nexus/content/repositories/public/org/ow2/asm/asm-all/5.1/
后续是所有代码:详细解释看代码
MyPlugin
package com.example.arouterplugin;
import com.android.build.gradle.AppExtension;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
/**
* @author tgw
* @date 2021/10/16
* @describe
*
* //Asm的使用:https://www.jianshu.com/p/905be2a9a700
* 使用asm提供的通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载 asm-all.jar 库,我下载的是最新的 asm-all-5.2.jar,然后使用如下命令,即可生成
* 命令:
* java -classpath E:\googleDowmload\asm-all-5.1.jar org.objectweb.asm.util.ASMifier E:\MyLearing\MyAptUseLearing\base-arouter\build\tmp\kotlin-classes\debug\com\example\base_arouter\ARouterUtils.class
*
* jar包下载地址:
* http://nexus.neeveresearch.com/nexus/content/repositories/public/org/ow2/asm/asm-all/5.1/
*/
class MyPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
AppExtension android = (AppExtension) project.getExtensions().getByType(AppExtension.class);
android.registerTransform(new ScanAllFileTransform(project));
System.out.println("MyPlugin自定义独立插件2.................");
}
}
ScanUtil
package com.example.arouterplugin
import java.util.jar.JarEntry
import java.util.jar.JarFile
class ScanUtil {
//扫描directoryInput用到的 这两个条件用于判断是否是我们apt生成的文件,
// 这里排除了前面的路径 eg:
static final PROXY_CLASS_PREFIX ="/com/example/myaptuselearing/routes/"
static final PROXY_CLASS_SUFFIX = "RouteInterfaceImp.class"
//扫描jar文件所用到的 注意class文件名中的包名是以“/”分隔开,而不是“.”分隔的,这个包名是我们通过APT生成的代理类的包名
static final PROXY_CLASS_PACKAGE_NAME = "com/example/myaptuselearing/routes/"
//ARouterUtils是应用生命周期框架初始化方法调用类
static final REGISTER_CLASS_FILE_NAME = "com/example/base_arouter/ARouterUtils.class"
//包含生命周期管理初始化类的文件,即包含 com.example.arouterb
// 扫描结束后,我们会生成注册代码到这个文件ARouterUtils类的class文件或者jar文件
static File FILE_CONTAINS_INIT_CLASS
/**
* 判断该class是否是我们的apt生成的目标类
*
* @param file
* @return
*/
static boolean isTargetProxyClass(String filePath) {
if (filePath.startsWith(PROXY_CLASS_PREFIX) && filePath.endsWith(PROXY_CLASS_SUFFIX)) {
return true
}
return false
}
/**
* 扫描jar包里的所有class文件:
* 1.通过包名识别所有需要注入的类名
* 2.找到AppLifeCycleManager类所在的jar包,后面我们会在该jar包里进行代码注入
*
* @param jarFile
* @param destFile
* @return
*/
static List<String> scanJar(File jarFile, File destFile) {
def file = new JarFile(jarFile)
Enumeration<JarEntry> enumeration = file.entries()
List<String> list = null
while (enumeration.hasMoreElements()) {
//遍历这个jar包里的所有class文件项
JarEntry jarEntry = enumeration.nextElement()
//class文件的名称,这里是全路径类名,包名之间以"/"分隔
String entryName = jarEntry.getName()
println "tgw2 file scanJar 包中的文件名: = "+entryName
if (entryName == REGISTER_CLASS_FILE_NAME) {
//标记这个jar包包含 ARouterUtils.class
//扫描结束后,我们会生成注册代码到这个文件里
FILE_CONTAINS_INIT_CLASS = destFile
} else {
//通过包名来判断,严谨点还可以加上类名前缀、后缀判断
//通过APT生成的类,都有统一的前缀、后缀
if (entryName.startsWith(PROXY_CLASS_PACKAGE_NAME)) {
if (list == null) {
list = new ArrayList<>()
}
String classPath = entryName.substring(entryName.lastIndexOf("/") + 1)
println "tgw2 file scanJar APT生成的类: = "+classPath
list.addAll(classPath)
}
}
}
return list
}
static boolean shouldProcessPreDexJar(String path) {
//&& !path.contains("androidx.appcompat.")
return !path.contains("com.android.support") && !path.contains("/android/m2repository")
}
}
ScanAllFileTransform
package com.example.arouterplugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
import org.apache.commons.io.FileUtils
import org.apache.commons.codec.digest.DigestUtils
/**
* @author tgw* @date 2021/10/13
* @describe Gradle Transform
* <p>
* 然而前面这个插件MyPlugin并没有什么卵用,它仅仅只是在编译时,在
* 控制台打印一句话而已。那么怎么通过插件在打包前去扫描所有的class文件呢
* ,幸运的是官方给我们提供了 Gradle Transform技术,
* 简单来说就是能够让开发者在项目构建阶段即由class到dex转换期间修改class文件,
* Transform阶段会扫描所有的class文件和资源文件,具体技术我这里不详细展开,下面通过伪代码部分说下我的思路。
* 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
* <p>
* //要继承Transform 需要在本 模块下的build.gradle中依赖
* implementation "com.android.tools.build:gradle:4.1.0"
*/
class ScanAllFileTransform extends Transform {
Project project
ScanAllFileTransform(Project project) {
this.project = project
println "tgw start to tScanAllFileTransform"
System.out.println("MyPlugin自定义独立插件1>>>>>>>>>>>>>>>>")
}
//该Transform的名称,自定义即可,只是一个标识
@Override
String getName() {
return ScanAllFileTransform.getSimpleName()
}
//该Transform支持扫描的文件类型,分为class文件和资源文件,我们这里只处理class文件的扫描
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
//Transfrom的扫描范围,我这里扫描整个工程,包括当前module以及其他jar包、aar文件等所有的class
/**
* transform输入的文件所属的范围
* 值Transform 的作用范围,有一下7种类型:
* 1.EXTERNAL_LIBRARIES 只有外部库
* 2.PROJECT 只有项目内容
* 3.PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* 4.PROVIDED_ONLY 只提供本地或远程依赖项
* 5.SUB_PROJECTS 只有子项目
* 6.SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)
* 7.TESTED_CODE 由当前变量(包括依赖项)测试的代码
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
//是否增量扫描
@Override
boolean isIncremental() {
return true
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
//是否增量编译
boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
println "\ntgw1 是否增量编译-------------->>>>>>>" + isIncremental
//输入类型为消费类型的,需要设置输出,作为下个transform的输入
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
assert outputProvider != null
//不需要增量编译,先清除全部
if (!isIncremental)
outputProvider.deleteAll()
// URLClassLoader urlClassLoader = ClassLoaderHelper.getClassLoader(transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), project);
// if (getAbstractProcessor() != null)
// getAbstractProcessor().setClassLoader(urlClassLoader);
def appLikeProxyClassList = []
//inputs就是所有扫描到的class文件或者是jar包,一共2种类型
for (TransformInput input : transformInvocation.getInputs()) {
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
println (" tgw1 目录扫描开始")
//Transform扫描的class文件是输入文件(input),有输入必然会有输出(output),处理完成后需要将输入文件拷贝到一个输出目录下去,
//后面打包将class文件转换成dex文件时,直接采用的就是输出目录下的class文件了。
//必须这样获取输出路径的目录名称
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
//用于检测路径是否为检测斜杠
boolean leftSlash = File.separator == '/'
//递归扫描该目录下所有的class文件
if (directoryInput.file.isDirectory()) {
String root = directoryInput.file.absolutePath
println "tgw directoryInput 的路径: = " + root
directoryInput.file.eachFileRecurse { File file ->
//排除E:\MyLearing\MyAptUseLearing\app\build\intermediates\javac\debug\classes\com\example\myaptuselearing\VerifyCustomAnnotationActivity$$BindAdapterImp.class
//像上面路径的前一段 只保留和包名相关的 com\example\myaptuselearing\VerifyCustomAnnotationActivity$$BindAdapterImp.class
def path = file.absolutePath.replace(root, '')
//如果不是斜杠(/) 则将反斜杠(\)替换为斜杠(/)
if (!leftSlash) {
path = path.replaceAll("\\", "/")
}
//形如 Heima$$****$$Proxy.class 的类,是我们要找的目标class,直接通过class的名称来判断,也可以再加上包名的判断,会更严谨点
println "tgw1 directoryInput 目录下的文件的路径: = " + file.absolutePath
println "tgw1 directoryInput 目录下的文件的名称: = " + file.name
println "tgw1 directoryInput 筛选后的路径: = " + path
if (file.isFile() && ScanUtil.isTargetProxyClass(path)) {
println "tgw1 file name: 生产的自己代理类文件全路径: " + file.absolutePath
println "tgw1 file name: 生产的自己代理类文件包名路径: " + path
//如果是我们自己生产的代理类,保存该类的类名
appLikeProxyClassList.add(file.name)
}
}
}
// copy to dest
FileUtils.copyDirectory(directoryInput.file, dest)
}
for (JarInput jarInput : input.getJarInputs()) {
println (" tgw2 JarInput jar 包扫描: "+jarInput.getFile().getAbsolutePath() )
//与处理class文件一样,处理jar包也是一样,最后要将inputs转换为outputs
def jarName = jarInput.name
def md5 = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
//获取输出路径下的jar包名称,必须这样获取,得到的输出路径名不能重复,否则会被覆盖
def dest = outputProvider.getContentLocation(jarName + md5, jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
File src = jarInput.file
println (" tgw2 JarI 文件路径: "+src.getAbsolutePath())
//先简单过滤掉 support-v4 之类的jar包,只处理有我们业务逻辑的jar包
if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
//扫描jar包的核心代码在这里,主要做2件事情:
//1.扫描该jar包里有没有实现IAppLike接口的代理类;
//2.扫描AppLifeCycleManager这个类在哪个jar包里,并记录下来,后面需要在该类里动态注入字节码;
List<String> list = ScanUtil.scanJar(src, dest)
if (list != null) {
appLikeProxyClassList.addAll(list)
}
}
}
//将输入文件拷贝到输出目录下
FileUtils.copyFile(jarInput.file, dest)
}
}
println ""
appLikeProxyClassList.forEach({ fileName ->
println "tgw file name = " + fileName
})
println "\n包含AppLifeCycleManager类的jar文件"
println "需要进行插桩的文件:"+ScanUtil.FILE_CONTAINS_INIT_CLASS.getAbsolutePath()
println "开始自动注册"
//1.通过前面的步骤,我们已经扫描到所有实现了 IAppLike接口的代理类;
//2.后面需要在 AppLifeCycleManager 这个类的初始化方法里,动态注入字节码;
//3.将所有 IAppLike 接口的代理类,通过类名进行反射调用实例化
//这样最终生成的apk包里,AppLifeCycleManager调用init()方法时,已经可以加载所有组件的生命周期类了
new AppAroutCodeInjector(appLikeProxyClassList).execute()
println "transform finish0----------------<<<<<<<\n"
}
}
AppAroutCodeInjector
package com.example.arouterplugin
import org.objectweb.asm.*
import org.apache.commons.io.IOUtils
import org.objectweb.asm.commons.AdviceAdapter
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
class AppAroutCodeInjector {
//扫描出来的所有 IAppLike 类
List<String> proxyAppLikeClassList
AppAroutCodeInjector(List<String> list) {
proxyAppLikeClassList = list
}
void execute() {
println("开始执行ASM方法======>>>>>>>>")
File srcFile = ScanUtil.FILE_CONTAINS_INIT_CLASS
println("tgw3 开始执行ASM方法 操作的文件路径:"+srcFile.getAbsoluteFile())
//创建一个临时jar文件,要修改注入的字节码会先写入该文件里
def optJar = new File(srcFile.getParent(), srcFile.name + ".opt")
if (optJar.exists())
optJar.delete()
def file = new JarFile(srcFile)
Enumeration<JarEntry> enumeration = file.entries()
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = file.getInputStream(jarEntry)
jarOutputStream.putNextEntry(zipEntry)
//找到需要插入代码的class,通过ASM动态注入字节码
if (ScanUtil.REGISTER_CLASS_FILE_NAME == entryName) {
println "tgw3 insert register code to class >> " + entryName
ClassReader classReader = new ClassReader(inputStream)
// 构建一个ClassWriter对象,并设置让系统自动计算栈和本地变量大小
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new AppAroutClassVisitor(classWriter)
//开始扫描class文件
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
byte[] bytes = classWriter.toByteArray()
//将注入过字节码的class,写入临时jar文件里
jarOutputStream.write(bytes)
} else {
//不需要修改的class,原样写入临时jar文件里
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
inputStream.close()
jarOutputStream.closeEntry()
}
jarOutputStream.close()
file.close()
//删除原来的jar文件
if (srcFile.exists()) {
srcFile.delete()
}
//重新命名临时jar文件,新的jar包里已经包含了我们注入的字节码了
optJar.renameTo(srcFile)
}
//插入字节码的逻辑,都在这个类里面
class AppAroutClassVisitor extends ClassVisitor {
AppAroutClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor)
}
@Override
MethodVisitor visitMethod(int access, String name,
String desc, String signature,
String[] exception) {
println "tgw3 visit method: " + name
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exception)
//找到 AppLifeCycleManager里的loadAppLike()方法,我们在这个方法里插入字节码
if ("loadRouterMap" == name) {
mv = new LoadAppLikeMethodAdapter(mv, access, name, desc)
}
return mv
}
}
class LoadAppLikeMethodAdapter extends AdviceAdapter {
LoadAppLikeMethodAdapter(MethodVisitor mv, int access, String name, String desc) {
super(Opcodes.ASM5, mv, access, name, desc)
}
@Override
protected void onMethodEnter() {
super.onMethodEnter()
println "tgw3 -------onMethodEnter------"
mv.visitCode();
//遍历插入字节码,其实就是在 loadAppLike() 方法里插入类似registerAppLike("");的字节码
proxyAppLikeClassList.forEach({ proxyClassName ->
println "tgw3 开始注入代码:${proxyClassName}"
def fullName = ScanUtil.PROXY_CLASS_PACKAGE_NAME + proxyClassName.substring(0, proxyClassName.length() - 6)
println "tgw3 注入代码 full classname = ${fullName}"
mv.visitTypeInsn(NEW, fullName);
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, fullName, "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, ScanUtil.REGISTER_CLASS_FILE_NAME.substring(0, ScanUtil.REGISTER_CLASS_FILE_NAME.length() - 6), "mRouterMap", "Ljava/util/Map;");
mv.visitMethodInsn(INVOKEVIRTUAL, fullName, "register", "(Ljava/util/Map;)V", false);
})
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
//
// mv.visitCode();
// mv.visitTypeInsn(NEW, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp");
// mv.visitInsn(DUP);
// mv.visitMethodInsn(INVOKESPECIAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "<init>", "()V", false);
// mv.visitVarInsn(ALOAD, 0);
// mv.visitFieldInsn(GETFIELD, "com/example/base_arouter/ARouterUtils", "mRouterMap", "Ljava/util/Map;");
// mv.visitMethodInsn(INVOKEVIRTUAL, "com/example/myaptuselearing/routes/ArouterActivityARouteInterfaceImp", "register", "(Ljava/util/Map;)V", false);
// mv.visitInsn(RETURN);
// mv.visitMaxs(2, 1);
// mv.visitEnd();
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
println "-------onMethodEnter------"
}
}
}