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 个(或更多!),这表明某些东西正在保留旧的。你可以看看究竟是什么在做那件事。偶尔检查一下是好事!
我正在库模块中构建自定义视图,每 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 个(或更多!),这表明某些东西正在保留旧的。你可以看看究竟是什么在做那件事。偶尔检查一下是好事!