面試官: 你知道什麼是AOP嗎?AOP與OOP有什麼區別,談談AOP的原理是什麼
心理分析:一旦問到aop面試官在開發自己的項目中 肯定是用到了aop切面編程的。這個時候求職者需要格外注意,特別是aop 在編譯時的性能優勢,apk編譯的原理講起。切勿將aop的概念弄混,一定要將oop面向對象與aop面向切面的場景說出來
求職者: aop實現的三大方式(反射 (xutil) apt註解(ButterKnife) aspect (本文即將講到的)) 說出各自的優缺點
一、AOP概念
百度百科中對AOP的解釋如下: 在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
AOP是OOP的延續,是軟件開發中的一個熱點,也是很多框架如 java中的Spring框架中的一個重要內容,是函數式編程的一種衍生範型。 利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
AOP只是一種思想的統稱,實現這種思想的方法有挺多。AOP通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,提高開發效率。
(1)AOP與OOP的關係
**OOP(面向對象編程)**針對業務處理過程的實體及其屬性和行爲進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。但是也有它的缺點,最明顯的就是關注點聚焦時,面向對象無法簡單的解決這個問題,一個關注點是面向所有而不是單一的類,不受類的邊界的約束,因此OOP無法將關注點聚焦來解決,只能分散到各個類中。 AOP(面向切面編程)則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有着本質的差異。 AOP並不是與OOP對立的,而是爲了彌補OOP的不足。OOP解決了豎向的問題,AOP則解決橫向的問題。因爲有了AOP我們的調試和監控就變得簡單清晰。
簡單的來講,AOP是一種:可以在不改變原來代碼的基礎上,通過“動態注入”代碼,來改變原來執行結果的技術。
(2)AOP主要應用場景
日誌記錄,性能統計,安全控制,事務處理,異常處理等等。
(3)主要目標
將日誌記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來,通過對這些行爲的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行爲的時候不影響業務邏輯的代碼。
上圖是一個APP模塊結構示例,按照照OOP的思想劃分爲“視圖交互”,“業務邏輯”,“網絡”等三個模塊,而現在假設想要對所有模塊的每個方法耗時(性能監控模塊)進行統計。這個性能監控模塊的功能就是需要橫跨並嵌入衆多模塊裏的,這就是典型的AOP的應用場景。
AOP的目標是把這些橫跨並嵌入衆多模塊裏的功能(如監控每個方法的性能) 集中起來,放到一個統一的地方來控制和管理。如果說,OOP如果是把問題劃分到單個模塊的話,那麼AOP就是把涉及到衆多模塊的某一類問題進行統一管理。
對比:
功能 | OOP | AOP |
---|---|---|
增加日誌 | 所有功能模塊單獨添加,容易出錯 | 能夠將同一個關注點聚焦在一處解決 |
修改日誌 | 功能代碼分散,不方便調試 | 能夠實現一處修改,處處生效 |
例如:在不改變 main 方法的同時通過代碼注入的方式達到目的
/**
* Before
*/
public class Test {
public static void main(String[] args) {
// do something
}
}
/**
* After
*/
public class Test {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// do something
long end = System.currentTimeMillis() - start;
}
}
二、AOP代碼注入時機
代碼注入主要註解機制,根據註解時機的不同,主要分爲運行時、加載時和編譯時。
運行時:你的代碼對增強代碼的需求很明確,比如,必須使用動態代理(這可以說並不是真正的代碼注入)。 加載時:當目標類被Dalvik或者ART加載的時候修改纔會被執行。這是對Java字節碼文件或者Android的dex文件進行的注入操作。 編譯時:在打包發佈程序之前,通過向編譯過程添加額外的步驟來修改被編譯的類。aspect切面編程正是運用到編譯時
三、AOP的幾種實現方式
- Java 中的動態代理,運行時動態創建 Proxy 類實例
- APT,註解處理器,編譯時生成 .java 代碼
- Javassist for Android:一個移植到Android平臺的非常知名的操縱字節碼的java庫,對 class 字節碼進行修改
- AspectJ:和Java語言無縫銜接的面向切面的編程的擴展工具(可用於Android)。
四,Android中使用 AspectJ
代表項目:Hugo(打印每個方法的執行時間) sa-sdk-android(全埋點技術)
(1)原理
AspectJ 意思就是Java的Aspect,Java的AOP。它的核心是ajc(編譯器 aspectjtools)和 weaver(織入器 aspectjweaver)。
ajc編譯器:基於Java編譯器之上的,它是用來編譯.aj文件,aspectj在Java編譯器的基礎上增加了一些它自己的關鍵字和方法。因此,ajc也可以編譯Java代碼。
weaver織入器:爲了在java編譯器上使用AspectJ而不依賴於Ajc編譯器,aspectJ 5出現了 @AspectJ,使用註釋的方式編寫AspectJ代碼,可以在任何Java編譯器上使用。 由於AndroidStudio默認是沒有ajc編譯器的,所以在Android中使用@AspectJ來編寫。它在代碼的編譯期間掃描目標程序,根據切點(PointCut)匹配,將開發者編寫的Aspect程序編織(Weave)到目標程序的.class文件中,對目標程序作了重構(重構單位是JoinPoint),目的就是建立目標程序與Aspect程序的連接(獲得執行的對象、方法、參數等上下文信息),從而達到AOP的目的。
(2)AspectJ 術語
切面(Aspect):一個關注點的模塊化,這個關注點實現可能另外橫切多個對象。其實就是共有功能的實現。如日誌切面、權限切面、事務切面等。
通知(Advice):是切面的具體實現。以目標方法爲參照點,根據放置的地方不同,可分爲
- 前置通知(Before)、
- 後置通知(AfterReturning)、
- 異常通知(AfterThrowing)、
- 最終通知(After)
- 環繞通知(Around)5種。
在實際應用中通常是切面類中的一個方法,具體屬於哪類通知由配置指定的。
切入點(Pointcut):用於定義通知應該切入到哪些連接點上。不同的通知通常需要切入到不同的連接點上,這種精準的匹配是由切入點的正則表達式來定義的。 連接點(JoinPoint):就是程序在運行過程中能夠插入切面的地點。例如,方法調用、異常拋出或字段修改等。
目標對象(Target Object):包含連接點的對象,也被稱作被通知或被代理對象。這些對象中已經只剩下乾乾淨淨的核心業務邏輯代碼了,所有的共有功能等代碼則是等待AOP容器的切入。
AOP代理(AOP Proxy):將通知應用到目標對象之後被動態創建的對象。可以簡單地理解爲,代理對象的功能等於目標對象的核心業務邏輯功能加上共有功能。代理對象對於使用者而言是透明的,是程序運行過程中的產物。
編織(Weaving):將切面應用到目標對象從而創建一個新的代理對象的過程。這個過程可以發生在編譯期、類裝載期及運行期,當然不同的發生點有着不同的前提條件。譬如發生在編譯期的話,就要求有一個支持這種AOP實現的特殊編譯器(如AspectJ編譯器);
發生在類裝載期,就要求有一個支持AOP實現的特殊類裝載器;只有發生在運行期,則可直接通過Java語言的反射機制與動態代理機制來動態實現(如搖一搖)。
**引入(Introduction):**添加方法或字段到被通知的類。
(3)在Android項目中使用AspectJ
- gradle配置的方式:引入AspectJ是有點複雜的,需要引入大量的gradle命令配置有點麻煩,在build文件中添加了一些腳本,文章出處:https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
- 使用 gradle 插件(也是對 gradle 命令進行了包裝):Jake Wharton 大神的 hugo 項目(一款日誌打印的插件)
上海滬江團隊的 gradle_plugin_android_aspectjx 一個基於AspectJ並在此基礎上擴展出來可應用於Android開發平臺的AOP框架,可作用於java源碼,class文件及jar包,同時支持kotlin的應用。
AOP的用處非常廣,從spring到Android,各個地方都有使用,特別是在後端,Spring中已經使用的非常方便了,而且功能非常強大,但是在Android中,AspectJ的實現是略閹割的版本,並不是所有功能都支持,但對於一般的客戶端開發來說,已經完全足夠用了。
(4)以 AspectJX 接入說明
- 首先,需要在項目根目錄的build.gradle中增加依賴:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
- 然後module項目的 build.gradle 中加入 AspectJ 的依賴:
apply plugin: 'android-aspectjx'
dependencies {
compile 'org.aspectj:aspectjrt:1.8.+'
}
aspectjx {
//排除所有package路徑中包含`android.support`的class文件及庫(jar文件)
exclude 'org.apache.httpcomponents'
exclude 'android.support'
}
- 具體配置參見github地址 https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
- 我們通過一段簡單的代碼來了解下基本的使用方法和功能,新建一個AspectTest類文件,代碼如下:
@Aspect
public class AspectTest {
private static final String TAG = "xuyisheng";
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toString();
Log.e(TAG, "onActivityMethodBefore: " + key);
}
@After("execution(* android.app.Activity.on**(..))")
public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toString();
Log.e(TAG, "onActivityMethodAfter: " + key);
}
@Around("execution(* android.app.Activity.on**(..))")
public void onActivityMethodAfter(ProceedingJoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toString();
Log.e(TAG, "onActivityMethodBefore: " + key);
joinPoint.proceed();
Log.e(TAG, "onActivityMethodAfter: " + key);
}
}
在類的最開始,我們使用 @Aspect 註解來定義這樣一個AspectJ文件,編譯器在編譯的時候,就會自動去解析,並不需要主動去調用AspectJ類裏面的代碼。
(5)編織速度優化建議
- 盡量使用精確的匹配規則,降低匹配時間。
- 排除不需要掃描的包。
通過這種方式編譯後,我們來看下生成的代碼是怎樣的。AspectJ的原理實際上是在編譯的時候,根據一定的規則解析,然後插入一些代碼,通過aspectj生成的代碼,會在Build目錄下:
四、總結:
Aspectj:
- AspectJ除了hook之外,AspectJ還可以爲目標類添加變量,接口。另外,AspectJ也有抽象,繼承等各種更高級的玩法。它能夠在編譯期間直接修改源代碼生成class。
- AspectJ語法比較多,但是掌握幾個簡單常用的,就能實現絕大多數切片,完全兼容Java(純Java語言開發,然後使用AspectJ註解,簡稱@AspectJ。)
學習資料:
-
2020 Android複習資料彙總
2. 2019一線互聯網Android 三方源碼面試題解析大全
3. Android面試指導
因資料太多就不一一的展示出來了,如有想要領取的小夥伴可以點擊到我的主頁加入粉絲裙領取或者點擊立即領取,即可免費領取這些資料。