大家好,三月已到,正是退税、赏桃花、看掘金的好日子,这次给大家分享下使用动态代理对象作为监听器注入中埋藏的隐患,发生在一个业务场景中,且听我一一道来。
前情回顾
假设当前有一个需求,我们需要动态监听一个人一天内执行的一些动作,作为上层应用,咱们肯定是不care具体怎么实现人动作的监测,只需要找个能干活的三方的SDK,该SDK暴漏监听的方法给上层应用,上层应用只用注册个监听器给SDK就行,当人动作发生的时候,就由SDK通过传入的监听器来通知上层应用。
SDK中定义人动作的接口如下:
kotlin
interface PersonAction {
/**
* 起床
*/
fun getUp()
/**
* 吃饭
*/
fun eat()
/**
* 上厕所
*/
fun goToilet()
/**
* 打球
*/
fun playBall()
/**
* 做家庭作业
*/
fun doHomeWork()
/**
* 购物
*/
fun buyGoods()
/**
* 开车
*/
fun driveCar()
/**
* 睡觉
*/
fun sleep()
}
然后SDK提供一个注入监听器的方法,并且管理上层应用注册的监听器集合,比如增删改查,以及在特定时机通知监听器人动作的执行:
kotlin
object PersonActionManager {
private val mActions: MutableList<PersonAction> = mutableListOf()
fun registerPersonAction(personAction: PersonAction?) {
if (personAction == null || mActions.contains(personAction)) {
return
}
mActions.add(personAction)
}
fun removePersonAction(personAction: PersonAction?) {
if (personAction == null || !mActions.contains(personAction)) {
return
}
mActions.remove(personAction)
}
/**
* 通知人起床了
*/
fun dispatchPersonActionGetUp() {
if (mActions.isEmpty()) {
return
}
mActions.forEach {
it.getUp()
}
}
/**
* 通知人吃饭了
*/
fun dispatchPersonActionEat() {
if (mActions.isEmpty()) {
return
}
mActions.forEach {
it.eat()
}
}
//此处省略人其他动作执行的通知
}
如果咱们上层应用要监听人动作的执行,只需要调用SDK的方法注入一个监听器即可:
kotlin
class App {
/**
* 上层应用借助SDK监听人动作的执行
*/
fun listener() {
PersonActionManager.registerPersonAction(object : PersonAction {
override fun getUp() {
}
override fun eat() {
}
override fun goToilet() {
}
override fun playBall() {
}
override fun doHomeWork() {
}
override fun buyGoods() {
}
override fun driveCar() {
}
override fun sleep() {
}
})
}
}
可以看到,这样写有一个弊端:如果我应用中有多个地方都需要监听人动作的执行,那就会往SDK注入多个监听器,由于这个SDK是面向很多应用的,每个应用都这么搞,那SDK中监听器队列集合就会无限膨胀了,毕竟不是每个应用都会及时反注册监听器或者干脆就不反注册,随着时间长了,该SDK就会面临三个困境了:
-
存在应用层忘记移除的监听器仍保存在SDK监听集合中,浪费资源,并且存在内存泄漏的风险,毕竟注入监听器是个匿名内部类,会持有外部类的使用;
-
集合大小不断的增加,可能集合内容会发生多次内存拷贝进行扩容,影响性能;等达到一定大小后,还会拖慢对集合的增删改查效率等;
-
不方便对应用侧注入的监听器进行统一管理,比如做通用埋点等
这个时候比较负责任的应用层避免上述问题的发生,就采取了一个方法:
在应用侧增加一层监听器集合管理,并且保证应用只会向该SDK注入一个监听器,当应用某些地方需要监听人动作执行时,将这个监听器注入到应用侧的监听器集合,然后通过前面只注入SDK一个的监听器来通知整个应用侧的监听器集合,具体请看下面代码:
kotlin
/**
* 应用侧人动作监听集合管理类
*/
object AppPersonActionManager {
private val mAppActions: MutableList<PersonAction> = mutableListOf()
private var mProxy: PersonAction? = null
fun registerPersonAction(personAction: PersonAction?) {
if (personAction == null || mAppActions.contains(personAction)) {
return
}
mAppActions.add(personAction)
//保证只向SDK注入一次监听器
if (mAppActions.size == 1) {
PersonActionManager.registerPersonAction(createPersonActionProxy().apply {
mProxy = this
})
}
}
fun removePersonAction(personAction: PersonAction?) {
if (personAction == null || !mAppActions.contains(personAction)) {
return
}
mAppActions.remove(personAction)
if (mAppActions.size == 0) {
PersonActionManager.removePersonAction(mProxy)
}
}
private fun createPersonActionProxy(): PersonAction {
val proxy =
Proxy.newProxyInstance(AppPersonActionManager::class.java.classLoader, arrayOf(PersonAction::class.java),
object : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
var result: Any? = null
mAppActions.forEach {
result = method?.invoke(it, args)
}
println("Proxy#Method: ${method?.name}, result = $result")
return result
}
}) as PersonAction
return proxy
}
}
上面的AppPersonActionManager
就是应用侧核心的管理类,下面对每个方法进行解释:
registerPersonAction()
应用侧暴漏的注入监听器的方法,且当集合
mAppActions
中的元素等于1时,才真正向SDK注入 一个监听器,并且为了避免重写无用的监听方法,这个监听器是通过动态代理生成的;在不考虑 多线程操作的情况下,保证了只会向SDK注入一个监听器,避免注入很多个带来的成本风险;
removePersonAction()
应用侧暴漏的移除监听器的方法,且当集合
mAppActions
中的元素等于0时,就会真正从SDK移 除之前通过动态代理生成的监听器;
createPersonActionProxy()
通过动态代理生成的监听器,并在该代理
InvocationHandler
中实现人动作分发给应用各个地 方注册的应用侧监听器,看起来使用的相当方便,但是坑就在里面埋下了;
以上就是我们当前的业务逻辑了,大家仔细看看里面是不是有什么不对劲的地方呢!比如:
- 通过动态代理生成的监听器对象,调用其toString()方法会不会返回null?
- 通过动态代理生成的监听器对象,真的能从SDK反注册成功吗?
- 会不会发生崩溃等等....
下面就给大家揭晓谜底了哈。
书接上回,开坑
上面的代码写完了,那我们写个测试,看看运行起来会不会有问题:
测试代码:
运行下,输出结果直接发生了空指针异常崩溃:
可以看到是在从应用侧到SDK侧PersonActionManager#removePersonAction
间接调用到了ArrayList#indexOfRange
方法中崩溃了,我们看下源码:
纳尼,o是咱们传入的动态代理创建的监听器对象,怎么调用下equals方法崩溃了,并且源码中也对o进行了判断处理。并且进一步分析,好像是咱们的equals方法直接返回了null?卧槽,equals方法还能返回null!!
这时候就得想到咱们的o是个动态代理对象,而动态代理对象调用的方法都会被分发到InvocationHandler
对象中:
也就是说,equals方法的调用逻辑是上面红框表示的内容。回顾下什么情况下会从SDK移除监听器:只有咱们应用侧监听器集合为0,即mAppActions
大小为0时,才会调用PersonActionManager#removePersonAction
从SDK移除注入的唯一监听器。
当SDK侧触发代理对象equals方法调用时, mAppActions
大小已经为0了,此时会直接返回result为null,所以就发生了上面的问题现象。
所以,在这种场景下,equals()
方法会返回null,toString()
方法也会返回为null,和我们一贯的思维存在出入,而且这种情况下,从SDK实现反注册就非常不现实了。
脱坑的几种方式
- 不用动态代理,这是最直接最简单最有效的方法,不搞那些花里胡哨的东西,模板代码直接写起来;
- 动态代理的
InvocationHandler
特殊处理toString()
、equals()
方法,保证和其他类的行为一致,比如特殊处理下equals()
,保证不为null且还能正常从SDK反注册:
输出信息:
可以看到,没有发生equals空指针崩溃,也能正常从集合中移除。
大结局
本篇文章给大家分享了使用动态代理作为监听器时,需要注意的地方,涉及到动态代理所有的方法调用都会走到InvocationHandler
中,具体的返回值依照具体逻辑实现,而不是大家想当然的equals返回true、toString()返回不会为null等等,一旦处理不当,就会给程序埋下隐患。
祝大家能写出更加高质量的代码,感谢阅读!
历史文章
FullyDrawnReporter---一个官方冷启动耗时统计小工具