过去总是听到面向切面编程(Aspect Oriented Programming)话题,一直没有找到合适的机会去了解它。最近在不同项目之间切换,每次都要写一些重复的代码,想减轻一下工作量。正好每个项目都需要打log,那么就从最简单的打log开始吧。
实现打log
对于Android应用开发者,应该都会通过log输出查看自己程序的运行情况。因此在代码适当位置添加log输出代码是必不可少的。接下来探讨一下怎样简化log输出方法的调用。
初级版
最开始,我们一般这样打log来观察Activity
的状态变换:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
}
启动和关闭应用时可以看到类似如下的log输出:1
2
3
4
5
62019-02-23 15:08:24.671 26599-26599/com.ihuntto.aop_simplelogger D/MainActivity: onCreate
2019-02-23 15:08:24.673 26599-26599/com.ihuntto.aop_simplelogger D/MainActivity: onStart
2019-02-23 15:08:24.678 26599-26599/com.ihuntto.aop_simplelogger D/MainActivity: onResume
2019-02-23 15:09:15.982 26599-26599/com.ihuntto.aop_simplelogger D/MainActivity: onPause
2019-02-23 15:09:16.558 26599-26599/com.ihuntto.aop_simplelogger D/MainActivity: onStop
2019-02-23 15:09:16.559 26599-26599/com.ihuntto.aop_simplelogger D/MainActivity: onDestroy
慢慢地开始发现tag
就是类名,而msg
就是方法名,当类和方法变多的时候,tag
和msg
的传入就变得很无聊了,而且tag
和msg
有很大的概率会输错。那有没有什么方法可以省去tag
和msg
的输入呢(能少写点代码就尽量少写点代码)?那就要进入进阶版了。
进阶版
既然tag
和msg
分别是固定的类名和方法名,可以通过StackTrace
找到Log.d(*)
调用者对应的类名和方法名。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public final class SimpleLog {
private static final boolean D = BuildConfig.DEBUG;
private static final int STACK_INDEX = 4;
public static void d() {
if (D) {
Log.d(getClassName(), getMethodName());
}
}
private static String getMethodName() {
return Thread.currentThread().getStackTrace()[STACK_INDEX].getMethodName();
}
private static String getClassName() {
return Thread.currentThread().getStackTrace()[STACK_INDEX].getClassName();
}
private SimpleLog() {
}
}
把前面MainActivity
中的Log
换成SimpleLog
后,可能会看到类似如下log输出:1
2
3
4
5
62019-02-23 15:15:53.632 27446-27446/com.ihuntto.aop_simplelogger D/com.ihuntto.aop_simplelogger.MainActivity: onCreate
2019-02-23 15:15:53.634 27446-27446/com.ihuntto.aop_simplelogger D/com.ihuntto.aop_simplelogger.MainActivity: onStart
2019-02-23 15:15:53.639 27446-27446/com.ihuntto.aop_simplelogger D/com.ihuntto.aop_simplelogger.MainActivity: onResume
2019-02-23 15:15:56.306 27446-27446/com.ihuntto.aop_simplelogger D/com.ihuntto.aop_simplelogger.MainActivity: onPause
2019-02-23 15:15:56.466 27446-27446/com.ihuntto.aop_simplelogger D/com.ihuntto.aop_simplelogger.MainActivity: onStop
2019-02-23 15:15:56.467 27446-27446/com.ihuntto.aop_simplelogger D/com.ihuntto.aop_simplelogger.MainActivity: onDestroy
为什么说是可能呢?如果有注意到STACK_INDEX
这个变量的值为4,4是怎么得出来的呢?在我的Android运行环境中这个值是4,换了其它的Android环境就可能是5或者6了。因此,这种方法看似可行,但不够通用。
高阶版
Aspect-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. –Wikipedia
了解了什么是AOP编程,那么怎么实现呢?其实百度谷歌一下就知道怎么在Android中实现AOP编程了。本文中主要介绍使用AspectJ来实现打log的高阶版。先抛开AspectJ的接口介绍,首要目的是使用最少的代码使它运行起来。
定义注解类
定义注解类,主要用于帮助面向切面编程找到合适的切入点,并注入log代码。1
2
3
4@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Logcat {
}
@Retention(RetentionPolicy.CLASS)
表示注解会被保留在class文件中,但在运行时期间不会被识别,@Target(ElementType.METHOD)
表示这个注解只能在方法上使用。
导入AspectJ
- 在Project的build.gradle文件中添加依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.1.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
其中gradle-android-plugin-aspectjx
是一个开源插件,通过这个插件可以很轻松的开始AspectJ编程。
- 在Module的build.gradle文件中添加依赖:
1
2
3
4
5
6
7
8
9
10
11apply plugin: 'com.android.application'
apply plugin: 'com.hujiang.android-aspectjx'
android {
... ...
}
dependencies {
... ...
implementation 'org.aspectj:aspectjrt:1.8.13'
}
编写切面类
1 | @Aspect |
意思是当代码执行到带有@Logcat
注解的方法时注入log输出的方法。
使用注解
现在可以在MainActivity
中的方法上添加@Logcat
注解了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45public class MainActivity extends AppCompatActivity {
@Override
@Logcat
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
@Logcat
protected void onRestart() {
super.onRestart();
}
@Override
@Logcat
protected void onStart() {
super.onStart();
}
@Override
@Logcat
protected void onResume() {
super.onResume();
}
@Override
@Logcat
protected void onPause() {
super.onPause();
}
@Override
@Logcat
protected void onStop() {
super.onStop();
}
@Override
@Logcat
protected void onDestroy() {
super.onDestroy();
}
}
如果不出意外的话,可以看到类似的输出:1
2
3
4
5
62019-02-23 16:30:24.528 2991-2991/com.ihuntto.aop_simplelogger D/MainActivity: onCreate
2019-02-23 16:30:24.570 2991-2991/com.ihuntto.aop_simplelogger D/MainActivity: onStart
2019-02-23 16:30:24.574 2991-2991/com.ihuntto.aop_simplelogger D/MainActivity: onResume
2019-02-23 16:30:27.494 2991-2991/com.ihuntto.aop_simplelogger D/MainActivity: onPause
2019-02-23 16:30:27.717 2991-2991/com.ihuntto.aop_simplelogger D/MainActivity: onStop
2019-02-23 16:30:27.718 2991-2991/com.ihuntto.aop_simplelogger D/MainActivity: onDestroy
和初级版的log输出是一模一样的。初步看起来已经满足了当初的需求,以最少的代码实现打log。但是AspectJ
会不会有副作用呢?
分析优化
如果使用的是AndroidStudio作为开发工具,那么可以很方便的分析APK文件。而本例就要分析使用AspectJ会不会增加APK文件的大小,以及是否会产生更多的方法,以及如何避免这些缺点。
生成APK文件
首先需要生成APK文件。如果在app/build/outputs/apk/debug目录下没有对应的APK文件,那么可以通过点击菜单 Build >> Build Bundle(s)/APK(s) >> Build APK(s)生成APK文件。
解析APK文件
双击打开app/build/outputs/apk/debug/app-debug.apk文件(不需要通过复杂的反编译手段),如图所示。

