個人全部原創Android知識體系,已打包整理到GitHub.努力打造一系列適合初中高級工程師可以看得懂的優質文章,歡迎star~java
建議閱讀本篇文章以前掌握如下相關知識點: Android打包流程+Gradle插件+Java字節碼android
在Android Gradle Plugin中,有一個叫Transform API(從1.5.0版本纔有的)的東西.利用這個Transform API咱能夠在.class文件轉換成dex文件以前,對.class文件進行處理.好比監控,埋點之類的.git
而對.class文件進行處理這個操做,我們這裏使用ASM.ASM是一個通用的Java字節碼操做和分析框架。它能夠直接以二進制形式用於修改現有類或動態生成類.我們在打包的時候,直接操做字節碼修改class,對運行時性能是沒有任何影響的,因此它的效率是至關高的.github
本篇文章給你們簡單介紹一下Transform和ASM的使用,最後再結合一個小栗子練習一下.文中demo源碼地址算法
首先寫一個Plugin,而後經過registerTransform方法進行註冊自定義的Transform.api
class MethodTimeTransformPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//註冊方式1
AppExtension appExtension = project.extensions.getByType(AppExtension)
appExtension.registerTransform(new MethodTimeTransform())
//註冊方式2
//project.android.registerTransform(new MethodTimeTransform())
}
}
複製代碼
經過獲取module的Project的AppExtension,經過它的registerTransform方法註冊的Transform.markdown
這裏註冊以後,會在編譯過程當中的TransformManager#addTransform中生成一個task,而後在執行這個task的時候會執行到咱們自定義的Transform的transform方法.這個task的執行時機其實就是.class
文件轉換成.dex
文件的時候,轉換的邏輯是定義在transform方法中的.多線程
先讓你們看一下比較標準的Transform模板代碼:併發
class MethodTimeTransform extends Transform {
@Override
String getName() {
return "MethodTimeTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
//須要處理的數據類型,這裏表示class文件
return TransformManager.CONTENT_CLASS
}
@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)
//TransformOutputProvider管理輸出路徑,若是消費型輸入爲空,則outputProvider也爲空
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//transformInvocation.inputs的類型是Collection<TransformInput>,能夠從中獲取jar包和class文件夾路徑。須要輸出給下一個任務
transformInvocation.inputs.each { input -> //這裏的input是TransformInput
input.jarInputs.each { jarInput ->
//處理jar
processJarInput(jarInput, outputProvider)
}
input.directoryInputs.each { directoryInput ->
//處理源碼文件
processDirectoryInput(directoryInput, outputProvider)
}
}
}
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
//將修改過的字節碼copy到dest,就能夠實現編譯期間干預字節碼的目的
println("拷貝文件 $dest -----")
FileUtils.copyFile(jarInput.file, dest)
}
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format
.DIRECTORY)
//將修改過的字節碼copy到dest,就能夠實現編譯期間干預字節碼的目的
println("拷貝文件夾 $dest -----")
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
複製代碼
getName()
: 表示當前Transform名稱,這個名稱會被用來建立目錄,它會出如今app/build/intermediates/transforms目錄下面.getInputTypes()
: 須要處理的數據類型,用於肯定咱們須要對哪些類型的結果進行轉換,好比class,資源文件等:
CONTENT_CLASS
:表示須要處理java的class文件CONTENT_JARS
:表示須要處理java的class與資源文件CONTENT_RESOURCES
:表示須要處理java的資源文件CONTENT_NATIVE_LIBS
:表示須要處理native庫的代碼CONTENT_DEX
:表示須要處理DEX文件CONTENT_DEX_WITH_RESOURCES
:表示須要處理DEX與java的資源文件getScopes()
: 表示Transform要操做的內容範圍(上面demo裏面使用的SCOPE_FULL_PROJECT
是Scope的集合,包含了Scope.PROJECT
,Scope.SUB_PROJECTS
,Scope.EXTERNAL_LIBRARIES
這幾個東西.固然,TransformManager裏面還有一些其餘集合,這裏不作舉例).
isIncremental()
: 是否支持增量更新
transform()
: 進行具體轉換邏輯.
能夠看出,最關鍵的核心代碼就是transform()方法裏面,咱們須要作一些class文件字節碼的修改,才能讓Transform發揮其效果.app
道理是這個道理,可是字節碼那玩意兒想改就能改麼? 忘記字節碼是什麼的小夥伴能夠看我以前發的文章 Java字節碼解讀 複習一下. 字節碼比較複雜,連"讀懂"都很是很是困難,還讓我去改它,那更是難上加難.
不過,幸虧我們能夠藉助後面介紹的ASM工具進行方便的修改字節碼工做.
就是Transform中的isIncremental()
方法返回值,若是是false的話,則表示不開啓增量編譯,每次都得處理每一個文件,很是很是拖慢編譯時間. 咱們能夠藉助該方法,返回值改爲true,開啓增量編譯.固然,開啓了增量編譯以後須要檢查每一個文件的Status,而後根據這個文件的Status進行不一樣的操做.
具體的Status以下:
來看一下代碼如何實現,咱將上面的dmeo代碼簡單改改:
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
printCopyRight()
//TransformOutputProvider管理輸出路徑,若是消費型輸入爲空,則outputProvider也爲空
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//當前是不是增量編譯,由isIncremental方法決定的
// 當上面的isIncremental()寫的返回true,這裏獲得的值不必定是true,還得看當時環境.好比clean以後第一次運行確定就不是增量編譯嘛.
boolean isIncremental = transformInvocation.isIncremental()
if (!isIncremental) {
//不是增量編譯則刪除以前的全部文件
outputProvider.deleteAll()
}
//transformInvocation.inputs的類型是Collection<TransformInput>,能夠從中獲取jar包和class文件夾路徑。須要輸出給下一個任務
transformInvocation.inputs.each { input -> //這裏的input是TransformInput
input.jarInputs.each { jarInput ->
//處理jar
processJarInput(jarInput, outputProvider, isIncremental)
}
input.directoryInputs.each { directoryInput ->
//處理源碼文件
processDirectoryInput(directoryInput, outputProvider, isIncremental)
}
}
}
/** * 處理jar * 將修改過的字節碼copy到dest,就能夠實現編譯期間干預字節碼的目的 */
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
def status = jarInput.status
File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (isIncremental) {
switch (status) {
case Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
transformJar(jarInput.file, dest)
break
case Status.REMOVED:
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
break
}
} else {
transformJar(jarInput.file, dest)
}
}
void transformJar(File jarInputFile, File dest) {
//println("拷貝文件 $dest -----")
FileUtils.copyFile(jarInputFile, dest)
}
/** * 處理源碼文件 * 將修改過的字節碼copy到dest,就能夠實現編譯期間干預字節碼的目的 */
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format
.DIRECTORY)
FileUtils.forceMkdir(dest)
println("isIncremental = $isIncremental")
if (isIncremental) {
String srcDirPath = directoryInput.getFile().getAbsolutePath()
String destDirPath = dest.getAbsolutePath()
Map<File, Status> fileStatusMap = directoryInput.getChangedFiles()
for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
Status status = changedFile.getValue()
File inputFile = changedFile.getKey()
String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath)
File destFile = new File(destFilePath)
switch (status) {
case Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
FileUtils.touch(destFile)
transformSingleFile(inputFile, destFile)
break
case Status.REMOVED:
if (destFile.exists()) {
FileUtils.forceDelete(destFile)
}
break
}
}
} else {
transformDirectory(directoryInput.file, dest)
}
}
void transformSingleFile(File inputFile, File destFile) {
println("拷貝單個文件")
FileUtils.copyFile(inputFile, destFile)
}
void transformDirectory(File directoryInputFile, File dest) {
println("拷貝文件夾 $dest -----")
FileUtils.copyDirectory(directoryInputFile, dest)
}
複製代碼
根據是否爲增量更新,若是不是,則刪除以前的全部文件.而後對每一個文件進行狀態判斷,根據其狀態來決定究竟是該刪除,或者複製.開啓增量編譯以後,速度會有特別大的提高.
畢竟是在電腦上進行編譯,儘管壓榨電腦性能,咱們把併發編譯給搞起.說來也輕巧,就下面幾行代碼就行
private WaitableExecutor mWaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()
transformInvocation.inputs.each { input -> //這裏的input是TransformInput
input.jarInputs.each { jarInput ->
//處理jar
mWaitableExecutor.execute(new Callable<Object>() {
@Override
Object call() throws Exception {
//多線程
processJarInput(jarInput, outputProvider, isIncremental)
return null
}
})
}
//處理源碼文件
input.directoryInputs.each { directoryInput ->
//多線程
mWaitableExecutor.execute(new Callable<Object>() {
@Override
Object call() throws Exception {
processDirectoryInput(directoryInput, outputProvider, isIncremental)
return null
}
})
}
}
//等待全部任務結束
mWaitableExecutor.waitForTasksWithQuickFail(true)
複製代碼
增長的代碼很少,其餘都是以前的.就是讓處理邏輯的地方放線程裏面去執行,而後得等這些線程都處理完成才結束任務.
到這裏Transform基本的API也將介紹完了,原理(系統有一些列Transform用於在class轉dex的過程當中的處理邏輯,咱們也能夠自定義Transform參與其中,這個Transform最終實際上是在一個Task裏面執行的.)的話也知曉了個大概,接下來咱們看看如何利用ASM修改字節碼實現炫酷的功能吧.
官網上是這樣介紹ASM的: ASM是一個通用的Java字節碼操做和分析框架。它能夠直接以二進制形式用於修改現有類或動態生成類。ASM提供了一些常見的字節碼轉換和分析算法,可從中構建定製的複雜轉換和代碼分析工具。ASM提供了與其餘Java字節碼框架相似的功能,可是側重於 性能。由於它的設計和實現是儘量的小和儘量快,因此它很是適合在動態系統中使用(但固然也能夠以靜態方式使用,例如在編譯器中)。(可能翻譯得不是很準確,英文好的同窗能夠去官網看原話)
下面是個人demo中的buildSrc裏面build.gradle配置.它包含了Plugin+Transform+ASM的全部依賴,放心拿去用.
dependencies {
implementation gradleApi()
implementation localGroovy()
//經常使用io操做
implementation "commons-io:commons-io:2.6"
// Android DSL Android編譯的大部分gradle源碼
implementation 'com.android.tools.build:gradle:3.6.2'
implementation 'com.android.tools.build:gradle-api:3.6.2'
//ASM
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-util:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
複製代碼
在使用以前咱們先來看一些經常使用的對象
上面這些對象先簡單過一下,眼熟就行,待會兒會使用到這些對象.
大致工做流程: 經過ClassReader讀取class字節碼文件,而後ClassReader將讀取到的數據經過一個ClassVisitor(上面的ClassWriter其實就是一個ClassVisitor)將數據表現出來.表現形式: 將字節碼的每一個細節按順序經過接口的方式傳遞給ClassVisitor.就好比說,訪問到了class文件的xx方法,就會回調ClassVisitor的visitMethod方法;訪問到了class文件的屬性,就會回調ClassVisitor的visitField方法.
ClassWriter是一個繼承了ClassVisitor的類,它保存了這些由ClassReader讀取出來的字節流數據,最後經過它的toByteArray方法得到完整的字節流.
上面的概念比較生硬,我們先來寫一個簡單的複製class文件的方法:
private void copyFile(File inputFile, File outputFile) {
FileInputStream inputStream = new FileInputStream(inputFile)
FileOutputStream outputStream = new FileOutputStream(outputFile)
//1. 構建ClassReader對象
ClassReader classReader = new ClassReader(inputStream)
//2. 構建ClassVisitor的實現類ClassWriter
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
//3. 將ClassReader讀取到的內容回調給ClassVisitor接口
classReader.accept(classWriter, ClassReader.EXPAND_FRAMES)
//4. 經過classWriter對象的toByteArray方法拿到完整的字節流
outputStream.write(classWriter.toByteArray())
inputStream.close()
outputStream.close()
}
複製代碼
看到這裏,可能有的同窗已經有點感受了.ClassReader對象就是專門負責讀取字節碼文件的,而ClassWriter就是一個繼承了ClassVisitor的類,當ClassReader讀取字節碼文件的時候,數據會經過ClassVisitor回調回來.我們能夠自定義一個ClassWriter用來接收讀取到的字節數據,接收數據的同時,我們再插入一點東西到這些數據的前面或者後面,最後經過ClassWriter的toByteArray方法將這些字節碼數據導出,寫入新的文件,這就是咱們所說的插樁了.
如今我們舉個栗子,到底插樁能有啥用?就實現一個簡單的需求吧,在每一個方法的最前面插入一句打印Hello World!
的代碼.
修改前的代碼以下所示:
private void test() {
System.out.println("test");
}
複製代碼
預期修改後的代碼:
private void test() {
System.out.println("Hello World!");
System.out.println("test");
}
複製代碼
將上面的複製文件的代碼簡單改改
void traceFile(File inputFile, File outputFile) {
FileInputStream inputStream = new FileInputStream(inputFile)
FileOutputStream outputStream = new FileOutputStream(outputFile)
ClassReader classReader = new ClassReader(inputStream)
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
classReader.accept(new HelloClassVisitor(classWriter)), ClassReader.EXPAND_FRAMES)
outputStream.write(classWriter.toByteArray())
inputStream.close()
outputStream.close()
}
複製代碼
惟一有變化的地方就是classReader的accept方法傳入的ClassVisitor對象變了,咱自定義了一個HelloClassVisitor.
class HelloClassVisitor extends ClassVisitor {
HelloClassVisitor(ClassVisitor cv) {
//這裏須要指定一下版本Opcodes.ASM7
super(Opcodes.ASM7, cv)
}
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
return new HelloMethodVisitor(api, methodVisitor, access, name, descriptor)
}
}
複製代碼
咱們自定義了一個ClassVisitor,它將ClassWriter傳入其中.在ClassVisitor的實現中,只要傳入了classVisitor對象,那麼就會將功能委託給這個classVisitor對象.至關於我傳入的這個ClassWriter就讀取到了字節碼,最後toByteArray就是全部的字節碼.多說無益,看看代碼:
public abstract class ClassVisitor {
/** The class visitor to which this visitor must delegate method calls. May be null. */
protected ClassVisitor cv;
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
if (api != Opcodes.ASM7 && api != Opcodes.ASM6 && api != Opcodes.ASM5 && api != Opcodes.ASM4) {
throw new IllegalArgumentException("Unsupported api " + api);
}
this.api = api;
this.cv = classVisitor;
}
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
if (cv != null) {
return cv.visitAnnotation(descriptor, visible);
}
return null;
}
public MethodVisitor visitMethod( final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
...
}
複製代碼
有了咱們傳入的ClassWriter,我們在自定義ClassVisitor的時候,只須要關注須要修改的地方便可.我們是想對方法進行插樁,天然就得關心visitMethod方法,該方法會在ClassReader閱讀class文件裏面的方法時會回調.這裏咱們首先是在HelloClassVisitor的visitMethod中調用了ClassVisitor的visitMethod方法,拿到MethodVisitor對象.
而MethodVisitor是和ClassVisitor是相似的,在ClassReader閱讀方法的時候會回調這個類裏面的visitParameter(訪問方法參數),visitAnnotationDefault(訪問註解的默認值),visitAnnotation(訪問註解)等等.
因此爲了可以對方法插樁,我們須要再包一層,本身實現一下MethodVisitor,咱們將ClassWriter.visitMethod返回的MethodVisitor傳入自定義的MethodVisitor,並在方法剛開始的地方進行插樁.AdviceAdapter是一個繼承自MethodVisitor的類,它可以方便的回調方法進入(onMethodEnter)和方法退出(onMethodExit). 咱們只須要在方法進入,也就是onMethodEnter方法裏面進行插樁便可.
class HelloMethodVisitor extends AdviceAdapter {
HelloMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor)
}
//方法進入
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//這裏的mv是MethodVisitor
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
複製代碼
插樁的核心代碼,須要一些字節碼的核心知識,這裏不展開介紹,推薦你們閱讀《深刻理解Java虛擬機》關於字節碼的章節.
固然,要想快速地寫出這些代碼也是有捷徑的,安裝一個ASM Bytecode Outline
插件,而後隨便寫一個Test類,而後隨便寫一個方法
public class Test {
public void hello() {
System.out.println("Hello World!");
}
}
複製代碼
而後選中該Test.java文件,右鍵菜單,點擊Show ByteCode outline
在右側窗口內選擇ASMified,便可獲得以下代碼:
mv = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(42, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(43, l1);
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLocalVariable("this", "Lcom/xfhy/gradledemo/Test;", null, l0, l2, 0);
mv.visitMaxs(2, 1);
mv.visitEnd();
複製代碼
其中關於Label的咱不須要,因此只剩下核心代碼
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
複製代碼
到這裏,ASM的基本使用已經告一段落.ASM可操做性很是強,人有多大膽,地有多大產.只要你想實現的,基本都能實現.關鍵在於你的想法.可是有個小問題,上面的插件只能生成一些簡單的代碼,若是須要寫一些複雜的邏輯,就必須深刻Java字節碼,才能本身寫出來或者是看懂ASM的插樁代碼.
上面那個小demo在每一個方法裏面打印一句"Hello World!"好像沒什麼實際意義..咱決定作個有實際意義的東西,通常狀況下,咱們在作開發的會去防止用戶快速點擊某個View.這是爲了追求更好的用戶體驗,若是不處理的話,在快速點擊Button的時候可能會連續打開2個相同的界面,在用戶看來確實有點奇怪,影響體驗.因此,通常狀況下,咱們會去作一下限制.
處理的時候,其實也很簡單,咱們只須要取快速點擊事件中的其中一次點擊事件就好了.有哪些方案進行處理呢?下面是我想到的幾種
ACTION_DOWN
&&快速點擊則返回true就行.下面是我簡單實現的一個工具類FastClickUtil.java
public class FastClickUtil {
private static final int FAST_CLICK_TIME_DISTANCE = 300;
private static long sLastClickTime = 0;
public static boolean isFastDoubleClick() {
long time = System.currentTimeMillis();
long timeDistance = time - sLastClickTime;
if (0 < timeDistance && timeDistance < FAST_CLICK_TIME_DISTANCE) {
return true;
}
sLastClickTime = time;
return false;
}
}
複製代碼
有了這個工具類,那我們就能夠在每一個onClick方法的最前面插入isFastDoubleClick()
判斷語句,簡單判斷一下便可實現防抖.就像下面這樣:
public void onClick(View view) {
if (!FastClickUtil.isFastDoubleClick()) {
......
}
}
複製代碼
爲了實現上面這個最終效果,咱們其實只須要這樣作:
除了自定義ClassVisitor,其餘代碼是和上面的demo差很少的,咱直接看自定義ClassVisitor.
class FastClickClassVisitor extends ClassVisitor {
FastClickClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor)
}
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
if (name == "onClick" && descriptor == "(Landroid/view/View;)V") {
return new FastMethodVisitor(api, methodVisitor, access, name, descriptor)
} else {
return methodVisitor
}
}
}
複製代碼
在ClassVisitor裏面的visitMethod裏面,只須要找到onClick方法,而後自定義本身的MethodVisitor.
class FastMethodVisitor extends AdviceAdapter {
FastMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor)
}
//方法進入
@Override
protected void onMethodEnter() {
super.onMethodEnter()
mv.visitMethodInsn(INVOKESTATIC, "com/xfhy/gradledemo/FastClickUtil", "isFastDoubleClick", "()Z", false)
Label label = new Label()
mv.visitJumpInsn(IFEQ, label)
mv.visitInsn(RETURN)
mv.visitLabel(label)
}
}
複製代碼
在方法進入(onMethodEnter()
)裏面調用FastClickUtil的靜態方法isFastDoubleClick()判斷一下便可.到此,咱們的小案例計算所有完成了.能夠看到,利用ASM輕輕鬆鬆就能實現咱們以前看起來比較麻煩的功能,並且低侵入性,不用改動以前的全部代碼.
插樁以後能夠將編譯完成的apk直接拖入jadx裏面看一下最終源碼驗證,也能夠直接將apk安裝到手機上進行驗證.
固然了,上面的這種實現有些不太人性化的地方.好比某些View的點擊事件,不須要防抖.怎麼辦?用上面這種方式不太合適,咱能夠自定義一個註解,在不須要處理防抖的onClick方法上標註一下這個註解.而後在ASM這邊判斷一下,若是某onClick方法上有這個註解就不進行插樁.事情完美解決.這裏就不帶着你們實現了,留給你們課後實踐.