Skip to content
leavesCZY edited this page Oct 12, 2024 · 15 revisions

一、Track

Track 是一个适用于 Android 的字节码插桩库

包含以下几个功能点:

  • View Click 双击防抖
  • Jetpack Compose Click 双击防抖
  • 收拢应用内所有的 Toast.show 方法,可用于解决在 Android 7.1 系统上 Toast 由于 WindowToken 失效从而导致应用崩溃的问题
  • 收拢应用内所有的 Executors 线程池方法,可用于实现线程整治,统一应用内所有的线程池实例
  • 修改指定类的继承关系,将指定父类替换为另一个类
  • 修改指定字段的调用链,将指定字段替换为另一个字段
  • 修改指定方法的调用链,将指定方法替换为另一个方法

相关联的文章:

引入插件

plugins {
    id("io.github.leavesczy.track").version("latestVersion").apply(false)
}

在项目主模块中应用插件,需要哪些功能点就为其设置对应的参数

plugins {
    id("io.github.leavesczy.track")
}

viewClickTrack {
    isEnabled = true
    include = setOf()
    exclude = setOf()
    onClickClass = ""
    onClickMethodName = ""
    uncheckViewOnClickAnnotation = ""
}

composeClickTrack {
    isEnabled = true
    onClickClass = ""
    onClickWhiteList = ""
}

toastTrack {
    isEnabled = true
    include = setOf()
    exclude = setOf()
    proxyOwner = ""
}

optimizedThreadTrack {
    isEnabled = true
    include = setOf()
    exclude = setOf()
    proxyOwner = ""
    methods = setOf()
}

replaceClassTrack {
    isEnabled = true
    include = setOf()
    exclude = setOf()
    originClass = ""
    targetClass = ""
}

replaceFieldTrack {
    isEnabled = true
    include = setOf()
    exclude = setOf()
    instructions = setOf()
}

replaceMethodTrack {
    isEnabled = true
    include = setOf()
    exclude = setOf()
    instructions = setOf()
}

Track 的每个功能点分别对应一个 xxxTrack,除了 composeClickTrack 外,其它均包含以下三个基础参数

  • isEnabled。设置是否启用插桩功能
  • include。设置 xxxTrack 的生效范围,参数值在为空的情况下代表着对整个项目包括三方依赖库均生效,传值后则只对参数值代表的模块生效
  • exclude。设置 xxxTrack 的排除范围,用于在 include 限定的范围内再排除特定模块

includeexclude 均通过正则表达式来进行传值,xxxTrack 每当遍历到一个类时,均会拿其类名和 include 以及 exclude 进行匹配,均匹配通过后才会对该类进行插桩

例如,以下参数就表示:对 github.leavesczy.track.xxx 包名下的类执行插桩,但 github.leavesczy.track.mylibrary.xxx 包名下的类除外

xxxTrack {
    include = setOf("^github\\.leavesczy\\.track.*")
    exclude = setOf("^github\\.leavesczy\\.track\\.mylibrary.*")
}

二、viewClickTrack

viewClickTrack 用于为 Android 原生的 View 体系实现双击防抖功能

包含两个必需参数和一个可选参数

viewClickTrack {
    //必需参数
    onClickClass = ""
    onClickMethodName = ""
    //可选参数
    uncheckViewOnClickAnnotation = ""
}

viewClickTrack 实现应用双击防抖功能的本质,就是为项目中所有 View.OnClickListener 的回调方法都插入一段逻辑代码,该段代码会计算前后两次点击事件的时间间隔,如果判断到时间间隔小于某个阈值的话就直接 return,否则就让其继续执行

伪代码如下所示

//插桩前
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
    }
})

开发者需要在自己的项目中提供一个方法,用于承接 viewClickTrack 转发的所有 View 点击事件。viewClickTrack 就负责将开发者提供的 ViewClickMonitor.isEnabled(View) 方法插入到 View.OnClickListener 的回调函数中,由方法返回值来决定是否要执行本次点击事件

