DroidAssist
DroidAssist (源码) 是一个轻量级的 Android 字节码编辑插件,基于 Javassist 对字节码操作,根据 xml 配置处理 class 文件,以达到对 class 文件进行动态修改的效果。和其他 AOP 方案不同,DroidAssist 提供了一种更加轻量,简单易用,无侵入,可配置化的字节码操作方式,你不需要 Java 字节码的相关知识,只需要在 Xml 插件配置中添加简单的 Java 代码即可实现类似 AOP 的功能,同时不需要引入其他额外的依赖。
使用方式
DroidAssist 适用于 Android Studio 工程 application model 或者 library model,使用 DroidAssist 需要接入 DroidAssist 插件并编写专有配置文件。
在 root project 的 build.gradle 里添加:
dependencies {
classpath "com.didichuxing.tools:droidassist:1.1.1"
}
注意:目前似乎只在 jcenter 有发布,所以需要在 repositories 里添加 jcenter() 。
在需要处理的 model project 的 build.gradle 里添加:
apply plugin: 'com.didichuxing.tools.droidassist'
droidAssistOptions {
config file("droidassist.xml"),file("droidassist2.xml") //插件配置文件(必选配置,支持多配置文件)
}
埋点配置
1、埋点代码 ViewInject.kt
package com.example.droidassisttest
import android.app.Activity
import android.content.DialogInterface
import android.os.SystemClock
import android.util.Log
import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
import android.widget.SeekBar
import androidx.fragment.app.Fragment
/**
* @author ganmin.he
* @date 2022/2/11
*/
object ViewInject {
private const val TAG = "ViewInject"
@JvmStatic
fun injectClick(view: View) {
Log.d(TAG, "injectClick on $view")
}
@JvmStatic
fun injectDialogClick(dialog: DialogInterface, which: Int) {
Log.d(TAG, "injectDialogClick on $dialog: $which")
}
@JvmStatic
fun injectGroupCheckedChanged(radioGroup: RadioGroup, which: Int) {
Log.d(TAG, "injectGroupCheckedChanged on $radioGroup: $which")
}
@JvmStatic
fun injectSeekClick(seekBar: SeekBar) {
Log.d(TAG, "injectSeekClick on $seekBar")
}
@JvmStatic
fun injectCheckedChanged(button: CompoundButton, value: Boolean) {
Log.d(TAG, "injectCheckedChanged on $button: $value")
}
@JvmStatic
fun injectActivityOnResume(activity: Activity) {
Log.d(TAG, "injectActivityOnResume $activity")
activity.window.decorView.setTag(R.id.activity_resume_tag, SystemClock.uptimeMillis())
}
@JvmStatic
fun injectActivityOnPause(activity: Activity) {
val pauseTime = SystemClock.uptimeMillis()
val resumeTime =
(activity.window.decorView.getTag(R.id.activity_resume_tag) as? Long) ?: pauseTime
Log.d(TAG, "injectActivityOnPause $activity time=${pauseTime - resumeTime}")
}
@JvmStatic
fun injectFragmentOnResume(fragment: Fragment) {
Log.d(TAG, "injectFragmentOnResume $fragment")
}
@JvmStatic
fun injectFragmentOnPause(fragment: Fragment) {
Log.d(TAG, "injectFragmentOnPause $fragment")
}
@JvmStatic
fun injectFragmentOnResume(fragment: android.app.Fragment) {
Log.d(TAG, "injectFragmentOnResume $fragment")
}
@JvmStatic
fun injectFragmentOnPause(fragment: android.app.Fragment) {
Log.d(TAG, "injectFragmentOnPause $fragment")
}
}
2、埋点 xml 配置表
路径为 DroidAssistTest/app/droidassist.xml
记得在 xml 头部添加 https://github.com/didi/DroidAssist/blob/master/docs/droidassist.dtd 的内容,为了方便编写配置文件,在 IDE 中能自动提示。
<!DOCTYPE DroidAssist [<!ELEMENT DroidAssist ((Global?|Replace?|Insert?|Around?|Enhance?)?,(Global?|Replace?|Insert?|Around?|Enhance?)?,(Global?|Replace?|Insert?|Around?|Enhance?)?,(Global?|Replace?|Insert?|Around?|Enhance?)?,(Global?|Replace?|Insert?|Around?|Enhance?)?)><!ELEMENT Filter (Include*|Exclude*)*><!ELEMENT Include (#PCDATA)><!ELEMENT Exclude (#PCDATA)><!ELEMENT Source (#PCDATA)><!ELEMENT Target (#PCDATA)><!ELEMENT TargetBefore (#PCDATA)><!ELEMENT TargetAfter (#PCDATA)><!ELEMENT Exception (#PCDATA)><!ELEMENT MethodCall (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT MethodExecution (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT ConstructorCall (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT ConstructorExecution (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT InitializerExecution (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT FieldRead (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT FieldWrite (Source,(Target?|(TargetBefore?,TargetAfter?)),Filter?)><!ELEMENT BeforeMethodCall (Source,Target,Filter?)><!ELEMENT AfterMethodCall (Source,Target,Filter?)><!ELEMENT BeforeMethodExecution (Source,Target,Filter?)><!ELEMENT AfterMethodExecution (Source,Target,Filter?)><!ELEMENT BeforeConstructorCall (Source,Target,Filter?)><!ELEMENT AfterConstructorCall (Source,Target,Filter?)><!ELEMENT BeforeConstructorExecution (Source,Target,Filter?)><!ELEMENT AfterConstructorExecution (Source,Target,Filter?)><!ELEMENT BeforeInitializerExecution (Source,Target,Filter?)><!ELEMENT AfterInitializerExecution (Source,Target,Filter?)><!ELEMENT BeforeFieldRead (Source,Target,Filter?)><!ELEMENT AfterFieldRead (Source,Target,Filter?)><!ELEMENT BeforeFieldWrite (Source,Target,Filter?)><!ELEMENT AfterFieldWrite (Source,Target,Filter?)><!ELEMENT TryCatchMethodCall (Source,Exception?,Target,Filter?)><!ELEMENT TryCatchMethodExecution (Source,Exception?,Target,Filter?)><!ELEMENT TryCatchConstructorCall (Source,Exception?,Target,Filter?)><!ELEMENT TryCatchConstructorExecution (Source,Exception?,Target,Filter?)><!ELEMENT TryCatchInitializerExecution (Source,Exception?,Target,Filter?)><!ELEMENT TimingMethodCall (Source,Target,Filter?)><!ELEMENT TimingMethodExecution (Source,Target,Filter?)><!ELEMENT TimingConstructorCall (Source,Target,Filter?)><!ELEMENT TimingConstructorExecution (Source,Target,Filter?)><!ELEMENT TimingInitializerExecution (Source,Target,Filter?)><!ELEMENT ReparentClass (Source,Target,Filter?)><!ELEMENT Global (Filter?)><!ELEMENT Replace (MethodCall*|MethodExecution*|ConstructorCall*|ConstructorExecution*|InitializerExecution*|FieldRead*|FieldWrite*)*><!ELEMENT Around (MethodCall*|MethodExecution*|ConstructorCall*|ConstructorExecution*|InitializerExecution*|FieldRead*|FieldWrite*)*><!ELEMENT Insert (BeforeMethodCall*|AfterMethodCall*|BeforeMethodExecution*|AfterMethodExecution*|BeforeConstructorCall*|AfterConstructorCall*|BeforeConstructorExecution*|AfterConstructorExecution*|BeforeInitializerExecution*|AfterInitializerExecution*|BeforeFieldRead*|AfterFieldRead*|BeforeFieldWrite*|AfterFieldWrite*)*><!ELEMENT Enhance (TryCatchMethodCall*|TryCatchMethodExecution*|TryCatchConstructorCall*|TryCatchConstructorExecution*|TryCatchInitializerExecution*|TimingMethodCall*|TimingMethodExecution*|TimingConstructorCall*|TimingConstructorExecution*|TimingInitializerExecution*|ReparentClass*)*><!ATTLIST Source extend (true|false) "true"><!ATTLIST Filter ignoreGlobalExcludes (true|false) "false"><!ATTLIST Filter ignoreGlobalIncludes (true|false) "false">]>
<DroidAssist>
<Global>
<Filter>
<Include>*</Include>
<Exclude>androidx.*</Exclude>
<!--<Exclude>android.*</Exclude>-->
<!--<Exclude>com.android.*</Exclude>-->
</Filter>
</Global>
<Insert>
<BeforeMethodExecution>
<Source>
void android.view.View$OnClickListener.onClick(android.view.View)
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectClick($1);}
</Target>
</BeforeMethodExecution>
<BeforeMethodExecution>
<Source>
void
android.content.DialogInterface$OnClickListener.onClick(android.content.DialogInterface,int)
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectDialogClick($1,$2);}
</Target>
</BeforeMethodExecution>
<BeforeMethodExecution>
<Source>
void
android.widget.RadioGroup$OnCheckedChangeListener.onCheckedChanged(android.widget.RadioGroup,int)
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectGroupCheckedChanged($1,$2);}
</Target>
</BeforeMethodExecution>
<BeforeMethodExecution>
<Source>
void
android.widget.SeekBar$OnSeekBarChangeListener.onStopTrackingTouch(android.widget.SeekBar)
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectSeekClick($1);}
</Target>
</BeforeMethodExecution>
<BeforeMethodExecution>
<Source>
void
android.widget.CompoundButton$OnCheckedChangeListener.onCheckedChanged(android.widget.CompoundButton,boolean)
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectCheckedChanged($1,$2);}
</Target>
</BeforeMethodExecution>
<BeforeMethodExecution>
<Filter ignoreGlobalExcludes="true" />
<Source extend="false">
void androidx.fragment.app.FragmentActivity.onResume()
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectActivityOnResume(this);}
</Target>
</BeforeMethodExecution>
<BeforeMethodExecution>
<Filter ignoreGlobalExcludes="true" />
<Source extend="false">
void androidx.fragment.app.FragmentActivity.onPause()
</Source>
<Target>
{com.example.droidassisttest.ViewInject.injectActivityOnPause(this);}
</Target>
</BeforeMethodExecution>
</Insert>
</DroidAssist>
3、使用样例
布局文件 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="View OnClick"
app:layout_constraintBottom_toTopOf="@id/show_dialog"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/show_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dialog OnClick"
app:layout_constraintBottom_toTopOf="@id/switch_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_view" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/radio_group"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/show_dialog" />
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/seekbar_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/switch_view">
<androidx.appcompat.widget.AppCompatRadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RadioButton1" />
<androidx.appcompat.widget.AppCompatRadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RadioButton2" />
</RadioGroup>
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/seekbar_view"
android:layout_width="200dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/radio_group" />
</androidx.constraintlayout.widget.ConstraintLayout>
Activity 代码 MainActivity.kt
package com.example.droidassisttest
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.CompoundButton
import android.widget.RadioGroup
import android.widget.SeekBar
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.example.droidassisttest.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val dialogBuilder = AlertDialog.Builder(this@MainActivity).apply {
setCancelable(true)
setTitle("AlertDialog Test")
setPositiveButton("Ok", object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
Log.d(TAG, "$dialog: onClick $which")
}
})
}
binding.run {
/*buttonView.setOnClickListener {
Log.d(TAG, "$it: onClick")
}*/
buttonView.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
Log.d(TAG, "$v: onClick")
}
})
showDialog.setOnClickListener {
dialogBuilder.show()
}
/*switchView.setOnCheckedChangeListener { buttonView, isChecked ->
Log.d(TAG, "$buttonView isChecked=$isChecked")
}*/
switchView.setOnCheckedChangeListener(object : CompoundButton.OnCheckedChangeListener {
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
Log.d(TAG, "$buttonView isChecked=$isChecked")
}
})
/*radioGroup.setOnCheckedChangeListener { group, checkedId ->
Log.d(TAG, "$group checkedId=$checkedId")
}*/
radioGroup.setOnCheckedChangeListener(object : RadioGroup.OnCheckedChangeListener {
override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) {
Log.d(TAG, "$group checkedId=$checkedId")
}
})
seekbarView.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progress: Int,
fromUser: Boolean
) {
Log.d(TAG, "$seekBar onProgressChanged progress=$progress")
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
Log.d(TAG, "$seekBar onStartTrackingTouch")
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
Log.d(TAG, "$seekBar onStopTrackingTouch")
}
})
}
}
}
Log 输出
D/ViewInject: injectActivityOnResume com.example.droidassisttest.MainActivity@1a24434
D/ViewInject: injectClick on com.google.android.material.button.MaterialButton{42efbfd VFED..C.. ...P.... 355,151-725,277 #7f080064 app:id/button_view}
D/ViewInject: injectDialogClick on androidx.appcompat.app.AlertDialog@c607c20: -1
D/ViewInject: injectCheckedChanged on androidx.appcompat.widget.SwitchCompat{3b12a95 VFED..C.. ...P..ID 477,706-603,832 #7f080198 app:id/switch_view}: true
D/ViewInject: injectGroupCheckedChanged on android.widget.RadioGroup{42a6f5b V.E...... .......D 0,983-1080,1235 #7f080150 app:id/radio_group}: 1
D/ViewInject: injectSeekClick on androidx.appcompat.widget.AppCompatSeekBar{3677909 VFED..... ...P.... 278,1386-803,1433 #7f08016e app:id/seekbar_view}
4、注意事项
对于需要自动埋点的地方,不支持使用 Java 或者 kotlin 的 lambda 表达式,否则不会自动埋点。
比如:
buttonView.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
Log.d(TAG, "$v: onClick")
}
}
不能用 lambda 表达式代替:
buttonView.setOnClickListener {
Log.d(TAG, "$it: onClick")
}