大众号:字节数组

期望对你有所协助

一、Trace

在最近的一年多时刻里,我先后写过六篇关于 Android 字节码插桩的文章,总共对应四个功用点

  • ASM 字节码插桩:完成双击防抖
  • ASM 字节码插桩:进行线程整治
  • ASM 字节码插桩:助力隐私合规
  • ASM 字节码插桩:监控大图加载
  • ASM 字节码插桩:从 Lambda 表达式讲起
  • ASM 字节码插桩:Jetpack Compose 完成双击防抖

发布这几篇文章后,有读者问过我是否支撑直接长途依靠运用,因为其时我并没有将代码保管到公共 Maven,仅是在 Github 开源了相关的完成代码罢了:asm-samples,所以在其时是不支撑的

最近比较有空,想着好久没有写文章了,想水一篇,就花了点时刻将其中两个我觉得还比较实用的功用点抽取了出来,并新增了一个功用点一起发布到了 Github:Trace,然后也将 Trace 发布到了 Gradle Plugins 官网,便利开发者在自己项目中直接长途依靠运用

这三个功用点包括:

  • 使用双击防抖。包括 Android 原生的 View 体系以及现在盛行的 Jetpack Compose,对应第一篇和最后两篇文章
  • 替换 Class 的承继关系。可用于非侵入式地完成监控大图加载的功用,对应第四篇文章
  • 修正 Toast 在 Android 7.1 上的体系 bug。这是我新写的一个功用点,用于解决在 Android 7.1 体系上 Toast 因为 WindowToken 失效然后导致使用溃散的问题

下面就来介绍如安在项目中接入 Trace,首要的完成思路参照以上文章即可

二、引进

在项目根目录下的 build.gradle 或许 build.gradle.kts 中引进插件

//grovy
plugins {
    id "io.github.leavesczy.trace" version "latestVersion" apply false
}
//kts
plugins {
    id("io.github.leavesczy.trace").version("latestVersion").apply(false)
}

在项目主模块下的 build.gradle 或许 build.gradle.kts 中使用插件,需求哪些功用点就为其设置对应的参数即可

//grovy
plugins {
    id("io.github.leavesczy.trace")
}
clickTrace {
    view.onClickClass = "x"
    view.onClickMethodName = "x"
    view.uncheckViewOnClickAnnotation = "x"
    view.include = []
    view.exclude = []
    compose.onClickClass = "x"
    compose.onClickWhiteList = "x"
}
replaceClassTrace {
    originClass = "x"
    targetClass = "x"
    include = []
    exclude = []
}
toastTrace {
    toasterClass = "x"
    showToastMethodName = "x"
}
//kts
plugins {
    id("io.github.leavesczy.trace")
}
clickTrace {
    view {
        onClickClass = "x"
        onClickMethodName = "x"
        uncheckViewOnClickAnnotation = "x"
        include = listOf()
        exclude = listOf()
    }
    compose {
        onClickClass = "x"
        onClickWhiteList = "x"
    }
}
replaceClassTrace {
    originClass = "x"
    targetClass = "x"
    include = listOf()
    exclude = listOf()
}
toastTrace {
    toasterClass = "x"
    showToastMethodName = "x"
}

三、ClickTrace

ClickTrace 用于完成使用双击防抖功用,一起支撑 Android 原生的 View 体系以及现在盛行的 Jetpack Compose

ClickTrace 完成使用双击防抖功用的本质,就是为项目中一切运用了 View.OnClickListener(View 体系)和 Modifier.clickable、Modifier.combinedClickable(Jetpack Compose 体系)的回调办法中都刺进一段逻辑代码,该段代码会计算前后两次点击事情的时刻距离,假如判断到时刻距离小于某个阈值的话就直接 return,不然就让其持续履行

1、View

想要完成 Android View 体系下的双击防抖功用,开发者总共需求设置两个必填参数和三个可选参数

clickTrace {
    //必填参数
    view.onClickClass = "x"
    view.onClickMethodName = "x"
    //可选参数
    view.uncheckViewOnClickAnnotation = "x"
    view.include = []
    view.exclude = []
}

View 体系的双击防抖功用对应的伪代码如下所示。开发者需求在自己的项目中供给一个办法,用于接受 ClickTrace 转发的一切 View 点击事情。ClickTrace 就担任将开发者供给的 ViewClickMonitor.isEnabled(View) 办法刺进到 View.OnClickListener 的回调函数中,由办法返回值来决议是否要履行本次点击事情

//插桩前
view.setOnClickListener(object : View.OnClickListener {
    override fun onClick(view: View) {
        //TODO
    }
})
//插桩后
view.setOnClickListener(object : View.OnClickListener {
    override fun onClick(view: View) {
        if (!ViewClickMonitor.isEnabled(view)){
            return
        }
        //TODO
    }
})

