Утечка канарейки обнаруживает утечку памяти в образце приложения MediaBrowserServiceCompat

Я создал тестовое приложение, которое реализует MediaBrowserServiceCompat. Я следовал этому руководству: https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowservice Созданы MediaPlaybackService и MainActivity. Я добавил канарейку утечки и добавил AppWatcher.objectWatcher.watch (this) в метод onDestroy. При открытии и выходе из приложения Leak Canary находит утечку:

6153 bytes retained
    ┬
    ├─ android.service.media.MediaBrowserService$ServiceBinder
    │    Leaking: UNKNOWN
    │    GC Root: Global variable in native code
    │    ↓ MediaBrowserService$ServiceBinder.this$0
    │                                        ~~~~~~
    ├─ androidx.media.MediaBrowserServiceCompat$MediaBrowserServiceImplApi26$MediaBrowserServiceApi26
    │    Leaking: UNKNOWN
    │    MediaBrowserServiceCompat$MediaBrowserServiceImplApi26$MediaBrowserServiceApi26 does not wrap an activity context
    │    ↓ MediaBrowserServiceCompat$MediaBrowserServiceImplApi26$MediaBrowserServiceApi26.mBase
    │                                                                                      ~~~~~
    ╰→ com.example.mediabrowsertestapp.MediaPlaybackService
    ​     Leaking: YES (ObjectWatcher was watching this)
    ​     MediaPlaybackService does not wrap an activity context
    ​     key = 11f40383-1498-4743-9f20-208cbd2839a1
    ​     watchDurationMillis = 5191
    ​     retainedDurationMillis = 183

Please include this in bug reports and Stack Overflow questions.

    Build.VERSION.SDK_INT: 28
    Build.MANUFACTURER: HMD Global
    LeakCanary version: 2.0
    App process name: com.example.mediabrowsertestapp
    Analysis duration: 8967 ms
    Heap dump file path: /data/user/0/com.example.mediabrowsertestapp/files/leakcanary/2019-12-10_10-21-47_693.hprof
    Heap dump timestamp: 1575969720525

Поскольку приложение содержит только код из образца Google, я не могу понять, что делать с этой утечкой. Мне просто игнорировать это?

код: https://github.com/finneapps/MediaBrowserService-memory-leak

package com.example.mediabrowsertestapp

import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.MediaBrowserServiceCompat
import leakcanary.AppWatcher

private const val LOG_TAG = "MediaPlaybackService"

class MediaPlaybackService : MediaBrowserServiceCompat() {

    private var mediaSession: MediaSessionCompat? = null
    private lateinit var stateBuilder: PlaybackStateCompat.Builder

    override fun onCreate() {
        super.onCreate()
        mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply {
            setFlags(
                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                        or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
            )
            stateBuilder = PlaybackStateCompat.Builder()
                .setActions(
                    PlaybackStateCompat.ACTION_PLAY
                            or PlaybackStateCompat.ACTION_PLAY_PAUSE
                )
            setPlaybackState(stateBuilder.build())
            setSessionToken(sessionToken)
        }
    }

    override fun onGetRoot(
        clientPackageName: String, clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        return BrowserRoot(LOG_TAG, null)
    }

    override fun onLoadChildren(
        parentMediaId: String,
        result: Result<List<MediaBrowserCompat.MediaItem>>
    ) {
        result.sendResult(emptyList())
    }

    override fun onDestroy() {
        super.onDestroy()
        AppWatcher.objectWatcher.watch(this)
    }
}
package com.example.mediabrowsertestapp

import android.content.ComponentName
import android.media.AudioManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.PlaybackStateCompat

class MainActivity : AppCompatActivity() {
    private val controllerCallback = object : MediaControllerCompat.Callback() {

        override fun onMetadataChanged(metadata: MediaMetadataCompat?) {}

        override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {}
    }

    private lateinit var mediaBrowser: MediaBrowserCompat

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mediaBrowser = MediaBrowserCompat(
            this,
            ComponentName(this, MediaPlaybackService::class.java),
            connectionCallbacks,
            null 
        )
    }

    override fun onStart() {
        super.onStart()
        mediaBrowser.connect()
    }

    override fun onResume() {
        super.onResume()
        volumeControlStream = AudioManager.STREAM_MUSIC
    }

    override fun onStop() {
        super.onStop()
        MediaControllerCompat.getMediaController(this)?.unregisterCallback(controllerCallback)
        mediaBrowser.disconnect()
    }

    private val connectionCallbacks = object : MediaBrowserCompat.ConnectionCallback() {
        override fun onConnected() {
            mediaBrowser.sessionToken.also { token ->
                val mediaController = MediaControllerCompat(
                    this@MainActivity, // Context
                    token
                )
                MediaControllerCompat.setMediaController(this@MainActivity, mediaController)
            }

        }

        override fun onConnectionSuspended() {

        }

        override fun onConnectionFailed() {

        }
    }
}
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.example.mediabrowsertestapp"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation "androidx.media:media:1.1.0"
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

