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

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

前情回顾

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

相关推荐
Ciderw13 分钟前
MySQL为什么使用B+树?B+树和B树的区别
c++·后端·b树·mysql·面试·golang·b+树
计算机-秋大田16 分钟前
基于微信小程序的汽车保养系统设计与实现(LW+源码+讲解)
spring boot·后端·微信小程序·小程序·课程设计
齐雅彤20 分钟前
Bash语言的并发编程
开发语言·后端·golang
编程、小哥哥33 分钟前
python操作mysql
android·python
峰子201235 分钟前
B站评论系统的多级存储架构
开发语言·数据库·分布式·后端·golang·tidb
Couvrir洪荒猛兽1 小时前
Android实训十 数据存储和访问
android
秋淮安1 小时前
后端开发Web
后端·web
马剑威(威哥爱编程)3 小时前
2025春招 SpringCloud 面试题汇总
后端·spring·spring cloud
闲暇部落3 小时前
kotlin内联函数——let,run,apply,also,with的区别
kotlin·内联函数
Quantum&Coder3 小时前
Objective-C语言的计算机基础
开发语言·后端·golang