ViewClickMonitor 的包名、类名、方法名均可以随意命名,viewClickTrack 仅要求其包含一个静态方法,方法签名和 isEnabled 保持一致即可,返回值为 true 即代表允许执行本次点击事件

object ViewClickMonitor {

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

}

例如,开发者可以参照以下代码来实现 ViewClickMonitor,将每次点击事件的最小时间间隔设为五百毫秒。开发者可以根据自己的需要来进行自定义,不必局限于以下实现

package github.leavesczy.track.click.view

internal object ViewClickMonitor {

    private var lastClickTime = 0L

    @JvmStatic
    fun isEnabled(view: View): Boolean {
        val currentTime = SystemClock.elapsedRealtime()
        val isEnabled = currentTime - lastClickTime > 500L
        if (isEnabled) {
            lastClickTime = currentTime
        }
        return isEnabled
    }

}

然后将 ViewClickMonitor 的类名和对应的方法名传给 viewClickTrack 即可

viewClickTrack {
    onClickClass = "github.leavesczy.track.click.view.ViewClickMonitor"
    onClickMethodName = "isEnabled"
}

在默认情况下,viewClickTrack 会对整个项目中的所有 onClick 事件均进行拦截检测。如果想过滤特定的点击事件,除了可以通过 includeexclude 过滤特定包名和特定类外,还可以通过 viewClickTrackuncheckViewOnClickAnnotation 参数来实现

viewClickTrack {
    //过滤包含特定注解的 onClick 事件
    uncheckViewOnClickAnnotation = ""
    //仅对特定包名和特定类中的 onClick 事件进行拦截检测
    include = setOf()
    //过滤特定包名和特定类中的 onClick 事件
    exclude = setOf()
}

例如,开发者可以自己声明一个 UncheckViewOnClick 注解

package github.leavesczy.track.click.view

@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()
        }
    })

例如,以下参数就表示:

  • 包含 UncheckViewOnClick 注解的 onClick 回调不会进行双击防抖
  • 仅在 github.leavesczy.track.xxx 包名下的类会进行双击防抖,但 github.leavesczy.track.mylibrary.xxx 包名下的类除外
viewClickTrack {
    uncheckViewOnClickAnnotation = "github.leavesczy.track.click.view.UncheckViewOnClick"
    include = setOf("^github\\.leavesczy\\.track.*")
    exclude = setOf("^github\\.leavesczy\\.track\\.mylibrary.*")
}

三、composeClickTrack

composeClickTrack 用于为 Jetpack Compose 实现双击防抖功能

包含一个必需参数和一个可选参数

composeClickTrack {
    //必需参数
    onClickClass = ""
    //可选参数
    onClickWhiteList = ""
}

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

class ComposeOnClick(private val onClick: () -> Unit) : Function0<Unit> {

    override fun invoke() {
        //TODO
    }
    
}

例如,开发者可以参照以下代码来实现 ComposeOnClick

package github.leavesczy.track.click.compose

class ComposeOnClick(private val onClick: () -> Unit) : Function0<Unit> {

    companion object {

        private var lastClickTime = 0L

    }

    override fun invoke() {
        val currentTime = SystemClock.elapsedRealtime()
        val isEnabled = currentTime - lastClickTime > 500
        if (isEnabled) {
            lastClickTime = currentTime
            onClick()
        }
    }

}

另外,onClickWhiteList 即点击事件的白名单,对于某些不希望执行双击防抖的 Modifier.clickableModifier.combinedClickable 方法,通过将其 onClickLabel 设置为 onClickWhiteList 的属性值后就不会进行双击防抖

例如,以下参数就表示:Modifier.clickableModifier.combinedClickable 方法触发的点击事件均会被移交给 ComposeOnClick 处理,但 onClickLabel 属性值为 notCheck 的点击事件除外

composeClickTrack {
    onClickClass = "github.leavesczy.track.click.compose.ComposeOnClick"
    onClickWhiteList = "notCheck"
}

四、toastTrack

toastTrack 用于收拢应用内所有的 Toast.show 方法,可用于解决在 Android 7.1 系统上 Toast 由于 WindowToken 失效从而导致应用崩溃的问题

包含一个必需参数

