4

I want to implement the new In-App Update library in my app, but I've noticed that it trigger a memory leak in my activity when it's recreated/rotated.

Here's the only detail I have from LeakCanary:

LeakCanary trace

Obviously, I've nothing if I remove the code from the In-App Update lib especially the addOnSuccessListener :

appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
        && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
            updateInfo.value = appUpdateInfo
            updateAvailable.value = true
        }else{
            updateInfo.value = null
            updateAvailable.value = false
        }
    }

According to this post, I have first used some LiveData, but the problem was the same, so I used a full class to handle the callback, with LiveData :

My Service class :

class AppUpdateService {

    val updateAvailable: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateDownloaded: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateInfo: MutableLiveData<AppUpdateInfo> by lazy { MutableLiveData<AppUpdateInfo>() }

    fun checkForUpdate(appUpdateManager: AppUpdateManager){
        appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
                    && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
                updateInfo.value = appUpdateInfo
                updateAvailable.value = true
            }else{
                updateInfo.value = null
                updateAvailable.value = false
            }
        }
    }

    fun checkUpdateOnResume(appUpdateManager: AppUpdateManager){
        appUpdateManager.appUpdateInfo.addOnSuccessListener {
            updateDownloaded.value = (it.installStatus() == InstallStatus.DOWNLOADED)
        }
    }
}

My Activity simplified :

class MainActivity : BaseActivity(), InstallStateUpdatedListener {

    override fun contentViewID(): Int { return R.layout.activity_main }

    private val UPDATE_REQUEST_CODE = 8000

    private lateinit var appUpdateManager : AppUpdateManager

    private val appUpdateService = AppUpdateService()

    override fun onStateUpdate(state: InstallState?) {
        if(state?.installStatus() == InstallStatus.DOWNLOADED){ notifyUser() }
    }

    // Called in the onCreate()
    override fun setupView(){
        appUpdateManager = AppUpdateManagerFactory.create(this)
        appUpdateManager.registerListener(this)
        setupAppUpdateServiceObservers()
        // Check for Update
        appUpdateService.checkForUpdate(appUpdateManager)
    }

    private fun setupAppUpdateServiceObservers(){
        appUpdateService.updateAvailable.observe(this, Observer {
            if (it)
                requestUpdate(appUpdateService.updateInfo.value)
        })

        appUpdateService.updateDownloaded.observe(this, Observer {
            if (it)
                notifyUser()
        })
    }

    private fun requestUpdate(appUpdateInfo: AppUpdateInfo?){
        appUpdateManager.startUpdateFlowForResult(appUpdateInfo, AppUpdateType.FLEXIBLE, this, UPDATE_REQUEST_CODE)
    }

    private fun notifyUser(){
        showSnackbar(getString(R.string.updated_downloaded), getString(R.string.restart)) {
            appUpdateManager.completeUpdate()
            appUpdateManager.unregisterListener(this)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == UPDATE_REQUEST_CODE) {
            if (resultCode != RESULT_OK) {
                Timber.d("Update flow failed! Result code: $resultCode")
            }
        }
    }

    override fun onDestroy() {
        appUpdateManager.unregisterListener(this)
        super.onDestroy()
    }

    override fun onResume() {
        super.onResume()
        appUpdateService.checkUpdateOnResume(appUpdateManager)
    }

}

I don't really understand how to avoid the memory leak as the appUpdateManager has to be created with the context of the activity, and it looks to be the thing that causes the memory leak with the callback.

Does someone already implement it without having this issue?

Borombo
  • 170
  • 2
  • 10

2 Answers2

1

Using weak reference to the context will probably solve your memory leak problem. Write this in your activity:

WeakReference<Context> contextWeakReference = new WeakReference<Context>(this);

Context context = contextWeakReference.get();
if (context != null) {
   // Register using context here
}

There are lots of good articles on WeakReference, Garbage Collection and Memory Leaks to read more on the subject.

Also, onDestroy() is not guaranteed to be called. When you start another Activity, onPause() and onStop() method called instead of onDestroy().

The onDestroy() calls when you hit back button or call finish() method. So, unregister Listener in onPause() or onStop(). If you unregister in onDestroy() method, it might cause a memory leak.

Another idea is that since AppUpdateService class in not a subclass of ViewModel, it is not lifecycle aware. I'm not sure, but, you might need to remove observers in onstop/onDestroy of the activity and add them in onResume. (observers has a strong reference to the LifecycleOwner, here the activiy) To do that you need to define observers to be able to remove them later. Something like:

MutableLiveData<Boolean> someData = new MutableLiveData<>;

and then in onResume:

someData = appUpdateService.updateAvailable;
someData.observe()

and in onStop:

someData.removeObservers()

It's just a guess, but, I hope it would help somehow.

Sina
  • 2,683
  • 1
  • 13
  • 25
  • Thanks for your answer ! Actually I tried what you said, but I still have the same problem. For info, the code is : `val contextWeakReference = WeakReference(this) contextWeakReference.get()?.let {weakContext -> appUpdateManager = AppUpdateManagerFactory.create(weakContext) } ` And I moved the unregisterListener to the onStop instead of onDestroy – Borombo Oct 18 '19 at 16:06
  • I've just added another scenario which might causing a memory leak in my answer. – Sina Oct 19 '19 at 01:16
  • Thanks for the update ! Actually, with you indication, I took some time to test a lot of different ways (with/without liveData, weakReference on different object...) and the result it that just by calling `appUpdateManager.appUpdateInfo` the memory leaks occurs, even if I don't add a listener, use a weakReference for the context, and even for the appUpdateManager... I'll continue to seach how to be able to call `appUpdateManager.appUpdateInfo` without getting a memory leak... – Borombo Oct 19 '19 at 11:11
