可能看簡介有點抽象,接下來我們從一個開發中常見的問題入手,應該會更具體一些。我們知道在app開發中經常有判斷當前是否登入的情況,比如在抖音首頁中就算我們沒有登入還是可以看視訊,但是如果想要點贊評論或者切換下面tab時就會需要我們登入了。按照最簡單的思路,我們當然可以在點選這所有的按鈕的時候判斷一下當前的登入狀態,如果未登入的話就跳轉到登入頁面。但是,這樣做的話是否太過複雜?該頁面中有上十個按鈕都需要判斷登入狀態,重複的程式碼太多,這種情況就需要優化了。那麼應該如何進行優化呢?我們可以參考一下ARouter的實現,在ARouter中有一個攔截器,我們通過實現攔截器可以攔截下來跳轉請求,並且在其中進行判斷登入狀態。重點是攔截器只需要寫一次程式碼,節省重複勞動。
那麼攔截器是什麼原理呢?這個我們通過ARouter原始碼可以看到,ARouter中的攔截器是通過@Interceptor註解進行標識,然後在ARouter初始化時會呼叫init方法,其中就是執行自定義攔截器的process方法。這裡的將Interceptor統一處理攔截的方式就是AOP操作,提煉出了這些操作的共性,並且進行統一處理,共性就是需要攔截和判斷。
除了APT來實現AOP還有其他的方式,具體的方式以及優劣見下圖。
我們知道APT是在編譯器通過註解來採集資訊,然後通過註解處理器來生成程式碼,生成的程式碼和普通的java程式碼一樣會被打包成class使用。AspectJ是第三方提供的框架,是在編譯以後,按照class檔案的規則通過檔案流去修改class檔案。AspectJ底層是使用了ASM,和ASM原理類似。Qzone超級補丁、AS中的Instant run功能都是使用的ASM直接操作位元組碼。以上三種方式都是屬於預編譯方式來實現AOP,而不是執行期動態代理的方式,所以不影響效率。
接下來筆者會以工作中經常碰到的其他2種場景舉例,演示一下AOP的簡單實現。那麼應該用以上三種方式的哪一種呢,APT的方式時通過生成一個class類檔案,然後再執行的時候去呼叫這個新檔案中的程式碼來實現我們想要的功能,因為生成以後還需要呼叫所以侵入性很強,適合那種寫模板程式碼的情況,但現在的需求是修改已有程式碼,那麼ASM?ASM是最輕量的、效能最好、最強大的,但是使用起來太複雜,所以優先使用AspectJ來實現。
在此之前,先簡單介紹一下AspectJ的簡單使用方式
步驟一 導包
前面說過AspectJ是一個第三方庫,所以需要匯入相關的依賴到工程中
1.在buildscript中加入
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程式設計,還有執行期動態代理的方式沒有介紹,等以後有空再更新。