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项目地址

相关文章
相关标签/搜索