toastTrack {
    proxyOwner = ""
}

开发者需要在自己的项目中提供一个方法,用于承接 toastTrack 转发的所有 Toast 显示操作。toastTrack 就负责将项目中所有的 toast.show() 操作都聚拢到开发者指定的方法下,开发者可以在该方法内对 Android 7.1 中 Toast 的系统 bug 进行修复

例如,开发者可以像以下代码一样来承接 toast.show() 操作,在 Android 7.1 系统版本上捕获系统抛出的异常。ToastProxy 的包名和类名均可以随意命名,但需包含一个方法签名符合 show 规则的静态方法

package github.leavesczy.track.toast

object ToastProxy {

    @JvmStatic
    fun show(toast: Toast) {
        hookToastIfNeed(toast)
        toast.setText("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()
            }
        }

    }

}

然后,将 ToastProxy 类路径传给 toastTrack 即可

toastTrack {
    proxyOwner = "github.leavesczy.track.toast.ToastProxy"
}

五、optimizedThreadTrack

optimizedThreadTrack 用于收拢应用内所有的 Executors 线程池方法,可用于实现线程整治,统一应用内所有的线程池实例

包含两个必需参数

optimizedThreadTrack {
    proxyOwner = ""
    methods = setOf()
}

这个功能有什么意义呢?

我们知道,JDK 中的 java.util.concurrent.Executors 类内包含多个创建线程池 ThreadPoolExecutor 的方法,例如 newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool 等。除了我们的自有代码会去通过 Executors 创建线程池外,很多引用的三方库也会使用到。optimizedThreadTrack 就用于将项目内的这些方法均归拢到开发者指定的类中,开发者可在此类中统一线程名、线程优先级等属性,甚至也可以直接返回一个单例模式的线程池实例,从而缩减应用内的线程数量

例如,开发者可以像以下代码一样来承接 Executors 的线程池方法,在里面自定义线程池

package github.leavesczy.track.thread

internal object OptimizedExecutors {

    private const val DEFAULT_THREAD_KEEP_ALIVE_TIME = 3000L

    @JvmStatic
    @JvmOverloads
    fun newSingleThreadExecutor(threadFactory: ThreadFactory? = null): ExecutorService {
        return getOptimizedExecutorService(
            corePoolSize = 1,
            maximumPoolSize = 1,
            keepAliveTime = 0L,
            unit = TimeUnit.MILLISECONDS,
            workQueue = LinkedBlockingQueue(),
            threadFactory = threadFactory
        )
    }

    @JvmStatic
    @JvmOverloads
    fun newCachedThreadPool(threadFactory: ThreadFactory? = null): ExecutorService {
        return getOptimizedExecutorService(
            corePoolSize = 0,
            maximumPoolSize = Integer.MAX_VALUE,
            keepAliveTime = 60L,
            unit = TimeUnit.SECONDS,
            workQueue = SynchronousQueue(),
            threadFactory = threadFactory
        )
    }

    @JvmStatic
    @JvmOverloads
    fun newFixedThreadPool(
        corePoolSize: Int,
        threadFactory: ThreadFactory? = null
    ): ExecutorService {
        return getOptimizedExecutorService(
            corePoolSize = corePoolSize,
            maximumPoolSize = corePoolSize,
            keepAliveTime = 0L,
            unit = TimeUnit.MILLISECONDS,
            workQueue = LinkedBlockingQueue(),
            threadFactory = threadFactory
        )
    }

    @JvmStatic
    @JvmOverloads
    fun newScheduledThreadPool(
        corePoolSize: Int,
        threadFactory: ThreadFactory? = null
    ): ScheduledExecutorService {
        return getOptimizedScheduledExecutorService(
            corePoolSize = corePoolSize,
            threadFactory = threadFactory
        )
    }

    @JvmStatic
    @JvmOverloads
    fun newSingleThreadScheduledExecutor(threadFactory: ThreadFactory? = null): ScheduledExecutorService {
        return newScheduledThreadPool(
            corePoolSize = 1,
            threadFactory = threadFactory
        )
    }

