内容简介:AOP:利用Aspectj注入代码,无侵入实现各种功能
这篇文章我想了很久不太知道该怎么去写,因为AOP(面向切面编程)在Android上的实践早有人写过,但可能是出于畏难或不了解其应用场景抑或其他什么原因,大家似乎都对它不太感冒。所以今天我以一些Android上的实例,希望能引起大家一些兴趣,适当地使用,真的能减少很多重复工作,而且比手动完成更优质,因为耦合性低,而且几乎是无侵入性的。
简单介绍
Aspect Oriented Programming(AOP),面向切面编程,是一个比较热门的话题。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
以上摘自百度百科。似懂非懂?没关系。
简单来说,比方我们现在有一个面包(面向对象里的对象),需要把它做成汉堡,所需要的操作就是把它中间切一刀(这就是切面了),然后向切面里塞入一些肉和菜什么的。
对应的Android中呢,比方我们现在有一个Activity,需要把它变成一个带toolbar的Activity,那思考一下,我们需要的就是在onCreate方法这里切一刀,然后塞入一些toolbar的创建和添加的代码。
大概清楚一些了的话,我们就正式开始了。
Gradle接入
今天我们使用的是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正常。
实例一:为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了
1、第一次尝试
新建一个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三次的情况。
这么着肯定不行的,那么该怎么解决呢?
2、进行调整
思考一下,我们上边的问题归根结底就是匹配的面太广了,所以,我们要做的就是再给它加限定条件,缩窄匹配的条件,不让它所有的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
1、通过@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的区别有所理解吧。
2、使用@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(面向切面编程)产生了一点点兴趣的话,不妨再去网上找一些“正式”一点的教程学习一下,对其中的一些概念有个认知吧!:blush:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 无侵入埋点
- Decorators 低侵入性探索
- 如何低侵入的记录调用日志
- StateShot - 无侵入的历史状态管理库
- 从侵入式服务治理到 Service Mesh
- Matrix勒索病毒PRCP变种侵入政企单位
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Intersectional Internet
Safiya Umoja Noble、Brendesha M. Tynes / Peter Lang Publishing / 2016
From race, sex, class, and culture, the multidisciplinary field of Internet studies needs theoretical and methodological approaches that allow us to question the organization of social relations that ......一起来看看 《The Intersectional Internet》 这本书的介绍吧!