Утечка фрагмента при выходе из формы активности - PullRequest
0 голосов
/ 23 января 2020

Я не могу понять, как устранить утечку в моей PlabackActivity

, вот важная часть PlaybackActivity:

    class PlaybackActivity : BaseActivity() {

    private var playbackFragment: PlaybackFragment? = null


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

        val f = playbackFragment
            ?: run {
                val fragment = PlaybackFragment.newInstance()
                playbackFragment = fragment
                fragment
            }

        replacePlaybackFragment(f)

    }


    private fun replacePlaybackFragment(fragment: BasePlaybackFragment) {

        val ft = supportFragmentManager.beginTransaction()
        ft.replace(R.id.playbackFragmentContainer, fragment, fragment.javaClass.simpleName )
        ft.addToBackStack(null)
        ft.commit()
    }


    override fun onDestroy() {
        Timber.d("onDestroy()")
        playbackFragment = null
        super.onDestroy()
    }
}

ExoPlayerAdapter:

    class ExoPlayerAdapter(val context: Context) {

    var callback: ExoPlayerAdapterCallback? = null

    private val player: SimpleExoPlayer

    private val runnable: Runnable = object : Runnable {
        override fun run() {
            callback?.onCurrentPositionChanged(this@ExoPlayerAdapter)
            callback?.onBufferedPositionChanged(this@ExoPlayerAdapter)
            handler.postDelayed(this, 1000)
        }
    }
    private val handler = Handler()
    private var initialized = false

    private var mediaSourceUri: Uri? = null
    private var mediaSourceDrmUri: Uri? = null

    private var hasDisplay: Boolean = false
    private var bufferingStart: Boolean = false

    @C.StreamType
    var audioStreamType: Int = 0

    init {
        player = initExo2()

        player.addAnalyticsListener(object : AnalyticsListener {
            override fun onDownstreamFormatChanged(eventTime: AnalyticsListener.EventTime?, mediaLoadData: MediaSourceEventListener.MediaLoadData?) {
                mediaLoadData?.trackFormat
            }
        })
    }

    private fun initExo2(): SimpleExoPlayer {

        val defaultDrmSessionManager = DefaultDrmSessionManager<FrameworkMediaCrypto>(
            C.WIDEVINE_UUID,
            LicenceExoMediaDrm(),
            HttpMediaDrmCallback(),
            HashMap()
        )

        val exoPlayer = ExoPlayerFactory.newSimpleInstance(
            context,
            DefaultRenderersFactory(context),
            DefaultTrackSelector(AdaptiveTrackSelection.Factory()),
            DefaultLoadControl
                .Builder()
                .setAllocator(DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE * 512))
                .createDefaultLoadControl(),
            defaultDrmSessionManager
        )

        return exoPlayer
    }

    fun reset() {
        changeToUninitialized()
        player.stop()
    }

    private fun changeToUninitialized() {
        if (initialized) {
            initialized = false
            if (hasDisplay) {
                callback?.onPreparedStateChanged(this)
                onPreparedStateChanged()
            }
        }
    }

    fun release() {
        changeToUninitialized()
        hasDisplay = false
        player.release()
    }

    fun onDetachedFromHost() {
        reset()
        release()
    }
    internal fun setDisplay(surfaceHolder: SurfaceHolder?) {
        val hadDisplay = hasDisplay
        hasDisplay = surfaceHolder != null

        if (hadDisplay == hasDisplay) {
            return
        }

        player.setVideoSurfaceHolder(surfaceHolder)
        if (hasDisplay) {
            if (initialized) {
                callback?.onPreparedStateChanged(this)
                onPreparedStateChanged()
            }
        } else {
            if (initialized) {
                callback?.onPreparedStateChanged(this)
                onPreparedStateChanged()
            }
        }
    }

    fun setProgressUpdatingEnabled(enabled: Boolean) {
        handler.removeCallbacks(runnable)
        if (!enabled) {
            return
        }
        handler.postDelayed(runnable, 1000)
    }

    fun setDataSource(mediaSourceUri: Uri?, drmUri: Uri): Boolean {

        Timber.d("setDataSource uri=$mediaSourceUri drm=$drmUri")
        if (if (this.mediaSourceUri != null) this.mediaSourceUri == mediaSourceUri else mediaSourceUri == null) {
            return false
        }
        this.mediaSourceUri = mediaSourceUri
        this.mediaSourceDrmUri = drmUri

        prepareMediaForPlaying()
        return true
    }

    private fun onCreateMediaSource(uri: Uri): MediaSource {
        Timber.d("onCreateMediaSource() $uri")
        val userAgent = Util.getUserAgent(context, "ExoPlayerAdapter")
        val defaultDataSourceFactory = DefaultDataSourceFactory(context, userAgent)
        return createDashSource(uri, defaultDataSourceFactory)

    }

    private fun createDashSource(url: Uri, dataSourceFactory: DefaultDataSourceFactory): DashMediaSource {
        return DashMediaSource.Factory(
            DefaultDashChunkSource.Factory(dataSourceFactory),
            dataSourceFactory
        )
            .setManifestParser(object : DashManifestParser() {})
            .setLoadErrorHandlingPolicy(AmbLoadErrorHandlingPolicy())
            .createMediaSource(url)
    }

    private fun prepareMediaForPlaying() {
        reset()
        if (mediaSourceUri != null) {
            val mediaSource = onCreateMediaSource(mediaSourceUri)
            player.prepare(mediaSource, true, true)
        } else {
            return
        }

        notifyBufferingStartEnd()
        callback?.onPlayStateChanged(this@ExoPlayerAdapter)

        //after reload player with new live stream the progress bar are not updated
        //so enable updating is needed.
        setProgressUpdatingEnabled(true)
    }

    var playWhenPreparedCallback: () -> Unit = {}

    fun playWhenPrepared() {
        if (isPrepared()) {
            play()
        } else {
            playWhenPreparedCallback = { play() }
        }
    }

    private fun onPreparedStateChanged() {
        if (isPrepared()) {
            playWhenPreparedCallback()
            playWhenPreparedCallback = {}
        }
    }
    fun play() {
        player.playWhenReady = true
    }
    }

