Android ViewModel 正在泄漏

Android ViewModel is leaking

LeakCanary 告诉我我的一个 ViewModel 正在泄漏,但在玩了 2 天后我无法消除泄漏。

Here is why LeakCanary shows

这是获取 ViewModel 的 Fragment

viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
        getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
    }

这是视图模型

class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {

private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()

fun getStrains(breederId: String) {
    viewModelScope.launch {
        breederRepository.getMinimalStrains(breederId).observeForever {
            strainList.value = it
        }
    }
}

}

这里是 BreederRepository:

class BreederRepository(context: Context) {

private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi

init {
    val database: Db = Db.getInstance(
        context
    )!!
    dao = database.breederDao()
}

suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
    withContext(Dispatchers.IO) {
        dao.getMinimalStrains(breederId)
    }

}

这是 Db class

@Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
@TypeConverters(RoomDateConverter::class)

abstract class Db : RoomDatabase() {

abstract fun breederDao(): BreederDao

companion object {
    private var instance: Db? = null

    @JvmStatic
    fun getInstance(context: Context): Db? {
        if (instance == null) {
            synchronized(Db::class) {
                instance = Room.databaseBuilder(
                    context.applicationContext,
                    Db::class.java, "seedfinder_db"
                )
                    .build()
            }
        }
        return instance
    }
}

}

您正在使用 observeForever,顾名思义,即使在您的 ViewModel 被清除后,它也将永远保持观察。 Room 不需要对 return 和 LiveData 的 DAO 方法使用 suspend 方法,这在任何情况下都不是正确的方法 - LiveData 已经是异步的。

相反,您应该 transforming your LiveData,使用 breederId 作为 strainList LiveData 的输入:

class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {

    private val breederRepository = BreederRepository(application)
    private val currentBreederId = MutableLiveData<String>()

    // Here we use the switchMap method from the lifecycle-livedata-ktx artifact
    val strainList: LiveData<String> = currentBreederId.switchMap {
            breederId -> breederRepository.getMinimalStrains(breederId)
    }

    private fun setBreederId(breederId: String) {
        currentBreederId.value = breederId
    }
}

你的 getMinimalStrains 变成:

fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
    dao.getMinimalStrains(breederId)

您可以通过在 UI 中设置 breederId 并像以前一样观察 strainList 来使用它:

viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
    setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
  // use your updated list
}

如果您使用 Saved State module for ViewModels(如果您使用最新的稳定 Fragments / Activity 库,这是默认设置),那么您可以使用 SavedStateHandle,这是从您的片段参数自动填充并完全跳过 setBreederId()

class ViewBreederViewModel(
    application: Application,
    savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {

    private val breederRepository = BreederRepository(application)

    // Here we use the switchMap method from the lifecycle-livedata-ktx artifact
    val strainList: LiveData<String> = savedStateHandle
        .getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
        .switchMap {
            breederId -> breederRepository.getMinimalStrains(breederId)
        }
}

这意味着您的代码可以简单地变成:

viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
  // use your updated list
}

如果您使用 fragment-ktx 神器,您可以进一步简化为:

// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()

viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
  // use your updated list
}