欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android?ASM插樁探索實(shí)戰(zhàn)詳情

 更新時(shí)間:2022年09月05日 08:53:36   作者:孝之請回答  
這篇文章主要介紹了Android?ASM插樁探索實(shí)戰(zhàn)詳情,文章圍繞主題展開詳細(xì)的內(nèi)容戒殺,具有一定的參考價(jià)值,需要的小伙伴可以參考一下

前言

我們都知道,在Android編譯過程中,Java代碼會(huì)被編譯成Class文件,Class文件再被打進(jìn)Dex文件,虛擬機(jī)最終會(huì)加載Dex文件去執(zhí)行。 

 插樁,就是干涉代碼的編譯過程,在編譯期間生成新的代碼或者修改已有的代碼。

常用的EventBus、ARouter,內(nèi)部就是使用了APT(Annotation Process Tool),在編譯的最開始解析Java文件中的注解,并生成新的Java文件。

但如果有以下兩個(gè)需求:

  • 給Activity的attach()方法加日志
  • 將第三方庫中所有調(diào)用getDeviceId()的地方替換為我們自己的方法,使其符合隱私規(guī)范

這兩個(gè)需求一個(gè)是需要修改Android SDK的Activity文件,一個(gè)是需要修改三方庫中的某個(gè)方法。而我們集成它們的方式是通過Jar包/AAR,本質(zhì)上也就是Class文件。這時(shí)候就需要我們能夠在編譯階段去修改Class文件,這也就是ASM發(fā)揮作用的地方。

通過本文,你可以解決如下問題:

  • ASM的作用是什么?
  • 如何使用ASM?
  • 如何將ASM運(yùn)用到我們的實(shí)際項(xiàng)目中來?

ASM的作用是什么?

在介紹ASM插樁之前,首先來回顧一下Java Class文件。在AS中,我們可以看到打開一個(gè)Class文件是這樣的:

但其實(shí)這是IDE為了方便開發(fā)者查閱,特意解析渲染了CLASS文件。如果直接拖進(jìn)編輯器查看這個(gè)文件的話,我們可以看到它其實(shí)是這樣的:

上圖是CLASS文件的16進(jìn)制代碼。一般人都看不懂這些代碼的含義...但既然AS可以將這些代碼解析成開發(fā)者可以看懂的樣子,說明CLASS文件肯定是遵循某個(gè)格式規(guī)范的。所以,一個(gè)熟悉CLASS文件格式規(guī)范的開發(fā)者,是完全有能力解析所有的CLASS文件,甚至修改CLASS文件的。

ASM的開發(fā)者就是這么做的,并且提供一套完整的API幫助我們在不需要了解CLASS文件格式規(guī)范的情況下,可以解析并修改CLASS文件

如何使用ASM?

基本使用方式

下面我們就來使用一下ASM,看一下它能達(dá)到的效果。假設(shè)現(xiàn)在我們需要統(tǒng)計(jì)MainActivity所有方法的耗時(shí),原先的MainActivity.Class文件是這樣的:

用ASM修改過后的MainActivity.Class文件:

 具體的實(shí)現(xiàn)代碼:

// 讀取Class文件
String clazzFilePath = "/Users/xiaozhi/AndroidStudioProjects/ASMTest/app/build/intermediates/javac/debug/classes/com/xiaozhi/asmtest/MainActivity.class";
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);

// 寫入Class文件
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(clazzFilePath);
fos.write(bytes);
fos.flush();
fos.close();

首先在第6行,通過ClassReader.accept(classVisitor, parsingOptions)讀取Class文件。然后將修改完的字節(jié)碼用FileOutputStream寫回原文件,原先的Class代碼也就被修改了。但這里我們看不到是怎么修改的,因?yàn)?strong>修改其實(shí)就發(fā)生在讀取階段,ClassReader負(fù)責(zé)讀取解析Class文件,遇到相應(yīng)節(jié)點(diǎn)后,調(diào)用ClassVisitor中的方法去修改相應(yīng)的節(jié)點(diǎn)代碼(4、5行)。

