activity 停止时删除自定义侦听器

Remove custom listener when activity stops

我正在库模块中构建自定义视图,每 30 秒显示一个问题(从 REST Api 获得)并允许用户select 可能的答案之一。

此外,我需要在应用程序中使用该库,在问题下方显示视频。

所有业务和 UI 逻辑都应该在库中处理。

我创建了一个 ErrorListener 接口,以便能够在应用的 Activity.

中显示来自库的错误消息

如果 activity 暂停或停止,我是否需要在我的自定义视图中有一个方法来删除侦听器?当 activity 为 paused/stopped 或被销毁时,还有什么我应该考虑的吗? (比如停止 intervalsHandler、countDownTimer 等)

我的自定义视图

class BuffView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
    : LinearLayout(context, attrs) {

    private val apiErrorHandler = ApiErrorHandler()
    private val getBuffUseCase = GetBuffUseCase(apiErrorHandler)

    private val intervalsHandler = Handler()

    private val buffView: LinearLayout = inflate(context, R.layout.buff_view, this) as LinearLayout

    private var errorListener: ErrorListener? = null

    private var countDownTimer: CountDownTimer? = null

    private var buffIdCount = 1
    private var getBuffs = false
    
    fun init() {
        getBuffs = true
        getBuff()
    }

    private fun getBuff() {
        if (!getBuffs) return
        getBuffUseCase.invoke(Params(buffIdCount.toLong()), object : UseCaseResponse<Buff> {
            override fun onSuccess(result: Buff) {
                if (isDataValid(result)) displayBuff(result) else hideBuff()
            }

            override fun onError(errorModel: ErrorModel?) {
                errorListener?.onError(errorModel?.message ?: context.getString(R.string.generic_error_message))
                hideBuff()
            }
        })

        if (buffIdCount < TOTAL_BUFFS ) {
            intervalsHandler.postDelayed({
                buffIdCount++
                getBuff()
                stopCountDownTimer()
            }, REQUEST_BUFF_INTERVAL_TIME)
        }
    }

    private fun isDataValid(buff: Buff): Boolean {
        if (buff.author.firstName.isEmpty() && buff.author.lastName.isEmpty()) {
            showErrorInvalidData(context.getString(R.string.author_reason_error_message))
            return false
        }

        if (buff.question.title.isEmpty()) {
            showErrorInvalidData(context.getString(R.string.question_reason_error_message))
            return false
        }

        if (buff.timeToShow == null || buff.timeToShow < 0) {
            showErrorInvalidData(context.getString(R.string.timer_reason_error_message))
            return false
        }

        if (buff.answers == null) {
            showErrorInvalidData(context.getString(R.string.answers_reason_error_message))
            return false
        }
        return true
    }

    private fun showErrorInvalidData(reason: String) {
        errorListener?.onError(context.getString(R.string.data_incomplete_error_message, reason))
    }

    private fun displayBuff(buff: Buff) {
        setQuestion(buff.question.title)
        setAuthor(buff.author)
        setAnswer(buff.answers!!)
        setProgressBar(buff.timeToShow!!)
        setCloseButton()
        invalidate()
        showBuff()
    }

    private fun setQuestion(questionText: String) {
        question_text.text = questionText
    }

    private fun setAuthor(author: Buff.Author) {
        val firstName = author.firstName
        val lastName = author.lastName
        sender_name.text = "$firstName $lastName"

        Glide.with(context)
            .load(author.image)
            .into(sender_image)
    }

    private fun setAnswer(answers: List<Buff.Answer>) {
        val answersContainer = findViewById<LinearLayout>(R.id.answersContainer)
        answersContainer.removeAllViews()
        for(answer in answers) {
            val answerView: View = LayoutInflater.from(answersContainer.context).inflate(
                R.layout.buff_answer,
                answersContainer,
                false
            )

            answer.answerImage?.x0?.url?.let {
                Glide.with(context)
                    .load(it)
                    .into(answerView.answer_image)
            }

            answerView.setOnClickListener {
                answerView.background = ContextCompat.getDrawable(
                    context,
                    R.drawable.answer_selected_bg
                )
                answerView.answer_text.setTextColor(
                    ContextCompat.getColor(
                        context,
                        android.R.color.white
                    )
                )
                //freeze timer
                stopCountDownTimer()

                //hideView() after 2 seconds
                it.postDelayed({
                    hideBuff()
                }, HIDE_BUFF_AFTER_SELECTED_ANSWER_DURATION)
            }

            answerView.answer_text?.text = answer.title
            answersContainer.addView(answerView)
        }
    }

    private fun setProgressBar(timeToShow: Int) {
        question_time_progress.max = timeToShow
        countDownTimer = object : CountDownTimer(
            timeToShow * ONE_SECOND_INTERVAL,
            ONE_SECOND_INTERVAL
        ) {
            override fun onTick(millisUntilFinished: Long) {
                question_time.text = (millisUntilFinished / ONE_SECOND_INTERVAL).toString()
                question_time_progress.progress = timeToShow - (millisUntilFinished / ONE_SECOND_INTERVAL).toInt()
            }

            override fun onFinish() {
               hideBuff()
            }
        }.start()
    }

    private fun showBuff() {
        buffView.visibility = VISIBLE
    }

    private fun hideBuff() {
        buffView.visibility = GONE
    }

    private fun stopCountDownTimer() {
        countDownTimer?.cancel()
    }

    private fun setCloseButton() {
        buff_close.setOnClickListener {
            hideBuff()
            stopCountDownTimer()
        }
    }

     fun addErrorListener(errorListener: ErrorListener) {
        this.errorListener = errorListener
    }
}

