泄漏金丝雀检测 MediaBrowserServiceCompat 示例应用程序中的内存泄漏

Kas*_*sen 5 android memory-leaks android-service leakcanary mediabrowserservice

我创建了一个实现 MediaBrowserServiceCompat 的测试应用程序。我已遵循本指南:\n https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice \n创建了 MediaPlaybackService 和 MainActivity。我添加了泄漏金丝雀,并在 onDestroy 方法中添加了 AppWatcher.objectWatcher.watch(this) 。打开和退出应用程序时,leak canary 会发现泄漏:

\n\n
6153 bytes retained\n    \xe2\x94\xac\n    \xe2\x94\x9c\xe2\x94\x80 android.service.media.MediaBrowserService$ServiceBinder\n    \xe2\x94\x82    Leaking: UNKNOWN\n    \xe2\x94\x82    GC Root: Global variable in native code\n    \xe2\x94\x82    \xe2\x86\x93 MediaBrowserService$ServiceBinder.this$0\n    \xe2\x94\x82                                        ~~~~~~\n    \xe2\x94\x9c\xe2\x94\x80 androidx.media.MediaBrowserServiceCompat$MediaBrowserServiceImplApi26$MediaBrowserServiceApi26\n    \xe2\x94\x82    Leaking: UNKNOWN\n    \xe2\x94\x82    MediaBrowserServiceCompat$MediaBrowserServiceImplApi26$MediaBrowserServiceApi26 does not wrap an activity context\n    \xe2\x94\x82    \xe2\x86\x93 MediaBrowserServiceCompat$MediaBrowserServiceImplApi26$MediaBrowserServiceApi26.mBase\n    \xe2\x94\x82                                                                                      ~~~~~\n    \xe2\x95\xb0\xe2\x86\x92 com.example.mediabrowsertestapp.MediaPlaybackService\n    \xe2\x80\x8b     Leaking: YES (ObjectWatcher was watching this)\n    \xe2\x80\x8b     MediaPlaybackService does not wrap an activity context\n    \xe2\x80\x8b     key = 11f40383-1498-4743-9f20-208cbd2839a1\n    \xe2\x80\x8b     watchDurationMillis = 5191\n    \xe2\x80\x8b     retainedDurationMillis = 183\n\nPlease include this in bug reports and Stack Overflow questions.\n\n    Build.VERSION.SDK_INT: 28\n    Build.MANUFACTURER: HMD Global\n    LeakCanary version: 2.0\n    App process name: com.example.mediabrowsertestapp\n    Analysis duration: 8967 ms\n    Heap dump file path: /data/user/0/com.example.mediabrowsertestapp/files/leakcanary/2019-12-10_10-21-47_693.hprof\n    Heap dump timestamp: 1575969720525\n
Run Code Online (Sandbox Code Playgroud)\n\n

由于该应用程序仅包含来自谷歌示例的代码,因此我无法弄清楚如何处理此泄漏。我应该忽略它吗?

\n\n

代码:\n https://github.com/finneapps/MediaBrowserService-memory-leak

