leak canary 检测到 MediaBrowserServiceCompat 示例应用程序中的内存泄漏
leak canary detects memory leak in MediaBrowserServiceCompat sample app
我创建了一个实现 MediaBrowserServiceCompat 的测试应用程序。我遵循了本指南:
https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice
创建了 MediaPlaybackService 和 MainActivity。我添加了 leak canary 并在 onDestroy 方法中添加了 AppWatcher.objectWatcher.watch(this) 。打开和退出应用程序时,leak canary 发现泄漏:
6153 bytes retained
┬
├─ android.service.media.MediaBrowserService$ServiceBinder
│ Leaking: UNKNOWN
│ GC Root: Global variable in native code
│ ↓ MediaBrowserService$ServiceBinder.this[=10=]
│ ~~~~~~
├─ 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'
}
您的媒体服务扩展了 MediaBrowserServiceCompat。起初,这看起来像是 MediaBrowserServiceCompat 的问题。 androidx.media:media:1.1.0
是最新版本,MediaBrowserServiceCompat 的最新来源目前是 here。
MediaBrowserServiceCompat 是一项基本服务 class,它委托给 AOSP MediaBrowserService class (sources) 的子 class。这里有一个棘手的问题是,虽然 MediaBrowserService 是一项服务,但当它被 MediaBrowserServiceCompat 使用时,它实际上并不是作为真正的 Android 服务创建的,而是作为 MediaBrowserServiceCompat 将回调传递给的委托创建的。这本身就意味着很容易出错。
MediaBrowserService subclass 持有对 MediaBrowserServiceCompat 实例的引用,因此 t
泄漏跟踪显示存在对 MediaBrowserService$ServiceBinder 的本机引用。当 MediaBrowserServiceCompat 收到它的 onBind() 时,调用它 returns 来自 MediaBrowserService 的活页夹。只要 MediaBrowserServiceCompat 还活着,就应该持有该活页夹,并在它被销毁时释放。此时我们需要一个堆转储来进一步挖掘。
我下载了源代码,构建了应用程序并将其部署在模拟器 (API 29) 上,并且能够通过按回键来重现泄漏。我注意到 MediaSessionCompat 构造函数 javadoc 指出 "You must call {@link #release()} when finished with the session."。我尝试在 onDestroy() 中调用它,但泄漏仍然发生。
我想知道这是否只发生在应用程序兼容性上,或者也发生在 AOSP 上。我将代码移植回 AOSP(没有兼容),同样的事情发生了。
┬
├─ android.service.media.MediaBrowserService$ServiceBinder
│ Leaking: UNKNOWN
│ GC Root: Global variable in native code
│ ↓ MediaBrowserService$ServiceBinder.this[=10=]
│ ~~~~~~
╰→ 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 被销毁时释放它对其外部 class MediaBrowserService 的引用。
这是在 AOSP 中复制它的 PR:https://github.com/finneapps/MediaBrowserService-memory-leak/pull/1
我创建了一个实现 MediaBrowserServiceCompat 的测试应用程序。我遵循了本指南: https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice 创建了 MediaPlaybackService 和 MainActivity。我添加了 leak canary 并在 onDestroy 方法中添加了 AppWatcher.objectWatcher.watch(this) 。打开和退出应用程序时,leak canary 发现泄漏:
6153 bytes retained
┬
├─ android.service.media.MediaBrowserService$ServiceBinder
│ Leaking: UNKNOWN
│ GC Root: Global variable in native code
│ ↓ MediaBrowserService$ServiceBinder.this[=10=]
│ ~~~~~~
├─ 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'
}
您的媒体服务扩展了 MediaBrowserServiceCompat。起初,这看起来像是 MediaBrowserServiceCompat 的问题。 androidx.media:media:1.1.0
是最新版本,MediaBrowserServiceCompat 的最新来源目前是 here。
MediaBrowserServiceCompat 是一项基本服务 class,它委托给 AOSP MediaBrowserService class (sources) 的子 class。这里有一个棘手的问题是,虽然 MediaBrowserService 是一项服务,但当它被 MediaBrowserServiceCompat 使用时,它实际上并不是作为真正的 Android 服务创建的,而是作为 MediaBrowserServiceCompat 将回调传递给的委托创建的。这本身就意味着很容易出错。
MediaBrowserService subclass 持有对 MediaBrowserServiceCompat 实例的引用,因此 t
泄漏跟踪显示存在对 MediaBrowserService$ServiceBinder 的本机引用。当 MediaBrowserServiceCompat 收到它的 onBind() 时,调用它 returns 来自 MediaBrowserService 的活页夹。只要 MediaBrowserServiceCompat 还活着,就应该持有该活页夹,并在它被销毁时释放。此时我们需要一个堆转储来进一步挖掘。
我下载了源代码,构建了应用程序并将其部署在模拟器 (API 29) 上,并且能够通过按回键来重现泄漏。我注意到 MediaSessionCompat 构造函数 javadoc 指出 "You must call {@link #release()} when finished with the session."。我尝试在 onDestroy() 中调用它,但泄漏仍然发生。
我想知道这是否只发生在应用程序兼容性上,或者也发生在 AOSP 上。我将代码移植回 AOSP(没有兼容),同样的事情发生了。
┬
├─ android.service.media.MediaBrowserService$ServiceBinder
│ Leaking: UNKNOWN
│ GC Root: Global variable in native code
│ ↓ MediaBrowserService$ServiceBinder.this[=10=]
│ ~~~~~~
╰→ 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 被销毁时释放它对其外部 class MediaBrowserService 的引用。
这是在 AOSP 中复制它的 PR:https://github.com/finneapps/MediaBrowserService-memory-leak/pull/1