天天都是重複的工做,這樣可不行,已經嚴重影響個人平常摸魚,爲了減小本身平常的開發時間,我決定走一條歧路,鋌而走險,將項目中的各類手動埋點統計替換成自動化埋點。之後不再用擔憂沒時間摸魚了~java
做爲Android
屆開發的一員,今天我決定將摸魚方案分享給你們,但願更多的廣大羣衆可以的加入到摸魚的行列中~android
爲了更好的理解與簡化實現步驟,我將會結合動態代理分析與仿Retrofit實踐中埋點Demo
來進行拆解,畢竟實際項目比這要複雜,經過簡單的Demo
來了解核心點便可。git
在真正實現代碼注入以前,咱們先來看正常手動打點的步驟.github
在動態代理分析與仿Retrofit實踐中已經將打點的步驟進行了簡化。算法
沒看過上面的文章也不影響接下的閱讀api
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)
}
複製代碼
StatisticService
接口引用private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
複製代碼
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)
}
}
複製代碼
其中二、3步驟都是在對應埋點的類中使用,這裏對應的是ProxyActivity
markdown
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是建立新的類,不在代碼注入的範圍以內。自動生成類可使用註解+process+JavaPoet
來實現。相似於ButterKnife
、Dagger2
、Room
等。以前我也有寫過相關的demo
與文章。因爲不在本篇文章的範圍以內,感興趣的能夠自行去了解。架構
這裏咱們須要作的是:須要在ProxyActiviy
中將二、3步驟的代碼轉成自動注入。app
自動注入就是在現有的類中自動加入咱們預期的代碼,不須要咱們額外的進行編寫。異步
既然已經知道了須要注入的代碼,那麼接下的問題就是何時進行注入這些代碼。
這就涉及到Android
構建與打包的流程,Android
使用Gradle
進行構建與打包,
在打包的過程當中將源文件轉化成.class
文件,而後再將.class
文件轉成Android
能識別的.dex
文件,最終將全部的.dex
文件組合成一個.apk
文件,提供用戶下載與安裝。
而在將源文件轉化成.class
文件以後,Google
提供了一種Transform
機制,容許咱們在打包以前對.class
文件進行修改。
這個修改時機就是咱們代碼自動注入的時機。
transform
是由gradle
提供,在咱們平常的構建過程當中也會看到系統自身的transform
身影,gradle
由各類task
組成,transform
就穿插在這些task
中。
圖中高亮的部分就是本次自定義的TraceTransform
, 它會在.class
轉化成.dex
以前進行執行,目的就是修改目標.class
文件內容。
Transform
的實現須要結合Gradle Plugin
一塊兒使用。因此接下來咱們須要建立一個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'
複製代碼
這裏的插件包括系統自帶、第三方的與自定義的。其中trace_plugin
就是本次自定義的插件。爲了可以讓項目使用自定義的插件,Gradle
提供了三種打包插件的方式
Build Script
: 將插件的源代碼直接包含在構建腳本中。這樣作的好處是,無需執行任何操做便可自動編譯插件並將其包含在構建腳本的類路徑中。但缺點是它在構建腳本以外不可見,經常使用在腳本自動構建中。buildSrc project
:gradle
會自動識別buildSrc
目錄,因此能夠將plugin
放到buildSrc
目錄中,這樣其它的構建腳本就能自動識別這個plugin
, 多用於自身項目,對外不共享。Standalone project
: 建立一個獨立的plugin
項目,經過對外發布Jar
與外部共享使用。這裏使用第三種方式來建立Plugin
。因此建立完以後的目錄結構大概是這樣的
爲了讓別的項目可以引用這個Plugin
,咱們須要對外聲明,能夠發佈到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'
// 實現這個插件的類的路徑
implementationClass = 'com.rousetime.trace_plugin.TracePlugin'
}
}
}
複製代碼
該Plugin
的id
爲trace_plugin
,實現入口爲com.rousetime.trace_plugin.TracePlugin
。
聲明完以後,就能夠直接在項目的根目錄下的build.gradle
中引入該id
plugins {
id "trace_plugin" apply false
}
複製代碼
爲了能在app
項目中apply
這個plugin
,還須要建立一個META-INF.gradle-plugins
目錄,對應的位置以下
注意這裏的trace_plugin.properties
文件名很是重要,前面的trace_plugin
就表明你在build.gradle
中apply
的插件名稱。
文件中的內容很簡單,只有一行,對應的就是TracePlugin
的實現入口
implementation-class=com.rousetime.trace_plugin.TracePlugin
複製代碼
上面都準備就緒以後,就能夠在build.gradle
進行apply plugin
apply plugin: 'trace_plugin'
複製代碼
這個時候咱們自定義的plugin
就引入到項目中了。
再回到剛剛的Plugin
入口TracePlugin
,來看下它的具體實現
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
,在該方法中咱們打印一行文本,而後從新構建項目,在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
已經建立成功,而且已經集成到咱們的項目中。
第一步已經完成。下面進入第二步。
在TracePlugin
的apply
方法中,對項目的appExtension
註冊了一個TraceTransform
。重點來了,這個TraceTransform
就是咱們在gradle
構建的過程當中插入的Transform
,也就是注入代碼的入口。來看下它的具體實現
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()
}
}
}
複製代碼
代碼很簡單,只須要實現幾個特定的方法。
getName
: Transform
對外顯示的名稱getInputTypes
: 掃描的文件類型,CONENT_JARS
表明CLASSES
與RESOURCES
isIncremental
: 是否開啓增量,開啓後會提升構建速度,對應的須要手動處理增量的邏輯getScopes
: 掃描做用範圍,SCOPE_FULL_PROJECT
表明整個項目transform
: 須要轉換的邏輯都在這裏處理transform
是咱們接下來.class
文件的入口,這個方法有個參數TransformInvocation
,該參數提供了上面定義範圍內掃描到的所用jar
文件與directory
文件。
在transform
中咱們主要作的就是在這些jar
與directory
中解析出.class
文件,這是找到目標.class
的第一步。只有解析出了全部的.class
文件,咱們才能進一步過濾出咱們須要注入代碼的.class
文件。
而transform
的工做流程是:解析.class
文件,而後咱們過濾出須要處理的.class
文件,寫入對應的邏輯,而後再將處理過的.class
文件從新拷貝到以前的jar
或者directory
中。
經過這種解析、處理與拷貝的方式,實現偷天換日的效果。
既然有一套固定的流程,那麼天然有對應的一套固定是實現。在這三個步驟中,真正須要實現的是處理邏輯,不一樣的項目有不一樣的處理邏輯,
對於解析與拷貝操做,已經有相對完整的一套通用實現方案。若是你的項目中有多個這種類型的Transform
,就能夠將其抽離出來單個module
,增長複用性。
下面咱們來看一下它的核心實現步驟。
fun transform() {
if (!isIncremental) {
// 不是增量編譯,將以前的輸出目錄中的內容所有刪除
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
,從中解析出對應的.class
文件。
遍歷的核心代碼以下
while (enumeration.hasMoreElements()) {
val jarEntry = enumeration.nextElement()
val inputStream = originalFile.getInputStream(jarEntry)
val entryName = jarEntry.name
// 構建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
說明當前是.class
文件,咱們須要單獨拿出來進行後續的處理。
後續的處理邏輯交給了transformProcess.process
。具體處理先放一放。
處理完以後,再將處理後的字節碼拷貝保存到以前的jar
中。
對應的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
方法進行統一處理。最後將處理完的字節碼拷貝保存到原路徑中。
以上就是Transform
的解析與拷貝的核心處理。
上面提到.class
的處理都轉交給process
方法,這個方法的具體實如今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
來處理過濾出來的字節碼。最終的處理會來到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
的內容,也是咱們接下來實現自動注入代碼的重點。
ASM
是操做Java
字節碼的一個工具。
其實操做字節碼的除了ASM
還有javassist
,但我的以爲ASM
更方便,由於它有一系列的輔助工具,能更好的幫助咱們實現代碼的注入。
在上面咱們已經獲得了.class
的字節碼文件。如今咱們須要作的就是掃描整個字節碼文件,判斷是不是咱們須要注入的文件。
這裏我將這些邏輯封裝到了ClassFilterVisitor
文件中。
ASM
爲咱們提供了ClassVisitor
、MethodVisitor
、FieldVisitor
等API
。每當ASM
掃描類的字節碼時,都會調用它的visit
、visitField
、visitMethod
與visitAnnotation
等方法。
有了這些方法,咱們就能夠判斷並處理咱們須要的字節碼文件。
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)
// 掃描當前類的信息
}
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)
}
}
}
}
複製代碼
假設咱們須要在onClickListener
中注入LogUtils.d
這個行代碼,本質就是在點擊的時候輸出一行日誌。
首先咱們須要明白,setOnClickListener
本質是實現了一個OnClickListener
接口的匿名內部類。
因此能夠在掃描類的時候判斷是否實現了OnClickListener
這個接口,若是實現了,咱們再去匹配它的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 {
// 判斷當前類是否實現了onClickListener
if (mInterface != null && mInterface?.size ?: 0 > 0) {
mInterface?.forEach {
// 判斷當前掃描的方法是不是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
方法中,咱們保存當前類實現的接口;在visitMethod
中再對當前接口進行判斷,看它是否有onClick
方法。
name
與desc
分別爲onClick
方法的方法名稱與方法參數描述。這是字節碼匹配方法的一種規範。
若是有的話,說明是咱們須要插入的方法,這個時候返回AdviceAdapter
。它是ASM
提供的便捷針對方法注入的類。咱們重寫它的onMethodEnter
方法。表明咱們將在方法的開頭注入代碼。
onMethodEnter
方法中的代碼就是LogUtils.d
的ASM
注入實現。你可能會說這個是什麼,徹底看不懂,更別說寫字節碼注入了。
別急,下面就是ASM
的方便之處,咱們只需在Android Studio
中下載ASM Bytecode Viewer Support Kotlin
插件。
該插件能夠幫助咱們查看kotlin
字節碼,只需右鍵彈窗中選擇ASM Bytecode Viewer
。稍後就會彈出轉化後的字節碼彈窗。
在彈窗中找到須要注入的代碼,具體就是下面這幾行
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
方法中。這樣注入的代碼就已經完成。
若是你想查看是否注入成功,除了運行項目,查看效果以外,還能夠直接查看注入的源碼。
在項目的build/intermediates/transforms
目錄下,找到自定義的TraceTransform
,再找到對應的注入文件,就能夠查看注入源碼。
其實到這來核心內容基本已經結束了,不論是注入什麼代碼均可以經過這種方法來獲取注入的ASM
的代碼,不一樣的只是注入的時機判斷。
有了上面的基礎,咱們來實現開頭的自動埋點。
爲了讓自動化埋點可以靈活的傳遞打點數據,咱們使用註解的方式來傳遞具體的埋點數據與類型。
有了這些註解,剩下咱們要作的就很簡單了
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
聲明打點的數據;使用TrackScan
與TrackClick
聲明打點的類型與自動化插入代碼的入口方法。
咱們再回到注入代碼的類ClassFilterVisitor
,來實現具體的埋點代碼的注入。
在這裏咱們須要作的是解析聲明的註解,拿到打點的數據,而且聲明的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
方法來掃描具體的類文件,在這裏保存當前掃描的類的信息,爲以後注入代碼作準備
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
方法用來掃描類文件中聲明的字段。在該方法中,咱們返回並實現FieldVisitor
,並從新它的visitAnnotation
方法,目的是找到以前TrackClickData
與TrackScanData
聲明的埋點字段。對應的就是mTrackModel
與mTrackScanData
。
主要包括字段名稱name
與字段的描述desc
,爲咱們以後注入埋點數據作準備。
另一旦匹配到埋點數據的註解,說明該類中須要進行自動化埋點,因此還須要自動建立StatisticService
。這是打點的接口方法,具體打點的都是經過StatisticService
來實現。
在visitField
中,經過createFiled
方法來建立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
)
複製代碼
建立的字段名爲mStatisticService
,它的類型是StatisticService
到這裏咱們已經拿到了埋點的數據字段,並建立了埋點的調用字段mStatisticService
;接下來要作的就是注入埋點代碼。
核心注入代碼在visitMethod
方法中,該方法用來掃描類中的方法。因此類中聲明的方法都會在這個方法中進行掃描回調。
在visitMethod
中,咱們找到目標的埋點方法,即以前聲明的方法註解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)
// 默認構造方法init
if (name == INIT_METHOD_NAME /** && desc == INIT_METHOD_DESC **/ && mFieldPresent) {
// 注入:向默認構造方法中,實例化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) {
// 數據類型爲List<*>
LIST_DESC -> {
// 注入:日誌
injectLogUtils(mv, defaultLogUtilsConfig.copy(ldc = "inject track scan success."))
// 注入:List 類型的TrackScan 曝光
injectListTrackScan(mv, TrackModel.INSTANCE, StatisticService.INSTANCE)
}
// 數據類型爲TrackModel
TrackModel.DESC -> {
// 注入:日誌
injectLogUtils(mv, defaultLogUtilsConfig.copy(ldc = "inject track scan success."))
// 注入: TrackScan 曝光
injectTrackScan(mv, TrackModel.INSTANCE, StatisticService.INSTANCE)
}
else -> {
}
}
}
}
}
}
複製代碼
返回並實現AdviceAdapter
,重寫它的visitAnnotation
方法。
該方法會自動掃描方法的註解,因此能夠經過該方法來保存當前方法的註解。
而後在onMethodExit
中,即方法的開頭處進行注入代碼。
在該方法中主要作三件事
statisticService
TrackClick
點擊TrackScan
曝光具體的ASM
注入代碼能夠經過以前說的SM Bytecode Viewer Support Kotlin
插件獲取。
有了上面的實現,再來運行運行主項目,你就會發現埋點代碼已經自動注入成功。
咱們反編譯一下.class
文件,來看下注入後的java
代碼
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());
}
複製代碼
以上自動化埋點代碼就已經完成了。
簡單總結一下,所用到的技術有
gradle plugin
插件的自定義gradle transform
提供編譯中字節碼的修改入口asm
提供代碼的注入實現其中1
、2
都有現成的實現套路,咱們真正須要作的不多,核心部分仍是經過asm
來編寫須要注入的代碼邏輯。不論是直接注入,仍是藉助註解來注入,本質都是同樣的。
只要掌握以上幾點,你就能夠實現任意的自動化代碼注入。今後之後讓咱們進入摸魚時代,之後不再用加班啦~
另外文章中的代碼均可以到Github
的android-api-analysis
項目中查看。
查看時請將分支切換到feat_transform_dev
若是有疑問歡迎在留言區進行討論,或者關注公衆號:Android補給站,獲取更多關於Android
的進階文章。
android_startup: 提供一種在應用啓動時可以更加簡單、高效的方式來初始化組件。開發人員可使用android-startup
來簡化啓動序列,並顯式地設置初始化順序與組件之間的依賴關係。 與此同時android-startup
支持同步與異步等待,並經過有向無環圖拓撲排序的方式來保證內部依賴組件的初始化順序。
AwesomeGithub: 基於Github
客戶端,純練習項目,支持組件化開發,支持帳戶密碼與認證登錄。使用Kotlin
語言進行開發,項目架構是基於Jetpack&DataBinding
的MVVM
;項目中使用了Arouter
、Retrofit
、Coroutine
、Glide
、Dagger
與Hilt
等流行開源技術。
flutter_github: 基於Flutter
的跨平臺版本Github
客戶端,與AwesomeGithub
相對應。
android-api-analysis: 結合詳細的Demo
來全面解析Android
相關的知識點, 幫助讀者可以更快的掌握與理解所闡述的要點。
daily_algorithm: 算法進階,由淺入深,歡迎加入一塊兒共勉。