ViewClickMonitor 的包名、类名、办法名均能够随意命名,ClickTrace 仅要求其包括一个静态办法,办法签名和 isEnabled 保持共同即可,返回值为 true 即代表允许履行本次点击事情

object ViewClickMonitor {
    @JvmStatic
    fun isEnabled(view: View): Boolean {
        val isEnabled: Boolean
        //TODO
        return isEnabled
    }
}

例如,开发者能够照着以下代码来完成 ViewClickMonitor,将每次点击事情的最小时刻距离设为五百毫秒

package github.leavesczy.trace
object ViewClickMonitor {
    private const val MIN_DURATION = 500L
    private var lastClickTime = 0L
    private var clickIndex = 0
    @JvmStatic
    fun isEnabled(view: View): Boolean {
        clickIndex++
        val currentTime = SystemClock.elapsedRealtime()
        val isEnabled = currentTime - lastClickTime > MIN_DURATION
        if (isEnabled) {
            lastClickTime = currentTime
        }
        log("onClick $clickIndex , isEnabled : $isEnabled")
        return isEnabled
    }
    private fun log(log: String) {
        Log.e(javaClass.simpleName, log)
    }
}

然后将 ViewClickMonitor 的类名和对应的办法名传给 ClickTrace 即可

clickTrace {
    view.onClickClass = "github.leavesczy.trace.ViewClickMonitor"
    view.onClickMethodName = "isEnabled"
}

在默认情况下,ClickTrace 会对整个项目中的一切 onClick 事情均进行阻拦检测。假如想过滤特定的点击事情,或许是想过滤特定类或许是特定包名,能够经过 ClickTrace 的别的三个可选参数来完成

clickTrace {
    //过滤包括特定注解的 onClick 事情
    view.uncheckViewOnClickAnnotation = "x"
    //仅对特定类或许特定包名中的 onClick 事情进行阻拦检测
    view.include = ["x"]
    //过滤特定类或许特定包名中的 onClick 事情
    view.exclude = ["x"]
}

例如,开发者能够自己声明一个 UncheckViewOnClick 注解

package github.leavesczy.trace
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class UncheckViewOnClick

将该注解的全途径传给 uncheckViewOnClickAnnotation 后,以下点击事情就会被过滤

findViewById<View>(R.id.tvObjectUnCheck).setOnClickListener(
    object : View.OnClickListener {
        @UncheckViewOnClick
        override fun onClick(view: View) {
            onClickView()
        }
    })

includeexclude 两个参数则用于以类名或包名为单位,共同控制 ClickTrace 的收效规模

  • include 用于设定 ClickTrace 的收效规模。参数值在为空的情况下代表着对一切一切模块均收效,传值后则只对该参数值代表的模块收效
  • exclude 用于设定 ClickTrace 的扫除规模。用于在 include 限定的规模内再扫除特定模块

includeexclude 均经过正则表达式来进行传值,ClickTrace 每当遍历到一个类时,均会拿其类名和 includeexclude 一起进行匹配,均匹配经过后才会对该类进行双击防抖

例如,以下参数就表明:

  • 包括 UncheckViewOnClick 注解的 onClick 回调不会进行双击防抖
  • 仅在 github.leavesczy.trace.xxx 包名下的类会进行双击防抖,但 github.leavesczy.trace.mylibrary.xxx 包名下的类在外
clickTrace {
    view.uncheckViewOnClickAnnotation = "github.leavesczy.trace.UncheckViewOnClick"
    view.include = ["^github\\.leavesczy\\.trace.*"]
    view.exclude = ["^github\\.leavesczy\\.trace\\.mylibrary.*"]
}

2、Jetpack Compose

想要完成 Jetpack Compose 的双击防抖功用,开发者总共需求设置一个必填参数和一个可选参数

clickTrace {
	//必填参数
	compose.onClickClass = "x"
	//可选参数
	compose.onClickWhiteList = "x"
}

和 View 体系一样,开发者也需求在自己项目中声明一个契合以下签名的类,ComposeOnClick 的包名和类名均能够随意命名,将该类的全途径作为参数值传递给 onClickClass 即可

class ComposeOnClick(private val onClick: () -> Unit) : Function0<Unit> {
    override fun invoke() {
        //TODO
    }
}

例如,开发者能够照着以下代码来完成 ComposeOnClick

package github.leavesczy.trace
class ComposeOnClick(private val onClick: () -> Unit) : Function0<Unit> {
    companion object {
        private const val MIN_DURATION = 500L
        private var lastClickTime = 0L
    }
    override fun invoke() {
        val currentTime = SystemClock.elapsedRealtime()
        val isEnabled = currentTime - lastClickTime > MIN_DURATION
        log("onClick isEnabled : $isEnabled")
        if (isEnabled) {
            lastClickTime = currentTime
            onClick()
        }
    }
    private fun log(log: String) {
        Log.e(
            javaClass.simpleName,
            "${System.identityHashCode(this)} ${System.identityHashCode(onClick)} $log"
        )
    }
}

