大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術&職場瓶頸,從而度過難關,進入心儀的公司。
詳情見文章:沒錯!皇叔開了個訓練營
作者:樹獺非懶 鏈接:https://juejin.cn/post/7067128525929021471
如果你接觸過Java後台開發,一定聽過AOP的概念,它到底是什麼東西,對我們Android開發有什麼用呢?本博客站在Android工程師的角度探索一下AOP這個熟悉又陌生的概念:

AOP是Aspect Oriented Program的首字母縮寫,翻譯過來就是面向切面編程。這個概念中面向容易理解,編程可以理解,最關鍵的是切面是指什麼?
在理解切面之前,先回顧一下我們熟悉的OOP(Object-Oriented Programming),面向對象編程。我們知道,面向對象的特點是繼承、多態和封裝。而封裝就要求將功能分散到不同的類中去,這在軟件設計中往往稱為職責分配,這樣做的好處是降低了代碼的複雜程度,使類具備了可重用性。但是在分散代碼的同時,也增加了代碼的重複性。什麼意思呢?
舉個栗子,
假設我們要對兩個類的每個方法添加日誌。按面向對象的設計方法,我們就必須在這兩個類的方法中都加上日誌的代碼。也許添加的日誌代碼是完全相同的,但也正是因為面向對象的設計,讓類與類之間無法聯繫,而不能將這些重複的代碼統一起來。
也許你想到了方法:我們可以將這段代碼寫在一個獨立的類的某個方法裡(比如工具類),然後在兩個類中調用。但是,這樣一來,這兩個類和新增獨立的類就有耦合了,也就是說獨立類代碼的改變會直接影響這兩個類。那麼,有沒有什麼辦法,可以不需要干涉到兩個類原本的關係而實現功能呢?
我們可以用抽象思維去思考這個問題,對類的每個方法添加日誌這個操作是一個相對更泛的操作。
它不同於對A類的B方法添加日誌這種偏向於具體的點的操作。所以它更像是對一個面來操作,這類操作就叫做切面。
一般而言,我們管切入到指定類指定方法的代碼片段稱為切面,而切入到哪些類、哪些方法則叫切入點。有了AOP,我們就可以把幾個類共有的代碼,抽取到一個切片中,等到需要時再切入對象中去,從而改變其原有的行為。
由於OOP的編程思路,並不能幫我們實現這類切面操作,而我們確實有這類需求,所以就有了AOP的概念,AOP像OOP一樣,只是一種編程範式,它本身並沒有規定說一定要用什麼方式去實現。從上面一大串的解釋,也可以看出AOP其實是對OOP的一個補充。
簡單的概括上面的內容就是,面向切面編程可以對多個沒有關聯的類的某一類功能進行管理。
AOP的實現方式 靜態AOP在編譯器,切面直接以字節碼的形式編譯到目標字節碼文件中。
1.AspectJAspectJ屬於靜態AOP,它是在編譯時進行增強,會在編譯時期將AOP邏輯織入到代碼中。
由於是在編譯器織入,所以它的優點是不影響運行時性能,缺點是不夠靈活。
2.AbstractProcessor自定義一個AbstractProcessor,在編譯期去解析編譯的類,並且根據需求生成一個實現了特定接口的子類(代理類)
動態AOP1.JDK動態代理通過實現InvocationHandler接口,可以實現對一個類的動態代理,通過動態代理可以生成代理類,從而在代理類方法中,在執行被代理類方法前後,添加自己的實現內容,從而實現AOP。
2.動態字節碼生成在運行期,目標類加載後,動態構建字節碼文件生成目標類的子類,將切面邏輯加入到子類中,沒有接口也可以織入,但擴展類的實例方法為final時,則無法進行織入。比如Cglib
CGLIB是一個功能強大,高性能的代碼生成包。它為沒有實現接口的類提供代理,為JDK的動態代理提供了很好的補充。通常可以使用Java的動態代理創建代理,但當要代理的類沒有實現接口或者為了更好的性能,CGLIB是一個好的選擇。
3.自定義類加載器在運行期,目標加載前,將切面邏輯加到目標字節碼里。如:Javassist
Javassist是可以動態編輯Java字節碼的類庫。它可以在Java程序運行時定義一個新的類,並加載到JVM中;還可以在JVM加載時修改一個類文件。
4.ASMASM可以在編譯期直接修改編譯出的字節碼文件,也可以像Javassit一樣,在運行期,類文件加載前,去修改字節碼。
AspectJ的應用 AspectJ的介紹AspectJ提供了兩套強大的機制:
第一套是切面語法。就是網上隨便一搜的AspectJ使用方法,它把決定是否使用切面的權利還給了切面。就是說在寫切面的時候就可以決定哪些類的哪些方法會被代理,從而從邏輯上不需要侵入業務代碼。由於這套語法實在是太有名,導致很多人都誤以為AspectJ就是這一套切面語法,其實不然。
第二套是織入工具。上面介紹的切面語法能夠讓切面從邏輯上與業務代碼解耦,但是從操作上來講,當JVM運行業務代碼的時候,他無從得知旁邊還有個類想橫插一刀。解決思路就是在編譯期(或者類加載期)我們優先考慮一下切面代碼,並將切面代碼通過某種形式插入到業務代碼中,這樣業務代碼不就知道自己被「切」了麼?這種思路的一個實現就是aspectjweaver,就是這裡的織入工具。
AspectJ提供了兩套對切面的描述方法:
一種就是我們常見的基於java註解切面描述的方法,這種方法兼容java語法,寫起來十分方便,不需要IDE的額外語法檢測支持。另外一種是基於aspect文件的切面描述方法,這種語法本身並不是java語法,因此寫的時候需要IDE的插件支持才能進行語法檢查。
AspectJ的使用方法本文主要介紹基於java註解的這種常用使用方式。先了解下AspectJ提供的註解:
Join Point 表示連接點,即 AOP 可織入代碼的點:

