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\n6153 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\nRun Code Online (Sandbox Code Playgroud)\n\n由于该应用程序仅包含来自谷歌示例的代码,因此我无法弄清楚如何处理此泄漏。我应该忽略它吗?
\n\n代码:\n https://github.com/finneapps/MediaBrowserService-memory-leak
\n\npackage 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}\nRun Code Online (Sandbox Code Playgroud)\n\npackage 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}\nRun Code Online (Sandbox Code Playgroud)\n\napply 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}\nRun Code Online (Sandbox Code Playgroud)\n
您的媒体服务扩展了 MediaBrowserServiceCompat。乍一看,这看起来像是 MediaBrowserServiceCompat 的问题。androidx.media:media:1.1.0是最新版本,MediaBrowserServiceCompat 的最新源目前位于此处。
MediaBrowserServiceCompat 是一个基服务类,它委托给 AOSP MediaBrowserService 类 ( sources ) 的子类。这里的一个棘手之处是,虽然 MediaBrowserService 是一项服务,但当由 MediaBrowserServiceCompat 使用时,它实际上并没有创建为真正的 Android 服务,而是仅创建为 MediaBrowserServiceCompat 向其传递回调的委托。这本身就意味着很容易犯错误。
\n\nMediaBrowserService 子类保存对 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\nRun Code Online (Sandbox Code Playgroud)\n\n我删除了尽可能多的代码,然后发现泄漏仍然发生。这是最终的代码:
\n\nclass 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}\nRun Code Online (Sandbox Code Playgroud)\n\nclass 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}\nRun 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