А вот отчет LeakCanary:

    HEAP ANALYSIS RESULT
====================================
2 APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.

232640 bytes retained by leaking objects
Signature: b79dd5a31e7bdd68a5fe42fe4139e4a5625ce7
┬───
│ GC Root: Input or output parameters in native code
│
├─ android.os.MessageQueue instance
│    Leaking: NO (MessageQueue#mQuitting is false)
│    ↓ MessageQueue.mMessages
│                   ~~~~~~~~~
├─ android.os.Message instance
│    Leaking: UNKNOWN
│    ↓ Message.callback
│              ~~~~~~~~
├─ tvapp.player.exo2.ExoPlayerAdapter$runnable$1 instance
│    Leaking: UNKNOWN
│    Anonymous class implementing java.lang.Runnable
│    ↓ ExoPlayerAdapter$runnable$1.this$0
│                                  ~~~~~~
├─ tvapp.player.exo2.ExoPlayerAdapter instance
│    Leaking: UNKNOWN
│    ↓ ExoPlayerAdapter.context
│                       ~~~~~~~
╰→ tvapp.player.PlaybackActivity instance
​     Leaking: YES (ObjectWatcher was watching this because tvapp.player.PlaybackActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
​     key = cdf5901a-cd24-49ba-b61f-d8d36e092dfa
​     watchDurationMillis = 15583
​     retainedDurationMillis = 10583

10621 bytes retained by leaking objects
Signature: 21cac0892d97b1f1d0dd68f578a458aa63ddc4f
┬───
│ GC Root: Input or output parameters in native code
│
├─ android.os.MessageQueue instance
│    Leaking: NO (MessageQueue#mQuitting is false)
│    ↓ MessageQueue.mMessages
│                   ~~~~~~~~~
├─ android.os.Message instance
│    Leaking: UNKNOWN
│    ↓ Message.callback
│              ~~~~~~~~
├─ tvapp.player.exo2.ExoPlayerAdapter$runnable$1 instance
│    Leaking: UNKNOWN
│    Anonymous class implementing java.lang.Runnable
│    ↓ ExoPlayerAdapter$runnable$1.this$0
│                                  ~~~~~~
├─ tvapp.player.exo2.ExoPlayerAdapter instance
│    Leaking: UNKNOWN
│    ↓ ExoPlayerAdapter.callback
│                       ~~~~~~~~
├─ tvapp.player.PlaybackFragment$setupUi$4 instance
│    Leaking: UNKNOWN
│    Anonymous subclass of tvapp.player.exo2.ExoPlayerAdapterCallback
│    ↓ PlaybackFragment$setupUi$4.this$0
│                                    ~~~~~~
╰→ tvapp.player.PlaybackFragment instance
​     Leaking: YES (ObjectWatcher was watching this because tvapp.player.PlaybackFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​     key = e4624d0a-b31a-4813-969b-08345bff6423
​     watchDurationMillis = 15489
​     retainedDurationMillis = 10488
​     key = eb37451d-0ff6-45be-ad7b-7120f391f03d
​     retainedDurationMillis = 10489

1 Ответ

0 голосов
/ 24 января 2020

ExoPlayerAdapter имеет поле context, являющееся экземпляром действия, оно имеет поле callback, являющееся внутренним классом PlaybackFragment, и непрерывно отправляет runnable в основной поток, который содержит для экземпляра ExoPlayerAdapter.

На Android объекты имеют жизненный цикл. Действия создаются, а затем уничтожаются, то же самое для фрагментов. Вы должны решить, каким должен быть жизненный цикл ExoPlayerAdapter, и соответственно написать свой код.

  • PlaybackFragment должен очистить обратный вызов ExoPlayerAdapter в Fragment.onDestroy()
  • Если ExoPlayerAdapter должен жить дольше, чем экземпляр действия, тогда вы должны передать ему контекст приложения вместо контекста действия. Если он должен жить так же долго, как экземпляр действия, тогда вы должны вызвать handler.removeCallbacks(runnable), когда действие уничтожено.
...