首先可以看到
org.aspectj
包大小为73.1K;其次可以看到MainActivity
类中有下面这些方法:1 | void onCreate_aroundBody0(Lcom/ihuntto/aop_simplelogger/MainActivity;Landroid/os/Bundle;Lorg/aspectj/lang/JoinPoint;)V |
因此可以得出结论:
- 使用AspectJ会使APK大小增加73.1K
- 每使用一个
@Logcat
注解,就会多产生两个方法尝试优化
由于这种非常简单的log对开发阶段是非常有用的,可以让开发人员了解程序的运行情况,但是在应用发布以后,这些信息变得不那么重要了。那么可不可以在发布版本中去掉AspectJ包和新增的方法呢?下面给出了两种尝试方法。
- 在release版本的编译过程中不执行AspectJ相关的操作。将Module的build.gradle修改如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25apply plugin: 'com.android.application'
boolean isDebug = false
android {
... ...
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
isDebug = true
}
}
}
dependencies {
... ...
implementation 'org.aspectj:aspectjrt:1.8.13'
}
if(isDebug) {
apply plugin: 'com.hujiang.android-aspectjx'
}
因为在release
中没有执行AspectJ相关的编译,所以这一步可以减少MainActivity
中由于使用@Logcat
新增的方法,但仍然会保留org.aspectj
包。
- 在release版本中不依赖
SimpleLogAspect
类。将SimpleLogAspect
移至新的Module中,就取名simplelogger-aspect
,同时将Logcat
移至新的模块simplelogger-annotations
中;然后修改app的build.gradle文件:1
2
3
4
5
6
7
8
9
10
11apply plugin: 'com.android.application'
apply plugin: 'com.hujiang.android-aspectjx'
... ...
dependencies {
... ...
implementation project(':simplelogger-annotations')
debugImplementation project(':simplelogger-aspect')
}
... ...
其中simplelogger-annotations
只包含注解类,而simplelogger-aspect
只包含切面类。通过此方法可以在release
版本中去除APK文件中org.aspectj
包,还可以避免由于使用@Logcat
注解新增方法。
生产工具
将上一步中提到的simplelogger-annotations
和simplelogger-aspect
这两个模块打包上传到meavn仓库。这样每次编写应用时,只需要导入相应的依赖即可,不需要编写注解类和切面类了,也算是一劳永逸了。开源项目SimpleLogger。
总结
本文只是简单介绍了如何使用AspectJ进行面向切面编程,并没有详细介绍AspectJ
的使用方法,如果想要详细了解AspectJ
,可移步官方文档。在平时编写代码时,要更多的注重复用,而不是复制,养成良好的编程习惯,既省时又高效。
参考文档
[1] 利用AspectJ实现Android端非侵入式埋点
[2] Android面向切面编程(AOP)
[3] 安卓AOP三剑客:APT,AspectJ,Javassist
[4] 关于android中使用AspectJ