簡介
AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
常見的AOP工具按照生效時機區分主要分為兩大類:預編譯期及運行期,以下列舉出市面上常用的AOP工具及對應開源框架:
1.APT工具
代表開源框架:ButterKnife、Dagger2、DBFlow、AndroidAnnotation 注解處理器 Java5 中叫APT(Annotation Processing Tool),在Java6開始,規范化為 Pluggable Annotation Processing。Apt應該是這其中我們最常見到的了,難度也最低。定義編譯期的注解,再通過繼承Proccesor實現代碼生成邏輯,實現了編譯期生成代碼的邏輯。
2.AspectJ工具
AspectJ是一種嚴格意義上的AOP技術,因為它提供了完整的面向切面編程的注解,這樣讓使用者可以在不關心字節碼原理的情況下完成代碼的織入,因為編寫的切面代碼就是要織入的實際代碼。
AspectJ實現代碼織入有兩種方式,一是自行編寫.ajc文件,二是使用AspectJ提供的@Aspect、@Pointcut等注解,二者最終都是通過ajc編譯器完成代碼的織入。
舉個簡單的例子,假設我們想統計所有view的點擊事件,使用AspectJ只需要寫一個類即可。
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect5";
//切面表達式,聲明需要過濾的類和方法
@Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
public void callMethod() {
}
//before表示在方法調用前織入
@before("callMethod()")
public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
//編寫業務代碼
}
}
復制代碼
注解簡明直觀,上手難度近乎為0。
常用的函數耗時統計工具Hugo,就是AspectJ的一個實際應用,Android平臺Hujiang開源的AspectJX插件靈感也來自于Hugo,詳情見舊文Android 函數耗時統計工具之Hugo。
AspectJ雖然好用,但也存在一些嚴重的問題。
- 重復織入、不織入
AspectJ切面表達式支持繼承語法,雖然方便了開發,但存在致命的問題,就是在繼承樹上的類可能都會織入代碼,這在多數業務場景下是不適用的,比如無埋點。
另外Java8語法在aspectjx 2.0.0版本開始支持。
3.ASM
ASM是非常底層的面向字節碼編程的AOP框架,理論上可以實現任何關于字節碼的修改,非常硬核。許多字節碼生成API底層都是用ASM實現,常見比如Groovy、cglib,因此在Android平臺下使用ASM無需添加額外的依賴。完整的學習ASM必須了解字節碼和JVM相關知識。
比如要織入一句簡單的日志輸出
Log.d("tag", " onCreate");
復制代碼使用ASM編寫是下面這個樣子,沒錯因為JVM是基于棧的,函數的調用需要參數先入棧,然后執行函數入棧,最后出棧,總共四條JVM指令。
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
復制代碼可以看出ASM與AspectJ有很大的不同,AspectJ織入的代碼就是實際編寫的代碼,但ASM必須使用其提供的API編寫指令。一行java代碼可能對應多行ASM API代碼,因為一行java代碼背后可能隱藏這多個JVM指令。
你不必擔心不會編寫ASM代碼,官方提供了ASM Bytecode Outline插件可以直接將java代碼生成ASM代碼。
4.Javassist
javassit是一個開源的字節碼創建、編輯類庫,現屬于Jboss web容器的一個子模塊,特點是簡單、快速,與AspectJ一樣,使用它不需要了解字節碼和虛擬機指令,這里是官方文檔。
javassit核心的類庫包含ClassPool,CtClass ,CtMethod和CtField。
- ClassPool:一個基于HashMap實現的CtClass對象容器。
- CtClass:表示一個類,可從ClassPool中通過完整類名獲取。
- CtMethods:表示類中的方法。
- CtFields :表示類中的字段。
javassit API簡潔直觀,比如我們想動態創建一個類,并添加一個helloWorld方法。
ClassPool pool = ClassPool.getDefault();
//通過makeClass創建類
CtClass ct = pool.makeClass("test.helloworld.Test");//創建類
//為ct添加一個方法
CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);
ct.addMethod(helloMethod);
//寫入文件
ct.writeFile();
//加載進內存
// ct.toClass();
復制代碼
然后,我們想在helloWorld方法前后織入代碼。
ClassPool pool = ClassPool.getDefault();
//獲取class
CtClass ct = pool.getCtClass("test.helloworld.Test");
//獲取helloWorld方法
CtMethod m = ct.getDeclaredMethod("helloWorld");
//在方法開頭織入
m.insertBefore("{ System.out.print(\"before insert\");");
//在方法末尾織入 可使用this關鍵字
m.insertAfter("{System.out.println(this.x); }");
//寫入文件
ct.writeFile();
復制代碼
javassit的語法直觀簡潔的特點,使得在很多開源項目中都有它的身影。
5.動態代理
動態代理是代理模式的一種實現,用于在運行時動態增強原始類的行為,實現方式是運行時直接生成class字節碼并將其加載進虛擬機。
各類框架總結
下面我們就以ASM這個框架給大家舉例講解
一、最終實現的效果
這次我們的目標是在Demo App啟動后在MainActivity的onCreate()方法之前自動輸出一段簡單的日志信息“Log.e("TAG", "===== This is just a test message =====");”也就是最終我們需要將這個 代碼插入到MainActivity的onCreate()方法之前。**
要達到這樣的目的我們就需要使用ASM,ASM 是一個 Java 字節碼操控的框架,也就是說我們可以直接操作.class文件。這樣我們就可以在不侵入MainActivity類的情況下,直接達到目的。
為了實現目標我們首先需要知道幾個簡單的類:
1.1、ClassVisitor
首先我們是要處理單個.class文件,那肯定需要訪問到這個.class文件的內容,ClassVisitor就是處理這些的,他可以拿到class文件的類名,父類名,接口,包含的方法,等等信息。
1.2、MethodVisitor
因為我們需要在方法執行前插入一些字節碼,所以我們需要MethodVisitor來幫我們處理并插入字節碼。真正進行方法插樁的地方。
1.3、Transform
Transform是gradle構建的時候從class文件轉換到dex文件期間處理class文件的一套方案,也就是說處理class的吧。上文的ClassVisitor可以是看做處理單個class文件,那這里的話Transform可以處理一系列的class文件:從查找到所有class文件,到交給ClassVisitor和MethodVisitor處理后,再到重新覆蓋原來的class文件這么一個流程。
二、開始編程
根據上文的步驟我們順序在gradleAOP工程的plugin模塊中編寫ClassVisitor、MethodVisitor、以及Transform。
這里選用kotlin來編寫所有腳本。所以plugin插件的module看起來是這樣的:main文件夾下kotlin來分別存儲對應的代碼
另外要想實現這樣根據語言分文件夾的效果需要在插件module的build.gradle中配置一下sourceSets ,如下代碼所示。除了這些,還添加了kotlin插件以及kotlin和gradle的依賴,因為開發Transform的需要。最后是插件倉庫地址的配置信息.
apply plugin: 'kotlin'
apply plugin: 'maven'
sourceSets {
main {
kotlin {
srcDir "src/main/kotlin"
}
resources {
srcDir 'src/main/resources'
}
}
}
dependencies {
implementation gradleApi()
implementation 'org.ow2.asm:asm:7.1'
implementation 'com.android.tools.build:gradle:4.0.2'
}
uploadArchives {
repositories {
mavenDeployer {
pom.groupId = 'com.cjh.plugin'
pom.artifactId = 'plugin'
pom.version = '1.0'
//生成的文件地址
repository(url: uri('E:/Repo'))
}
}
}
2.1、ClassVisitor
在ClassVisitor中我們拿到相應class的類名,比如這時候是MainActivity.class,那么類名就是““com/example/mygradleaop/MainActivity””,你可以自行打印嘗試【注意這里的包名是app工程的包名,而不是gradleAOP工程的包名,因為我們是要處理的是app對吧】。匹配到類名后覆寫visitMethod()方法,根據當前方法名是否匹配onCreate方法來將具體的插樁操作交給DemoMethodVisitor處理。
DemoClassVisitor類源碼如下
class DemoClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, classVisitor) {
private var className: String? = null
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
className = name
}
//關鍵方法重寫visitMethod方法
//匹配MainActivity的onCreate方法
//匹配到之后進去DemoMethodVisitor方法進行插樁
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
//com.example.mygradleaop.MainActivity
if (className.equals("com/example/mygradleaop/MainActivity")) {
if (name.equals("onCreate")) {
return DemoMethodVisitor(methodVisitor)
}
}
return methodVisitor
}
}
2.2、MethodVisitor
經過上一步ClassVisitor的處理我們已經匹配到onCreate方法了,此時我們需要在DemoMethodVisitor類中進行插入字節碼操作。如下所示,直接繼承自MethodVisitor,并覆寫visitCode()方法。其中的代碼就是我們要插入的代碼了,乍一看完全不是我們平常那種Log.e("TAG", "===== This is just a test message =====");的寫法,而是復雜了很多。是的,這時候你就知道visitCode中的代碼和我們上邊的Log信息等價就好了,等這篇文章閱讀完,咱們就可以去深入學習JVM字節碼的相關信息了,現在不要想那么多,直接拿去用。
DemoMethodVisitor類源碼如下:
class DemoMethodVisitor(methodVisitor: MethodVisitor) : MethodVisitor(Opcodes.ASM5, methodVisitor) {
//插入:Log.e("TAG", "===== This is just a test message =====");
override fun visitCode() {
super.visitCode()
mv.visitLdcInsn("TAG")
mv.visitLdcInsn("===== This is just a test message cjh=====")
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"android/util/Log",
"e",
"(Ljava/lang/String;Ljava/lang/String;)I",
false
)
mv.visitInsn(Opcodes.POP)
}
}
2.3、Transform
經過前兩步的處理我們已經可以將字節碼插入到MainActivity.class的onCreate方法前了,但是此時我們怎么去找到想要的.class文件呢,字節碼插入完后我們又要怎么寫回到.class文件呢?Transform就可以登場了,如下所示,DemoTransform繼承自Transform,同時實現Plugin接口,這個plugin接口還熟悉吧,應用到resources/META-INF/gradle-plugins/xxx.properties的時候需要。然后依次實現所有必須的方法,除了transform()方法其他都是一些比較固定的寫法了,直接搬過去即可:
package com.cooloongwu.plugin1
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.TransformInvocation
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream
class DemoTransform : Transform(), Plugin<Project> {
override fun apply(project: Project) {
println(">>>>>> 1.1.1 this is a log just from DemoTransform")
val appExtension = project.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(this)
}
override fun getName(): String {
return "KotlinDemoTransform"
}
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
override fun isIncremental(): Boolean {
return false
}
override fun transform(transformInvocation: TransformInvocation?) {
super.transform(transformInvocation)
}
}
接下來是transform()方法里的內容,大致流程就是查找到所有的.class文件【代碼中還添加了一些條件,過濾掉了一些class文件】,然后通過ClassReader讀取并解析class文件,然后又經由我們編寫的ClassVisitor和MethodVisitor處理后交給ClassWriter,最后通過FileOutputStream將新的字節碼內容寫回到class文件。
/*
* 接下來是transform()方法里的內容,大致流程就是查找到所有的.class文件
* 【代碼中還添加了一些條件,過濾掉了一些class文件】,
* 然后通過ClassReader讀取并解析class文件,然后又經由
* 我們編寫的ClassVisitor和MethodVisitor處理后交給ClassWriter,
* 最后通過FileOutputStream將新的字節碼內容寫回到class文件。
*
*
* */
override fun transform(transformInvocation: TransformInvocation?) {
super.transform(transformInvocation)
val inputs = transformInvocation?.inputs
val outputProvider = transformInvocation?.outputProvider
if (!isIncremental) {
outputProvider?.deleteAll()
}
inputs?.forEach { it ->
it.directoryInputs.forEach {
if (it.file.isDirectory) {
FileUtils.getAllFiles(it.file).forEach {
val file = it
val name = file.name
//1.過濾其他不合符條件的class文件
if (name.endsWith(".class") && name != ("R.class")
&& !name.startsWith("R\$") && name != ("BuildConfig.class")
) {
val classPath = file.absolutePath
println(">>>>>> classPath :$classPath")
//2.ClassReader讀取并解析class文件
val cr = ClassReader(file.readBytes())
val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
//3.經由我們編寫的ClassVisitor和MethodVisitor處理
val visitor = DemoClassVisitor(cw)
cr.accept(visitor, ClassReader.EXPAND_FRAMES)
//4.通過FileOutputStream將新的字節碼內容寫回到class文件
val bytes = cw.toByteArray()
val fos = FileOutputStream(classPath)
fos.write(bytes)
fos.close()
}
}
}
val dest = outputProvider?.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.DIRECTORY
)
FileUtils.copyDirectoryToDirectory(it.file, dest)
}
// !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!! !!!!!!!!!!
//使用androidx的項目一定也注意jar也需要處理,否則所有的jar都不會最終編譯到apk中,千萬注意
//導致出現ClassNotFoundException的崩潰信息,當然主要是因為找不到父類,因為父類AppCompatActivity在jar中
it.jarInputs.forEach {
val dest = outputProvider?.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.JAR
)
FileUtils.copyFile(it.file, dest)
}
}
至此,所有的插件內容基本完成了,最后就是在resources/META-INF/gradle-plugins/myplugin.properties文件中寫入我們新的Plugin類:
implementation-class=com.example.gradleaop.DemoTransform
然后右側gradle任務中執行uploadArchives,發布我們的插件到本地倉庫中。
發布完成后在Demo的根build.gradle中添加依賴信息如下:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
maven{
url 'E:/Repo'
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//implementation-class=com.example.gradleaop.DemoTransform
//這里的路徑就是gradle插件里面發布本地插件時寫的
//classpath 'groupId:artifactId:version'
/*
mavenDeployer {
pom.groupId = 'com.cjh.plugin'
pom.artifactId = 'plugin'
pom.version = '1.0'
//生成的文件地址
repository(url: uri('E:/Repo'))
}
*/
classpath 'com.cjh.plugin:plugin:1.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven{
url 'E:/Repo'
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
最后在app model下面build.gradle添加插件.這里的名稱就是我們gradle插件里面清單文件
resources/META-INF/gradle-plugins/com.geo.plugin.properties的名稱com.geo.plugin
apply plugin: 'com.geo.plugin'
此時直接運行Demo工程,app運行起來后在控制臺是不是就看到了相應的信息呢:
2020-04-08 21:50:17.750 3804-3804/com.cooloongwu.asmdemo E/TAG: ===== This is just a test message =====
此時我們最終在MainActivity的onCreate方法前面插入了這行日志代碼
三、總結
1)先明白自己想要干什么,像這個例子我們是需要在某個類的某個方法前面插入一行代碼,那我們其實就是對方法進行插樁。
2)先通過DemoClassVisitor匹配到需要插樁的類,這里就是MainActivity.class.匹配到onCreate方法后,就對方法進行插樁,實現類是DemoMethodVisitor
3)DemoMethodVisitor里面重寫visitCode方法,把需要插入的代碼轉換成字節碼的形式就是插入即可。這里就是最關鍵的地方,我們 可以利用ASM插件把對應的java代碼轉換成這種字節碼,然后照著寫入即可。
4)最后一步也就是使用Transform進行關聯。需要用Transform拿到所有的類,然后中途交給前面我們編寫的DemoClassVisitor和DemoMethodVisitor處理進行插樁,最后還是通過Transform寫回去,這樣就實現中途插入字節碼的功能了,這就是字節碼插樁。
項目源碼:https://gitee.com/canjunhao/MyGradleAOP
引用:https://blog.csdn.net/u010976213/article/details/105395590