WHY
如果说OOP(面向对象的程序设计)的主要思想是将问题归分到相应的对象(类)中去实现,再把相关类模块化使得模块内聚、模块间低耦。正是因为高度的模块化使得每个模块处理类似需求的时候,很容易造成冗余,分散的情况
那么AOP面向切面编程(Aspect-Oriented Programming)就是把涉及到众多模块的某一类问题进行统一管理,因为AOP是方法论,你可以用任何的方式按照这个思想去实现。
那么问题来了:怎么样不需要显示的去修改原来的业务逻辑,最少的改动插入代码,去优雅的实现AOP切面。
WHO
AOP从实现原理上可以分为运行时AOP和编译时AOP,对于Android来讲运行时AOP的实现主要是hook某些关键方法,编译时AOP主要是在Apk打包过程中对java,class,dex文件进行扫描更改。
Android主流的aop 框架和工具类库:
- AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展
- Javassist for Android: 用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。
- DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
- ASMDEX: 一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。
- APT: 通过Android-apt的Gradle插件(官方),以及jre原生代码在编译器生成Java文件
本文主要探究一下通过预编译方式和运行期动态代理在Android中的AOP实现方式,包括***APT,AspectJ ***
其实APT,AspectJ,Javassist正好对应的是Android不通编译期,进行代码或者字节码操作的,因为实现的方式,解决的问题也有不同
其实通俗来说,
APT就是单独用一个Java的module来生成app内的java代码
AspectJ就是提供了很多横切关注点来在编译class文件的时候动态来修改你的java的代码
HOW
APT
代表框架:DataBinding,Dagger2, ButterKnife, EventBus3 、AndroidAnnotation等
注解处理器 Java5 中叫APT(Annotation Processing Tool),在Java6开始,规范化为 Pluggable Annotation Processing。Apt应该是这其中我们最常见到的了,难度也最低。定义编译期的注解,再通过继承Proccesor实现代码生成逻辑,实现了编译期生成代码的逻辑。
Talk is cheap. Show me your code .
下面我来用APT实现一个简单的全局路由来演示一下APT的工作原理
-
1.配置
使用官方gradle插件(如果之前有使用框架用到了android-apt插件的最好去掉,原作者不再维护了,而且两个插件兼容会有问题)
主app的module的gradle配置,工程依赖apt入口
dependencies {
annotationProcessor project(':apt')
compile project(':apt-lib')
}
创建一个apt-lib的module用来提供路由需要的注解
gradle配置
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
// 解决build警告:编码GBK的不可映射字符
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
全局路由注解类
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Rounter {
String value();
}
路由参数注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Extra {
String value();
}
创建一个apt的module用来负责编译期生成java代码
gradle配置
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':lib')
}
// 解决build警告:编码GBK的不可映射字符
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
com.google.auto.service:auto-service:1.0-rc2
谷歌提供的Java 生成源代码库
com.squareup:javapoet:1.7.0
提供了各种 API 让你用各种姿势去生成 Java 代码文件
- 2.AnnotationProcessor
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes({//标注注解处理器支持的注解类型
"com.app.annotation.apt.Rounter"})
public class AnnotationProcessor extends AbstractProcessor {
public Filer mFiler; //文件相关的辅助类
public Elements mElements; //元素相关的辅助类
public Messager mMessager; //日志相关的辅助类
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mFiler = processingEnv.getFiler();
mElements = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
new RouterProcessor().process(roundEnv, this);
return true;
}
}
@AutoService(Processor.class)
指定了AnnotationProcessor
类为Processor
的入口process方法相当于java的main()方法
动态生成路由类TRouter
的RouterProcessor
类
public class RouterProcessor implements IProcessor {
@Override
public void process(RoundEnvironment roundEnv, AnnotationProcessor mAbstractProcessor) {
String CLASS_NAME = "TRouter";
TypeSpec.Builder tb = classBuilder(CLASS_NAME).addModifiers(PUBLIC, FINAL).addJavadoc("@ 全局路由器 此类由apt自动生成");
FieldSpec extraField = FieldSpec.builder(ParameterizedTypeName.get(DataMap.class), "mCurActivityExtra")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.build();
tb.addField(extraField);
MethodSpec.Builder methodBuilder1 = MethodSpec.methodBuilder("go")
.addJavadoc("@此方法由apt自动生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "name").addParameter(DataMap.class, "extra")
.addParameter(ClassName.get("android.view", "View"), "view");
MethodSpec.Builder methodBuilder2 = MethodSpec.methodBuilder("bind")
.addJavadoc("@此方法由apt自动生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(ClassName.get("android.app", "Activity"), "mContext");
List<ClassName> mList = new ArrayList<>();
CodeBlock.Builder blockBuilderGo = CodeBlock.builder();
CodeBlock.Builder blockBuilderBind = CodeBlock.builder();
ClassName appClassName = ClassName.get("com.wingjay.jianshi.global", "ActivityStackManager");
blockBuilderGo.addStatement("mCurActivityExtra=extra");
blockBuilderGo.addStatement("Activity mContext=$T.getInstance().getTopActivity()", appClassName);
blockBuilderGo.beginControlFlow(" switch (name)");//括号开始
blockBuilderBind.add(" if (mCurActivityExtra == null) return;\n");
blockBuilderBind.beginControlFlow(" switch (mContext.getClass().getSimpleName())");//括号开始
List<RouterActivityModel> mRouterActivityModels = new ArrayList<>();
try {
for (TypeElement element : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Rounter.class))) {
ClassName currentType = ClassName.get(element);
if (mList.contains(currentType)) continue;
mList.add(currentType);
RouterActivityModel mRouterActivityModel = new RouterActivityModel();
mRouterActivityModel.setElement(element);
mRouterActivityModel.setActionName(element.getAnnotation(Rounter.class).value());
List<Element> mExtraElements = new ArrayList<>();
List<String> mExtraElementKeys = new ArrayList<>();
for (Element childElement : element.getEnclosedElements()) {
SceneTransition mSceneTransitionAnnotation = childElement.getAnnotation(SceneTransition.class);
if (mSceneTransitionAnnotation != null) {
mRouterActivityModel.setSceneTransitionElementName(mSceneTransitionAnnotation.value());
mRouterActivityModel.setSceneTransitionElement(childElement);
}
Extra mExtraAnnotation = childElement.getAnnotation(Extra.class);
if (mExtraAnnotation != null) {
mExtraElementKeys.add(mExtraAnnotation.value());
mExtraElements.add(childElement);
}
}
mRouterActivityModel.setExtraElementKeys(mExtraElementKeys);
mRouterActivityModel.setExtraElements(mExtraElements);
boolean isNeedBind = (mExtraElementKeys != null && mExtraElementKeys.size() > 0
|| mRouterActivityModel.getSceneTransitionElement() != null);
mRouterActivityModel.setNeedBind(isNeedBind);
mRouterActivityModels.add(mRouterActivityModel);
}
ClassName mActivityCompatName = ClassName.get("android.support.v4.app", "ActivityCompat");
ClassName mIntentClassName = ClassName.get("android.content", "Intent");
ClassName mActivityOptionsCompatName = ClassName.get("android.support.v4.app", "ActivityOptionsCompat");
for (RouterActivityModel item : mRouterActivityModels) {
blockBuilderGo.add("case $S: \n", item.getActionName());//1
if (item.isNeedBind())
blockBuilderBind.add("case $S: \n", item.getElement().getSimpleName());//1
if (item.getExtraElements() != null && item.getExtraElements().size() > 0) {
for (int i = 0; i < item.getExtraElements().size(); i++) {
Element mFiled = item.getExtraElements().get(i);
blockBuilderBind.add("(($T)mContext)." +//1
"$L" +//2
"= ($T) " +//3
"mCurActivityExtra.get(" +//4
"$S);\n",//5
item.getElement(),//1
mFiled,//2
mFiled,//3
item.getExtraElementKeys().get(i)//5
);//5
}
}
if (item.getSceneTransitionElement() != null) {
blockBuilderGo.add("$L.startActivity(mContext," +//2
"\nnew $L(mContext," +//3
"\n$L.class)," +//4
"\n$T.makeSceneTransitionAnimation(" +//5
"\nmContext,view," +//6
"\n$S).toBundle());", //7
mActivityCompatName,//2
mIntentClassName,//3
item.getElement(),//4
mActivityOptionsCompatName,//5
item.getSceneTransitionElementName());//6
blockBuilderBind.add(
"$T.setTransitionName(" +//2
"(($T)mContext).mViewBinding." +//3
"$L, " +//4
"$S);\n",//5
ClassName.get("android.support.v4.view", "ViewCompat"),//2
item.getElement(),//3
item.getSceneTransitionElement(),//4
item.getSceneTransitionElementName());//5
} else {
blockBuilderGo.add("mContext.startActivity(" +//2
"\nnew $L(mContext," +//3
"\n$L.class));", //7
mIntentClassName,//3
item.getElement()//4
);
}
blockBuilderGo.addStatement("\nbreak");//1
if (item.isNeedBind()) blockBuilderBind.addStatement("break");//1
}
blockBuilderGo.addStatement("default: break");
blockBuilderGo.endControlFlow();
methodBuilder1.addCode(blockBuilderGo.build());
blockBuilderBind.addStatement("default: break");
blockBuilderBind.endControlFlow();
methodBuilder2.addCode(blockBuilderBind.build());
tb.addMethod(methodBuilder1.build());
tb.addMethod(methodBuilder2.build());
//增加go(action)和go(action,extra):两个重载方法
tb.addMethod(MethodSpec.methodBuilder("go")
.addJavadoc("@此方法由apt自动生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "name")
.addParameter(DataMap.class, "extra")
.addCode("go(name,extra,null);\n").build());
tb.addMethod(MethodSpec.methodBuilder("go")
.addJavadoc("@此方法由apt自动生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "name")
.addCode("go(name,null,null);\n").build());
tb.addMethod(MethodSpec.methodBuilder("getMap")
.returns(DataMap.class)
.addJavadoc("@此方法由apt自动生成")
.addModifiers(PUBLIC, STATIC)
.addCode("return new DataMap();\n").build());
JavaFile javaFile = JavaFile.builder(AptConstants.PackageName, tb.build()).build();// 生成源代码
javaFile.writeTo(mAbstractProcessor.mFiler);// 在 app module/build/generated/source/apt 生成一份源代码
} catch (FilerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
看结尾javaFile.writeTo(mAbstractProcessor.mFiler)
,
OK这个类就是使用了com.squareup:javapoet提供的一些很方便写.java文件的类和方法来生成最终的TRounter.java
-
3.使用路由
下面就可以在原来的项目中使用路由了
@Rounter(RounterConstants.MAIN)
public class MainActivity extends BaseActivity {
...
}
@Rounter(RounterConstants.EDIT)
public class EditActivity extends BaseActivity {
@Extra(DataMap.DATA_HEAD)
public String diaryUUID;
...
}
路由跳转
TRouter.go(RounterConstants.MAIN);
TRouter.go(RounterConstants.EDIT, TRouter.getMap().addHead(diary.getUuid()));
那么生成的这个TRounter在哪呢,可以在\build\generated\source\apt\debug中相应的包名下查看apt生成的代码,如下
/**
* @ 全局路由器 此类由apt自动生成 */
public final class TRouter {
public static DataMap mCurActivityExtra;
/**
* @此方法由apt自动生成 */
public static void go(String name, DataMap extra, View view) {
mCurActivityExtra=extra;
Activity mContext=ActivityStackManager.getInstance().getTopActivity();
switch (name) {
case "rounter_diary":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.DiaryListActivity.class));
break;
case "rounter_edit":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.EditActivity.class));
break;
case "rounter_main":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.MainActivity.class));
break;
case "rounter_setting":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.SettingActivity.class));
break;
case "rounter_signup":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.SignupActivity.class));
break;
case "rounter_view":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.ViewActivity.class));
break;
default: break;
}
}
/**
* @此方法由apt自动生成 */
public static void bind(Activity mContext) {
if (mCurActivityExtra == null) return;
switch (mContext.getClass().getSimpleName()) {
case "EditActivity":
((EditActivity)mContext).diaryUUID= (String) mCurActivityExtra.get("data_head");
break;
case "ViewActivity":
((ViewActivity)mContext).diaryUuid= (String) mCurActivityExtra.get("data_head");
break;
default: break;
}
}
/**
* @此方法由apt自动生成 */
public static void go(String name, DataMap extra) {
go(name,extra,null);
}
/**
* @此方法由apt自动生成 */
public static void go(String name) {
go(name,null,null);
}
/**
* @此方法由apt自动生成 */
public static DataMap getMap() {
return new DataMap();
}
}
好了如上就简单可以用APT实现一个简单的全局路由了,如果不是开发框架的团队的话,我认为APT在搭建团队工程的框架方面还是有很大的利用场景的,比方说绑定model和View和present,比方说生成一些约定的接口或者方法
End of APT
AspectJ
代表框架:我就只知道JakeWharton/hugo了 = =||
如果说APT还很难体现AOP的概念的话,那么AspectJ就是先行者特别为AOP开发的一套语言支持了。它是一种几乎和Java完全一样的语言,或者说AspectJ应该就是一种扩展Java。当然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。
AspectJ现在托管于Eclipse项目中
|【这是官方网站】
|【这是参考文档】
|【这是接口文档】
主要术语
- Cross-cutting concerns(横切关注点): 尽管面向对象模型中大多数类会实现单一特定的功能,但通常也会开放一些通用的附属功能给其他类。例如,我们希望在数据访问层中的类中添加日志,同时也希望当UI层中一个线程进入或者退出调用一个方法时添加日志。尽管每个类都有一个区别于其他类的主要功能,但在代码里,仍然经常需要添加一些相同的附属功能。
- Advice(通知): 注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
- Joint point(连接点): 程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
- Pointcut(切入点): 告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。
- Aspect(切面): Pointcut 和 Advice 的组合看做切面。例如,我们在应用中通过定义一个 pointcut 和给定恰当的advice,添加一个日志切面。
-
Weaving(织入): 注入代码(advices)到目标位置(joint points)的过程。
整体原理就如下图:
- 其实我的理解就是AspectJ提供了各种各样的切入点(关注点/代码插入点),在生成class文件的时候可以动态替换你原来的代码或者插入你想要的代码
举几个栗子
- 性能监控切片
- 用户登录切片
- 防止频繁点击
- 用户行为路径
环境配置
项目的Gradle
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
classpath 'org.aspectj:aspectjtools:1.8.1'
}
app的Gradle
import com.app.plugin.AspectjPlugin
dependencies {
compile 'org.aspectj:aspectjrt:1.8.9'
}
性能监控切片
性能监控的注解
@Retention(RetentionPolicy.CLASS)//这个注解周期声明在 class 文件上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})//可以注解构造函数和方法
public @interface MonitorLog {
}
切片实现类,如果看不懂的可以去上面看下语法
@Aspect
public class MonitorAspect {
@Pointcut("execution(@com.zzj.jianshi.aspect.annotation.MonitorLog * *(..))")//方法切入点
public void methodAnnotated() {
}
@Pointcut("execution(@com.zzj.jianshi.aspect.annotation.MonitorLog *.new(..))")//构造器切入点
public void constructorAnnotated() {
}
@Around("methodAnnotated() || constructorAnnotated()")//在连接点进行方法替换
public Object aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (!isMonitorOpened()) {//后台下发监控开关
return joinPoint.proceed();
}
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
long startTime = System.nanoTime();
Object result = joinPoint.proceed();//执行原方法
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(methodName + ":");
for (Object obj : joinPoint.getArgs()) {
if (obj instanceof String) keyBuilder.append((String) obj);
else if (obj instanceof Class) keyBuilder.append(((Class) obj).getSimpleName());
}
String key = keyBuilder.toString();
Timber.e("MonitorAspect --->: " + className + "." + key + joinPoint.getArgs().toString() +
" --->:" + "[" + (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) + "ms]");// 记录时间差
return result;
}
}
业务代码中,在耗时操作或者关键方法中添加注解,如下:
@Override
@MonitorLog
protected void onStart() {
可以看下日志输出
06-08 15:49:02.457 729-729/? E/MonitorAspect: MonitorAspect --->: JianShiApplication.initLog:[Ljava.lang.Object;@c567935 --->:[2ms]
06-08 15:49:02.465 729-729/? E/MonitorAspect: MonitorAspect --->: JianShiApplication.onCreate:[Ljava.lang.Object;@3a1d85d --->:[53ms]
06-08 15:49:03.684 729-729/com.wingjay.android.jianshi E/MonitorAspect: MonitorAspect --->: MainActivity.onCreate:[Ljava.lang.Object;@64c8c14 --->:[369ms]
06-08 15:49:03.816 729-729/com.wingjay.android.jianshi E/MonitorAspect: MonitorAspect --->: MainActivity.onStart:[Ljava.lang.Object;@970c60a --->:[130ms]
06-08 15:50:20.288 729-729/com.wingjay.android.jianshi E/MonitorAspect: MonitorAspect --->: MainActivity.onStart:[Ljava.lang.Object;@efb02be --->:[2ms]
用户登录切片
@Aspect
public class CheckLoginAspect {
@Pointcut("execution(@com.app.annotation.aspect.CheckLogin * *(..))")//方法切入点
public void methodAnnotated() {
}
@Around("methodAnnotated()")//在连接点进行方法替换
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (null == SpUtil.getUser()) {
Snackbar.make(App.getAppContext().getCurActivity().getWindow().getDecorView(), "请先登录!", Snackbar.LENGTH_LONG)
.setAction("登录", new View.OnClickListener() {
@Override
public void onClick(View view) {
TRouter.go(C.LOGIN);//跳转到登录页面
}
}).show();
return;
}
joinPoint.proceed();//执行原方法
}
}
防止频繁点击
@Aspect
public class SingleClickAspect {
static int TIME_TAG = R.id.click_time;
public static final int MIN_CLICK_DELAY_TIME = 600;
@Pointcut("execution(@com.app.annotation.aspect.SingleClick * *(..))")//方法切入点
public void methodAnnotated() {
}
@Around("methodAnnotated()")//在连接点进行方法替换
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
View view = null;
for (Object arg : joinPoint.getArgs())
if (arg instanceof View) view = (View) arg;
if (view != null) {
Object tag = view.getTag(TIME_TAG);
long lastClickTime = ((tag != null) ? (long) tag : 0);
LogUtils.showLog("SingleClickAspect", "lastClickTime:" + lastClickTime);
long currentTime = Calendar.getInstance().getTimeInMillis();
if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {//过滤掉600毫秒内的连续点击
view.setTag(TIME_TAG, currentTime);
LogUtils.showLog("SingleClickAspect", "currentTime:" + currentTime);
joinPoint.proceed();//执行原方法
}
}
}
}
OK,用户行为路径什么的我就没有实现了,是用来装逼的
那么问题来了,说好的AspectJ执行之后修改的class代码去哪里了?
我们来看\build\intermediates\classes下面对应的class的原来代码变了么?
源代码
@Override
protected void onPause() {
super.onPause();
Timber.e("befor testAspect() ");
testAspect();
Timber.e("after testAspect() ");
}
@MonitorLog
public void testAspect() {
Timber.e("testAspect() inn start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Timber.e("testAspect() inn end");
}
\build\intermediates\classes下面
protected void onPause() {
super.onPause();
Timber.e("testAspect() befor", new Object[0]);
this.testAspect();
Timber.e("testAspect() after", new Object[0]);
}
@MonitorLog
public void testAspect() {
JoinPoint var2 = Factory.makeJP(ajc$tjp_2, this, this);
MonitorAspect var10000 = MonitorAspect.aspectOf();
Object[] var3 = new Object[]{this, var2};
var10000.aroundJoinPoint((new MainActivity$AjcClosure5(var3)).linkClosureAndJoinPoint(69648));
}
为什么要加日志?没错我就是想看一下AspectJ切片对原逻辑性能是否有影响,我相信这个对于加入项目的选型可能是致命的,下面来看我实际测试的日志
06-08 16:23:33.895 31713-31713/ E/MainActivity: befor testAspect()
06-08 16:23:33.895 31713-31713/ E/MonitorAspect: MonitorAspect start
06-08 16:23:33.895 31713-31713/ E/MainActivity: testAspect() inn start
06-08 16:23:33.997 31713-31713/ E/MainActivity: testAspect() inn end
06-08 16:23:33.997 31713-31713/ E/MonitorAspect: MonitorAspect --->: MainActivity.testAspect:[Ljava.lang.Object;@7f6e61b --->:[101ms]
06-08 16:23:33.998 31713-31713/ E/MonitorAspect: MonitorAspect end
06-08 16:23:33.998 31713-31713/ E/MainActivity: after testAspect()
这就说明在编译器做的代理性能还是非常高的
可以说AspectJ是面向切面编程最好的切入点了