一、什么是AOP
AOP是Aspect Oriented Programming的缩写,即面向切面编程。平时我们接触比较多的是OOP,即面向对象编程。
OOP
提倡的是将功能模块化,对象化,每个模块专心于自己的事情。但是有些功能是每个模块都需要的,比如日志模块,性能监控模块,按照我们平常的做法就是每个模块中再各自加上这些功能代码,这样做一方面显得代码很冗余,另一方面也不利于后期的拓展和维护。
AOP
提倡的是针对同一类问题的统一处理,我们不需要关注是哪个类哪个对象在使用,我们关注的是具体的方法和功能点。
二、AOP的实现
我认为不管是何种方法实现AOP,最主要的部分就是代码注入。
常见的有三种方式实现AOP,分别是APT、AspectJ、Javassist ,区别在于它们作用的时间不一样,即上面所说的代码注入时机不一样。如下图:
APT
实现方法类似butterknife,具体可以参考我的另一篇文章,Android编译时注解项目学习,用到了编译时注解的技术。
AspectJ
- AspectJ是一个代码生成工具(Code Generator)。
- AspectJ语法就是用来定义代码生成规则的语法。
使用AspectJ有两种方法:
- 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
- 或者使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ,一般都用这种方法进行开发。
语法
深入理解Android之AOP这边文章中介绍的比较好。这里我就简单说下几个比较重要的点,
PointCut切入点,告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。
JPoints连接点,程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
Advice通知,注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
下面这张图简要总结了一下上述这些概念。
简单概括:PointCut声明执行范围,JPoints声明执行位置,Advice声明执行时机
具体实现
我们可以直接引入AspectJ但是Android上使用的话需要另外配置build文件,可以参考【翻译】Android中的AOP编程这篇文章
如果不想自己配置也可以引入现成的SDK,github地址
上述两种方法只是引入的方法不一样具体语法是一致的
这里我们写个简单的例子,实现对Activity的onCreate方法的监听
- 引入AspectJ
具体方法参考上方
- 声明AspectJ类
/**
* 声明一个AspectJ
*/
@Aspect
public class LifeAspect {
private static final String TAG = "LifeAspect";
private long onCreateTime;
/**
* Pointcut,告诉代码在每个onCreate方法中注入
* execution,JPoints类型,函数内部执行,还有call等
*/
@Pointcut("execution(* android.app.Activity.onCreate(..))" )
public void lifeOnCreate(){
}
/**
* Before,Advice的一种类型,切入点之前执行
* @param joinPoint
*/
@Before("lifeOnCreate()")
public void lifeOnCreateHandle(JoinPoint joinPoint){
Log.v(TAG, "lifeOnCreate -->");
onCreateTime = System.currentTimeMillis();
Log.v(TAG, "onCreateTime -->"+onCreateTime);
}
}
- 执行结果如下:
执行原理
AspectJ是通过对目标工程的.class文件进行代码注入的方式将通知(Advise)插入到目标代码中。
第一步:根据pointCut切点规则匹配的joinPoint;
第二步:将Advise插入到目标JoinPoint中。
这样在程序运行时被重构的连接点将会回调Advise方法,就实现了AspectJ代码与目标代码之间的连接。
Javassist
Javassist作用是在编译器间修改class文件,需要解决下面的问题:
1.首先要知道什么时候编译完成,
2.并且要赶在class文件被转化为dex文件之前去修改
在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的task中可以使用javassist或者asm对class进行操作。
Transform更为方便,Transform会有他自己的执行时机,不需要我们插入到某个Task前面。Transform一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。
我们想要使用Transform就必须自定义Plugin,这里我们还要初步了解一下Gradle
Gradle
Android Studio项目是使用Gradle构建的,构建工具Gradle可以看做是一个脚本,包含一系列的Task,依次执行这些Task后,项目就打包成功了。
Task有个很重要的概念,就是inputs和outputs
Task通过inputs拿到一些东西,处理完毕之后就输出outputs,而下一个Task的inputs则是上一个Task的outputs。
例如:一个Task的作用是将java编译成class,这个Task的inputs就是java文件的保存目录,outputs这是编译后的class的输出目录,它的下一个Task的inputs就会是编译后的class的保存目录了。
Gradle由一个个Task组成,而这些Task都是由Plugin来定义的。
比如:
apply plugin : 'com.android.application' 这个 插件定义了将 Module 编译成 application 的一系列 Task。
apply plugin : 'com.android.library' 这个 插件定义了将 Module 编译成 library 的一系列 Task。
不同的 Plugin 提供了不同的 Task 来实际不同的功能。
可以简单的理解为: Gradle只是一个框架,真正起作用的是plugin。而plugin的主要作用是往Gradle脚本中添加Task。我们需要在整个 Gradle 工作的过程中,找到合适的时机来插入自定义的 Plugin,然后在 Plugin 中使用 Javassist 对字节进行操作 ,所以使用 Javassit 的前提是掌握自定义 Gradle 插件。具体可以参考这篇文章。
具体实现
这里通过一个demo来讲解具体的步骤,功能是实现在每个onClick方法中弹出一个Toast。
- 自定义Gradle插件,具体方法参考上方
- 导入Javassit
- 自定义Transform,遍历class,再通过Javassit的API对代码进行修改
package javassist.huangm2.wangsu.com
import com.android.SdkConstants
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.ClassPool
import javassist.CtClass
import javassist.CtField
import javassist.CtMethod
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
class ModifyTransform extends Transform {
private static final def CLICK_LISTENER = "android.view.View\$OnClickListener"
def pool = ClassPool.default
def project
ModifyTransform(Project project) {
this.project = project
}
@Override
String getName() {
return "ModifyTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
project.android.bootClasspath.each {
pool.appendClassPath(it.absolutePath)
}
transformInvocation.inputs.each {
it.jarInputs.each {
pool.insertClassPath(it.file.absolutePath)
// 重命名输出文件(同目录copyFile会冲突)
def jarName = it.name
def md5Name = DigestUtils.md5Hex(it.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(
jarName + md5Name, it.contentTypes, it.scopes, Format.JAR)
FileUtils.copyFile(it.file, dest)
}
it.directoryInputs.each {
def preFileName = it.file.absolutePath
pool.insertClassPath(preFileName)
findTarget(it.file, preFileName)
// 获取output目录
def dest = transformInvocation.outputProvider.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.DIRECTORY)
println "copy directory: " + it.file.absolutePath
println "dest directory: " + dest.absolutePath
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(it.file, dest)
}
}
}
private void findTarget(File dir, String fileName) {
if (dir.isDirectory()) {
dir.listFiles().each {
findTarget(it, fileName)
}
} else {
modify(dir, fileName)
}
}
private void modify(File dir, String fileName) {
def filePath = dir.absolutePath
if (!filePath.endsWith(SdkConstants.DOT_CLASS)) {
return
}
if (filePath.contains('R$') || filePath.contains('R.class')
|| filePath.contains("BuildConfig.class")) {
return
}
def className = filePath.replace(fileName, "")
.replace("\\", ".")
.replace("/", ".")
def name = className.replace(SdkConstants.DOT_CLASS, "")
.substring(1)
CtClass ctClass = pool.get(name)
CtClass[] interfaces = ctClass.getInterfaces()
if (interfaces.contains(pool.get(CLICK_LISTENER))) {
if (name.contains("\$")) {
println "class is inner class:" + ctClass.name
println "CtClass: " + ctClass
CtClass outer = pool.get(name.substring(0, name.indexOf("\$")))
CtField field = ctClass.getFields().find {
return it.type == outer
}
if (field != null) {
println "fieldStr: " + field.name
def body = "android.widget.Toast.makeText(" + field.name + "," +
"\"javassist\", android.widget.Toast.LENGTH_SHORT).show();"
addCode(ctClass, body, fileName)
}
} else {
println "class is outer class: " + ctClass.name
//更改onClick函数
def body = "android.widget.Toast.makeText(\$1.getContext(), \"javassist\", android.widget.Toast.LENGTH_SHORT).show();"
addCode(ctClass, body, fileName)
}
}
}
private void addCode(CtClass ctClass, String body, String fileName) {
ctClass.defrost()
CtMethod method = ctClass.getDeclaredMethod("onClick", pool.get("android.view.View"))
method.insertAfter(body)
ctClass.writeFile(fileName)
ctClass.detach()
println "write file: " + fileName + "\\" + ctClass.name
println "modify method: " + method.name + " succeed"
}
}
- 运行结果
javassist的API介绍
具体可以参考这篇文章
常用类:
ClassPool:javassist的类池,使用ClassPool类可以跟踪和控制所操作的类,它的工作方式与 JVM类装载器非常相似,
CtClass: CtClass提供了检查类数据(如字段和方法)以及在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方法。不过,Javassist 并未提供删除类中字段、方法或者构造函数的任何方法。
CtField:用来访问域
CtMethod :用来访问方法
CtConstructor:用来访问构造器
基本用法
1、添加类搜索路径
ClassPool pool =ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");
2、添加方法
CtClass point =ClassPool.getDefault().get("Point");
CtMethod m =CtNewMethod.make( "public int xmove(int dx) { x += dx; }", point);point.addMethod(m);
3、修改方法
CtClass point =ClassPool.getDefault().get("Point");
CtMethod m= point.getDeclaredMethod(“show", null)
m.insertAfter(“System.out.prinln(“x:” + x + “,y:) + y”))
4、添加字段
CtClass point =ClassPool.getDefault().get("Point");
CtField f = newCtField(CtClass.intType, "z", point);
point.addField(f);
ASM
ASM通过修改字节码来实现AOP,我们知道Android打包过程是:.java文件 -> .class文件 -> .dex文件,它的实现和Javassist类似,也是需要自定义Gradle插件,利用Transform获取class。不同的是,它是直接修改字节码,一般是获取修改后的class文件,然后查看它对应的字节码文件,再通过ASM的api进行添加。
关于ASM操作字节码的介绍可以参考这篇文章。
这里可以利用Jbe工具来查看class文件的字节码,具体如下图:
具体实现
这里同样是通过demo来说明操作步骤,demo的功能是在newFunc方法的头部增加一行输出
public void newFunc(String str) {
System.out.println(str);
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
System.out.println(i);
}
}
}
也就是实际上要实现下面这个效果:
public void newFunc(String str) {
System.out.println("=========start=========");
System.out.println(str);
for (int i = 0; i < 100; i++) {
if (i % 10 == 0) {
System.out.println(i);
}
}
}
- 自定义Gradle插件
- 获取最终实现效果代码的字节码,即我们要获取System.out.println("=========start=========");这行代码的字节码,一般做法是可以把代码编译成class文件再用jbe工具进行查看,
- 自定义Transform遍历class
public class AutoTransform extends Transform {
@Override
String getName() {
return "AutoTrack"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
//此处会遍历所有文件
/**遍历输入文件*/
inputs.each { TransformInput input ->
/**
* 遍历jar
*/
input.jarInputs.each { JarInput jarInput ->
}
/**
* 遍历目录
*/
input.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (name.endsWith(".class") && !name.startsWith("R\$") &&
!"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
println name + ' is changing...'
//解析编译过的字节码文件
ClassReader cr = new ClassReader(file.bytes)
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
//自定义Visitor访问具体的成员信息
ClassVisitor cv = new AutoClassVisitor(cw)
cr.accept(cv, EXPAND_FRAMES)
byte[] code = cw.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each { JarInput jarInput ->
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}
- 通过ClassVisitor进行筛选
class AutoClassVisitor extends ClassVisitor {
AutoClassVisitor(final ClassVisitor cv) {
super(Opcodes.ASM4, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
//如果methodName是newFunc,则返回我们自定义的TestMethodVisitor
if ("newFunc".equals(name)) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
return new TestMethodVisitor(mv);
}
if (cv != null) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
return null;
}
}
public class TestMethodVisitor extends MethodVisitor {
public TestMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
//方法体内开始时调用
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("========start=========");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
//每执行一个指令都会调用
// if (opcode == Opcodes.RETURN) {
// mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// mv.visitLdcInsn("========end=========");
// mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// }
super.visitInsn(opcode);
}
}
- 执行结果
ASM的一些核心类介绍
可以参考这篇文章。
ASM框架中的核心类有以下几个:
ClassReader:该类用来解析编译过的class字节码文件。
ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
ClassVisitor:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。
AdviceAdapter:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。
总结
AOP的实现方法有多种,可以根据具体项目去选择,但是核心的应该是它面向切面编程的思想,拓宽了问题处理的思路。