這里涉及到兩個(gè)類,ClassWriter與MethodTimeConsumeClassVisitor,這兩個(gè)類都繼承于ClassVisitor。結(jié)合第9行我們可以猜測,ClassWriter肯定可以記錄我們修改后的字節(jié)碼。既然ClassWriter是用來記錄的,而第6行ClassReader.accept(classVisitor, parsingOptions)讀取Class文件又只能接收一個(gè)classVisitor,那我們怎么用另一個(gè)ClassVisitor去修改Class文件呢?

我們可以看到ClassVisitor有這么一個(gè)構(gòu)造函數(shù):

public ClassVisitor(final int api, final ClassVisitor classVisitor)

所以我們第5行的代碼,實(shí)際上是用自定義的ClassVisitor-MethodTimeConsumeClassVisitor,代理了ClassWriter,在需要修改的Class節(jié)點(diǎn)復(fù)寫方法進(jìn)行修改就可以了。

另外我們額外了解一下構(gòu)造函數(shù)中的幾個(gè)參數(shù)。

// 接收Flag參數(shù),用于設(shè)置方法的操作數(shù)棧的深度。COMPUTE_MAXS可以自動(dòng)幫我們計(jì)算stackSize。
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

// 接收api與ClassVisitor。 Opcodes.ASM4~Opcodes.ASM9標(biāo)識了ASM的版本信息
MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter);

// 接收ClassVisitor與parsingOptions參數(shù)。 parsingOptions用來決定解析Class的方式,SKIP_FRAMES代表跳過MethodVisitor.visitFrame方法
classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES);

自定義ClassVisitor

下面我們具體看一下怎么通過自定義ClassVisitor修改Class文件。

public class MethodTimeConsumeClassVisitor extends ClassVisitor {
    private String mOwner;

    public MethodTimeConsumeClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        mOwner = name;
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new TimeConsumeMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);
    }
}

我們可以看到第9行與第15行,分別是visit方法與visitMethod方法,對應(yīng)的是訪問Class文件頭部與Class文件方法這兩個(gè)節(jié)點(diǎn)。

類似的還有很多節(jié)點(diǎn):

visit visitSource? visitOuterClass? ( visitAnnotation |
   visitAttribute )*
   ( visitInnerClass | visitField | visitMethod )*
   visitEnd

我們要統(tǒng)計(jì)MainActivity所有方法的耗時(shí),就需要重寫visitMethod方法。第15行的visitMethod返回了一個(gè)MethodVisitor,顧名思義就是用來遍歷修改Method,跟ClassVisitor是一個(gè)道理只不過維度不同罷了。類似的還有AnnotationVisitor與FiledVisitor,它們分別在visitAnnotation和visitField方法中返回,用來訪問修改注解與字段。

然后我們來看這個(gè)MethodVisitor是怎么修改方法的:

static class TimeConsumeMethodVisitor extends AdviceAdapter {
    private final String methodName;
    private final int access;
    private final String descriptor;
    private final String owner;

    private static final String METHOD_TIME_CONSUME_LOG_TAG = "METHOD_TIME_CONSUME_ASM_HOOK";
    private static final String METHOD_TIME_CONSUME_LOG = "method time consume:";

    protected TimeConsumeMethodVisitor(String owner, int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
        this.owner = owner;
        this.methodName = name;
        this.access = access;
        this.descriptor = descriptor;
    }

    @Override
    protected void onMethodEnter() {
        System.out.println("TimeConsumeMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);

        visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        visitVarInsn(LSTORE, 1);

        super.onMethodEnter();
    }

    @Override
    protected void onMethodExit(int opcode) {
        System.out.println("TimeConsumeMethodVisitor onMethodExit. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor:" + descriptor);

        visitLdcInsn(METHOD_TIME_CONSUME_LOG_TAG);
        visitTypeInsn(NEW, "java/lang/StringBuilder");
        visitInsn(DUP);
        visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        visitLdcInsn(METHOD_TIME_CONSUME_LOG);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        visitVarInsn(LLOAD, 1);
        visitInsn(LSUB);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        visitLdcInsn("ms" + ", clazz:" + owner + ", method:" + methodName);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        visitMethodInsn(INVOKESTATIC, "com/taobao/yyds/common/utils/Logger", "d", "(Ljava/lang/String;Ljava/lang/String;)V", false);
        super.onMethodExit(opcode);
    }
}

在第19行onMethodEnter()方法中,我們實(shí)現(xiàn)了

long startTime = System.currentTimeMillis();

在第29行onMethodExit()方法中,我們實(shí)現(xiàn)了

Logger.d("METHOD_TIME_CONSUME_ASM_HOOK", "method time consume:" + (System.currentTimeMillis() - var1) + "ms, clazz:com/xiaozhi/asmtest/MainActivity, method:");

至于其中所用的API,我們可以通過函數(shù)名大致推斷出什么意思,感興趣的話可以去學(xué)習(xí)這些API的使用。但因?yàn)閯側(cè)腴T,我這里使用了一個(gè)偷懶的方式:ASM ByteCode Viewer。