1

Thanks to @Sina Farahzadi I searched and try a lot of things and figured that the problem was the appUpdateManager.appUdateInfo call with the Task object.

The way I found to solve the memory leak is to use the applicationContext instead of the context of the activity. I'm not sure it's the best solution, but it's the one I've found for now. I've exported all in my service class so here's my code :

AppUpdateService.kt :

class AppUpdateService : InstallStateUpdatedListener {

    val updateAvailable: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateDownloaded: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val notifyUser: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateInfo: MutableLiveData<AppUpdateInfo> by lazy { MutableLiveData<AppUpdateInfo>() }

    private var appUpdateManager : AppUpdateManager? = null
    private var appUpdateInfoTask: Task<AppUpdateInfo>? = null

    override fun onStateUpdate(state: InstallState?) {
        notifyUser.value =  (state?.installStatus() == InstallStatus.DOWNLOADED)
    }

    fun setupAppUpdateManager(context: Context){
        appUpdateManager = AppUpdateManagerFactory.create(context)
        appUpdateManager?.registerListener(this)
        checkForUpdate()
    }

    fun onStopCalled(){
        appUpdateManager?.unregisterListener(this)
        appUpdateInfoTask = null
        appUpdateManager = null
    }

    fun checkForUpdate(){
        appUpdateInfoTask = appUpdateManager?.appUpdateInfo
        appUpdateInfoTask?.addOnSuccessListener { appUpdateInfo ->
            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
                    && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
                updateInfo.value = appUpdateInfo
                updateAvailable.value = true
            }else{
                updateInfo.value = null
                updateAvailable.value = false
            }
        }
    }

    fun startUpdate(activity: Activity, code: Int){
        appUpdateManager?.startUpdateFlowForResult(updateInfo.value, AppUpdateType.FLEXIBLE, activity, code)
    }

    fun updateComplete(){
        appUpdateManager?.completeUpdate()
        appUpdateManager?.unregisterListener(this)
    }

    fun checkUpdateOnResume(){
        appUpdateManager?.appUpdateInfo?.addOnSuccessListener {
            updateDownloaded.value = (it.installStatus() == InstallStatus.DOWNLOADED)
        }
    }

}

MainActivity simplified :

class MainActivity : BaseActivity(){

    override fun contentViewID(): Int { return R.layout.activity_main }

    private val UPDATE_REQUEST_CODE = 8000

    private var appUpdateService: AppUpdateService? = AppUpdateService()

    /**
     * Setup the view of the activity (navigation and menus)
     */
    override fun setupView(){
        val contextWeakReference = WeakReference<Context>(applicationContext)
        contextWeakReference.get()?.let {weakContext ->
            appUpdateService?.setupAppUpdateManager(weakContext)
        }
    }

    private fun setupAppUpdateServiceObservers(){
        appUpdateService?.updateAvailable?.observe(this, Observer {
            if (it)
                requestUpdate()
        })

        appUpdateService?.updateDownloaded?.observe(this, Observer {
            if (it)
                notifyUser()
        })

        appUpdateService?.notifyUser?.observe(this, Observer {
            if (it)
                notifyUser()
        })
    }

    private fun removeAppUpdateServiceObservers(){
        appUpdateService?.updateAvailable?.removeObservers(this)
        appUpdateService?.updateDownloaded?.removeObservers(this)
        appUpdateService?.notifyUser?.removeObservers(this)
    }

    private fun requestUpdate(){
        appUpdateService?.startUpdate(this, UPDATE_REQUEST_CODE)
    }

    private fun notifyUser(){
        showSnackbar(getString(R.string.updated_downloaded), getString(R.string.restart)) {
            appUpdateService?.updateComplete()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == UPDATE_REQUEST_CODE) {
            if (resultCode != RESULT_OK) {
                Timber.d("Update flow failed! Result code: $resultCode")
            }
        }
    }

    override fun onStop() {
        appUpdateService?.onStopCalled()
        removeAppUpdateServiceObservers()
        appUpdateService = null
        super.onStop()
    }

    override fun onResume() {
        super.onResume()
        setupAppUpdateServiceObservers()
        appUpdateService?.checkUpdateOnResume()
    }

}

For now, I will keep it that way and continue to search for another way to do it. Let me know if someone has a better way to do it.

Borombo
  • 170
  • 2
  • 10
  • Not sure if you found a solution you preferred more, but just to say that I think using the `applicationContext` may be the "approved" way of doing it — as usual, official documentation/examples are sketchy but the [Java docs](https://developer.android.com/reference/com/google/android/play/core/appupdate/AppUpdateManagerFactory) for `AppUpdateManagerFactory.create` mention "Parameters `context`: The application Context for your app." — so seems like it's expecting application context here? This is also the only thing that plugged this memory leak for me – anotherdave Apr 21 '20 at 09:40