如何正确清除Fragments,避免泄露?
How to clear Fragments properly and avoid leakage?
我有一个主从布局我有一个 activity 和 5 个片段,我使用一个名为 selectedService
的整数 MutableLiveData(我将其保存在共享首选项中)在 ServiceAdapter
添加到所点击服务的 ID,并且在 activity 中观察到打开所选服务的正确对应片段
这是我的共享首选项 class Settings.kt
:
object Settings {
private const val NAME = "MyPreferences"
private const val MODE = Context.MODE_PRIVATE
// Keys
private const val LANGUAGE_ID_KEY = "language_id"
private lateinit var preferences: SharedPreferences
fun init(context: Context) {
preferences = context.getSharedPreferences(NAME, MODE)
mappingServicesWithFragments()
}
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = edit()
operation(editor)
editor.apply()
}
var languageID: Int
get() = preferences.getInt(LANGUAGE_ID_KEY, 1)
set(value) = preferences.edit { it.putInt(LANGUAGE_ID_KEY, value) }
// to handle service clicks and opening the correct fragments associated with those services
var selectedService: MutableLiveData<Int> = MutableLiveData()
private val serviceFragmentMap = HashMap<Int, Fragment>()
private fun mappingServicesWithFragments() {
serviceFragmentMap[1] = InpatientFragment()
serviceFragmentMap[2] = OutpatientFragment()
serviceFragmentMap[3] = ConsultationFragment()
serviceFragmentMap[4] = ReleasedPatientsFragment()
serviceFragmentMap[5] = FavoritesListsFragment()
serviceFragmentMap[6] = PatientProfileFragment()
serviceFragmentMap[7] = VitalSignsFragment()
serviceFragmentMap[8] = DiagnosisFragment()
serviceFragmentMap[9] = NurseNotesFragment()
serviceFragmentMap[10] = RadiologyFragment()
serviceFragmentMap[11] = LaboratoryFragment()
serviceFragmentMap[12] = MedicationsFragment()
serviceFragmentMap[13] = ProceduresFragment()
serviceFragmentMap[14] = OperationsFragment()
serviceFragmentMap[15] = PatientConsultationsFragment()
}
fun getMappedFragment(key: Int): Fragment? {
return serviceFragmentMap[key]
}
这是我在 selectedService
上观察到的主要 activity:
class MainDoctorActivity : BaseActivity() {
private lateinit var services: List<Service>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_doctor)
initialViews()
}
private fun initialViews() {
// Static List of services for the (Doctor)
services = listOf(
Service(1, "Inpatient", R.drawable.ic_inpatients, true),
Service(2, "Outpatient", R.drawable.ic_outpatients, false),
Service(3, "Consultation", R.drawable.ic_consultation, false),
Service(4, "Released patients", R.drawable.ic_released_patients, false),
Service(5, "Favorites lists", R.drawable.ic_favorites_lists, false)
)
// Setting up the Name and ID of the doctor in main screen
doctorNameTv.text = Settings.loggedInDoctor!!.doctorName
doctorIdTv.text = Settings.loggedInDoctor!!.doctorID
rvServices.apply {
adapter = ServicesAdapter(services)
addItemDecoration(
DividerItemDecoration(
this@MainDoctorActivity,
LinearLayoutManager.VERTICAL
)
)
}
// Observe selectedService
Settings.selectedService.observe(this@MainDoctorActivity, {
openFragment(supportFragmentManager, Settings.getMappedFragment(it))
})
}
fun logout(view: View) {
Utils.animateClickingButton(view)
Settings.loggedInDoctor = null
val intent = Intent(this@MainDoctorActivity, LoginActivity::class.java)
startActivity(intent)
}
override fun onResume() {
super.onResume()
// Open "Inpatient Fragment" as soon as you login/open the app
openFragment(supportFragmentManager, Settings.getMappedFragment(services[0].id))
}
openFragment()
函数是我用来打开所需片段的扩展函数,下面是它的代码:
internal fun openFragment(manager: FragmentManager, fragment: Fragment?) {
manager.beginTransaction().replace(R.id.container, fragment!!).commit()
}
这是整个布局的图片(用于可视化目的)
这里是ReleasedPatientsFragment.kt
:
class ReleasedPatientsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_released_patients, container, false)
}
}
正如你所知道的那样,除了在 mainActivity 中设置的容器中显示片段外,我实际上什么也没做,但是每当我在片段之间移动时我就会泄漏,我可以'好像不能解决问题。
以下是 CanaryLeak 分析:
┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│ Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
│ ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│ Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton)
│ ↓ InputMethodManager.mNextServedView
├─ com.android.internal.policy.DecorView instance
│ Leaking: NO (LinearLayout↓ is not leaking and View attached)
│ mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.example.emr.ui.activities.maindoctor.MainDoctorActivity with mDestroyed = false
│ Parent android.view.ViewRootImpl not a android.view.View
│ View#mParent is set
│ View#mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ ↓ DecorView.mContentRoot
├─ android.widget.LinearLayout instance
│ Leaking: NO (MainDoctorActivity↓ is not leaking and View attached)
│ mContext instance of com.example.emr.ui.activities.maindoctor.MainDoctorActivity with mDestroyed = false
│ View.parent com.android.internal.policy.DecorView attached as well
│ View#mParent is set
│ View#mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ ↓ LinearLayout.mContext
├─ com.example.emr.ui.activities.maindoctor.MainDoctorActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ ↓ MainDoctorActivity.services
│ ~~~~~~~~
├─ java.util.Arrays$ArrayList instance
│ Leaking: UNKNOWN
│ ↓ Arrays$ArrayList.a
│ ~
├─ com.example.emr.model.Service[] array
│ Leaking: UNKNOWN
│ ↓ Service[].[1]
│ ~~~
├─ com.example.emr.model.Service instance
│ Leaking: UNKNOWN
│ ↓ Service.fragment
│ ~~~~~~~~
╰→ com.example.emr.ui.fragments.outpatient.OutpatientFragment instance
Leaking: YES (ObjectWatcher was watching this because com.example.emr.ui.fragments.outpatient.OutpatientFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
key = ca0075c3-8df2-423f-8adf-48cd230a692f
watchDurationMillis = 8513
retainedDurationMillis = 3512
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.4
App process name: com.example.emr
Analysis duration: 5271 ms
所以从这个分析我可以看出原因是这样的:
Leaking: YES (ObjectWatcher was watching this because
com.example.emr.ui.fragments.outpatient.OutpatientFragment received Fragment#onDestroy() callback and
Fragment#mFragmentManager is null)
但我不知道如何解决,谁能解释一下为什么会出现泄漏,以及要采取哪些步骤来确保碎片不会造成任何泄漏?
我不熟悉 CanaryLeak,但可能是因为您在 serviceFragmentMap
中持有每个片段的实例?所以系统实际上永远不会对它们进行垃圾收集(即使它们停止显示)。还是只发生在 OutpatientFragment
?
顺便说一下,您可以这样做来创建地图(而不是在函数中初始化它):
val serviceFragmentMap = mapOf(
1 to InpatientFragment(),
2 to OutpatientFragment(),
...
)
如果您实际上不想保存实例,而只想传递一个 ID 并取回正确类型的片段,您可以改为这样做:
// you don't need the map type really, it can infer it
val serviceFragmentMap = mapOf<Int, Class<out Fragment>(
1 to InpatientFragment::class.java,
2 to OutpatientFragment::class.java,
...
)
fun getMappedFragment(key: Int): Fragment? {
return serviceFragmentMap[key]?.newInstance()
}
这样一来,无论何时调用它,您总是会得到一个新的片段,这更接近于框架的工作方式(例如,如果系统销毁并重新创建您的片段,它不会是您在你的地图)
此外,我建议您熟悉 Heap Dumps 作为发现内存泄漏的一种方法 - 它比看起来更容易!您只需做一些可能导致泄漏的事情,执行垃圾收集以清理任何松散的对象,然后捕获转储。然后您可以按包排序,深入了解您应用程序的内容,并查看每个内容中有多少是潜伏在周围的。如果有太多的东西(比如特定的片段类型)你可以检查它并查看是什么持有对它的引用并将它保存在内存中
我有一个主从布局我有一个 activity 和 5 个片段,我使用一个名为 selectedService
的整数 MutableLiveData(我将其保存在共享首选项中)在 ServiceAdapter
添加到所点击服务的 ID,并且在 activity 中观察到打开所选服务的正确对应片段
这是我的共享首选项 class Settings.kt
:
object Settings {
private const val NAME = "MyPreferences"
private const val MODE = Context.MODE_PRIVATE
// Keys
private const val LANGUAGE_ID_KEY = "language_id"
private lateinit var preferences: SharedPreferences
fun init(context: Context) {
preferences = context.getSharedPreferences(NAME, MODE)
mappingServicesWithFragments()
}
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = edit()
operation(editor)
editor.apply()
}
var languageID: Int
get() = preferences.getInt(LANGUAGE_ID_KEY, 1)
set(value) = preferences.edit { it.putInt(LANGUAGE_ID_KEY, value) }
// to handle service clicks and opening the correct fragments associated with those services
var selectedService: MutableLiveData<Int> = MutableLiveData()
private val serviceFragmentMap = HashMap<Int, Fragment>()
private fun mappingServicesWithFragments() {
serviceFragmentMap[1] = InpatientFragment()
serviceFragmentMap[2] = OutpatientFragment()
serviceFragmentMap[3] = ConsultationFragment()
serviceFragmentMap[4] = ReleasedPatientsFragment()
serviceFragmentMap[5] = FavoritesListsFragment()
serviceFragmentMap[6] = PatientProfileFragment()
serviceFragmentMap[7] = VitalSignsFragment()
serviceFragmentMap[8] = DiagnosisFragment()
serviceFragmentMap[9] = NurseNotesFragment()
serviceFragmentMap[10] = RadiologyFragment()
serviceFragmentMap[11] = LaboratoryFragment()
serviceFragmentMap[12] = MedicationsFragment()
serviceFragmentMap[13] = ProceduresFragment()
serviceFragmentMap[14] = OperationsFragment()
serviceFragmentMap[15] = PatientConsultationsFragment()
}
fun getMappedFragment(key: Int): Fragment? {
return serviceFragmentMap[key]
}
这是我在 selectedService
上观察到的主要 activity:
class MainDoctorActivity : BaseActivity() {
private lateinit var services: List<Service>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_doctor)
initialViews()
}
private fun initialViews() {
// Static List of services for the (Doctor)
services = listOf(
Service(1, "Inpatient", R.drawable.ic_inpatients, true),
Service(2, "Outpatient", R.drawable.ic_outpatients, false),
Service(3, "Consultation", R.drawable.ic_consultation, false),
Service(4, "Released patients", R.drawable.ic_released_patients, false),
Service(5, "Favorites lists", R.drawable.ic_favorites_lists, false)
)
// Setting up the Name and ID of the doctor in main screen
doctorNameTv.text = Settings.loggedInDoctor!!.doctorName
doctorIdTv.text = Settings.loggedInDoctor!!.doctorID
rvServices.apply {
adapter = ServicesAdapter(services)
addItemDecoration(
DividerItemDecoration(
this@MainDoctorActivity,
LinearLayoutManager.VERTICAL
)
)
}
// Observe selectedService
Settings.selectedService.observe(this@MainDoctorActivity, {
openFragment(supportFragmentManager, Settings.getMappedFragment(it))
})
}
fun logout(view: View) {
Utils.animateClickingButton(view)
Settings.loggedInDoctor = null
val intent = Intent(this@MainDoctorActivity, LoginActivity::class.java)
startActivity(intent)
}
override fun onResume() {
super.onResume()
// Open "Inpatient Fragment" as soon as you login/open the app
openFragment(supportFragmentManager, Settings.getMappedFragment(services[0].id))
}
openFragment()
函数是我用来打开所需片段的扩展函数,下面是它的代码:
internal fun openFragment(manager: FragmentManager, fragment: Fragment?) {
manager.beginTransaction().replace(R.id.container, fragment!!).commit()
}
这是整个布局的图片(用于可视化目的)
这里是ReleasedPatientsFragment.kt
:
class ReleasedPatientsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_released_patients, container, false)
}
}
正如你所知道的那样,除了在 mainActivity 中设置的容器中显示片段外,我实际上什么也没做,但是每当我在片段之间移动时我就会泄漏,我可以'好像不能解决问题。
以下是 CanaryLeak 分析:
┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│ Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
│ ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│ Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton)
│ ↓ InputMethodManager.mNextServedView
├─ com.android.internal.policy.DecorView instance
│ Leaking: NO (LinearLayout↓ is not leaking and View attached)
│ mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.example.emr.ui.activities.maindoctor.MainDoctorActivity with mDestroyed = false
│ Parent android.view.ViewRootImpl not a android.view.View
│ View#mParent is set
│ View#mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ ↓ DecorView.mContentRoot
├─ android.widget.LinearLayout instance
│ Leaking: NO (MainDoctorActivity↓ is not leaking and View attached)
│ mContext instance of com.example.emr.ui.activities.maindoctor.MainDoctorActivity with mDestroyed = false
│ View.parent com.android.internal.policy.DecorView attached as well
│ View#mParent is set
│ View#mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ ↓ LinearLayout.mContext
├─ com.example.emr.ui.activities.maindoctor.MainDoctorActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ ↓ MainDoctorActivity.services
│ ~~~~~~~~
├─ java.util.Arrays$ArrayList instance
│ Leaking: UNKNOWN
│ ↓ Arrays$ArrayList.a
│ ~
├─ com.example.emr.model.Service[] array
│ Leaking: UNKNOWN
│ ↓ Service[].[1]
│ ~~~
├─ com.example.emr.model.Service instance
│ Leaking: UNKNOWN
│ ↓ Service.fragment
│ ~~~~~~~~
╰→ com.example.emr.ui.fragments.outpatient.OutpatientFragment instance
Leaking: YES (ObjectWatcher was watching this because com.example.emr.ui.fragments.outpatient.OutpatientFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
key = ca0075c3-8df2-423f-8adf-48cd230a692f
watchDurationMillis = 8513
retainedDurationMillis = 3512
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.4
App process name: com.example.emr
Analysis duration: 5271 ms
所以从这个分析我可以看出原因是这样的:
Leaking: YES (ObjectWatcher was watching this because
com.example.emr.ui.fragments.outpatient.OutpatientFragment received Fragment#onDestroy() callback and
Fragment#mFragmentManager is null)
但我不知道如何解决,谁能解释一下为什么会出现泄漏,以及要采取哪些步骤来确保碎片不会造成任何泄漏?
我不熟悉 CanaryLeak,但可能是因为您在 serviceFragmentMap
中持有每个片段的实例?所以系统实际上永远不会对它们进行垃圾收集(即使它们停止显示)。还是只发生在 OutpatientFragment
?
顺便说一下,您可以这样做来创建地图(而不是在函数中初始化它):
val serviceFragmentMap = mapOf(
1 to InpatientFragment(),
2 to OutpatientFragment(),
...
)
如果您实际上不想保存实例,而只想传递一个 ID 并取回正确类型的片段,您可以改为这样做:
// you don't need the map type really, it can infer it
val serviceFragmentMap = mapOf<Int, Class<out Fragment>(
1 to InpatientFragment::class.java,
2 to OutpatientFragment::class.java,
...
)
fun getMappedFragment(key: Int): Fragment? {
return serviceFragmentMap[key]?.newInstance()
}
这样一来,无论何时调用它,您总是会得到一个新的片段,这更接近于框架的工作方式(例如,如果系统销毁并重新创建您的片段,它不会是您在你的地图)
此外,我建议您熟悉 Heap Dumps 作为发现内存泄漏的一种方法 - 它比看起来更容易!您只需做一些可能导致泄漏的事情,执行垃圾收集以清理任何松散的对象,然后捕获转储。然后您可以按包排序,深入了解您应用程序的内容,并查看每个内容中有多少是潜伏在周围的。如果有太多的东西(比如特定的片段类型)你可以检查它并查看是什么持有对它的引用并将它保存在内存中