Search code examples
androidleakcanary

Memory leak in DatePickerDialog. How to identify what is the problem?


I am using this MaterialDateTimePicker library in my app. I have a problem with finding the root cause of the leak. DatePickerDialog is leaking based on what the log shows. I am not sure if the lib has a problem but I think I am doing something wrong.

I have a View that displays the current date. The user is able to click it, then DatePickerDialog displays. Then s/he selects a day and click on the ok button of the DatePickerDialog. I receive the date, format it and then present it. After a few seconds, Leak Canary lib says hey you leaked. It would be great if you can tell me what's wrong with my code.

So, this is my custom widget.

import android.content.Context
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import com.atco.forsite.R
import com.atco.forsite.screens.utility.DatePickerOFIState.NOW_FUTURE
import com.atco.forsite.screens.utility.DatePickerOFIState.PAST_NOW
import com.jakewharton.rxrelay2.PublishRelay
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import java.util.Calendar

enum class DatePickerOFIState {
    PAST_NOW,
    NOW_FUTURE
}

class DatePickerOFI(
    private val context: Context,
    private val fm: FragmentManager,
    private val state: DatePickerOFIState
) : DatePickerDialog.OnDateSetListener {

    /**
     * Subscribe to this observer in order to be notified when the result is ready.
     */
    val relay: PublishRelay<Calendar> = PublishRelay.create()

    private val datePicker: DatePickerDialog

    companion object {
        const val TAG = "DatePickerDialog"
    }

    init {
        val now = Calendar.getInstance()
        datePicker = DatePickerDialog.newInstance(
                this,
                now.get(Calendar.YEAR),
                now.get(Calendar.MONTH),
                now.get(Calendar.DAY_OF_MONTH)
        )
        datePicker.version = DatePickerDialog.Version.VERSION_2
        datePicker.accentColor = ContextCompat.getColor(context, R.color.primaryBlue)
        datePicker.setOkColor(ContextCompat.getColor(context, R.color.white))
        datePicker.setCancelColor(ContextCompat.getColor(context, R.color.white))

        when (state) {
            PAST_NOW -> datePicker.maxDate = now
            NOW_FUTURE -> datePicker.minDate = now
        }
    }

    fun show() {
        datePicker.show(fm, TAG)
    }

    override fun onDateSet(view: DatePickerDialog?, year: Int, monthOfYear: Int, dayOfMonth: Int) {
        val calendar = Calendar.getInstance()
        calendar.set(year, monthOfYear, dayOfMonth)
        relay.accept(calendar)
    }
}

There is nothing special in my Activity. I open the DatePickerDialog like this:

class SafetyExchangeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_safety_exchange)

        val datePicker = DatePickerOFI(this, supportFragmentManager, DatePickerOFIState.PAST_NOW)
        datePicker.relay.safeSubscribe(createDefaultObserver(logger) {
            setReportDate(this)
        })

        etReportedDate.setOnClickListener {
            datePicker.show()
        }
    }
}

Leak trace:

┬───
│ 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.mCurRootView
├─ 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.atco.forsite.screens.records.safetyExchange.SafetyExchangeActivity 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 (SafetyExchangeActivity↓ is not leaking and View attached)
│    mContext instance of com.atco.forsite.screens.records.safetyExchange.SafetyExchangeActivity 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.atco.forsite.screens.records.safetyExchange.SafetyExchangeActivity instance
│    Leaking: NO (Activity#mDestroyed is false)
│    ↓ SafetyExchangeActivity.datePicker
│                             ~~~~~~~~~~
├─ com.atco.forsite.screens.utility.DatePickerOFI instance
│    Leaking: UNKNOWN
│    ↓ DatePickerOFI.datePicker
│                    ~~~~~~~~~~
╰→ com.wdullaer.materialdatetimepicker.date.DatePickerDialog instance
​     Leaking: YES (ObjectWatcher was watching this because com.wdullaer.materialdatetimepicker.date.DatePickerDialog received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​     key = dd153a49-8e66-45f7-8736-16342134e7f6
​     watchDurationMillis = 5297
​     retainedDurationMillis = 297

METADATA

Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.2
App process name: com.atco.forsite
Analysis duration: 8193 ms

Solution

  • According to the leak trace, I think the source of the issue is this line

    private val datePicker: DatePickerDialog
    

    in DatePickerOFI class.

    As a solution, I think you need to nullify datePicker instance variable once your job is done i.e the dialog is dismissed.