本文相关代码Github地址mk_aspectj,有帮助的话Star一波吧。
作为Android开发,多多少少遇到过这种情况,我的App很卡,大概知道问题出现在哪?却无从下手,接受别人的工程代码写的一团糟,出现天大的bug,却因为不熟悉业务不敢乱动,有没有一种侵入性比较低,比较和谐的方式去修改业务代码呢
学习设计模式可以一定程度降低业务耦合度,不过那都是 OOP 的思想,今天我给大家带来一份 AOP 的切面编程思想,无侵入方式织入代码到业务。如果对你有所启发,欢迎点赞转发
那么问题来了?
- 什么是 AOP?
- AOP 有什么用?
- AOP怎样学习?
小朋友,你是否有很多问号?
AOP 维基百科是这么说的?
spect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification, such as "log all function calls when the function's name begins with 'set'". This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. AOP forms a basis for aspect-oriented software development
是不是挺晦涩难懂的,我这边简单总结一下吧。
- AOP 即 面向切面编程,通过 AOP ,可以在编译器对代码进行动态管理,以达到统一维护的目的。
- AOP 其实是 OOP 思想的一种延续,同时也是 Spring 框架的一个重要的模块
对 Java 后端开发,其实并不陌生,因为 Spring 动态代理织入 其实就是借助AOP 思想。它解决了什么问题呢?
- 利用 AOP ,我们可以各个业务模块进行隔离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时也会提高开发效率,比如:我们怎样做耗时测量,怎样在不修改代码前提下给指定业务库插入代码。
- 利用 AOP ,我们可以在无侵入的状态下在宿主中插入一些代码逻辑,从而实现一些特殊的功能,比如: 日志埋点, 性能监控,动态权限控制,代码调试等。
既然 AOP 这么好,那么我们怎么学习他呢?学习一个工具,先得了解他的一些专业术语如:
-
Advice: 增强
- 增强是织入到目标类连接点的一段程序代码,在Spring框架中,增强除了被用于描述一段程序代码外,还拥有另外一个连接点相关信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就能找到特地的连接
-
JoinPoint: 连接点
-
什么是连接点?
- 一个类或者一个程序代码拥有一些具有边界性质的特定点,这种特定点称为JoinPoint
-
连接点执行的某个特定的位置
-
- 类初始化前
-
- 类初始化后
-
- 类中某个方法调用前
-
- 方法抛出异常后
-
-
Spring 框架的缺陷
- 只支持方法的连接点
- 方法调用前 方法调用后 方法抛出异常 方法调用前后程序执行点织入
- 只支持方法的连接点
-
-
PointCut: 切点
- 定位到某个连接点的查询工具,需要提供方位信息
-
Aspect: 切面
- 组成部分 : 增强 + 切点
-
- 织入实现方式
- 编译器织入
- ajc 编译器提供
- 类装载期织入
- ClassLoader提供
- 动态代理织入
- 运行期为目标类添加增强生成子类
- 编译器织入
- 织入实现方式
-
Target: 目标对象
- 定义 PointCut
- 我们需要对哪些地方增加额外的操作,通过PointCut查询JoinPoint
- 告诉程序 JointCut 怎样增强 Advice
- Aspect 里面被修复的对象叫 Target,完成AOP操作叫Weaving
- 定义 PointCut
掌握这些基础知识,AOP差不多就学会了,等等,这么快就学会了,不是还没走源码嘛,这个Spring我也没有太多研究过,后面有机会再和大伙过一遍,我们开始直接正入主题,进入我们的Title AspectJ 吧 ,学习 AspectJ 有一个很重要的基础,就是得了解AspectJ 注解,常见的切点表达式,只有这样,才能正确使用AspectJ,基于自定义Gradle Plugin 实现代码织入等一系列好玩的事情。
- 切面类,目的是让 ajc 编译器识别
@Aspect
public class MkOnClickListenerAspectJ {}
MkOnClickListenerAspectJ 类 在编译器 会被 AspectJ 的 ajc 编译器识别
- 定义切点标记方法
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void fastClickViewPointcut(JoinPoint point) {
Log.e("fastClickViewPointcut", "------------------");
}
Pointcut 注解用来匹配 OnClickListener 注解的 onClick 方法
- 前置增强,在某个连接点之前执行
@Before("execution(void android.content.DialogInterface.OnClickListener.onClick(..))")
public void fastClickViewBefore(JoinPoint point) {
View view = (View) point.getArgs()[0];
Log.e("fastClickViewBefore", view.toString()+"------------------");
}
这个切点表达式,可以匹配所有 android.content.DialogInterface.OnClickListener.onClick 方法,并在方法之前获取view,然后将其打印
- 后置增强,在某个连接点之后执行
@After("execution(void android.support.v7.app.AppCompatViewInflater.DeclaredOnClickListener.onClick(..))")
public void fastClickViewAfter(JoinPoint point) {
View view = (View) point.getArgs()[0];
Log.e("fastClickViewAfter", view.toString()+"------------------");
}
上面的切点表达式,可以匹配所有DeclaredOnClickListener.onClick方法,并在方法之后获取 view 参数,然后将其打印
- 环绕增强,在切点前后执行
@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void fastClickViewAround(ProceedingJoinPoint point) throws Throwable {
Log.e("AspectJ", "before" );
long startNanoTime = System.nanoTime();
Object proceed = point.proceed();
Log.e("AspectJ", "after" );
long stopNanoTime = System.nanoTime();
MethodSignature signature = (MethodSignature) point.getSignature();
// 方法名
String name = signature.getName();
Log.e("AspectJ", "proceed" + name);
Method method = signature.getMethod();
Log.e("AspectJ", method.toGenericString());
// 返回值类型
Class returnType = signature.getReturnType();
Log.e("AspectJ", returnType.getSimpleName());
Class declaringType = signature.getDeclaringType();
Log.e("AspectJ", declaringType.getSimpleName());
Class signatureDeclaringType = signature.getDeclaringType();
Log.e("AspectJ", signatureDeclaringType.getSimpleName());
Class declaringType1 = signature.getDeclaringType();
Log.e("AspectJ", declaringType1.getSimpleName());
Class[] parameterTypes = signature.getParameterTypes();
for (Class parameterType : parameterTypes) {
Log.e("AspectJ", parameterType.getSimpleName());
}
for (String parameterName : signature.getParameterNames()) {
Log.e("AspectJ", parameterName);
}
Log.e("AspectJ", String.valueOf(stopNanoTime - startNanoTime));
}
方法在执行之前打印 Log.e("AspectJ", "before" ); 执行之后打印 Log.e("AspectJ", "after" );,主要是根据 proceed 返回值处理不同的业务场景
- 返回增强切入点方法返回结果后执行
@AfterReturning("execution(@butterknife.OnClick * *(..))")
public void fastClickViewAfterReturning(JoinPoint point) {
Log.e("AfterReturning", "------------------");
}
可以匹配所有 @butterknife.OnClick 方法,并在结果返回结果之后打印 "AfterReturning"
- 异常增强,切点抛出异常时执行
@AfterThrowing("execution(@butterknife.OnClick * *(..))")
public void fastClickViewThrowing(JoinPoint point) {
Log.e("fastClickViewThrowing", "------------------");
}
execution(@butterknife.OnClick * *(..)) 抛出异常的时候打印,可以用这个 API 做日志上报工作
了解了这么多切点以及切点表达式的使用,那么他们使用有什么通用的公式呢,下面我们进入总结阶段。
- execution
public class C {
public void foo() {
bar();
}
public void bar() {
}
}
public aspect A {
// the output will be:
// call(void C.bar())
// execution(void C.foo())
before() :
call(public void C.bar()) {
System.out.println(thisJoinPoint);
System.out.println(thisEnclosingJoinPointStaticPart);
}
// the output will be:
// execution(void C.bar())
// execution(void C.bar())
before() :
execution(public void C.bar()) {
System.out.println(thisJoinPoint);
System.out.println(thisEnclosingJoinPointStaticPart);
}
}
其实两者最大的区别是,一个是在调用点,一个是执行点。也就是说 execution 是切入方法中,call 是 在调用被切入的方法
- 通过 Gradle Plugin 如: AspectJx
- 通过Gradle 配置
之前在 akulaku
用这种方式做了一个需求,结果被领导批头盖脸骂了一顿,说我这边没把原理弄清楚,是的我是直接从github抄的参数,当时很无语,我用一天的时间做完了三天工时的需求,还给了两套方案,现在想想其实这种方法对不熟悉Gradle Plugin并不是很靠谱,除非你确实不想用三方插件实现
- 因为开发是基于 Groovy 语言的,所以插件 的代码放在 src/main/groovy 目录下,然后在该目录新建一个pageage,命名为: com.github.microkibaco.plugin.android
- 在 src/main 目录下,一次创建 resource/META-INFgradle-plugins 文件,在创建一个 后缀为.properties 的文件,用来声明名称以及对于常见的包名
- implementation-class= 包名 + 类名
- implementation-class=com.github.microkibaco.plugin.android.MkAspectjPlugin
- 编译这个 plugin,可以在 plugin/build 发现新生成的插件 .jar 文件
- 2.0.7.1 发布方式
- 修改 Project/build.gradle 配置,格式为: groupId.artfactId:version
- 修改 app/build.gradle 格式为 resource/META-INFgradle-plugins .properties前缀文件名
- 最核心的模块是 ajc 编译器,它其实就是将 AspectJ 代码在编译期 插入目标程序当中,使用 AspectJ 最关键的是使用 ajc 编译器 , 编译器将 AspectJ 代码插入切出来的 PointCut 中,达到AOP 目的
- Android 系统中的 View 它的点击处理逻辑,都是通过设置相应的 listener 对象并重写相应回调方法实现
- 在应用编译期间,如生成 .dex 前在 onClick 方法中插入相应埋点代码,即可做到自动埋点,也就是全埋点
- AspectJ 的处理脚本放到我们的自定义插件里,编写相应的切面类没在定义合适的 PointCut 用来匹配我们织入目标方法,如 onClick ,这样就可以在编译期插入埋点代码
初始化埋点SDK,一般在Application使用
获取埋点SDK实例对象
/**
* 获取 view 的 anroid:id 对应的字符串
*
* @param view View
* @return String
*/
private static String getViewId(View view) {
String idString = null;
try {
if (view.getId() != View.NO_ID) {
idString = view.getContext().getResources().getResourceEntryName(view.getId());
}
} catch (Exception e) {
//ignore
}
return idString;
}
/**
* 获取 View 所属 Activity
*
* @param view View
* @return Activity
*/
private static Activity getActivityFromView(View view) {
Activity activity = null;
if (view == null) {
return null;
}
try {
Context context = view.getContext();
if (context != null) {
if (context instanceof Activity) {
activity = (Activity) context;
} else if (context instanceof ContextWrapper) {
while (!(context instanceof Activity) && context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
}
if (context instanceof Activity) {
activity = (Activity) context;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return activity;
}
// 调用
activity.getClass().getCanonicalName()
/**
* 获取 Activity 的 title
*
* @param activity Activity
* @return Activity 的 title
*/
@androidx.annotation.RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static String getActivityTitle(Activity activity) {
try {
if (activity != null) {
try {
String activityTitle = null;
if (!TextUtils.isEmpty(activity.getTitle())) {
activityTitle = activity.getTitle().toString();
}
String toolbarTitle = getToolbarTitle(activity);
if (!TextUtils.isEmpty(toolbarTitle)) {
activityTitle = toolbarTitle;
}
if (TextUtils.isEmpty(activityTitle)) {
PackageManager packageManager = activity.getPackageManager();
if (packageManager != null) {
ActivityInfo activityInfo = packageManager.getActivityInfo(activity.getComponentName(), 0);
if (!TextUtils.isEmpty(activityInfo.loadLabel(packageManager))) {
activityTitle = activityInfo.loadLabel(packageManager).toString();
}
}
}
return activityTitle;
} catch (Exception e) {
return null;
}
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@androidx.annotation.RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static String getToolbarTitle(Activity activity) {
try {
ActionBar actionBar = activity.getActionBar();
if (actionBar != null) {
if (!TextUtils.isEmpty(actionBar.getTitle())) {
return actionBar.getTitle().toString();
}
} else {
if (activity instanceof AppCompatActivity) {
AppCompatActivity appCompatActivity = (AppCompatActivity) activity;
androidx.appcompat.app.ActionBar supportActionBar = appCompatActivity.getSupportActionBar();
if (supportActionBar != null) {
if (!TextUtils.isEmpty(supportActionBar.getTitle())) {
return Objects.requireNonNull(supportActionBar.getTitle()).toString();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 支持 TabHost.OnTabChangeListener.onTabChanged(String)
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.TabHost.OnTabChangeListener.onTabChanged(String))")
public void onTabChangedAop(final JoinPoint joinPoint) {
String tabName = (String) joinPoint.getArgs()[0];
SensorsDataPrivate.trackTabHost(tabName);
}
/**
* 设置View属性
*
* @param view 要设置的View
* @param properties 要设置的View的属性
*/
public void setViewProperties(View view, JSONObject properties) {
if (view == null || properties == null) {
return;
}
view.setTag(R.id.sensors_analytics_tag_view_properties, properties);
}
// 获取这个属性
Object pObject = view.getTag(R.id.sensors_analytics_tag_view_properties);
/**
* 支持 ButterKnife @OnClick 注解
*
* @param joinPoint JoinPoint
*/
@After("execution(@butterknife.OnClick * *(android.view.View))")
public void onButterknifeClickAop(final JoinPoint joinPoint) {
View view = (View) joinPoint.getArgs()[0];
SensorsDataPrivate.trackViewOnClick(view);
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensorsDataTrackViewOnClick {
}
/**
* 支持 @SensorsDataTrackViewOnClick 注解
*
* @param joinPoint JoinPoint
*/
@After("execution(@com.sensorsdata.analytics.android.sdk.SensorsDataTrackViewOnClick * *(android.view.View))")
public void onTrackViewOnClickAop(final JoinPoint joinPoint) {
View view = (View) joinPoint.getArgs()[0];
SensorsDataPrivate.trackViewOnClick(view);
}
/**
* 支持 onMenuItemSelected(int, android.view.MenuItem)
*
* @param joinPoint JoinPoint
*/
@After("execution(* android.app.Activity.onMenuItemSelected(int, android.view.MenuItem))")
public void onMenuItemSelectedAop(JoinPoint joinPoint) {
MenuItem view = (MenuItem) joinPoint.getArgs()[1];
SensorsDataPrivate.trackViewOnClick(joinPoint.getTarget(), view);
}
/**
* 支持 DialogInterface.OnClickListener.onClick(android.content.DialogInterface, int)
*
* @param joinPoint JoinPoint
*/
@After("execution(* android.content.DialogInterface.OnClickListener.onClick(android.content.DialogInterface, int))")
public void onDialogClickAop(final JoinPoint joinPoint) {
DialogInterface dialogInterface = (DialogInterface) joinPoint.getArgs()[0];
int which = (int) joinPoint.getArgs()[1];
SensorsDataPrivate.trackViewOnClick(dialogInterface, which);
}
/**
* 支持 CompoundButton.OnCheckedChangeListener.onCheckedChanged(android.widget.CompoundButton,boolean)
*
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.CompoundButton.OnCheckedChangeListener.onCheckedChanged(android.widget.CompoundButton,boolean))")
public void onCheckedChangedAop(final JoinPoint joinPoint) {
CompoundButton compoundButton = (CompoundButton) joinPoint.getArgs()[0];
boolean isChecked = (boolean) joinPoint.getArgs()[1];
SensorsDataPrivate.trackViewOnClick(compoundButton, isChecked);
}
/**
* 支持 RatingBar.OnRatingBarChangeListener.onRatingChanged(android.widget.RatingBar,float,boolean)
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.RatingBar.OnRatingBarChangeListener.onRatingChanged(android.widget.RatingBar,float,boolean))")
public void onRatingBarChangedAop(final JoinPoint joinPoint) {
View view = (View) joinPoint.getArgs()[0];
SensorsDataPrivate.trackViewOnClick(view);
}
/**
* 支持 SeekBar.OnSeekBarChangeListener.onStopTrackingTouch(android.widget.SeekBar)
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.SeekBar.OnSeekBarChangeListener.onStopTrackingTouch(android.widget.SeekBar))")
public void onStopTrackingTouchMethod(JoinPoint joinPoint) {
View view = (View) joinPoint.getArgs()[0];
SensorsDataPrivate.trackViewOnClick(view);
}
if (adapterView instanceof Spinner) {
properties.put("$element_type", "Spinner");
Object item = adapterView.getItemAtPosition(position);
if (item != null) {
if (item instanceof String) {
properties.put("$element_content", item);
}
}
}
/**
* 支持 TabHost.OnTabChangeListener.onTabChanged(String)
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.TabHost.OnTabChangeListener.onTabChanged(String))")
public void onTabChangedAop(final JoinPoint joinPoint) {
String tabName = (String) joinPoint.getArgs()[0];
SensorsDataPrivate.trackTabHost(tabName);
}
/**
* public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id)
*
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.ExpandableListView.OnChildClickListener.onChildClick(android.widget.ExpandableListView, android.view.View, int, int, long))")
public void onExpandableListViewChildClickAop(final JoinPoint joinPoint) {
ExpandableListView expandableListView = (ExpandableListView) joinPoint.getArgs()[0];
View view = (View) joinPoint.getArgs()[1];
int groupPosition = (int) joinPoint.getArgs()[2];
int childPosition = (int) joinPoint.getArgs()[3];
SensorsDataPrivate.trackExpandableListViewChildOnClick(expandableListView, view, groupPosition, childPosition);
}
/**
* public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l)
*
* @param joinPoint JoinPoint
*/
@After("execution(* android.widget.ExpandableListView.OnGroupClickListener.onGroupClick(android.widget.ExpandableListView, android.view.View, int, long))")
public void onExpandableListViewGroupClickAop(final JoinPoint joinPoint) {
ExpandableListView expandableListView = (ExpandableListView) joinPoint.getArgs()[0];
View view = (View) joinPoint.getArgs()[1];
int groupPosition = (int) joinPoint.getArgs()[2];
SensorsDataPrivate.trackExpandableListViewChildOnClick(expandableListView, view, groupPosition, -1);
}
- 无法织入第三方库
- 由于定义的切点以来编程语言,该方案无法兼容 Lambada 语法
- 会有一些兼容问题,如: D8 Gradle 4.x 等等
学习一个新的技术,最重要的是考虑他自身所携带的业务属性,那么AspectJ在实际开发中到底有什么用呢?贴一张前同事整理的脑图,后期遇到以下问题,可以优先考虑用切面的思想解决
你的 点赞、评论,是对我的巨大鼓励!