深坑,谨慎用动态代理对象作为监听器

大家好,三月已到,正是退税、赏桃花、看掘金的好日子,这次给大家分享下使用动态代理对象作为监听器注入中埋藏的隐患,发生在一个业务场景中,且听我一一道来。

前情回顾

假设当前有一个需求,我们需要动态监听一个人一天内执行的一些动作,作为上层应用,咱们肯定是不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就会面临三个困境了:

  1. 存在应用层忘记移除的监听器仍保存在SDK监听集合中,浪费资源,并且存在内存泄漏的风险,毕竟注入监听器是个匿名内部类,会持有外部类的使用;

  2. 集合大小不断的增加,可能集合内容会发生多次内存拷贝进行扩容,影响性能;等达到一定大小后,还会拖慢对集合的增删改查效率等;

  3. 不方便对应用侧注入的监听器进行统一管理,比如做通用埋点等

这个时候比较负责任的应用层避免上述问题的发生,就采取了一个方法:

在应用侧增加一层监听器集合管理,并且保证应用只会向该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中实现人动作分发给应用各个地 方注册的应用侧监听器,看起来使用的相当方便,但是坑就在里面埋下了

以上就是我们当前的业务逻辑了,大家仔细看看里面是不是有什么不对劲的地方呢!比如:

  1. 通过动态代理生成的监听器对象,调用其toString()方法会不会返回null?
  2. 通过动态代理生成的监听器对象,真的能从SDK反注册成功吗?
  3. 会不会发生崩溃等等....

下面就给大家揭晓谜底了哈。

书接上回,开坑

上面的代码写完了,那我们写个测试,看看运行起来会不会有问题:

测试代码:

运行下,输出结果直接发生了空指针异常崩溃:

可以看到是在从应用侧到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实现反注册就非常不现实了。

脱坑的几种方式

  1. 不用动态代理,这是最直接最简单最有效的方法,不搞那些花里胡哨的东西,模板代码直接写起来;
  2. 动态代理的InvocationHandler特殊处理toString()equals()方法,保证和其他类的行为一致,比如特殊处理下equals(),保证不为null且还能正常从SDK反注册:

输出信息:

可以看到,没有发生equals空指针崩溃,也能正常从集合中移除。

大结局

本篇文章给大家分享了使用动态代理作为监听器时,需要注意的地方,涉及到动态代理所有的方法调用都会走到InvocationHandler中,具体的返回值依照具体逻辑实现,而不是大家想当然的equals返回true、toString()返回不会为null等等,一旦处理不当,就会给程序埋下隐患。

祝大家能写出更加高质量的代码,感谢阅读!

历史文章

FullyDrawnReporter---一个官方冷启动耗时统计小工具

一文洞彻:Application为啥不能作为Dialog的context?

子线程刷UI->Barrier屏障->主线程装死->应用GG?太难了

相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh3 小时前
uiautomator案例
android
工业甲酰苯胺4 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
2401_857610034 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
少说多做3434 小时前
Android 不同情况下使用 runOnUiThread
android·java
凌冰_4 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞4 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货4 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng5 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip