AOP:利用Aspectj注入代碼,無侵入實現各類功能,好比一個註解請求權限

前言

這篇文章我想了好久不太知道該怎麼去寫,由於AOP(面向切面編程)在Android上的實踐早有人寫過,但多是出於畏難或不瞭解其應用場景抑或其餘什麼緣由,你們彷佛都對它不太感冒。因此今天我以一些Android上的實例,但願能引發你們一些興趣,適當地使用,真的能減小不少重複工做,並且比手動完成更優質,由於耦合性低,並且幾乎是無侵入性的。java

簡單介紹

Aspect Oriented Programming(AOP),面向切面編程,是一個比較熱門的話題。AOP主要實現的目的是針對業務處理過程當中的切面進行提取,它所面對的是處理過程當中的某個步驟或階段,以得到邏輯過程當中各部分之間低耦合性的隔離效果。android

以上摘自百度百科。似懂非懂?不要緊。git

簡單來講,比方咱們如今有一個麪包(面向對象裏的對象),須要把它作成漢堡,所須要的操做就是把它中間切一刀(這就是切面了),而後向切面裏塞入一些肉和菜什麼的。github

對應的Android中呢,比方咱們如今有一個Activity,須要把它變成一個帶toolbar的Activity,那思考一下,咱們須要的就是在onCreate方法這裏切一刀,而後塞入一些toolbar的建立和添加的代碼。正則表達式

大概清楚一些了的話,咱們就正式開始了。編程

Gradle接入

今天咱們使用的是Aspectj,Aspectj在Android上的集成是比較複雜的,且存在一些問題,但好在已經有人幫咱們解決了。數組

gradle_plugin_android_aspectjx項目地址bash

再貼一篇掘金上徐宜生大佬介紹的文章 看AspectJ在Android中的強勢插入app

根據github上的接入指南很容易就完成,先在根目錄的gradle文件引入ide

dependencies {
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.1.0'
        }
複製代碼

而後在app項目或library的gradle裏應用插件

apply plugin: 'android-aspectjx'
複製代碼

就完成了。我這邊使用最新的1.1.1版本報錯,使用1.1.0正常。

實例一:爲Activity添加Toolbar

話很少說,先看MainActivity代碼,很簡單,就在onCreate中打印了一個log。

class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, " --> onCreate")
    }
}
複製代碼

下面開始使用Aspectj了

一、第一次嘗試

新建一個MyAspect類,代碼以下

@Aspect
public class MyAspect {
    private static final String TAG = "AOPDemo";
    
    @After("execution(* android.app.Activity.onCreate(..))")
    public void addToolbar(JoinPoint joinPoint) throws Throwable {
        String signatureStr = joinPoint.getSignature().toString();
        Log.d(TAG, signatureStr + " --> addToolbar");
    }
}
複製代碼

首先,MyAspect類有一個@Aspect註解,它告訴編譯器這是一個Aspectj文件,在編譯的時候就會去解析這個類裏的方法。

下面看addToolbar這個方法,@After註解後有一個挺長的字符串,這個字符串是最關鍵的地方,它用來指示編譯器,咱們要在什麼地方「切一刀」,我以爲它跟正則表達式很相似,正則表達式是匹配字符串,而它則是匹配切面,即匹配方法或構造函數等。

具體的看一下,首先是execution,字面義:執行,後面一個括號,裏面用來指示是哪些方法或構造函數的執行。繼續看括號裏面,先是一個*,表明返回值,使用*是匹配的方法能夠是任意類型的返回值,你也能夠指定特定類型;再日後一個空格,後面是類名全路徑.方法名(參數),指明咱們要「切」的是Activity的onCreate方法,後邊的(..)是指定參數數量和類型的,兩個點是匹配任意數量、任意類型。

如今切面肯定了,還要指明是在切面以前仍是以後插入代碼,咱們想在onCreate以後添加toolbar,因此用的是@After註解,另外還有以前@Before,還有先後均可以處理甚至能夠攔截的@Around,這些都是後話,先不深究。

addToolbar方法裏的代碼就是咱們要插入的了,這裏並無真的建立一個toolbar,只是用一個log代替了,可是你建立toolbar用的任何東西,好比所切方法的參數啦,或者所在的對象啦,均可以從JoinPoint中獲得的。

如今編寫完了,運行一下看是否是咱們要的結果吧!

01-06 12:42:06.981 7696-7696/io.github.anotherjack.aopdemo D/AOPDemo: void android.support.v4.app.FragmentActivity.onCreate(Bundle) --> addToolbar
01-06 12:42:06.981 7696-7696/io.github.anotherjack.aopdemo D/AOPDemo: void android.support.v7.app.AppCompatActivity.onCreate(Bundle) --> addToolbar
01-06 12:42:07.007 7696-7696/io.github.anotherjack.aopdemo D/MainActivity:  --> onCreate
01-06 12:42:07.008 7696-7696/io.github.anotherjack.aopdemo D/AOPDemo: void io.github.anotherjack.aopdemo.MainActivity.onCreate(Bundle) --> addToolbar
複製代碼