别的,onClickWhiteList 即点击事情的白名单,对于某些不期望履行双击防抖的 Modifier.clickableModifier.combinedClickable 办法,经过将其 onClickLabel 设置为 onClickWhiteList 的特点值,就不会进行双击防抖

例如,以下参数就表明:Modifier.clickableModifier.combinedClickable 办法触发的点击事情均会被移交给 ComposeOnClick 处理,onClickLabel 特点值为 notCheck 的点击事情在外

clickTrace {
	compose.onClickClass = "github.leavesczy.trace.ComposeOnClick"
	compose.onClickWhiteList = "notCheck"
}

四、ReplaceClassTrace

ReplaceClassTrace 用于替换项目中类的承继关系。也就是说,ReplaceClassTrace 会将项目中每一个 originClass 的直接子类,均将其改为直接承继于 targetClass。此外,ReplaceClassTrace 还包括 includeexclude 两个可选参数,其效果和 ClickTrace 中的同名参数共同

replaceClassTrace {
    //必填参数
    originClass = "x"
    targetClass = "x"
    //可选参数
    include = []
    exclude = []
}

这个功用有什么意义呢?

以文章开头所说的 监控大图加载 功用作为例子

假定现在要来检测项目中的一切 ImageView 加载的图片尺寸是否过大,此时我们就能够自定义完成一个 ImageView 的子类 MonitorImageView,在其中完成好大图检测的功用,然后再经过 ReplaceClassTrace 将一切直接承继于 ImageView 的子类均改为直接承继于 MonitorImageView,然后使得大图检测的功用对整个项目均能收效,而且还不用手动修改现有代码

例如,以下参数就表明:将项目中一切直接承继于 ImageView 的子类,均改为直接承继于 MonitorImageView,但类名为 IgnoreImageView 的子类在外

replaceClassTrace {
    originClass = "android.widget.ImageView"
    targetClass = "github.leavesczy.trace.MonitorImageView"
    include = []
    exclude = [".*\\.IgnoreImageView\$"]
}

五、ToastTrace

ToastTrace 用于靠拢项目中一切调用体系 Toast 的显现操作,可用于解决在 Android 7.1 体系中 Toast 因为 WindowToken 失效然后导致使用溃散的问题

开发者总共需求设置两个必填参数

toastTrace {
    toasterClass = "x"
    showToastMethodName = "x"
}

开发者需求在自己的项目中供给一个办法,用于接受 ToastTrace 转发的一切 Toast 显现操作。ToastTrace 就担任将项目中一切调用了 toast.show() 的操作都靠拢到开发者指定的办法下,开发者能够在该办法内对 Android 7.1 中 Toast 的体系 bug 进行修正

例如,开发者能够像如下代码一样来接受 toast.show() 操作,在 Android 7.1 体系版别上捕获体系抛出的反常。Toaster 的包名、类名、办法名均能够随意命名,ToastTrace 仅要求其包括一个静态办法,办法签名契合 showToast 的规矩即可

package github.leavesczy.trace
object Toaster {
    @JvmStatic
    fun showToast(toast: Toast) {
        hookToastIfNeed(toast)
        toast.show()
    }
    @SuppressLint("DiscouragedPrivateApi")
    private fun hookToastIfNeed(toast: Toast) {
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
            try {
                val cToast = Toast::class.java
                val fTn = cToast.getDeclaredField("mTN")
                fTn.isAccessible = true
                val oTn = fTn.get(toast)
                val cTn = oTn.javaClass
                val fHandle = cTn.getDeclaredField("mHandler")
                fHandle.isAccessible = true
                fHandle.set(oTn, ProxyHandler(fHandle.get(oTn) as Handler))
            } catch (e: Throwable) {
                e.printStackTrace()
            }
        }
    }
    private class ProxyHandler(private val mHandler: Handler) : Handler(mHandler.looper) {
        override fun handleMessage(msg: Message) {
            try {
                mHandler.handleMessage(msg)
            } catch (e: Throwable) {
                e.printStackTrace()
            }
        }
    }
}

然后,将 Toaster 对应的全途径和办法名传给 ToastTrace 即可

toastTrace {
    toasterClass = "github.leavesczy.trace.Toaster"
    showToastMethodName = "showToast"
}

六、结尾

Trace 现阶段就包括以上三个功用点,后续看时刻规划我再持续更新

别的还有一些注意事项

  • Trace 在 AGP 7.0+ 和 8.0+ 均已测试经过,更低版别的 AGP 则没有再特意进行实验
  • Trace 现在处于刚起步阶段,或许还会存在一些 bug,但因为 Trace 是以 Gradle Plugin 的形式引进到项目中的,引进成本和移除成本都很低,有需求的话还是值得一试的
  • Trace Plugin 也保管到了 GradlePluginPortal,能够在 Trace 检查

期望对你有所协助 ~

Trace 的 Github 地址:Trace