在Android进阶宝典 — Handler应用于线上卡顿监控中,我简单介绍了一下关于ASM完结字节码插桩来完结办法耗时的监控,可是其时仅仅找了一个特定的class文件,针对某个特定的办法进行插桩,可是真实的开发中不可能这么做的,由于整个工程中会有成百上千的办法,而且存储的方位也各有不同,这个时分,咱们就需求借助gradle插件来完结ASM字节码插桩。
1 准备工作
凡是涉及到gradle开发,我一般都是会在buildSrc文件夹下进行,还有没有同伴不太了解buildSrc的,其实buildSrc是Android中默许的插件工程,在gradle编译的时分,会编译这个项目并配置到classpath下。这样的话在buildSrc中创立的插件,每个项目都能够引进。
在buildSrc中能够创立groovy目录(假如对groovy或许kotlin了解),也能够创立java目录,关于插件开发个人更便向运用groovy,由于更靠近gradle。
1.1 创立插件
创立插件,需求完结Plugin接口,在引进这个插件后,项目编译的时分,就会履行apply办法。
class ASMPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
def ext = project.extensions.getByType(AppExtension)
if (ext != null){
ext.registerTransform(new ASMTransform())
}
}
}
在apply办法中,能够履行自定义的Task,也能够履行自定义的Transform(其实也能够看做是一种特殊的Task),这儿咱们自定义了插桩相关的Transform。
1.2 创立Transform
什么是Transform呢?便是在class文件打包生成dex文件的过程中,对class字节码做处理,终究生成新的dex文件,那么有什么办法能够对字节码操作呢?ASM是一种办法,运用Javassist也能够织入字节码。
class ASMTransform extends Transform {
@Override
String getName() {
return "ASMTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
inputs.each { input ->
input.directoryInputs.each { dic ->
/**这儿会拿到两个途径,分别是java代码编译后的javac/debug/classes,以及kotlin代码编译后的 tmp/kotlin-classes/debug */
println("dic path == >${dic.file.path}")
/**一切的class文件的根途径,咱们现已拿到了,接下来便是剖析这些文件夹下的class文件*/
findAllClass(dic.file)
/**这儿必定不能忘掉写*/
def dest = outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(dic.file, dest)
}
input.jarInputs.each { jar ->
/**这儿也必定不能忘掉写*/
def dest = outputProvider.getContentLocation(jar.name,jar.contentTypes,jar.scopes,Format.JAR)
FileUtils.copyFile(jar.file,dest)
}
}
}
/**
* 查找class文件
* @param file 可能是文件也可能是文件夹
*/
private void findAllClass(File file) {
if (file.isDirectory()) {
file.listFiles().each {
findAllClass(it)
}
} else {
modifyClass(file)
}
}
/**
* 进行字节码插桩
* @param file 需求插桩的字节码文件
*/
private void modifyClass(File file) {
println("终究的class文件 ==> ${file.absolutePath}")
/**假如不是.class文件,扔掉*/
if (!file.absolutePath.endsWith(".class")) {
return
}
/**BuildConfig.class文件以及R文件都扔掉*/
if (file.absolutePath.contains("BuildConfig.class") || file.absolutePath.contains("R")) {
return
}
doASM(file)
}
/**
* 进行ASM字节码插桩
* @param file 需求插桩的class文件
*/
private void doASM(File file) {
def fis = new FileInputStream(file)
def cr = new ClassReader(fis)
def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
cr.accept(new ASMClassVisitor(Opcodes.ASM9, cw), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG)
/**重新覆盖*/
def bytes = cw.toByteArray()
def fos = new java.io.FileOutputStream(file.absolutePath)
fos.write(bytes)
fos.flush()
fos.close()
}
}
假如想要运用Transform,那么需求引进transform-api,其实在transform 1.5之后gradle就支撑Transform了。
implementation 'com.android.tools.build:transform-api:1.5.0'
当履行Transform使命的时分,终究会履行到transform办法,在这个办法中能够获取TransformInput的输入,主要包含两种:文件夹和Jar包;关于Jar包,咱们不需求处理,只需求拷贝到方针文件夹下即可。
关于文件夹咱们是需求处理的,由于这儿包含了咱们要处理的.class文件,关于Java编译后的class文件是存在javac/debug/classes根文件夹下,关于kotlin编译后的class文件是存在temp/classes根文件下。
所以在整个编译的过程中,只要是.class文件都会履行doASM这个办法,在这个办法中便是咱们在上节提到的关于字节码的插桩。
1.3 ASM字节码插桩
class ASMClassVisitor extends ClassVisitor {
ASMClassVisitor(int api) {
super(api)
}
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
println("visitMethod==>$name")
/**一切的办法都会在ASMMethodVisitor中刺进字节码*/
def method = super.visitMethod(access, name, descriptor, signature, exceptions)
return new ASMMethodVisitor(api, method, access, name, descriptor)
}
ASMClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor)
}
@Override
FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
return super.visitField(access, name, descriptor, signature, value)
}
@Override
AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return super.visitAnnotation(descriptor, visible)
}
}
class ASMMethodVisitor extends AdviceAdapter {
private def methodName
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param api the ASM API version implemented by this visitor. Must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param descriptor the method's descriptor (see {@link Type Type}).
*/
protected ASMMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor)
this.methodName = name
}
@Override
protected void onMethodEnter() {
super.onMethodEnter()
visitFieldInsn(GETSTATIC,
"com/lay/learn/base_net/LoggUtils",
"INSTANCE",
"Lcom/lay/learn/base_net/LoggUtils;")
visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "start", "()V", false)
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
visitFieldInsn(GETSTATIC,
"com/lay/learn/base_net/LoggUtils",
"INSTANCE",
"Lcom/lay/learn/base_net/LoggUtils;")
visitLdcInsn(methodName)
visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "end", "(Ljava/lang/String;)V",false)
}
}
这儿就不再细说了,贴上源码咱们能够学习一下哈。
终究在编译的过程中,对一切的办法刺进了咱们自己的耗时计算逻辑,当运行之后
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
虽然咱们没有显示地在MainActivity的onCreate中刺进耗时检测代码,可是在控制台中咱们能够看到,onCreate办法耗时180ms
2022-12-28 19:50:19.243 13665-13665/com.lay.learn.asm E/LoggUtils: <init> 耗时==>0
2022-12-28 19:50:19.458 13665-13665/com.lay.learn.asm E/LoggUtils: onCreate 耗时==>180
1.4 插件配置
当咱们完结一个插件之后,需求在META-INF文件夹下创立一个gradle-plugins文件夹,并在properties文件中声明插件全类名。
implementation-class=com.lay.asm.ASMPlugin
要注意插件id便是properties文件的姓名。
这样只要某个工程中需求字节码插桩,只需求引进asm_plugin这个插件即可在编译的时分扫描整个工程。
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'asm_plugin'
}
附上buildSrc中的gradle配置文件
plugins{
id 'groovy'
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'org.apache.commons:commons-io:1.3.2'
implementation "com.android.tools.build:gradle:7.0.3"
implementation 'com.android.tools.build:transform-api:1.5.0'
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-util:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
最后需求说一点便是,在Transform使命履行时,必定要将文件夹或许jar包传递到下一级的Transform中,否则会导致apk打包时短少文件导致apk无法运行。