主要Activity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        video.setVideoPath(url);
        video.setOnPreparedListener {
            video.start()
        }

        buff_view.addErrorListener(object : ErrorListener {
            override fun onError(msg: String) {
                Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show()
            }

        })
        buff_view.init()
    }
}

activity_main.xml

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    tools:context=".ui.MainActivity">

    <VideoView
        android:id="@+id/video"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:keepScreenOn="true"
        android:textSize="50sp"
        android:textStyle="bold" />

    <com.buffup.sdk.ui.BuffView
        android:id="@+id/buff_view"
        android:layout_gravity="bottom|start"
        android:padding="16dp"
        android:visibility="gone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</FrameLayout>

Activity 是非常庞大的数据结构,因此您确实需要确保系统能够在不再需要它们时对其进行垃圾回收。因此,确保当 activity 进入后台(或被销毁等)时,没有任何还活着的东西持有对它的引用绝对是个好主意。

所以首先是你的错误侦听器

buff_view.addErrorListener(object : ErrorListener {
        override fun onError(msg: String) {
            Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show()
        }
 })

BuffView 持有的对象引用了它需要在 Toast 调用中使用的 Activity。因此,只要该回调对象存在,activity 就会保存在内存中。只要 buff_view 持有该回调对象,它就会一直存在。

因此您可以清除侦听器,因此 buff_view 不再保留它并且可以对其进行 GC。但是 buff_view 上有什么?是什么让它留在记忆中?如果它首先是由 activity 创建的视图,对于它的布局,那么当 activity 被销毁时它将被销毁 - 没有任何东西 link 将它传递给其余的应用进程。

但是如果 buff_view 是 longer-lived(假设它是一个包含在 ViewModel 或其他东西中的组件,意味着比显示它的任何 activity 都长寿)那么你肯定需要确保它不会保留在被破坏的 activity.


这是一个总体概述,因此您对要查找的内容类型有所了解。如果您的实现全部是内部的,并且您能够推断出什么保留了什么以及它们的生命周期是什么,那么如果您知道它们无论如何都会被清除,那么让它们保持引用是很好的。

如果您需要管理您的推荐人,您真的有两个选择

  • 具有明确的 register/unregister 方法,并确保正确调用它们(例如,当 activity 进入后台时)
  • 使用不在内存中保留侦听器对象的 WeakReference(一旦应用删除对 activity 的引用,它就可以自由清理,并且您的 WeakReference 将变为空

(取消注册更容易 - 它需要更多工作,但它会迫使您推理您的应用程序以及它们如何组合在一起,并且当您明确管理事物时出现奇怪行为的可能性较小)

如果你正在创建一个库,并且你正在接受外部监听器(即它不仅仅是你自己的内部状态管理,你可以确定)那么你需要决定这些方法中的哪一个拿。在某些方面更简单 - 您接收的每个侦听器,为其提供注销功能,或使其成为弱引用。假设最坏的情况,要么保留弱引用,要么让用户负责清理。无论您选择哪种行为,请记录下来,以便用户知道他们是否需要清理或忽略它是否安全


我不知道你的 CountdownTimer 在做什么,但一般规则是当你的应用程序在后台时,它会“暂停”(除非它是音乐播放器或其他明显的东西!) 所以它应该停止做事。这意味着暂停工作线程,取消处理程序上发布的事件。您不希望 30 秒后有人浏览他们的 Instagram 提要时弹出祝酒词,您知道吗?所以你需要处理暂停和恢复的事情——具体涉及什么取决于你的应用程序和它在做什么


虽然我正在做一个 megapost,如果你还没有,学习如何使用 memory profiler 是个好主意。特别是堆转储,它向您显示内存中的内容。

您可以阅读 link 以查看详细信息,但基本上,您需要做一些创建一些对象的操作(例如旋转屏幕以便销毁 Activity 并创建新的 Activity),点击 GC 按钮垃圾收集任何松散的对象,然后进行堆转储。

按包对内容进行排序,找到您应用的包,然后查看您的 类 并查看内存中每个对象的数量。就像如果你只应该在内存中有 CoolActivity 中的 1 个,而你有 2 个(或更多!),这表明某些东西正在保留旧的。你可以看看究竟是什么在做那件事。偶尔检查一下是好事!