-
Notifications
You must be signed in to change notification settings - Fork 4
Home
Track 是一个适用于 Android 的字节码插桩库
包含以下几个功能点:
- View Click 双击防抖
- Jetpack Compose Click 双击防抖
- 收拢应用内所有的 Toast.show 方法,可用于解决在 Android 7.1 系统上 Toast 由于 WindowToken 失效从而导致应用崩溃的问题
- 收拢应用内所有的 Executors 线程池方法,可用于实现线程整治,统一应用内所有的线程池实例
- 修改指定类的继承关系,将指定父类替换为另一个类
- 修改指定字段的调用链,将指定字段替换为另一个字段
- 修改指定方法的调用链,将指定方法替换为另一个方法
相关联的文章:
- Android ASM 字节码插桩:实现双击防抖
- Android ASM 字节码插桩:进行线程整治
- Android ASM 字节码插桩:助力隐私合规
- Android ASM 字节码插桩:监控大图加载
- Android ASM 字节码插桩:从 Lambda 表达式讲起
- Android ASM 字节码插桩:Jetpack Compose 实现双击防抖
- Android ASM 字节码插桩:替换字节码指令
引入插件
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
限定的范围内再排除特定模块
include
和 exclude
均通过正则表达式来进行传值,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
用于为 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
事件均进行拦截检测。如果想过滤特定的点击事件,除了可以通过 include
和 exclude
过滤特定包名和特定类外,还可以通过 viewClickTrack
的 uncheckViewOnClickAnnotation
参数来实现
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
用于为 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.clickable
和 Modifier.combinedClickable
方法,通过将其 onClickLabel
设置为 onClickWhiteList
的属性值后就不会进行双击防抖
例如,以下参数就表示:Modifier.clickable
和 Modifier.combinedClickable
方法触发的点击事件均会被移交给 ComposeOnClick 处理,但 onClickLabel
属性值为 notCheck
的点击事件除外
composeClickTrack {
onClickClass = "github.leavesczy.track.click.compose.ComposeOnClick"
onClickWhiteList = "notCheck"
}
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
用于收拢应用内所有的 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 {
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 {
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 {
instructions = setOf()
}
这个功能有什么意义呢?
举个例子。假设当前项目中有某个三方依赖库会去获取手机的 imei,而此操作就涉及到了隐私合规安全问题,我们往往需要在用户已同意隐私协议后再去执行此类操作,而三方库的执行逻辑我们是很难手动修改的。replaceMethodTrack
就可用于解决此类问题,将原本调用某方法的操作改为调用另外一个自定义方法
例如,开发者可以像以下这样来实现一个自定义类,分别用于承接项目中所有执行以下方法的操作
- 调用
android.telephony.TelephonyManager
的public String getDeviceId()
实例方法 - 调用
android.telephony.TelephonyManager
的public String getImei(int slotIndex)
实例方法 - 调用
android.provider.Settings.Secure
的public 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
)
)
}
实际上,
toastTrack
和optimizedThreadTrack
均都是通过replaceMethodTrack
来实现的,Track 仅是对其进行了封装,使其更加容易接入。开发者直接通过replaceMethodTrack
也可以实现相同的功能。也因此toastTrack
和optimizedThreadTrack
才要求开发者自定义的方法名必须和原始的方法名保持一致,这样 Track 才能将方法一一对应上