androidkotlinmemory-leaksandroid-sensorssensormanager

Memory leak in SensorManager implementation in parent class


I have handled screen orientation using SensorManager and it's working fine

But there is some memory leak as it is set in BaseActivity like

abstract class BaseActivity : AppCompatActivity(), SensorOrientationChangeNotifier.Listener  {
   override fun onResume() {
        super.onResume()
        SensorOrientationChangeNotifier.getInstance(this)?.addListener(this)
    }

    override fun onPause() {
        super.onPause()
        SensorOrientationChangeNotifier.getInstance(this)?.remove(this)
    }
}

SensorOrientationChangeNotifier.kt

class SensorOrientationChangeNotifier private constructor(applicationContext: Context) {

    private val mListeners = ArrayList<WeakReference<Listener?>>(3)
    var orientation = 0
        private set
    private val mSensorEventListener: SensorEventListener
    private val mSensorManager: SensorManager

    init {
        mSensorEventListener = NotifierSensorEventListener()
        mSensorManager =
            applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }

    /**
     * Call on activity reset()
     */
    private fun onResume() {
        mSensorManager.registerListener(
            mSensorEventListener,
            mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
            SensorManager.SENSOR_DELAY_NORMAL
        )
    }

    /**
     * Call on activity onPause()
     */
    private fun onPause() {
        mSensorManager.unregisterListener(mSensorEventListener)
    }

    private inner class NotifierSensorEventListener : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent) {
            val x = event.values[0]
            val y = event.values[1]
            var newOrientation: Int = orientation
            if (x < 5 && x > -5 && y > 5) newOrientation =
                0 else if (x < -5 && y < 5 && y > -5) newOrientation =
                90 else if (x < 5 && x > -5 && y < -5) newOrientation =
                180 else if (x > 5 && y < 5 && y > -5) newOrientation = 270


            ///Timber.v("Matas_mOrientation = "+ newOrientation+"   ["+event.values[0]+","+event.values[1]+","+event.values[2]+"]");
            if (orientation != newOrientation) {
                Timber.v("Matas_mOrientation-Orientation-> NEW")
                orientation = newOrientation
                notifyListeners()
            } else {
                ///Timber.v("Matas_mOrientation-Orientation-> OLD");
            }
        }

        override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
    }

    interface Listener {
        fun onOrientationChange(orientation: Int)
    }

    fun addListener(listener: Listener) {
        if (get(listener) == null) // prevent duplications
            mListeners.add(WeakReference(listener))
        if (mListeners.size == 1) {
            onResume() // this is the first client
        }
    }

    fun remove(listener: Listener) {
        val listenerWR = get(listener)
        remove(listenerWR)
    }

    private fun remove(listenerWR: WeakReference<Listener?>?) {
        if (listenerWR != null) mListeners.remove(listenerWR)
        if (mListeners.size == 0) {
            onPause()
        }
    }

    private operator fun get(listener: Listener): WeakReference<Listener?>? {
        for (existingListener in mListeners) if (existingListener.get() === listener) return existingListener
        return null
    }

    private fun notifyListeners() {
        val deadLiksArr = ArrayList<WeakReference<Listener?>>()
        for (wr in mListeners) {
            if (wr.get() == null) deadLiksArr.add(wr) else wr.get()!!
                .onOrientationChange(orientation)
        }

        // remove dead references
        for (wr in deadLiksArr) {
            mListeners.remove(wr)
        }
    }

    val isPortrait: Boolean = orientation == 0 || orientation == 180
    val isLandscape: Boolean = !isPortrait

    companion object {
        val TAG = javaClass.simpleName
        private var mInstance: SensorOrientationChangeNotifier? = null
        fun getInstance(applicationContext: Context): SensorOrientationChangeNotifier? {
            if (mInstance == null) mInstance = SensorOrientationChangeNotifier(applicationContext)
            return mInstance
        }
    }
}

I think my context is leaking as user switch from one activity to another But mInstance is not null due to singleton object

Can someone suggest a better approach to implement it once without memory leak in BaseActivity

I think I should directly pass SensorManager with applicationContext

     val sensorManager = applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
     SensorOrientationChangeNotifier.getInstance(sensorManager)?.addListener(this)

Solution

  • Your SensorOrientationChangeNotifier class holds a reference to the Context you pass to it. Since you pass the Activity as your Context parameter, it is holding a reference to your Activity for as long as it is alive, which is forever since it is a singleton. You were naming your parameters applicationContext but in practice you were using an Activity instance instead.

    To fix it, make sure you pull the applicationContext out of the Activity and use that. You can't leak the Application's Context, because it lasts for the lifetime of the application instance.

    companion object {
        val TAG = javaClass.simpleName
        private var mInstance: SensorOrientationChangeNotifier? = null
        fun getInstance(context: Context): SensorOrientationChangeNotifier? {
            if (mInstance == null) mInstance = SensorOrientationChangeNotifier(context.applicationContext)
            return mInstance
        }
    }
    

    Also, there's no reason to be returning a nullable from your getInstance() function. And your TAG right now evaluates to "Companion", which isn't very useful. Here it is with both of those things fixed:

    companion object {
        val TAG = SensorOrientationChangeNotifier::class.simpleName
        private var instance: SensorOrientationChangeNotifier? = null
        fun getInstance(context: Context): SensorOrientationChangeNotifier {
            return instance ?: 
                SensorOrientationChangeNotifier(context.applicationContext).also { instance = it }
        }
    }