ASM ByteCode Viewer

ASM ByteCode Viewer是專門用于ASM插樁的AS插件。

安裝該插件后,我們可以很方便地查看一個(gè)Class文件怎么用ASM的API去寫出來:

 如果我想很快速地知道如何通過ASM API去給方法加耗時(shí)日志,只需要先在本地Java文件中寫好這一段邏輯,然后通過插件查看對應(yīng)的API是怎么樣的就可以了。但建議還是要多了解下這些API,因?yàn)槲覀儗懙腏ava代碼可能不是通用的,在其它Java文件中不一定就能順利地編譯成功,因此往往會(huì)有需要進(jìn)行適配的地方,排查的過程中就需要我們了解API才行了。

到這里,ASM的基本使用就已經(jīng)講好了。如果感興趣可以參考官方文檔asm.ow2.io/asm4-guide.…去實(shí)踐。

如何將ASM運(yùn)用都我們的實(shí)際項(xiàng)目中來?

上一節(jié)我們已經(jīng)知道如何用ASM對一個(gè)Class文件進(jìn)行修改,那么怎么運(yùn)用到我們的項(xiàng)目中來呢?Android打包過程中會(huì)將Class文件打包成Dex文件,在這個(gè)階段我們可以借助AGP(Android Gradle Plugin)與Android Transform來遍歷訪問到所有需要的Class文件,再通過ASM去修改。

引入工程

Android Gradle Plugin

自定義插件一共分為5個(gè)步驟:

  • 創(chuàng)建插件項(xiàng)目
  • 配置插件
  • 實(shí)現(xiàn)插件
  • 發(fā)布插件
  • 應(yīng)用插件

創(chuàng)建插件項(xiàng)目

跟其它子模塊一樣,我們需要?jiǎng)?chuàng)建一個(gè)插件模塊,然后在根目錄的settings.gradle中引入該模塊。

配置插件

首先,插件模塊的文件目錄需要嚴(yán)格遵守以下目錄結(jié)構(gòu)(因?yàn)槲覀冞x擇用groovy實(shí)現(xiàn)插件,所以要用groovy文件夾):

main
├── groovy
├── resources 
    ├── META-INF
        ├── gradle-plugins
            ├── *.properties

上圖中的配置文件需要特別注意,該配置文件代表著插件id->插件實(shí)現(xiàn)類的映射。

 其它項(xiàng)目應(yīng)用插件時(shí)所用的插件id,就是配置文件的文件前綴com.yyds.asm.plugin。而實(shí)際的實(shí)現(xiàn)類就是com.xiaozhi.plugin.ASMPlugin。

另外,我們需要在build.gradle中配置如下內(nèi)容:

plugins {
    id 'groovy'
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
}

sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }

        resources {
            srcDir 'src/main/resources'
        }
    }
}

實(shí)現(xiàn)插件

接下來就是實(shí)現(xiàn)我們自定義的插件了,我們可以在ASMPlugin中寫我們需要執(zhí)行的邏輯:

class ASMPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("ASMPlugin apply")
    }
}

發(fā)布插件

插件項(xiàng)目寫完后,我們需要將其發(fā)布到maven倉庫中去(這里可以選擇先將其發(fā)布到本地maven倉庫),從而讓其它模塊可以方便地進(jìn)行依賴。

我們需要在build.gradle中添加以下代碼:

