Android 基于 DroidAssist 插件实现无埋点

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")
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值