不太對勁,addToolbar的log竟然打印了三次,這要是真添加三個toolbar得多匪夷所思。而經過日誌裏的signature能夠發現,這三次分別是FragmentActivity、AppCompatActivity,到最後纔是MainActivity。

這裏說一下個人理解,aspectj是在編譯期插入的代碼,注意,編譯期,咱們的app代碼,和library是編譯期打包進去的,而手機系統的東西編譯期是改不了的,好比android.app.Activity就是存在於Android系統中的。也很好理解,你只是打包了一個apk,怎麼可以着把用戶的手機系統給改了呢。而aspectj匹配方法的時候也很實在,只要你是Activity,而且有onCreate方法,那我就給你插入代碼。咱們上邊的MainActivity是繼承自AppCompatActivity,而AppCompatActivity又繼承自FragmentActivity,FragmentActivity才繼承自了Activity,歸根結底,它們三個都是Activity,因此它們的onCreate方法都被插入了addToolbar方法。而MainActivity的onCreate調用了super.onCreate,另兩個同理,因此就出現了addToolbar三次的狀況。

這麼着確定不行的,那麼該怎麼解決呢?

二、進行調整

思考一下,咱們上邊的問題歸根結底就是匹配的面太廣了,因此,咱們要作的就是再給它加限定條件,縮窄匹配的條件,不讓它全部的Activity都匹配,只給特定條件的Activity插入代碼就好了。

下面我採用註解來限定,建立一個名爲ToolbarActivity的註解

@Target(ElementType.TYPE)
public @interface ToolbarActivity {

}
複製代碼

接着修改addToolbar方法上邊的@After註解

@After("execution(* android.app.Activity.onCreate(..)) && within(@io.github.anotherjack.testlib.annotation.ToolbarActivity *)")
複製代碼

能夠看到是在execution以後又經過&&增長了一個within條件,within字面義:在……裏面,這裏是限定所在的類有@ToolbarActivity註解。

最後在MainActivity上增長@ToolbarActivity,再運行一下,你會發現正常了。這樣,咱們若是但願哪一個Activity帶toolbar,只須要給它加@ToolbarActivity註解就行了……呃,也不徹底是。注意一下,編譯器真的真的很實在,它匹配方法就真的只是去你的類裏找有沒有onCreate這個方法,不會考慮從父類繼承到的onCreate方法,而不少人封裝BaseActivity的時候選擇把onCreate方法封裝一下,只暴露給子類一個initView方法,這時候編譯器會認爲子類Activity沒有onCreate方法,天然也就不會給它插入代碼了,這點要注意一下。

實例二:攔截並修改toast

一、經過@Before攔截Toast的show方法

下面咱們嘗試攔截toast。正如以前所說,由於android.widget.Toast是屬於系統裏的,因此編譯期是沒法經過execution給Toast的show方法插入代碼的。然而「執行」的代碼在系統裏,但是「調用」的代碼是咱們本身寫的啊。因此就輪到call登場啦!先上代碼

MainActivity中,點擊按鈕彈出toast。

beforeShowToast.setOnClickListener {
            Toast.makeText(this,"原始的toast",Toast.LENGTH_SHORT).show()
        }
複製代碼

MyAspect中

@Before("call(* android.widget.Toast.show())")
    public void changeToast(JoinPoint joinPoint) throws Throwable {
        Toast toast = (Toast) joinPoint.getTarget();
        toast.setText("修改後的toast");
        Log.d(TAG, " --> changeToast");
    }
複製代碼

此次使用@Before,與以前最大的不一樣,是再也不使用execution,而是call,字面義:調用。在方法內部咱們經過joinPoint.getTarget()獲取到了目標toast對象,並經過setText改變了文字,運行一下你會發現彈出來的是「修改後的toast」。完成。這個例子應該能讓你們對execution和call的區別有所理解吧。

二、使用@Around處理Toast的setText方法

仍是對toast,此次不是show方法了,此次對setText方法操刀。

MainActivity代碼,正常應該彈出「沒處理的toast」

handleToastText.setOnClickListener {
            val toast = Toast.makeText(this,"origin",Toast.LENGTH_SHORT)
            toast.setText("沒處理的toast")
            toast.show()
        }
複製代碼

MyAspect中代碼,記得先把上一個對show方法的攔截註釋掉

@Around("call(* android.widget.Toast.setText(java.lang.CharSequence))")
    public void handleToastText(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Log.d(TAG," start handleToastText");
        proceedingJoinPoint.proceed(new Object[]{"處理過的toast"}); //這裏把它的參數換了
        Log.d(TAG," end handleToastText");

    }
複製代碼

注意這個方法的參數再也不是JoinPoint了,而是ProceedingJoinPoint,經過它的proceed方法能夠調用攔截到的方法,在調用先後均可以插入代碼處理,甚至能夠不調用proceed方法,直接把這個方法攔截,不讓它調用。

這個例子中是在先後各打了一個log,同時proceed方法改變成了新的參數「處理過的toast」。固然你也能夠經過getTarget方法獲得toast對象,根據toast對象獲得文字,並作相應處理。運行一下彈出的是「處理過的toast」,且打印了兩行log,是咱們預期的結果。