uploadArchives {
    repositories {
        mavenDeployer {
            //設(shè)置插件的GAV參數(shù)
            pom.groupId = 'com.xiaozhi.plugin.asm'
            pom.artifactId = 'asmArt'
            pom.version = '1.0.1'
            //文件發(fā)布到下面目錄
            repository(url: uri('../maven_repo'))
        }
    }
}

sync后,我們就可以在gradle tasks中看到上傳插件的task:

執(zhí)行task,插件就會(huì)發(fā)布到本地的maven倉庫了,我們可以在本地的maven_repo文件夾中找到。

應(yīng)用插件

現(xiàn)在,我們的項(xiàng)目就可以很方便地依賴這個(gè)插件了。

只用做兩個(gè)步驟:

  1. 在工程根目錄build.gralde添加maven倉庫與插件依賴:
buildscript {
    repositories {
        ···
        maven { url uri('./maven_repo') }
    	···
    }
    dependencies {
        ···
        classpath "com.xiaozhi.plugin.asm:asmArt:1.0.1"
        ···
    }
}
  • 在想要依賴插件的項(xiàng)目的build.gradle中應(yīng)用插件:
plugins {
    id 'com.xiaozhi.plugin'
}

可以看到,這個(gè)id就是對應(yīng)的插件項(xiàng)目中配置文件的前綴名。

Android Transform

現(xiàn)在假設(shè)app模塊已經(jīng)應(yīng)用了我們的ASM插件,那么還需要使用Transform才能訪問到app模塊在編譯過程中產(chǎn)生/依賴的所有Class文件。

自定義一個(gè)Transform:

public class ASMTransform extends Transform {

    // transfrom名稱
    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    // 輸入源,class文件
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    // 文件范圍,整個(gè)工程
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    // 是否增量編譯,可用于編譯優(yōu)化
    @Override
    public boolean isIncremental() {
        return false;
    }

    // 核心方法
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        
    }
}

我們主要看第29行transform()方法,在這里我們就能訪問到app模塊的所有Class文件。

實(shí)現(xiàn)如下:

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation);
    if (!transformInvocation.isIncremental()) {
        //不是增量編譯刪除所有的outputProvider
        transformInvocation.getOutputProvider().deleteAll();
    }
    // 獲取輸入源
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    inputs.forEach(transformInput -> {
        Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
        Collection<JarInput> jarInputs = transformInput.getJarInputs();
        directoryInputs.forEach(new Consumer<DirectoryInput>() {
            @Override
            public void accept(DirectoryInput directoryInput) {
                try {
                    // 處理輸入源
                    handleDirectoryInput(directoryInput);
                } catch (IOException e) {
                    System.out.println("handleDirectoryInput error:" + e.toString());
                }
            }
        });
        for (DirectoryInput directoryInput : directoryInputs) {
            // 獲取output目錄
            File dest = transformInvocation.getOutputProvider().getContentLocation(
                    directoryInput.getName(),
                    directoryInput.getContentTypes(),
                    directoryInput.getScopes(),
                    Format.DIRECTORY);
            //這里執(zhí)行字節(jié)碼的注入,不操作字節(jié)碼的話也要將輸入路徑拷貝到輸出路徑
            try {
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            } catch (IOException e) {
                System.out.println("output copy error:" + e.toString());
            }
        }

        for (JarInput jarInput : jarInputs) {
            // 獲取output目錄
            File dest = transformInvocation.getOutputProvider().getContentLocation(
                    jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            //這里執(zhí)行字節(jié)碼的注入,不操作字節(jié)碼的話也要將輸入路徑拷貝到輸出路徑
            try {
                FileUtils.copyFile(jarInput.getFile(), dest);
            } catch (IOException e) {
                System.out.println("output copy error:" + e.toString());
            }
        }
    });
}

這里的邏輯主要是三端:

  • 獲取輸入源

第9行獲取到的TransformInput中可以訪問到所有的DirectoryInput和JarInput,分別代表著我們項(xiàng)目中的Class文件與依賴的JAR包/AAR包中的Class文件。DirectoryInput和JarInput都繼承于QualifiedContent,調(diào)用getFile()方法就可以拿到Class文件的所有信息了。

  • 處理輸入源

