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

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

前情回顾

假设当前有一个需求,我们需要动态监听一个人一天内执行的一些动作,作为上层应用,咱们肯定是不care具体怎么实现人动作的监测,只需要找个能干活的三方的SDK,该SDK暴漏监听的方法给上层应用,上层应用只用注册个监听器给SDK就行,当人动作发生的时候,就由SDK通过传入的监听器来通知上层应用。

SDK中定义人动作的接口如下:

interface PersonAction {


    /**
     * 起床
     */
    fun getUp()


    /**
     * 吃饭
     */
    fun eat()


    /**
     * 上厕所
     */
    fun goToilet()


    /**
     * 打球
     */
    fun playBall()


    /**
     * 做家庭作业
     */
    fun doHomeWork()


    /**
     * 购物
     */
    fun buyGoods()


    /**
     * 开车
     */
    fun driveCar()


    /**
     * 睡觉
     */
    fun sleep()
}

然后SDK提供一个注入监听器的方法,并且管理上层应用注册的监听器集合,比如增删改查,以及在特定时机通知监听器人动作的执行:

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的方法注入一个监听器即可:

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一个的监听器来通知整个应用侧的监听器集合,具体请看下面代码:

/**
 * 应用侧人动作监听集合管理类
 */
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. 会不会发生崩溃等等....

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

书接上回,开坑

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

测试代码:

7469da7828c67ca0d040376f689156de.jpeg

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

12f0e6226771b833f8fef36a15f7afbc.jpeg

可以看到是在从应用侧到SDK侧PersonActionManager#removePersonAction间接调用到了ArrayList#indexOfRange方法中崩溃了,我们看下源码:

1d1e55609a19425e71aa09eb028f41f6.jpeg

纳尼,o是咱们传入的动态代理创建的监听器对象,怎么调用下equals方法崩溃了,并且源码中也对o进行了判断处理。并且进一步分析,好像是咱们的equals方法直接返回了null?卧槽,equals方法还能返回null!!

这时候就得想到咱们的o是个动态代理对象,而动态代理对象调用的方法都会被分发到InvocationHandler对象中:

8e61dc2fc12ff36141fa2291fd14ccef.jpeg

也就是说,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反注册:

9b22c221b5bb44040e0b55874dea9f88.jpeg5b50b8887d65becb5ef5ba06abdb6a0c.jpeg

输出信息:

db6b036c8eb1e799ff8a2b899feae049.jpeg

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

大结局

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

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

作者:长安皈故里
链接:https://juejin.cn/post/7345105927357825078
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值