\n\n
package com.example.mediabrowsertestapp\n\nimport android.os.Bundle\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.session.MediaSessionCompat\nimport android.support.v4.media.session.PlaybackStateCompat\nimport androidx.media.MediaBrowserServiceCompat\nimport leakcanary.AppWatcher\n\nprivate const val LOG_TAG = "MediaPlaybackService"\n\nclass MediaPlaybackService : MediaBrowserServiceCompat() {\n\n    private var mediaSession: MediaSessionCompat? = null\n    private lateinit var stateBuilder: PlaybackStateCompat.Builder\n\n    override fun onCreate() {\n        super.onCreate()\n        mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply {\n            setFlags(\n                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS\n                        or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS\n            )\n            stateBuilder = PlaybackStateCompat.Builder()\n                .setActions(\n                    PlaybackStateCompat.ACTION_PLAY\n                            or PlaybackStateCompat.ACTION_PLAY_PAUSE\n                )\n            setPlaybackState(stateBuilder.build())\n            setSessionToken(sessionToken)\n        }\n    }\n\n    override fun onGetRoot(\n        clientPackageName: String, clientUid: Int,\n        rootHints: Bundle?\n    ): BrowserRoot? {\n        return BrowserRoot(LOG_TAG, null)\n    }\n\n    override fun onLoadChildren(\n        parentMediaId: String,\n        result: Result<List<MediaBrowserCompat.MediaItem>>\n    ) {\n        result.sendResult(emptyList())\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        AppWatcher.objectWatcher.watch(this)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n
package com.example.mediabrowsertestapp\n\nimport android.content.ComponentName\nimport android.media.AudioManager\nimport androidx.appcompat.app.AppCompatActivity\nimport android.os.Bundle\nimport android.support.v4.media.MediaBrowserCompat\nimport android.support.v4.media.MediaMetadataCompat\nimport android.support.v4.media.session.MediaControllerCompat\nimport android.support.v4.media.session.PlaybackStateCompat\n\nclass MainActivity : AppCompatActivity() {\n    private val controllerCallback = object : MediaControllerCompat.Callback() {\n\n        override fun onMetadataChanged(metadata: MediaMetadataCompat?) {}\n\n        override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {}\n    }\n\n    private lateinit var mediaBrowser: MediaBrowserCompat\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n        mediaBrowser = MediaBrowserCompat(\n            this,\n            ComponentName(this, MediaPlaybackService::class.java),\n            connectionCallbacks,\n            null \n        )\n    }\n\n    override fun onStart() {\n        super.onStart()\n        mediaBrowser.connect()\n    }\n\n    override fun onResume() {\n        super.onResume()\n        volumeControlStream = AudioManager.STREAM_MUSIC\n    }\n\n    override fun onStop() {\n        super.onStop()\n        MediaControllerCompat.getMediaController(this)?.unregisterCallback(controllerCallback)\n        mediaBrowser.disconnect()\n    }\n\n    private val connectionCallbacks = object : MediaBrowserCompat.ConnectionCallback() {\n        override fun onConnected() {\n            mediaBrowser.sessionToken.also { token ->\n                val mediaController = MediaControllerCompat(\n                    this@MainActivity, // Context\n                    token\n                )\n                MediaControllerCompat.setMediaController(this@MainActivity, mediaController)\n            }\n\n        }\n\n        override fun onConnectionSuspended() {\n\n        }\n\n        override fun onConnectionFailed() {\n\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n
apply plugin: \'com.android.application\'\n\napply plugin: \'kotlin-android\'\n\napply plugin: \'kotlin-android-extensions\'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion "29.0.2"\n    defaultConfig {\n        applicationId "com.example.mediabrowsertestapp"\n        minSdkVersion 15\n        targetSdkVersion 29\n        versionCode 1\n        versionName "1.0"\n        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile(\'proguard-android-optimize.txt\'), \'proguard-rules.pro\'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \'libs\', include: [\'*.jar\'])\n    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"\n    implementation \'androidx.appcompat:appcompat:1.1.0\'\n    implementation \'androidx.core:core-ktx:1.1.0\'\n    implementation \'androidx.constraintlayout:constraintlayout:1.1.3\'\n    implementation "androidx.media:media:1.1.0"\n    debugImplementation \'com.squareup.leakcanary:leakcanary-android:2.0\'\n    testImplementation \'junit:junit:4.12\'\n    androidTestImplementation \'androidx.test.ext:junit:1.1.0\'\n    androidTestImplementation \'androidx.test.espresso:espresso-core:3.1.1\'\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Pie*_*cau 8

您的媒体服务扩展了 MediaBrowserServiceCompat。乍一看,这看起来像是 MediaBrowserServiceCompat 的问题。androidx.media:media:1.1.0是最新版本,MediaBrowserServiceCompat 的最新源目前位于此处

\n\n

MediaBrowserServiceCompat 是一个基服务类,它委托给 AOSP MediaBrowserService 类 ( sources ) 的子类。这里的一个棘手之处是,虽然 MediaBrowserService 是一项服务,但当由 MediaBrowserServiceCompat 使用时,它实际上并没有创建为真正的 Android 服务,而是仅创建为 MediaBrowserServiceCompat 向其传递回调的委托。这本身就意味着很容易犯错误。

\n\n

MediaBrowserService 子类保存对 MediaBrowserServiceCompat 实例的引用,以便

\n\n

泄漏跟踪显示存在对 MediaBrowserService$ServiceBinder 的本机引用。当 MediaBrowserServiceCompat 收到 onBind() 调用时,它会从 MediaBrowserService 返回绑定器。只要 MediaBrowserServiceCompat 还活着,该绑定器就应该被保留,并在它被销毁时释放。此时我们需要一个堆转储来进一步挖掘。

\n\n

我下载了源代码,构建了应用程序并将其部署在模拟器(API 29)上,并且能够通过按回键来重现泄漏。我注意到 MediaSessionCompat 构造函数 javadoc 声明“会话完成后,您必须调用 {@link #release()}”。我尝试在 onDestroy() 中调用它,但泄漏仍然发生。

\n\n

我想知道这种情况是否仅发生在应用程序兼容性中,或者也发生在 AOSP 中。我将代码移植回 AOSP(无兼容),同样的事情发生了。

\n\n
\xe2\x94\xac\n\xe2\x94\x9c\xe2\x94\x80 android.service.media.MediaBrowserService$ServiceBinder\n\xe2\x94\x82    Leaking: UNKNOWN\n\xe2\x94\x82    GC Root: Global variable in native code\n\xe2\x94\x82    \xe2\x86\x93 MediaBrowserService$ServiceBinder.this$0\n\xe2\x94\x82                                        ~~~~~~\n\xe2\x95\xb0\xe2\x86\x92 com.example.mediabrowsertestapp.MediaPlaybackService\n\xe2\x80\x8b     Leaking: YES (ObjectWatcher was watching this)\n\xe2\x80\x8b     MediaPlaybackService2 does not wrap an activity context\n\xe2\x80\x8b     key = e9c30a2e-e06e-4c4b-b375-f8c8c1482761\n\xe2\x80\x8b     watchDurationMillis = 5214\n\xe2\x80\x8b     retainedDurationMillis = 179\n\nMETADATA\n\nBuild.VERSION.SDK_INT: 25\nBuild.MANUFACTURER: Google\nLeakCanary version: 2.0\nApp process name: com.example.mediabrowsertestapp\nAnalysis duration: 2159 ms\n
Run Code Online (Sandbox Code Playgroud)\n\n

我删除了尽可能多的代码,然后发现泄漏仍然发生。这是最终的代码:

\n\n
class MediaPlaybackService : MediaBrowserService() {\n\n    override fun onLoadChildren(\n        parentId: String,\n        result: Result<MutableList<MediaBrowser.MediaItem>>\n    ) {\n        result.sendResult(mutableListOf())\n    }\n\n    override fun onGetRoot(\n        clientPackageName: String, clientUid: Int,\n        rootHints: Bundle?\n    ): BrowserRoot? {\n        return BrowserRoot("MediaPlaybackService", null)\n    }\n\n\n    override fun onDestroy() {\n        super.onDestroy()\n        AppWatcher.objectWatcher.watch(this)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n
class MainActivity : Activity() {\n    private lateinit var mediaBrowser: MediaBrowser\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n        mediaBrowser = MediaBrowser(\n            this,\n            ComponentName(this, MediaPlaybackService::class.java),\n            connectionCallbacks,\n            null\n        )\n    }\n\n    override fun onStart() {\n        super.onStart()\n        mediaBrowser.connect()\n    }\n\n    override fun onStop() {\n        super.onStop()\n        mediaBrowser.disconnect()\n    }\n\n    private val connectionCallbacks = object : MediaBrowser.ConnectionCallback() {\n        override fun onConnected() {\n        }\n\n        override fun onConnectionSuspended() {\n\n        }\n\n        override fun onConnectionFailed() {\n\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这可能是针对最新 Android 版本提出的问题,尽管它已经存在了一段时间。根据设计,进程间调用会导致绑定程序在内存中保存的时间比预期的要长。当 MediaBrowserService 被销毁时,MediaBrowserService.ServiceBinder 应该释放对其外部类 MediaBrowserService 的引用。

\n\n

这是在 AOSP 中重现它的 PR:https ://github.com/finneapps/MediaBrowserService-memory-leak/pull/1

\n