Pointcuts是具體的切入點,可以確定具體織入代碼的地方。可以通過通配、正則表達式等指定點,常用的有:

區分execution和call:
AspectJ我們最常看到的是Spring中的應用,那麼在Android中有沒有它的用武之地呢?
那必然是有的,下面會用兩個栗子來看看有什麼應用場景會需要用到它
先準備好環境,需要使用AspectJX插件:AspectJX插件地址
project的build.gradle添加依賴
classpath'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'module的build.gradle應用插件
plugins{id'android-aspectjx'}AspectJX是一個基於AspectJ並在此基礎上擴展出來可應用於Android開發平台的AOP框架,可作用於java源碼,class文件及jar包,同時支持kotlin的應用。
為什麼選用AspectJX而不是基礎的AspectJ或其他?
目前其他的AspectJ相關插件和框架都不支持AAR或者JAR切入的,對於Kotlin更加無能為力(以下栗子均為kotlin實現)
栗子一:實現一種方式可防止View連續快速點擊相信客戶端的同學應該都會被測試這樣的手法摧殘過吧:快速點擊很多次某按鈕導致xxx...
對於這個栗子來說,防止view快速點擊這個操作就是一個切面,所以可以用AspectJ進行切面編程。
定義一個註解@FastClickView,參數interval表示多長時間內只有一次點擊生效:
@Target(AnnotationTarget.FUNCTION)@Retention(value=AnnotationRetention.RUNTIME)annotationclassFastClickView(valinterval:Long=3000L)定義FastClickViewAspect類,把該類作為切面類:對該類添加@Aspect註解
確定切入點:只要使用了FastClickView註解的方法均生效。
使用execution匹配方法執行
//@com.example.aopdemo.FastClickView**(..)表示任何支持FastClickView註解的方法@Pointcut("execution(@com.example.aopdemo.FastClickView**(..))")複製代碼完整代碼如下:@AspectclassFastClickViewAspect{@Pointcut("execution(@com.example.aopdemo.FastClickView**(..))")funexecuteFastClickViewLimit(){}@Around("executeFastClickViewLimit()")@Throws(Throwable::class)funaroundExecuteFastClickViewLimit(joinPoint:ProceedingJoinPoint){Log.d(TAG,"aroundClickCountLimit:")valsignature:MethodSignature=joinPoint.signatureasMethodSignature//取出JoinPoint的方法valmethod=signature.methodif(method.isAnnotationPresent(FastClickView::class.java)){valannotation:FastClickView?=method.getAnnotation(FastClickView::class.java)annotation?.let{valinterval=annotation.intervalvalview=joinPoint.args[0]asViewif(!FastClickCheckUtil.isFastClick(view,interval)){joinPoint.proceed()}}}}}objectFastClickCheckUtil{privateconstvalTAG="FastClickCheckUtil"/***判斷是否屬於快速點擊**@paramview點擊的View*@paraminterval快速點擊的閾值*@returntrue:快速點擊*/funisFastClick(view:View,interval:Long):Boolean{valkey:Int=view.idLog.d(TAG,"isFastClick:$view$interval")valcurrentClickTime:Long=System.currentTimeMillis()//如果兩次點擊間隔超過閾值,則不是快速點擊if(view.getTag(key)==null||currentClickTime-(view.getTag(key)asLong)>interval){//保存最近點擊時間view.setTag(key,currentClickTime)returnfalse}else{returntrue}}}//測試:2s內防止快速點擊overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_aop)btnFastClick.setOnClickListener(object:View.OnClickListener{@FastClickView(2000)overridefunonClick(view:View?){Log.d(TAG,"onClick:clickme...")}})}栗子二:統計方法的耗時在進行應用啟動優化時,我們需要對方法進行方法耗時的統計,常規做法是手動在方法前后里添加代碼,計算耗時時間。
這種方式侵入性強,且代碼重複。此時使用AOP方式就很優雅:
定義註解@TimeConsume:只要在方法上添加該註解就可以統計出方法耗時並通過日誌打印出來
@Target(AnnotationTarget.FUNCTION)@Retention(value=AnnotationRetention.RUNTIME)annotationclassTimeConsume複製代碼定義切面類TimeConsumeAspect:切入點:任何支持TimeConsume註解的方法切入時機:註解的方法前@Before和方法後@After@AspectclassTimeConsumeAspect{companionobject{privateconstvalTAG="TimeConsumeAspect"}varstartTime:Long=0@Pointcut("execution(@com.example.aopdemo.TimeConsume**(..))")funmethodTimeConsumePoint(){}@Before("methodTimeConsumePoint()")fundoBefore(joinPoint:JoinPoint){valsignature:MethodSignature=joinPoint.signatureasMethodSignaturevalmethod=signature.methodLog.d(TAG,"doBefore:$method")startTime=System.currentTimeMillis()}@After("methodTimeConsumePoint()")fundoAfter(){valendTime=System.currentTimeMillis()valconsumeTime=endTime-startTimeLog.d(TAG,"開始於${startTime},結束於$endTime,耗時$consumeTimems")}}測試代碼:
//test:@TimeConsumeoverridefunonStart(){try{Thread.sleep(3000)}catch(e:Exception){e.printStackTrace()}super.onStart()}@TimeConsumeoverridefunonResume(){super.onResume()}查看打印結果:
D/TimeConsumeAspect:doBefore:protectedvoidcom.example.aopdemo.MainActivity.onStart()D/TimeConsumeAspect:開始於1645418155237,結束於1645418158240,耗時3003msD/TimeConsumeAspect:doBefore:protectedvoidcom.example.aopdemo.MainActivity.onResume()D/TimeConsumeAspect:開始於1645418158247,結束於1645418158263,耗時16msdemo地址:https://github.com/Kingwentao/AopDemo.git
APT的應用:Dagger2、ButterKnife、ARouter Javassist:熱更新
參考博客以下博客對本文提供了很大的幫助:



微信改了推送機制,真愛請星標本公號👇