獲取Class文件后,其實(shí)我們就可以用ASM去修改它了。相當(dāng)于把我們之前ASM修改Class文件的代碼復(fù)制過來就可以了,這一部分留到下一節(jié)中講。

  • 將輸入源文件拷貝到目標(biāo)文件中

處理完之后,我們還要記得把輸入源文件拷貝到輸出路徑中去,否則下一個(gè)transform可能就要失敗了,因?yàn)樗也坏捷斎朐戳?。?7行transformInvocation.getOutputProvider().getContentLocation()可以確保我們獲取到最終的輸出路徑。

現(xiàn)在Transform寫好了,但我們還沒有應(yīng)用。應(yīng)用很簡單,只需要在插件中注冊一下就好了:

class ASMPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        def android = project.getExtensions().findByType(AppExtension)
        android.registerTransform(new ASMTransform())
    }
}

然后,重新發(fā)布插件到maven倉庫,sync一下,我們就可以在app模塊的gradle tasks中看到我們剛寫好的transform了:

至此,所有鏈路都已經(jīng)走通了,我們知道ASM如何修改Class文件,并可以利用AGP與Transfrom應(yīng)用到我們的工程中來。下面我們就用這條鏈路來實(shí)現(xiàn)一下方法節(jié)流。

方法節(jié)流

Android中最常見的方法節(jié)流就是防重復(fù)點(diǎn)擊。假設(shè)當(dāng)用戶在首頁2s內(nèi)頻繁點(diǎn)擊了商品或者誤觸了商品時(shí),我們期望只打開一次商詳頁面,這時(shí)候就需要對點(diǎn)擊事件做節(jié)流。

首先我們需要定義一個(gè)注解,并聲明到點(diǎn)擊事件上,同時(shí)支持設(shè)置節(jié)流時(shí)長duration:

private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        @MethodThrottle(duration = 3000)
        public void onClick(View view) {
            
        }
}

接下來就是ASM的舞臺(tái)了?;仡櫸覀兩弦还?jié)中處理輸入源相關(guān)的代碼

Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
directoryInputs.forEach(new Consumer<DirectoryInput>() {
    @Override
    public void accept(DirectoryInput directoryInput) {
        try {
            // 處理輸入源
            handleDirectoryInput(directoryInput);
        } catch (IOException e) {
            System.out.println("handleDirectoryInput error:" + e.toString());
        }
    }
});

在第7行handleDirectoryInput()方法中,我們利用ASM修改Class文件:

/**
 * 處理文件目錄下的class文件
 */
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {
    List<File> files = new ArrayList<>();
    //列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
    listFiles(files, directoryInput.getFile());
    for (File file: files) {
        ClassReader classReader = new ClassReader(new FileInputStream(file.getAbsolutePath()));
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        MethodThrottleClassVisitor methodThrottleClassVisitor = new MethodThrottleClassVisitor(Opcodes.ASM5, classWriter);
        classReader.accept(methodThrottleClassVisitor, ClassReader.SKIP_FRAMES);

        byte[] code = classWriter.toByteArray();
        FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
        fos.write(code);
        fos.close();
    }
}

關(guān)鍵是MethodThrottleClassVisitor類,我們看它主要是怎么實(shí)現(xiàn)的:

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
    MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
    throttleMethodVisitor = new ThrottleMethodVisitor(mOwner, api, methodVisitor, access, name, descriptor);
    return throttleMethodVisitor;
}

visitMethod()方法返回一個(gè)自定義的MethodVisitor。ThrottleMethodVisitor在訪問每個(gè)方法時(shí),若發(fā)現(xiàn)方法聲明了@MethodThrottle注解,就會(huì)插入節(jié)流代碼:

@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    System.out.println("ThrottleMethodVisitor visitAnnotation. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", annotationDesc:" + desc);
    throttle = "Lcom/taobao/yyds/common/annotation/MethodThrottle;".equals(desc);
    ···
}
@Override
protected void onMethodEnter() {
    System.out.println("ThrottleMethodVisitor onMethodEnter. clazzName:" + owner + ", methodName:" + methodName + ", access:" + access + ", descriptor" + descriptor + ", throttle:" + throttle);
    if (throttle) {
        // 插入方法節(jié)流代碼
    }
}