    private fun getOptimizedExecutorService(
        corePoolSize: Int,
        maximumPoolSize: Int,
        keepAliveTime: Long,
        unit: TimeUnit,
        workQueue: BlockingQueue<Runnable>,
        threadFactory: ThreadFactory?
    ): ExecutorService {
        val executor = ThreadPoolExecutor(
            corePoolSize, maximumPoolSize,
            keepAliveTime, unit,
            workQueue,
            NamedThreadFactory(threadFactory)
        )
        executor.setKeepAliveTime(DEFAULT_THREAD_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS)
        executor.allowCoreThreadTimeOut(true)
        return executor
    }

    private fun getOptimizedScheduledExecutorService(
        corePoolSize: Int,
        threadFactory: ThreadFactory?
    ): ScheduledExecutorService {
        val executor = ScheduledThreadPoolExecutor(
            corePoolSize,
            NamedThreadFactory(threadFactory)
        )
        executor.setKeepAliveTime(DEFAULT_THREAD_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS)
        executor.allowCoreThreadTimeOut(true)
        return executor
    }

    private class NamedThreadFactory(
        private val threadFactory: ThreadFactory?
    ) : ThreadFactory {

        private val threadId = AtomicInteger(0)

        override fun newThread(runnable: Runnable): Thread {
            val thread = threadFactory?.newThread(runnable) ?: Thread(runnable)
            val threadName = buildString {
                append("[threadId : ${threadId.getAndIncrement()}]")
                append(" - ")
                append("[threadName : ${thread.name}]")
            }
            thread.name = threadName
            if (thread.isDaemon) {
                thread.isDaemon = false
            }
            if (thread.priority != Thread.NORM_PRIORITY) {
                thread.priority = Thread.NORM_PRIORITY
            }
            return thread
        }

    }

}

然后,将 OptimizedExecutors 的类路径,以及想要转接的创建线程池的方法名传给 optimizedThreadTrack 即可

optimizedThreadTrack {
    proxyOwner = "github.leavesczy.track.thread.OptimizedExecutors"
    methods = setOf(
        "newSingleThreadExecutor",
        "newCachedThreadPool",
        "newFixedThreadPool",
        "newScheduledThreadPool",
        "newSingleThreadScheduledExecutor"
    )
}

六、replaceClassTrack

replaceClassTrack 用于修改指定类的继承关系,将指定父类替换为另一个类

包含两个必需参数

replaceClassTrack {
    originClass = ""
    targetClass = ""
}

也就是说,replaceClassTrack 会将项目中每一个 originClass 的直接子类,均将其改为直接继承于 targetClass

这个功能有什么意义呢?

举个例子。假设现在要来检测项目中的所有 ImageView 加载的图片尺寸是否过大,此时我们就可以自定义实现一个 ImageView 的子类 MonitorImageView,在其中实现好大图检测的功能,然后再通过 replaceClassTrack 将所有直接继承于 ImageView 的子类均改为直接继承于 MonitorImageView,从而使得大图检测的功能对整个项目均能生效,而不必手动修改现有代码

例如,以下参数就表示:将项目中所有直接继承于 ImageView 的子类,均改为直接继承于 MonitorImageView,但类名为 IgnoreImageView 的子类除外

replaceClassTrack {
    include = setOf()
    exclude = setOf(".*\\.IgnoreImageView\$")
    originClass = "android.widget.ImageView"
    targetClass = "github.leavesczy.track.replace.clazz.MonitorImageView"
}

七、replaceFieldTrack

replaceFieldTrack 用于修改指定字段的调用链,将指定字段替换为另一个字段

包含一个必需参数

replaceFieldTrack {
    instructions = setOf()
}

这个功能有什么意义呢?

举个例子。假设当前项目有个三方依赖库会去调取系统的 android.os.Build 类的 BRAND 字段,为了实现免侵入式的动态修改字段值,此时就可以通过replaceFieldTrack 来实现此目的

开发者可以像以下代码一样来参照实现一个类似的 BRAND 静态字段,用于替换项目中所有获取 android.os.Build 类的 BRAND 字段的指令

