I have created a test app that implements MediaBrowserServiceCompat. I have followed this guide: https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice Created MediaPlaybackService and MainActivity. I have added leak canary and added AppWatcher.objectWatcher.watch(this) in the onDestroy method. When opening and exiting the app, leak canary finds a leak:
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
Since the app only contains code from the google sample, I can't figure out what to do with this leak. Should I just ignore it?
code: 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'
}
Your media service extends MediaBrowserServiceCompat. At first, this looks like an issue with MediaBrowserServiceCompat. androidx.media:media:1.1.0
is the latest release, and the latest sources for MediaBrowserServiceCompat are currently here.
MediaBrowserServiceCompat is a base service class that delegates to a subclass of the AOSP MediaBrowserService class (sources). One tricky bit here is that while MediaBrowserService is a service, when used by MediaBrowserServiceCompat it's not actually created as a real Android service but instead created as a mere delegate that MediaBrowserServiceCompat passes callbacks to. That in itself means it's easy to make mistakes.
The MediaBrowserService subclass holds a reference to the MediaBrowserServiceCompat instance so that t
The leak trace shows there's a native reference to MediaBrowserService$ServiceBinder. When MediaBrowserServiceCompat receives its onBind() call it returns the binder from MediaBrowserService. That binder should be held as long as MediaBrowserServiceCompat is alived, and released when it's destroyed. At this point we need a heap dump to dig further.
I downloaded the sources, built the app and deployed it on an emulator (API 29) and was able to reproduce the leak by pressing back. I noticed that the MediaSessionCompat constructor javadoc states "You must call {@link #release()} when finished with the session.". I tried calling that in onDestroy() but the leak still happens.
I'm wondering if this happens only with app compat, or also with AOSP. I ported the code back to AOSP (no compat) and the same thing is happening.
┬
├─ 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
I removed as much code as I could and then saw that the leak was still happening. Here's the final code:
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() {
}
}
}
This should likely be an issue filed against the latest Android version, even though it has been around for a while. By design, inter process calls lead to binders being held in memory longer than expecetd. MediaBrowserService.ServiceBinder should release it's reference to its outer class MediaBrowserService when MediaBrowserService is destroyed.
Here's a PR that reproduces it in AOSP: https://github.com/finneapps/MediaBrowserService-memory-leak/pull/1