这篇文章我想了很久不太知道该怎么去写,因为 AOP(面向切面编程) 在 Android 上的实践早有人写过,但可能是出于畏难或不了解其应用场景抑或其他什么原因,大家似乎都对它不太感冒。所以今天我以一些 Android 上的实例,希望能引起大家一些兴趣,适当地使用,真的能减少很多重复工作,而且比手动完成更优质,因为耦合性低,而且几乎是无侵入性的。
Aspect Oriented Programming(AOP),面向切面编程,是一个比较热门的话题。AOP 主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
以上摘自百度百科。似懂非懂?没关系。
简单来说,比方我们现在有一个面包(面向对象里的对象),需要把它做成汉堡,所需要的操作就是把它中间切一刀(这就是切面了),然后向切面里塞入一些肉和菜什么的。
对应的 Android 中呢,比方我们现在有一个 Activity,需要把它变成一个带 toolbar 的 Activity,那思考一下,我们需要的就是在 onCreate 方法这里切一刀,然后塞入一些 toolbar 的创建和添加的代码。
大概清楚一些了的话,我们就正式开始了。
今天我们使用的是 Aspectj,Aspectj 在 Android 上的集成是比较复杂的,且存在一些问题,但好在已经有人帮我们解决了。
gradle_plugin_android_aspectjx 项目地址再贴一篇掘金上徐宜生大佬介绍的文章 看 AspectJ 在 Android 中的强势插入
根据 github 上的接入指南很容易就完成,先在根目录的 gradle 文件引入
- 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 正常。
话不多说,先看 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。正如之前所说,因为 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 的区别有所理解吧。
还是对 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(面向切面编程)产生了一点点兴趣的话,不妨再去网上找一些 "正式" 一点的教程学习一下,对其中的一些概念有个认知吧!
来源: https://juejin.im/post/5a52e8a8f265da3e303c53fb