在Android進(jìn)階寶典 -- Handler應(yīng)用于線上卡頓監(jiān)控中省店,我簡(jiǎn)單介紹了一下關(guān)于ASM實(shí)現(xiàn)字節(jié)碼插樁來(lái)實(shí)現(xiàn)方法耗時(shí)的監(jiān)控鲁纠,但是當(dāng)時(shí)只是找了一個(gè)特定的class文件,針對(duì)某個(gè)特定的方法進(jìn)行插樁呻拌,但是真正的開(kāi)發(fā)中不可能這么做的,因?yàn)檎麄€(gè)工程中會(huì)有成百上千的方法包雀,而且存儲(chǔ)的位置也各有不同谢谦,這個(gè)時(shí)候,我們就需要借助gradle插件來(lái)實(shí)現(xiàn)ASM字節(jié)碼插樁愿阐。
1 準(zhǔn)備工作
但凡涉及到gradle開(kāi)發(fā)微服,我一般都是會(huì)在buildSrc文件夾下進(jìn)行,還有沒(méi)有伙伴不太了解buildSrc的缨历,其實(shí)buildSrc是Android中默認(rèn)的插件工程以蕴,在gradle編譯的時(shí)候糙麦,會(huì)編譯這個(gè)項(xiàng)目并配置到classpath下。這樣的話在buildSrc中創(chuàng)建的插件丛肮,每個(gè)項(xiàng)目都可以引入赡磅。
在buildSrc中可以創(chuàng)建groovy目錄(如果對(duì)groovy或者kotlin了解),也可以創(chuàng)建java目錄宝与,對(duì)于插件開(kāi)發(fā)個(gè)人更便向使用groovy焚廊,因?yàn)楦N近gradle。
1.1 創(chuàng)建插件
創(chuàng)建插件习劫,需要實(shí)現(xiàn)Plugin接口咆瘟,在引入這個(gè)插件后,項(xiàng)目編譯的時(shí)候诽里,就會(huì)執(zhí)行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方法中,可以執(zhí)行自定義的Task谤狡,也可以執(zhí)行自定義的Transform(其實(shí)也可以看做是一種特殊的Task)灸眼,這里我們自定義了插樁相關(guān)的Transform。
1.2 創(chuàng)建Transform
什么是Transform呢墓懂?就是在class文件打包生成dex文件的過(guò)程中幢炸,對(duì)class字節(jié)碼做處理,最終生成新的dex文件拒贱,那么有什么方式能夠?qū)ψ止?jié)碼操作呢宛徊?ASM是一種方式,使用Javassist也可以織入字節(jié)碼逻澳。
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 ->
/**這里會(huì)拿到兩個(gè)路徑闸天,分別是java代碼編譯后的javac/debug/classes,以及kotlin代碼編譯后的 tmp/kotlin-classes/debug */
println("dic path == >${dic.file.path}")
/**所有的class文件的根路徑斜做,我們已經(jīng)拿到了苞氮,接下來(lái)就是分析這些文件夾下的class文件*/
findAllClass(dic.file)
/**這里一定不能忘記寫(xiě)*/
def dest = outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(dic.file, dest)
}
input.jarInputs.each { jar ->
/**這里也一定不能忘記寫(xiě)*/
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)
}
}
/**
* 進(jìn)行字節(jié)碼插樁
* @param file 需要插樁的字節(jié)碼文件
*/
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)
}
/**
* 進(jìn)行ASM字節(jié)碼插樁
* @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笼吟,其實(shí)在transform 1.5之后gradle就支持Transform了。
implementation 'com.android.tools.build:transform-api:1.5.0'
當(dāng)執(zhí)行Transform任務(wù)的時(shí)候霸旗,最終會(huì)執(zhí)行到transform方法贷帮,在這個(gè)方法中可以獲取TransformInput的輸入,主要包括兩種:文件夾和Jar包诱告;對(duì)于Jar包撵枢,我們不需要處理,只需要拷貝到目標(biāo)文件夾下即可。
對(duì)于文件夾我們是需要處理的锄禽,因?yàn)檫@里包含了我們要處理的.class文件潜必,對(duì)于Java編譯后的class文件是存在javac/debug/classes根文件夾下,對(duì)于kotlin編譯后的class文件是存在temp/classes根文件下沃但。
所以在整個(gè)編譯的過(guò)程中磁滚,只要是.class文件都會(huì)執(zhí)行doASM這個(gè)方法,在這個(gè)方法中就是我們?cè)谏瞎?jié)提到的對(duì)于字節(jié)碼的插樁宵晚。
1.3 ASM字節(jié)碼插樁
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")
/**所有的方法都會(huì)在ASMMethodVisitor中插入字節(jié)碼*/
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)
}
}
這里就不再細(xì)說(shuō)了垂攘,貼上源碼大家可以借鑒一下哈。
最終在編譯的過(guò)程中坝疼,對(duì)所有的方法插入了我們自己的耗時(shí)計(jì)算邏輯搜贤,當(dāng)運(yùn)行之后
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
雖然我們沒(méi)有顯示地在MainActivity的onCreate中插入耗時(shí)檢測(cè)代碼谆沃,但是在控制臺(tái)中我們可以看到钝凶,onCreate方法耗時(shí)180ms
2022-12-28 19:50:19.243 13665-13665/com.lay.learn.asm E/LoggUtils: <init> 耗時(shí)==>0
2022-12-28 19:50:19.458 13665-13665/com.lay.learn.asm E/LoggUtils: onCreate 耗時(shí)==>180
1.4 插件配置
當(dāng)我們完成一個(gè)插件之后,需要在META-INF文件夾下創(chuàng)建一個(gè)gradle-plugins文件夾唁影,并在properties文件中聲明插件全類(lèi)名耕陷。
implementation-class=com.lay.asm.ASMPlugin
要注意插件id就是properties文件的名字。
這樣只要某個(gè)工程中需要字節(jié)碼插樁据沈,只需要引入asm_plugin這個(gè)插件即可在編譯的時(shí)候掃描整個(gè)工程哟沫。
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
}
最后需要說(shuō)一點(diǎn)就是,在Transform任務(wù)執(zhí)行時(shí)锌介,一定要將文件夾或者jar包傳遞到下一級(jí)的Transform中嗜诀,否則會(huì)導(dǎo)致apk打包時(shí)缺少文件導(dǎo)致apk無(wú)法運(yùn)行。
作者:Vector7
鏈接:https://juejin.cn/post/7182178552207376421