具體的代碼就不放上去了,通過ASM ByteCode Viewer可以很方便地生成。這樣我們在平時(shí)代碼中要做方法節(jié)流時(shí),只需要給方法聲明一個(gè)注解就可以了。

方法耗時(shí)日志

類似的,我們可以用這個(gè)鏈路實(shí)現(xiàn)很多AOP邏輯。給方法加耗時(shí)日志這一點(diǎn),在上面的ASM基本使用那一節(jié)中其實(shí)已經(jīng)講過了。

運(yùn)用到工程中來,其實(shí)就跟方法節(jié)流一樣,自定義一個(gè)注解,然后在Transform中加一點(diǎn)處理輸入流的邏輯就好了:

/**
 * 處理文件目錄下的class文件
 */
private static void handleDirectoryInput(DirectoryInput directoryInput) throws IOException {
    List<File> files = new ArrayList<>();
    //列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
    listFiles(files, directoryInput.getFile());
    for (File file: files) {
        // 方法節(jié)流
        methodThrottleASM(file);

        // 方法耗時(shí)
        methodTimeConsumeASM(file);
    }
}

如何調(diào)試

另外有一點(diǎn)我個(gè)人覺得還是挺重要的,那就是Gradle插件該如何調(diào)試。即使有ASM ByteCode Viewer插件的幫助,我們在插件中寫的ASM代碼也不可能一鍵完成,很大概率會(huì)碰到各種各樣的編譯錯(cuò)誤問題。本地打日志又比較麻煩,所以調(diào)試手段是必不可少的。篇幅受限,這里就不額外寫了,可以參考Android Studio調(diào)試Gradle插件詳情

發(fā)布線上的額外工作

雖然我們已經(jīng)集成了ASM插件模塊,但并不意味著這樣就能上線了。至少還需要完成以下的工作才行。

插件項(xiàng)目的maven倉庫

因?yàn)樵谡{(diào)試階段,插件的maven倉庫是用的本地的maven倉庫。但如果要集成進(jìn)CI,肯定是需要線上的maven倉庫的,所以到時(shí)候需要申請上傳到某個(gè)maven倉中才行。

編譯影響評估

第一點(diǎn)是必須保證CI打包時(shí)沒問題。第二點(diǎn)就是看這樣做是否會(huì)影響到編譯時(shí)長,畢竟Transform是在編譯階段加了一個(gè)Task。如果對編譯時(shí)長有比較大的影響,還需要額外做一些編譯優(yōu)化的工作。可以用./gradlew --profile --rerun-tasks assembleDebug命令查看各環(huán)節(jié)的編譯耗時(shí)。

Tips

在做這個(gè)Demo的過程中,因?yàn)樽约阂彩堑谝淮谓佑|,遇到了不少坑,拿幾點(diǎn)貼一下:

  • 每次plugin改動(dòng)都要重新發(fā)布一下,否則plugin中transform、asm的邏輯都不會(huì)更新,因?yàn)槟玫氖莔aven倉中的。
  • transform每次debug前都要clean一下,否則debug不進(jìn)去。
  • 即使transform沒修改任何東西,也需要將源文件jar directory拷貝到目標(biāo)文件。否則最后編譯出的build/transforms文件夾中會(huì)少很多jar包,造成啟動(dòng)時(shí)找不到各種Class而崩潰。
  • classpath引入后,造成啟動(dòng)后某so崩潰。一直以為是插件寫的有什么問題,排查很久后也沒找到原因。最后拿崩潰棧去找so庫接口人,升級so版本后問題就解決了。

總結(jié)

通過本篇文章,我們了解到ASM的作用,學(xué)會(huì)ASM基本API的使用,進(jìn)而利用AGP與Transform將ASM運(yùn)用到實(shí)際項(xiàng)目中來。實(shí)戰(zhàn)中,ASM可以實(shí)現(xiàn)常見的AOP邏輯,如方法節(jié)流與方法耗時(shí)日志。不僅如此,當(dāng)我們以后為需要修改三方庫代碼而發(fā)愁時(shí),或許可以想想,ASM能幫助我們搞定嗎?

到此這篇關(guān)于Android ASM插樁探索實(shí)戰(zhàn)詳情的文章就介紹到這了,更多相關(guān)Android ASM插樁探內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論