實例三:動態請求權限

相比以上兩個例子,這個例子要更具實用性。

這裏咱們模擬點擊按鈕拍照的場景,6.0以上系統須要動態請求權限。MainActivity中的代碼以下

takePhoto.setOnClickListener {
            takePhoto()
        }
複製代碼

takePhoto方法代碼以下

//模擬拍照場景
    @RequestPermissions(Manifest.permission.CAMERA,Manifest.permission.WRITE_EXTERNAL_STORAGE)
    private fun takePhoto(){
        Toast.makeText(this,"咔嚓!拍了一張照片!",Toast.LENGTH_SHORT).show()
    }
複製代碼

能夠看到咱們又定義了一個@RequestPermissions註解,代碼以下

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestPermissions {
    String[] value() default {};
}
複製代碼

value是個String數組,是咱們要請求的權限,好比在takePhoto方法中咱們請求了相機和外部存儲的權限。

接着來看最重要的地方,MyAspect裏面

//任意註解有@RequestPermissions方法的調用
    @Around("call(* *..*.*(..)) && @annotation(requestPermissions)")
    public void requestPermissions(final ProceedingJoinPoint proceedingJoinPoint, RequestPermissions requestPermissions) throws Exception{
        Log.d(TAG,"----------request permission");
        String[] permissions = requestPermissions.value(); //獲取到註解裏的權限數組

        Object target = proceedingJoinPoint.getTarget();
        Activity activity = null;
        if (target instanceof Activity){
            activity = (Activity) target;
        }else if (target instanceof Fragment){
            activity = ((Fragment)target).getActivity();
        }

        RxPermissions rxPermissions = new RxPermissions(activity);
        final Activity finalActivity = activity;
        rxPermissions.request(permissions)
                .subscribe(new Consumer<Boolean>(){
                    @Override
                    public void accept(Boolean granted) throws Exception {
                        if(granted){
                            try {
                                proceedingJoinPoint.proceed();
                            } catch (Throwable throwable) {
                                throwable.printStackTrace();
                            }
                        }else {
                            Toast.makeText(finalActivity,"未獲取到權限,不能拍照",Toast.LENGTH_LONG).show();
                        }
                    }
                });

    }
複製代碼

先看這個方法的參數,以前的幾個例子中都是隻有一個JointPoint參數,而這個多了一個參數,是咱們上邊定義的那個註解類型,同時在方法上邊的@Around註解中有個 @annotation(requestPermissions),仔細看這個括號中本應是個全路徑的signature,但這裏倒是requestPermissions,沒錯,它就是對應的方法中的參數,這樣就至關因而參數類型的全路徑放在了那裏,而咱們也能夠在方法中直接使用這個註解了。咱們固然也能夠從JoinPoint利用反射獲取到註解,就像下面這樣,可是使用參數的形式很明顯要方便多了,並且反射是會影響性能的。同理,target、以及args等也均可以這樣轉成方法的參數,就很少介紹了。

RequestPermissions requestPermissions1 = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getAnnotation(RequestPermissions.class);

複製代碼

繼續看方法內的詳細代碼,先從註解中獲得了要請求的權限,而後獲取到了target,根據類型獲得activity,而後就是請求權限了,這裏我是經過RxPermissions處理的。若是獲取到了權限就proceedingJoinPoint.proceed()讓攔截到的方法正常執行,不然就toast提醒用戶沒獲取權限。最後記得在Manifest中增長相機和外部存儲的權限,運行項目,測試一下吧。

這樣之後咱們須要在哪一個方法調用前請求一些權限,只須要給該方法加上@RequestPermissions註解並把要請求的權限傳進去便可,是否是很方便。

以上算是舉了幾個例子,主要是讓你們對面向切面編程有個初步的認識,在實際開發中也能夠試着使用,但願你們能大開腦洞,琢磨出更多用法,讓Android開發更加簡單且富有樂趣。

最後

可能有些朋友感受咱們實現的效果就像hook到了方法同樣,其實我最初也是尋找hook方法的時候才接觸到了Aspectj,但慢慢我以爲它不像是一種hook,hook通常是運行時,而Aspectj更傾向因而一種在編譯期插入代碼的方式,和咱們手動插的效果同樣,只不過插入代碼的行爲由編譯器幫咱們作了。

面向切面編程最關鍵的是找到合適的切入點,而切入點的匹配可不僅是文章中用的execution、call和within等,還有不少其餘的。我在文章中也沒有扯出一些Pointcuts、Advice之類的專業名詞,相反是採用一種易於理解的方式,這種方式讓人容易接受,但缺點就是不夠系統,因此,若是這篇文章讓你對AOP(面向切面編程)產生了一點點興趣的話,不妨再去網上找一些「正式」一點的教程學習一下,對其中的一些概念有個認知吧!😊

參考

最後是demo的地址,demo就不求star了,以爲文章還行的話在掘金上點個喜歡就行了😄 AOPDemo項目地址

相關文章
相關標籤/搜索