package github.leavesczy.track.replace.instruction

internal object SystemFieldProxy {

    @JvmField
    var BRAND = "这是一个假的 BRAND"

}

android.os.Build 类的 BRAND 字段的字节码信息,以及 SystemFieldProxy 类的路径信息传给 replaceFieldTrack 即可。SystemFieldProxy 的包名和类名均可以自定义,但需保证其内部的 BRAND 变量名和其原始字段保持一致

之后项目中所有调取 android.os.Build.BRAND 的操作均会转交由 SystemFieldProxy 实现,开发者可以自由决定要返回什么值

replaceFieldTrack {
    instructions = setOf(
        ReplaceInstruction(
            owner = "android.os.Build",
            name = "BRAND",
            descriptor = "Ljava/lang/String;",
            proxyOwner = "github.leavesczy.track.replace.instruction.SystemFieldProxy"
        )
    )
}

需注意,replaceFieldTrack 仅支持替换静态字段

八、replaceMethodTrack

replaceMethodTrack 用于修改指定方法的调用链,将指定方法替换为另一个方法

包含一个必需参数

replaceMethodTrack {
    instructions = setOf()
}

这个功能有什么意义呢?

举个例子。假设当前项目中有某个三方依赖库会去获取手机的 imei,而此操作就涉及到了隐私合规安全问题,我们往往需要在用户已同意隐私协议后再去执行此类操作,而三方库的执行逻辑我们是很难手动修改的。replaceMethodTrack 就可用于解决此类问题,将原本调用某方法的操作改为调用另外一个自定义方法

例如,开发者可以像以下这样来实现一个自定义类,分别用于承接项目中所有执行以下方法的操作

  • 调用 android.telephony.TelephonyManagerpublic String getDeviceId() 实例方法
  • 调用 android.telephony.TelephonyManagerpublic String getImei(int slotIndex) 实例方法
  • 调用 android.provider.Settings.Securepublic static String getString(ContentResolver resolver, String name) 静态方法

开发者可以根据实际情况,动态判断是否要在 SystemMethodProxy 中返回真实值还是虚假的临时值

package github.leavesczy.track.replace.instruction

internal object SystemMethodProxy {
    
    @JvmStatic
    fun getDeviceId(telephonyManager: TelephonyManager): String {
        TODO()
    }

    @JvmStatic
    fun getImei(telephonyManager: TelephonyManager, slotIndex: Int): String {
        TODO()
    }

    @JvmStatic
    fun getString(resolver: ContentResolver, name: String): String {
        TODO()
    }

}

之后,开发者将想要替换的方法的字节码信息,以及 SystemMethodProxy 的类路径传给 replaceMethodTrack 即可

而为了能够将原始的方法和替换后的方法对应上,就需要开发者保证 SystemMethodProxy 内的 方法名 和替换前保持一致才行。此外,如果要替换的是实例方法的话,调用方将作为参数之一一起传入

replaceMethodTrack {
    val systemMethodProxyOwner = "github.leavesczy.track.replace.instruction.SystemMethodProxy"
    instructions = setOf(
        ReplaceInstruction(
            owner = "android.telephony.TelephonyManager",
            name = "getDeviceId",
            descriptor = "()Ljava/lang/String;",
            proxyOwner = systemMethodProxyOwner
        ),
        ReplaceInstruction(
            owner = "android.telephony.TelephonyManager",
            name = "getImei",
            descriptor = "(I)Ljava/lang/String;",
            proxyOwner = systemMethodProxyOwner
        ),
        ReplaceInstruction(
            owner = "android.provider.Settings\$Secure",
            name = "getString",
            descriptor = "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;",
            proxyOwner = systemMethodProxyOwner
        )
    )
}

实际上,toastTrackoptimizedThreadTrack 均都是通过 replaceMethodTrack 来实现的,Track 仅是对其进行了封装,使其更加容易接入。开发者直接通过 replaceMethodTrack 也可以实现相同的功能。也因此 toastTrackoptimizedThreadTrack 才要求开发者自定义的方法名必须和原始的方法名保持一致,这样 Track 才能将方法一一对应上