person Kasper Finne Nielsen    schedule 10.12.2019    source источник
comment
У меня точно такая же проблема. Есть ли обходной путь?   -  person Ken Zira    schedule 23.05.2020


Ответы (1)


Ваш медиа-сервис расширяет MediaBrowserServiceCompat. Сначала это выглядит как проблема с MediaBrowserServiceCompat. androidx.media:media:1.1.0 - это последний выпуск, а последние исходные коды MediaBrowserServiceCompat в настоящее время здесь.

MediaBrowserServiceCompat - это базовый класс службы, который делегирует подклассу класса AOSP MediaBrowserService (sources). Здесь есть одна хитрость: хотя MediaBrowserService - это служба, при использовании MediaBrowserServiceCompat она фактически не создается как настоящая служба Android, а вместо этого создается как простой делегат, которому MediaBrowserServiceCompat передает обратные вызовы. Это само по себе означает, что ошибиться легко.

Подкласс MediaBrowserService содержит ссылку на экземпляр MediaBrowserServiceCompat, так что t

Трассировка утечки показывает, что существует собственная ссылка на MediaBrowserService $ ServiceBinder. Когда MediaBrowserServiceCompat получает свой вызов onBind (), он возвращает связыватель из MediaBrowserService. Этот связыватель должен храниться, пока MediaBrowserServiceCompat активен, и освобождаться после его уничтожения. На этом этапе нам нужен дамп кучи, чтобы копать дальше.

Я загрузил исходники, собрал приложение и развернул его на эмуляторе (API 29) и смог воспроизвести утечку, нажав назад. Я заметил, что в javadoc конструктора MediaSessionCompat указано: «Вы должны вызвать {@link #release ()} по завершении сеанса». Я попытался вызвать это в onDestroy (), но утечка все еще происходит.

Мне интересно, происходит ли это только с совместимостью приложений или также с AOSP. Я перенес код обратно в AOSP (без совместимости), и происходит то же самое.

┬
├─ android.service.media.MediaBrowserService$ServiceBinder
│    Leaking: UNKNOWN
│    GC Root: Global variable in native code
│    ↓ MediaBrowserService$ServiceBinder.this$0
│                                        ~~~~~~
╰→ com.example.mediabrowsertestapp.MediaPlaybackService
​     Leaking: YES (ObjectWatcher was watching this)
​     MediaPlaybackService2 does not wrap an activity context
​     key = e9c30a2e-e06e-4c4b-b375-f8c8c1482761
​     watchDurationMillis = 5214
​     retainedDurationMillis = 179

METADATA

Build.VERSION.SDK_INT: 25
Build.MANUFACTURER: Google
LeakCanary version: 2.0
App process name: com.example.mediabrowsertestapp
Analysis duration: 2159 ms

Я удалил столько кода, сколько смог, а затем увидел, что утечка все еще происходит. Вот последний код:

class MediaPlaybackService : MediaBrowserService() {

    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) {
        result.sendResult(mutableListOf())
    }

    override fun onGetRoot(
        clientPackageName: String, clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        return BrowserRoot("MediaPlaybackService", null)
    }


    override fun onDestroy() {
        super.onDestroy()
        AppWatcher.objectWatcher.watch(this)
    }
}
class MainActivity : Activity() {
    private lateinit var mediaBrowser: MediaBrowser

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mediaBrowser = MediaBrowser(
            this,
            ComponentName(this, MediaPlaybackService::class.java),
            connectionCallbacks,
            null
        )
    }

    override fun onStart() {
        super.onStart()
        mediaBrowser.connect()
    }

    override fun onStop() {
        super.onStop()
        mediaBrowser.disconnect()
    }

    private val connectionCallbacks = object : MediaBrowser.ConnectionCallback() {
        override fun onConnected() {
        }

        override fun onConnectionSuspended() {

        }

        override fun onConnectionFailed() {

        }
    }
}

Скорее всего, это проблема, связанная с последней версией Android, хотя она существует уже некоторое время. По замыслу межпроцессные вызовы приводят к тому, что связыватели хранятся в памяти дольше, чем ожидалось. MediaBrowserService.ServiceBinder должен освободить ссылку на свой внешний класс MediaBrowserService при уничтожении MediaBrowserService.

Вот PR, который воспроизводит это в AOSP: https://github.com/finneapps/MediaBrowserService-memory-leak/pull/1

person Pierre-Yves Ricau    schedule 10.12.2019
comment
спасибо за очень подробный ответ, очень признателен. Я отправил сообщение о проблеме: Issuesetracker.google.com/issues/146008870 - person Kasper Finne Nielsen; 11.12.2019