【问题标题】:How to clear Fragments properly and avoid leakage?如何正确清除 Fragments 并避免泄漏?
【发布时间】:2021-01-25 12:42:42
【问题描述】:

我有一个 Master-Detail 布局我有一个活动和 5 个片段,我使用一个名为 selectedService 的 Integer MutableLiveData(我将其保存在共享首选项中),它在 ServiceAdapter 中设置为单击的 id服务,并且在活动中观察到打开所选服务的正确对应片段

这是我的共享偏好类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 上观察到的主要活动:

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)

但我不知道如何解决,谁能解释一下为什么会出现泄漏以及确保碎片不会导致任何泄漏的步骤?

【问题讨论】:

    标签: android kotlin android-fragments sharedpreferences leakcanary


    【解决方案1】:

    我不熟悉 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 来发现内存泄漏 - 它比看起来更容易!您只需做一些可能导致泄漏的事情,进行垃圾收集以清理任何松散的对象,然后捕获转储。然后,您可以按包排序,深入了解您的应用程序的内容,并查看每个内容中有多少潜伏在周围。如果一个东西太多(比如一个特定的片段类型),你可以检查它,看看是什么持有对它的引用并将它保存在内存中

    【讨论】:

    • 非常感谢你的朋友,mapOf 就像一个魅力......好吧,CanaryLeak 是一个即插即用的库,它会自动为你做所有事情(它留在后台”观察/倾听”可能发生的任何泄漏,一旦发生泄漏,它会转储堆并分析它,然后向您发送分析报告)它只是让您更容易,而不是在分析器中手动进行。
    • 持有实例几乎是不安全的,除非您首先尝试重新初始化来自 findFragmentByTag 的字段。
    猜你喜欢
    • 1970-01-01
    • 2013-01-23
    • 2017-09-09
    • 1970-01-01
    • 2012-02-12
    • 1970-01-01
    • 1970-01-01
    • 2019-06-06
    • 2020-05-13
    相关资源
    最近更新 更多