前言
每天都是重復(fù)的工作襟士,這樣可不行盗飒,已經(jīng)嚴(yán)重影響我的日常摸魚,為了減少自己日常的開發(fā)時間陋桂,我決定走一條歧路逆趣,鋌而走險,將項目中的各種手動埋點統(tǒng)計替換成自動化埋點嗜历。以后再也不用擔(dān)心沒時間摸魚了~
作為Android
屆開發(fā)的一員宣渗,今天我決定將摸魚方案分享給大家,希望更多的廣大群眾能夠的加入到摸魚的行列中~
為了更好的理解與簡化實現(xiàn)步驟梨州,我將會結(jié)合動態(tài)代理分析與仿Retrofit實踐中埋點Demo
來進行拆解痕囱,畢竟實際項目比這要復(fù)雜,通過簡單的Demo
來了解核心點即可暴匠。
在真正實現(xiàn)代碼注入之前鞍恢,我們先來看正常手動打點的步驟.
在動態(tài)代理分析與仿Retrofit實踐中已經(jīng)將打點的步驟進行了簡化。
沒看過上面的文章也不影響接下的閱讀
- 聲明打點的接口方法
interface StatisticService {
@Scan(ProxyActivity.PAGE_NAME)
fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name: String)
@Click(ProxyActivity.PAGE_NAME)
fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
@Scan(ProxyActivity.PAGE_NAME)
fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String)
@Click(ProxyActivity.PAGE_NAME)
fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
}
- 通過動態(tài)代理獲取
StatisticService
接口引用
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
- 在合適的埋點位置進行埋點統(tǒng)計每窖,例如
Click
埋點
fun onClick(view: View) {
if (view.id == R.id.button) {
mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
} else if (view.id == R.id.text) {
mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
}
}
其中2帮掉、3步驟都是在對應(yīng)埋點的類中使用,這里對應(yīng)的是ProxyActivity
class ProxyActivity : AppCompatActivity() {
// 步驟2
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
companion object {
private const val BUTTON = "statistic_button"
private const val TEXT = "statistic_text"
const val PAGE_NAME = "ProxyActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val extraData = getExtraData()
setContentView(extraData.layoutId)
title = extraData.title
// 步驟3 => 曝光點
mStatisticService.buttonScan(BUTTON)
mStatisticService.textScan(TEXT)
}
private fun getExtraData(): MainModel =
intent?.extras?.getParcelable(ActivityUtils.EXTRA_DATA)
?: throw NullPointerException("intent or extras is null")
// 步驟3 => 點擊點
fun onClick(view: View) {
if (view.id == R.id.button) {
mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
} else if (view.id == R.id.text) {
mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
}
}
}
步驟1是創(chuàng)建新的類窒典,不在代碼注入的范圍之內(nèi)蟆炊。自動生成類可以使用注解+process+JavaPoet
來實現(xiàn)。類似于ButterKnife
瀑志、Dagger2
涩搓、Room
等。之前我也有寫過相關(guān)的demo
與文章后室。由于不在本篇文章的范圍之內(nèi)缩膝,感興趣的可以自行去了解。
這里我們需要做的是:需要在ProxyActiviy
中將2岸霹、3步驟的代碼轉(zhuǎn)成自動注入。
自動注入就是在現(xiàn)有的類中自動加入我們預(yù)期的代碼将饺,不需要我們額外的進行編寫贡避。
既然已經(jīng)知道了需要注入的代碼,那么接下的問題就是什么時候進行注入這些代碼予弧。
這就涉及到Android
構(gòu)建與打包的流程刮吧,Android
使用Gradle
進行構(gòu)建與打包,
在打包的過程中將源文件轉(zhuǎn)化成.class
文件掖蛤,然后再將.class
文件轉(zhuǎn)成Android
能識別的.dex
文件杀捻,最終將所有的.dex
文件組合成一個.apk
文件,提供用戶下載與安裝蚓庭。
而在將源文件轉(zhuǎn)化成.class
文件之后致讥,Google
提供了一種Transform
機制仅仆,允許我們在打包之前對.class
文件進行修改浙垫。
這個修改時機就是我們代碼自動注入的時機叠荠。
transform
是由gradle
提供,在我們?nèi)粘5臉?gòu)建過程中也會看到系統(tǒng)自身的transform
身影刘陶,gradle
由各種task
組成请契,transform
就穿插在這些task
中咳榜。
圖中高亮的部分就是本次自定義的TraceTransform
, 它會在.class
轉(zhuǎn)化成.dex
之前進行執(zhí)行爽锥,目的就是修改目標(biāo).class
文件內(nèi)容涌韩。
Transform
的實現(xiàn)需要結(jié)合Gradle Plugin
一起使用。所以接下來我們需要創(chuàng)建一個Plugin
氯夷。
創(chuàng)建Plugin
在app
的build.gradle
中臣樱,我們能夠看到以下類似的插件引用方式
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'trace_plugin'
這里的插件包括系統(tǒng)自帶、第三方的與自定義的肠槽。其中trace_plugin
就是本次自定義的插件擎淤。為了能夠讓項目使用自定義的插件,Gradle
提供了三種打包插件的方式
-
Build Script
: 將插件的源代碼直接包含在構(gòu)建腳本中秸仙。這樣做的好處是嘴拢,無需執(zhí)行任何操作即可自動編譯插件并將其包含在構(gòu)建腳本的類路徑中。但缺點是它在構(gòu)建腳本之外不可見寂纪,常用在腳本自動構(gòu)建中席吴。 -
buildSrc project
:gradle
會自動識別buildSrc
目錄,所以可以將plugin
放到buildSrc
目錄中捞蛋,這樣其它的構(gòu)建腳本就能自動識別這個plugin
, 多用于自身項目孝冒,對外不共享。 -
Standalone project
: 創(chuàng)建一個獨立的plugin
項目拟杉,通過對外發(fā)布Jar
與外部共享使用庄涡。
這里使用第三種方式來創(chuàng)建Plugin
。所以創(chuàng)建完之后的目錄結(jié)構(gòu)大概是這樣的
為了讓別的項目能夠引用這個Plugin
搬设,我們需要對外聲明穴店,可以發(fā)布到maven
中,也可以本地聲明拿穴,為了簡便這里使用本地聲明泣洞。
apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.4.1'
}
gradlePlugin {
plugins {
version {
// 在 app 模塊需要通過 id 引用這個插件
id = 'trace_plugin'
// 實現(xiàn)這個插件的類的路徑
implementationClass = 'com.rousetime.trace_plugin.TracePlugin'
}
}
}
該Plugin
的id
為trace_plugin
,實現(xiàn)入口為com.rousetime.trace_plugin.TracePlugin
默色。
聲明完之后球凰,就可以直接在項目的根目錄下的build.gradle
中引入該id
plugins {
id "trace_plugin" apply false
}
為了能在app
項目中apply
這個plugin
,還需要創(chuàng)建一個META-INF.gradle-plugins
目錄,對應(yīng)的位置如下
注意這里的trace_plugin.properties
文件名非常重要呕诉,前面的trace_plugin
就代表你在build.gradle
中apply
的插件名稱缘厢。
文件中的內(nèi)容很簡單,只有一行义钉,對應(yīng)的就是TracePlugin
的實現(xiàn)入口
implementation-class=com.rousetime.trace_plugin.TracePlugin
上面都準(zhǔn)備就緒之后昧绣,就可以在build.gradle
進行apply plugin
apply plugin: 'trace_plugin'
這個時候我們自定義的plugin
就引入到項目中了。
再回到剛剛的Plugin
入口TracePlugin
捶闸,來看下它的具體實現(xiàn)
class TracePlugin : Plugin<Project> {
override fun apply(target: Project) {
println("Trace Plugin start to apply")
if (target.plugins.hasPlugin(AppPlugin::class.java)) {
val appExtension = target.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(TraceTransform())
}
val methodVisitorConfig = target.extensions.create("methodVisitor", MethodVisitorConfig::class.java)
LocalConfig.methodVisitorConfig = methodVisitorConfig
target.afterEvaluate {
println(methodVisitorConfig.name)
}
}
}
只有一個方法apply
夜畴,在該方法中我們打印一行文本,然后重新構(gòu)建項目删壮,在build
輸出窗口就能看到這行文本
....
> Configure project :app
Trace Plugin start to apply
mehtodVisitorConfig
Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
...
到這里我們自定義的plugin
已經(jīng)創(chuàng)建成功贪绘,并且已經(jīng)集成到我們的項目中。
第一步已經(jīng)完成央碟。下面進入第二步税灌。
實現(xiàn)Transform
在TracePlugin
的apply
方法中,對項目的appExtension
注冊了一個TraceTransform
亿虽。重點來了菱涤,這個TraceTransform
就是我們在gradle
構(gòu)建的過程中插入的Transform
,也就是注入代碼的入口。來看下它的具體實現(xiàn)
class TraceTransform : Transform() {
override fun getName(): String = this::class.java.simpleName
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_JARS
override fun isIncremental(): Boolean = true
override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
override fun transform(transformInvocation: TransformInvocation?) {
TransformProxy(transformInvocation, object : TransformProcess {
override fun process(entryName: String, sourceClassByte: ByteArray): ByteArray? {
// use ams to inject
return if (ClassUtils.checkClassName(entryName)) {
TraceInjectDelegate().inject(sourceClassByte)
} else {
null
}
}
}).apply {
transform()
}
}
}
代碼很簡單洛勉,只需要實現(xiàn)幾個特定的方法粘秆。
-
getName
:Transform
對外顯示的名稱 -
getInputTypes
: 掃描的文件類型,CONENT_JARS
代表CLASSES
與RESOURCES
-
isIncremental
: 是否開啟增量收毫,開啟后會提高構(gòu)建速度攻走,對應(yīng)的需要手動處理增量的邏輯 -
getScopes
: 掃描作用范圍,SCOPE_FULL_PROJECT
代表整個項目 -
transform
: 需要轉(zhuǎn)換的邏輯都在這里處理
transform
是我們接下來.class
文件的入口此再,這個方法有個參數(shù)TransformInvocation
昔搂,該參數(shù)提供了上面定義范圍內(nèi)掃描到的所用jar
文件與directory
文件。
在transform
中我們主要做的就是在這些jar
與directory
中解析出.class
文件输拇,這是找到目標(biāo).class
的第一步摘符。只有解析出了所有的.class
文件,我們才能進一步過濾出我們需要注入代碼的.class
文件策吠。
而transform
的工作流程是:解析.class
文件议慰,然后我們過濾出需要處理的.class
文件,寫入對應(yīng)的邏輯奴曙,然后再將處理過的.class
文件重新拷貝到之前的jar
或者directory
中。
通過這種解析草讶、處理與拷貝的方式洽糟,實現(xiàn)偷天換日的效果。
既然有一套固定的流程,那么自然有對應(yīng)的一套固定是實現(xiàn)坤溃。在這三個步驟中拍霜,真正需要實現(xiàn)的是處理邏輯,不同的項目有不同的處理邏輯薪介,
對于解析與拷貝操作祠饺,已經(jīng)有相對完整的一套通用實現(xiàn)方案。如果你的項目中有多個這種類型的Transform
汁政,就可以將其抽離出來單個module
道偷,增加復(fù)用性。
解析與拷貝
下面我們來看一下它的核心實現(xiàn)步驟记劈。
fun transform() {
if (!isIncremental) {
// 不是增量編譯勺鸦,將之前的輸出目錄中的內(nèi)容全部刪除
outputProvider?.deleteAll()
}
inputs?.forEach {
// jar
it.jarInputs.forEach { jarInput ->
transformJar(jarInput)
}
// directory
it.directoryInputs.forEach { directoryInput ->
transformDirectory(directoryInput)
}
}
executor?.invokeAll(tasks)
}
transform
方法主要做的就是分別遍歷jar
與directory
中的文件。在這兩大種類中分別解析出.class
文件目木。
例如jar
的解析transformJar
private fun transformJar(jarInput: JarInput) {
val status = jarInput.status
var destName = jarInput.file.name
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length - 4)
}
// 重命名, 可能同名被覆蓋
val hexName = DigestUtils.md2Hex(jarInput.file.absolutePath).substring(0, 8)
// 輸出文件
val dest = outputProvider?.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (isIncremental) { // 增量
when (status) {
Status.NOTCHANGED -> {
// nothing to do
}
Status.ADDED, Status.CHANGED -> {
foreachJar(jarInput, dest)
}
Status.REMOVED -> {
if (dest?.exists() == true) {
FileUtils.forceDelete(dest)
}
}
else -> {
}
}
} else {
foreachJar(jarInput, dest)
}
}
如果是增量編譯换途,就分別處理增量的不同操作,主要的是ADDED
與CHANGED
操作刽射。這個處理邏輯與非增量編譯的時候一樣军拟,都是去遍歷jar
,從中解析出對應(yīng)的.class
文件誓禁。
遍歷的核心代碼如下
while (enumeration.hasMoreElements()) {
val jarEntry = enumeration.nextElement()
val inputStream = originalFile.getInputStream(jarEntry)
val entryName = jarEntry.name
// 構(gòu)建zipEntry
val zipEntry = ZipEntry(entryName)
jarOutputStream.putNextEntry(zipEntry)
var modifyClassByte: ByteArray? = null
val sourceClassByte = IOUtils.toByteArray(inputStream)
if (entryName.endsWith(".class")) {
modifyClassByte = transformProcess.process(entryName, sourceClassByte)
}
if (modifyClassByte == null) {
jarOutputStream.write(sourceClassByte)
} else {
jarOutputStream.write(modifyClassByte)
}
inputStream.close()
jarOutputStream.closeEntry()
}
如果entryName
的后綴是.class
說明當(dāng)前是.class
文件懈息,我們需要單獨拿出來進行后續(xù)的處理。
后續(xù)的處理邏輯交給了transformProcess.process
现横。具體處理先放一放漓拾。
處理完之后,再將處理后的字節(jié)碼拷貝保存到之前的jar
中戒祠。
對應(yīng)的directory
也是類似
private fun foreachFile(dir: File, dest: File?) {
if (dir.isDirectory) {
FileUtils.copyDirectory(dir, dest)
getAllFiles(dir).forEach {
if (it.name.endsWith(".class")) {
val task = Callable {
val absolutePath = it.absolutePath.replace(dir.absolutePath + File.separator, "")
val className = ClassUtils.path2Classname(absolutePath)
val bytes = IOUtils.toByteArray(it.inputStream())
val modifyClassByte = process(className ?: "", bytes)
// 保存修改的classFile
modifyClassByte?.let { byte -> saveClassFile(byte, dest, absolutePath) }
}
tasks.add(task)
executor?.submit(task)
}
}
}
}
同樣是過濾出.class
文件骇两,然后交給process
方法進行統(tǒng)一處理。最后將處理完的字節(jié)碼拷貝保存到原路徑中姜盈。
以上就是Transform
的解析與拷貝的核心處理低千。
處理
上面提到.class
的處理都轉(zhuǎn)交給process
方法,這個方法的具體實現(xiàn)在TraceTransform
的transform
方法中
override fun transform(transformInvocation: TransformInvocation?) {
TransformProxy(transformInvocation, object : TransformProcess {
override fun process(entryName: String, sourceClassByte: ByteArray): ByteArray? {
// use ams to inject
return if (ClassUtils.checkClassName(entryName)) {
TraceInjectDelegate().inject(sourceClassByte)
} else {
null
}
}
}).apply {
transform()
}
}
在process
中使用TraceInjectDelegate
的inject
來處理過濾出來的字節(jié)碼馏颂。最終的處理會來到modifyClassByte
方法示血。
class TraceAsmInject : Inject {
override fun modifyClassByte(byteArray: ByteArray): ByteArray {
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
val classFilterVisitor = ClassFilterVisitor(classWriter)
val classReader = ClassReader(byteArray)
classReader.accept(classFilterVisitor, ClassReader.EXPAND_FRAMES)
return classWriter.toByteArray()
}
}
這里的ClassWriter
、ClassFilterVisitor
救拉、ClassReader
都是ASM
的內(nèi)容难审,也是我們接下來實現(xiàn)自動注入代碼的重點。
ASM
ASM
是操作Java
字節(jié)碼的一個工具亿絮。
其實操作字節(jié)碼的除了ASM
還有javassist
告喊,但個人覺得ASM
更方便麸拄,因為它有一系列的輔助工具,能更好的幫助我們實現(xiàn)代碼的注入黔姜。
在上面我們已經(jīng)得到了.class
的字節(jié)碼文件÷G校現(xiàn)在我們需要做的就是掃描整個字節(jié)碼文件,判斷是否是我們需要注入的文件秆吵。
這里我將這些邏輯封裝到了ClassFilterVisitor
文件中淮椰。
ASM
為我們提供了ClassVisitor
、MethodVisitor
纳寂、FieldVisitor
等API
主穗。每當(dāng)ASM
掃描類的字節(jié)碼時,都會調(diào)用它的visit
烈疚、visitField
黔牵、visitMethod
與visitAnnotation
等方法。
有了這些方法爷肝,我們就可以判斷并處理我們需要的字節(jié)碼文件猾浦。
class ClassFilterVisitor(cv: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, cv) {
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)
// 掃描當(dāng)前類的信息
}
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
// 掃描類中的方法
}
override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
// 掃描類中的字段
}
}
這是幾個主要的方法,也是接下來我們需要重點用到的方法灯抛。
首先我們來看個簡單的金赦,這個明白了其它的都是一樣的。
fun bindData(value: MainModel, position: Int) {
itemView.content.apply {
text = value.content
setOnClickListener {
// 自動注入這行代碼
LogUtils.d("inject success.")
if (position == 0) {
requestPermission(context, value)
} else {
navigationPage(context, value)
}
}
}
}
假設(shè)我們需要在onClickListener
中注入LogUtils.d
這個行代碼对嚼,本質(zhì)就是在點擊的時候輸出一行日志夹抗。
首先我們需要明白,setOnClickListener
本質(zhì)是實現(xiàn)了一個OnClickListener
接口的匿名內(nèi)部類纵竖。
所以可以在掃描類的時候判斷是否實現(xiàn)了OnClickListener
這個接口漠烧,如果實現(xiàn)了,我們再去匹配它的onClick
方法靡砌,并且在它的onClick
方法中進行注入代碼已脓。
而類的掃描與方法掃描分別可以使用visit
與visitMethod
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)
// 接口名
mInterface = interfaces
}
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
// 判斷當(dāng)前類是否實現(xiàn)了onClickListener
if (mInterface != null && mInterface?.size ?: 0 > 0) {
mInterface?.forEach {
// 判斷當(dāng)前掃描的方法是否是onClick
if ((name + desc) == "onClick(Landroid/view/View;)V" && it == "android/view/View\$OnClickListener") {
val mv = cv.visitMethod(access, name, desc, signature, exceptions)
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
override fun onMethodEnter() {
super.onMethodEnter()
mv.visitFieldInsn(GETSTATIC, "com/idisfkj/androidapianalysis/utils/LogUtils", "INSTANCE", "Lcom/idisfkj/androidapianalysis/utils/LogUtils;")
mv.visitLdcInsn("inject success.")
mv.visitMethodInsn(INVOKEVIRTUAL, "com/idisfkj/androidapianalysis/utils/LogUtils", "d", "(Ljava/lang/String;)V", false)
}
}
}
}
}
return super.visitMethod(access, name, desc, signature, exceptions)
}
在visit
方法中,我們保存當(dāng)前類實現(xiàn)的接口通殃;在visitMethod
中再對當(dāng)前接口進行判斷度液,看它是否有onClick
方法。
name
與desc
分別為onClick
方法的方法名稱與方法參數(shù)描述画舌。這是字節(jié)碼匹配方法的一種規(guī)范堕担。
如果有的話,說明是我們需要插入的方法曲聂,這個時候返回AdviceAdapter
霹购。它是ASM
提供的便捷針對方法注入的類。我們重寫它的onMethodEnter
方法朋腋。代表我們將在方法的開頭注入代碼厕鹃。
onMethodEnter
方法中的代碼就是LogUtils.d
的ASM
注入實現(xiàn)兢仰。你可能會說這個是什么,完全看不懂剂碴,更別說寫字節(jié)碼注入了。
別急轻专,下面就是ASM
的方便之處忆矛,我們只需在Android Studio
中下載ASM Bytecode Viewer Support Kotlin
插件。
該插件可以幫助我們查看kotlin
字節(jié)碼请垛,只需右鍵彈窗中選擇ASM Bytecode Viewer
催训。稍后就會彈出轉(zhuǎn)化后的字節(jié)碼彈窗。
在彈窗中找到需要注入的代碼宗收,具體就是下面這幾行
methodVisitor.visitFieldInsn(GETSTATIC, "com/idisfkj/androidapianalysis/utils/LogUtils", "INSTANCE", "Lcom/idisfkj/androidapianalysis/utils/LogUtils;");
methodVisitor.visitLdcInsn("inject success.");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/idisfkj/androidapianalysis/utils/LogUtils", "d", "(Ljava/lang/String;)V", false);
這就是LogUtils.d
的注入代碼漫拭,直接copy
到上面提到的onMethodEnter
方法中。這樣注入的代碼就已經(jīng)完成混稽。
如果你想查看是否注入成功采驻,除了運行項目,查看效果之外匈勋,還可以直接查看注入的源碼礼旅。
在項目的build/intermediates/transforms
目錄下,找到自定義的TraceTransform
洽洁,再找到對應(yīng)的注入文件痘系,就可以查看注入源碼。
其實到這來核心內(nèi)容基本已經(jīng)結(jié)束了饿自,不管是注入什么代碼都可以通過這種方法來獲取注入的ASM
的代碼汰翠,不同的只是注入的時機判斷。
有了上面的基礎(chǔ)昭雌,我們來實現(xiàn)開頭的自動埋點复唤。
實現(xiàn)
為了讓自動化埋點能夠靈活的傳遞打點數(shù)據(jù),我們使用注解的方式來傳遞具體的埋點數(shù)據(jù)與類型城豁。
- TrackClickData: 點擊的數(shù)據(jù)
- TrackScanData: 曝光的數(shù)據(jù)
- TrackScan: 曝光點
- TrackClick: 點擊點
有了這些注解苟穆,剩下我們要做的就很簡單了
class ProxyActivity : AppCompatActivity() {
@TrackClickData
private var mTrackModel = TrackModel()
@TrackScanData
private var mTrackScanData = mutableListOf<TrackModel>()
companion object {
private const val BUTTON = "statistic_button"
private const val TEXT = "statistic_text"
const val PAGE_NAME = "ProxyActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
..
onScan()
}
@TrackScan
fun onScan() {
mTrackScanData.add(TrackModel(name = BUTTON))
mTrackScanData.add(TrackModel(name = TEXT))
}
@TrackClick
fun onClick(view: View) {
mTrackModel.time = System.currentTimeMillis() / 1000
mTrackModel.name = if (view.id == R.id.button) BUTTON else TEXT
}
}
使用TrackClickData
與TrackScanData
聲明打點的數(shù)據(jù);使用TrackScan
與TrackClick
聲明打點的類型與自動化插入代碼的入口方法唱星。
我們再回到注入代碼的類ClassFilterVisitor
雳旅,來實現(xiàn)具體的埋點代碼的注入。
在這里我們需要做的是解析聲明的注解间聊,拿到打點的數(shù)據(jù)攒盈,并且聲明的TrackScan
與TrackClick
方法中插入埋點的具體代碼。
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)
mInterface = interfaces
mClassName = name
}
通過visit
方法來掃描具體的類文件哎榴,在這里保存當(dāng)前掃描的類的信息型豁,為之后注入代碼做準(zhǔn)備
override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
val filterVisitor = super.visitField(access, name, desc, signature, value)
return object : FieldVisitor(Opcodes.ASM5, filterVisitor) {
override fun visitAnnotation(annotationDesc: String?, visible: Boolean): AnnotationVisitor {
if (annotationDesc == TRACK_CLICK_DATA_DESC) { // TrackClickData 注解
mTrackDataName = name
mTrackDataValue = value
mTrackDataDesc = desc
createFiled()
} else if (annotationDesc == TRACK_SCAN_DATA_DESC) { // TrackScanData注解
mTrackScanDataName = name
mTrackScanDataDesc = desc
createFiled()
}
return super.visitAnnotation(annotationDesc, visible)
}
}
}
visitFiled
方法用來掃描類文件中聲明的字段僵蛛。在該方法中,我們返回并實現(xiàn)FieldVisitor
迎变,并重新它的visitAnnotation
方法充尉,目的是找到之前TrackClickData
與TrackScanData
聲明的埋點字段。對應(yīng)的就是mTrackModel
與mTrackScanData
衣形。
主要包括字段名稱name
與字段的描述desc
驼侠,為我們之后注入埋點數(shù)據(jù)做準(zhǔn)備。
另外一旦匹配到埋點數(shù)據(jù)的注解谆吴,說明該類中需要進行自動化埋點倒源,所以還需要自動創(chuàng)建StatisticService
。這是打點的接口方法句狼,具體打點的都是通過StatisticService
來實現(xiàn)笋熬。
在visitField
中,通過createFiled
方法來創(chuàng)建StatisticService
類型的字段
private fun createFiled() {
if (!mFieldPresent) {
mFieldPresent = true
// 注入:statisticService 字段
val fieldVisitor = cv.visitField(ACC_PRIVATE or ACC_FINAL, statisticServiceField.name, statisticServiceField.desc, null, null)
fieldVisitor.visitEnd()
}
}
其中statisticServiceField
是封裝好的StatisticService
字段信息腻菇。
companion object {
const val OWNER = "com/idisfkj/androidapianalysis/proxy/StatisticService"
const val DESC = "Lcom/idisfkj/androidapianalysis/proxy/StatisticService;"
val INSTANCE = StatisticService()
}
val statisticService = FieldConfig(
Opcodes.PUTFIELD,
"",
"mStatisticService",
DESC
)
創(chuàng)建的字段名為mStatisticService
胳螟,它的類型是StatisticService
到這里我們已經(jīng)拿到了埋點的數(shù)據(jù)字段,并創(chuàng)建了埋點的調(diào)用字段mStatisticService
芜繁;接下來要做的就是注入埋點代碼旺隙。
核心注入代碼在visitMethod
方法中,該方法用來掃描類中的方法骏令。所以類中聲明的方法都會在這個方法中進行掃描回調(diào)蔬捷。
在visitMethod
中,我們找到目標(biāo)的埋點方法榔袋,即之前聲明的方法注解TrackScan
與TrackClick
周拐。
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
val mv = cv.visitMethod(access, name, desc, signature, exceptions)
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
private var mMethodAnnotationDesc: String? = null
override fun visitAnnotation(desc: String?, visible: Boolean): AnnotationVisitor {
LocalConfig.methodVisitorConfig?.visitAnnotation?.invoke(desc, visible)
mMethodAnnotationDesc = desc
return super.visitAnnotation(desc, visible)
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
LocalConfig.methodVisitorConfig?.onMethodExit?.invoke(opcode)
// 默認(rèn)構(gòu)造方法init
if (name == INIT_METHOD_NAME /** && desc == INIT_METHOD_DESC **/ && mFieldPresent) {
// 注入:向默認(rèn)構(gòu)造方法中,實例化statisticService
injectStatisticService(mv, Statistic.INSTANCE, statisticServiceField.copy(owner = mClassName ?: ""))
} else if (mMethodAnnotationDesc == TRACK_CLICK_DESC && !mTrackDataName.isNullOrEmpty()) {
// 注入:日志
injectLogUtils(mv, defaultLogUtilsConfig.copy(ldc = "inject track click success."))
// 注入:trackClick 點擊
injectTrackClick(mv, TrackModel.INSTANCE, StatisticService.INSTANCE)
} else if (mMethodAnnotationDesc == TRACK_SCAN_DESC && !mTrackScanDataName.isNullOrEmpty()) {
when (mTrackScanDataDesc) {
// 數(shù)據(jù)類型為List<*>
LIST_DESC -> {
// 注入:日志
injectLogUtils(mv, defaultLogUtilsConfig.copy(ldc = "inject track scan success."))
// 注入:List 類型的TrackScan 曝光
injectListTrackScan(mv, TrackModel.INSTANCE, StatisticService.INSTANCE)
}
// 數(shù)據(jù)類型為TrackModel
TrackModel.DESC -> {
// 注入:日志
injectLogUtils(mv, defaultLogUtilsConfig.copy(ldc = "inject track scan success."))
// 注入: TrackScan 曝光
injectTrackScan(mv, TrackModel.INSTANCE, StatisticService.INSTANCE)
}
else -> {
}
}
}
}
}
}
返回并實現(xiàn)AdviceAdapter
凰兑,重寫它的visitAnnotation
方法妥粟。
該方法會自動掃描方法的注解,所以可以通過該方法來保存當(dāng)前方法的注解吏够。
然后在onMethodExit
中勾给,即方法的開頭處進行注入代碼。
在該方法中主要做三件事
- 向默認(rèn)構(gòu)造方法中锅知,實例化
statisticService
- 注入
TrackClick
點擊 - 注入
TrackScan
曝光
具體的ASM
注入代碼可以通過之前說的SM Bytecode Viewer Support Kotlin
插件獲取播急。
有了上面的實現(xiàn),再來運行運行主項目售睹,你就會發(fā)現(xiàn)埋點代碼已經(jīng)自動注入成功桩警。
我們反編譯一下.class
文件,來看下注入后的java
代碼
StatisticService初始化
public ProxyActivity() {
boolean var2 = false;
List var3 = (List)(new ArrayList());
this.mTrackScanData = var3;
// 以下是注入代碼
this.mStatisticService = (StatisticService)Statistic.Companion.getInstance().create(StatisticService.class);
}
曝光埋點
@TrackScan
public final void onScan() {
this.mTrackScanData.add(new TrackModel("statistic_button", 0L, 2, (DefaultConstructorMarker)null));
this.mTrackScanData.add(new TrackModel("statistic_text", 0L, 2, (DefaultConstructorMarker)null));
// 以下是注入代碼
LogUtils.INSTANCE.d("inject track scan success.");
Iterator var2 = this.mTrackScanData.iterator();
while(var2.hasNext()) {
TrackModel var1 = (TrackModel)var2.next();
this.mStatisticService.trackScan(var1.getName());
}
}
點擊埋點
@TrackClick
public final void onClick(@NotNull View view) {
Intrinsics.checkParameterIsNotNull(view, "view");
this.mTrackModel.setTime(System.currentTimeMillis() / (long)1000);
this.mTrackModel.setName(view.getId() == 2131230792 ? "statistic_button" : "statistic_text");
// 以下是注入代碼
LogUtils.INSTANCE.d("inject track click success.");
this.mStatisticService.trackClick(this.mTrackModel.getName(), this.mTrackModel.getTime());
}
以上自動化埋點代碼就已經(jīng)完成了昌妹。
簡單總結(jié)一下捶枢,所用到的技術(shù)有
-
gradle plugin
插件的自定義 -
gradle transform
提供編譯中字節(jié)碼的修改入口 -
asm
提供代碼的注入實現(xiàn)
其中1
握截、2
都有現(xiàn)成的實現(xiàn)套路,我們真正需要做的很少烂叔,核心部分還是通過asm
來編寫需要注入的代碼邏輯谨胞。不管是直接注入,還是借助注解來注入长已,本質(zhì)都是一樣的畜眨。
只要掌握以上幾點,你就可以實現(xiàn)任意的自動化代碼注入术瓮。從此以后讓我們進入摸魚時代,以后再也不用加班啦~
另外文章中的代碼都可以到Github
的android-api-analysis
項目中查看贰健。
查看時請將分支切換到feat_transform_dev
最后
如果有什么疑問可以直接在留言區(qū)進行留言討論胞四,或者關(guān)注公眾號:Android補給站,獲取更多Android
干貨伶椿。
推薦
android_startup: 提供一種在應(yīng)用啟動時能夠更加簡單辜伟、高效的方式來初始化組件。開發(fā)人員可以使用android-startup
來簡化啟動序列脊另,并顯式地設(shè)置初始化順序與組件之間的依賴關(guān)系导狡。 與此同時android-startup
支持同步與異步等待,并通過有向無環(huán)圖拓?fù)渑判虻姆绞絹肀WC內(nèi)部依賴組件的初始化順序偎痛。
AwesomeGithub: 基于Github
客戶端旱捧,純練習(xí)項目,支持組件化開發(fā)踩麦,支持賬戶密碼與認(rèn)證登陸枚赡。使用Kotlin
語言進行開發(fā),項目架構(gòu)是基于Jetpack&DataBinding
的MVVM
谓谦;項目中使用了Arouter
贫橙、Retrofit
、Coroutine
反粥、Glide
卢肃、Dagger
與Hilt
等流行開源技術(shù)。
flutter_github: 基于Flutter
的跨平臺版本Github
客戶端才顿,與AwesomeGithub
相對應(yīng)莫湘。
android-api-analysis: 結(jié)合詳細(xì)的Demo
來全面解析Android
相關(guān)的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點。
daily_algorithm: 算法進階娜膘,由淺入深逊脯,歡迎加入一起共勉。