可能看簡介有點抽象,接下來咱們從一個開發中常見的問題入手,應該會更具體一些。咱們知道在app開發中常常有判斷當前是否登陸的狀況,好比在抖音首頁中就算咱們沒有登陸仍是能夠看視頻,可是若是想要點贊評論或者切換下面tab時就會須要咱們登陸了。按照最簡單的思路,咱們固然能夠在點擊這全部的按鈕的時候判斷一下當前的登陸狀態,若是未登陸的話就跳轉到登陸頁面。可是,這樣作的話是否太過複雜?該頁面中有上十個按鈕都須要判斷登陸狀態,重複的代碼太多,這種狀況就須要優化了。那麼應該如何進行優化呢?咱們能夠參考一下ARouter的實現,在ARouter中有一個攔截器,咱們經過實現攔截器能夠攔截下來跳轉請求,而且在其中進行判斷登陸狀態。重點是攔截器只須要寫一次代碼,節省重複勞動。
java
那麼攔截器是什麼原理呢?這個咱們經過ARouter源碼能夠看到,ARouter中的攔截器是經過@Interceptor註解進行標識,而後在ARouter初始化時會調用init方法,其中就是執行自定義攔截器的process方法。這裏的將Interceptor統一處理攔截的方式就是AOP操做,提煉出了這些操做的共性,而且進行統一處理,共性就是須要攔截和判斷。android
除了APT來實現AOP還有其餘的方式,具體的方式以及優劣見下圖。編程
咱們知道APT是在編譯器經過註解來採集信息,而後經過註解處理器來生成代碼,生成的代碼和普通的java代碼同樣會被打包成class使用。AspectJ是第三方提供的框架,是在編譯之後,按照class文件的規則經過文件流去修改class文件。AspectJ底層是使用了ASM,和ASM原理相似。Qzone超級補丁、AS中的Instant run功能都是使用的ASM直接操做字節碼。以上三種方式都是屬於預編譯方式來實現AOP,而不是運行期動態代理的方式,因此不影響效率。bash
接下來筆者會以工做中常常碰到的其餘2種場景舉例,演示一下AOP的簡單實現。那麼應該用以上三種方式的哪種呢,APT的方式時經過生成一個class類文件,而後再運行的時候去調用這個新文件中的代碼來實現咱們想要的功能,由於生成之後還須要調用因此侵入性很強,適合那種寫模板代碼的狀況,但如今的需求是修改已有代碼,那麼ASM?ASM是最輕量的、性能最好、最強大的,可是使用起來太複雜,因此優先使用AspectJ來實現。app
在此以前,先簡單介紹一下AspectJ的簡單使用方式框架
步驟一 導包異步
前面說過AspectJ是一個第三方庫,因此須要導入相關的依賴到工程中
性能
1.在buildscript中加入
gradle
buildscript {
優化
repositories {
google()
jcenter()
} dependencies {
classpath 'org.aspectj:aspectjtools:1.9.2'
}
}
2.在模塊的的dependencies中加入
dependencies {
implementation 'org.aspectj:aspectjrt:1.9.2'
}
步驟二 配置Aspect執行腳本
須要將Aspect的執行腳本複製到gradle中
//在構建工程時,執行編織
project.android.applicationVariants.all { variant ->
JavaCompile javaCompile = variant.javaCompile
//在編譯後 增長行爲
javaCompile.doLast {
println "執行AspectJ編譯器......"
String[] args = [
"-1.7",
//aspectJ 處理的源文件
"-inpath", javaCompile.destinationDir.toString(),
//輸出目錄,aspectJ處理完成後的輸出目錄
"-d", javaCompile.destinationDir.toString(),
//aspectJ 編譯器的classpath aspectjtools
"-aspectpath", javaCompile.classpath.asPath,
//java的類查找路徑
"-classpath", javaCompile.classpath.asPath,
//覆蓋引導類的位置 android中使用android.jar 而不是jdk
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
new Main().runMain(args, false) }}複製代碼
案例1.進行線程切換時,每次都要調用一大堆的Handle/RxJava代碼API
步驟一 定義2個註解分別表明子線程和主線程
其中Async表明子線程標識,Main表明主線程標識,定義註解的緣由是爲了和Aspect配合使用,來標識須要使用切面的方法,畢竟不是全部的方法都須要使用線程切換的切面的。
步驟二 定義好子線程和主線程的切面
須要告訴系統,應該怎麼處理註解標識好的方法,這裏就要使用到Aspect庫了。咱們定義一個類,用@Aspect進行標記,而後定義一個方法,以異步線程爲例,在定義好的方法上面用@Around標註,而後標註中按照execution(@註解類名 返回值 方法名和參數)的方式進行標註,其中*爲通配符,以下圖中我用void *(..)表明處理無返回值的任意方法、任意參數的方法。也就是說只要是用Async註解進行了標註了而且沒有返回值的方法就會被攔截下來再也不執行,被咱們定義的doAsyncMethod方法代替。在攔截到方法後,咱們使用rxjava中的線程切換將當前線程切換到子線程。須要注意的是,這個joinPoint參數就是核心了,它表明着原方法中全部的執行步驟,以參數的形式傳到了切面,可供咱們在任何想要的地方進行調用。本例中,在線程切換完成後,咱們調用了原方法,因此達到了切換線程的目的。固然,主線程標記以同樣。
步驟三 用自定義的註解去標識須要使用切面的類
必須讓系統知道哪些方法須要切換線程,因此咱們須要用註解進行標識,這也是咱們定義註解的緣由。好比咱們在io流讀寫文件的時候用子線程,讀取完成之後須要更新UI必須再主線程,因此咱們將讀寫文件的方法用Async來標記,而後將UI操做的方法使用Main來操做。操做完成之後,AspectJ就會攔截這些方法,根據使用的哪一個註解來執行相應的切面。
案例2.須要輸入一些日誌,每次都須要組裝不一樣字符串
案例2和案例1很是相似,此次咱們新增一個參數表明日誌的類型吧,在定義註解的時候新增了一個value字段,用來蒐集打日誌的類型。而後須要注意的是,若是將註解中的參數傳遞過來,須要獲取到註解類,調用註解類的方法。具體的切面代碼就不貼了,和案例1相似,能夠參照案例1,而後下面貼出獲取到註解傳的參數的方式。
Logger logger = method.getAnnotation(Logger.class); //獲取到註解
String loggerType = looger.value(); //獲取到日誌類型參數複製代碼
總結:本文介紹了跳轉登陸、線程切換、打日誌等幾種狀況下AOP的應用。可是實際上AOP能用到的場景遠遠不止這些,好比參數校驗和判空、動態權限處理、埋點、性能統計等不少其餘地方均可以使用AOP進行優化。除此以外,本文只是介紹了編譯時修改的方式進行AOP編程,還有運行期動態代理的方式沒有介紹,等之後有空再更新。