在ComposeView悬浮窗中收集DataStore<Preference>作为状态时的内存泄漏
问题现象
开发前台服务加载悬浮ComposeView,视图依赖Preference DataStore的配置数据,同时通过每秒更新多次的time流触发重组。初始运行正常,但几分钟后程序因OutOfMemoryError崩溃。Android Studio Profiler显示内存占用持续上升,但未检测到内存泄漏。
相关GC日志:
I Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 45% free, 29MB/53MB, paused 372us,2.483ms total 112.599ms
I Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 42% free, 32MB/56MB, paused 290us,1.993ms total 121.798ms
I Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 39% free, 36MB/60MB, paused 165us,1.812ms total 118.597ms
I Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 37% free, 39MB/63MB, paused 185us,2.268ms total 156.244ms
测试验证:
- 仅保留
time流的收集,内存占用稳定,无OOM问题 - 将相同ComposeView逻辑放在Activity中,无法复现该问题
核心代码片段
ComposeView内容:
// val dataStore: DataStore<Preferences> is a singleton val settings by dataStore.data.map { /* whatever */ }.collectAsStateWithLifecycle(initialValue, context = context) // Variable that is updated very often just to force recomposition val time by time.collectAsStateWithLifecycle(context = context) Text("${settings.test} $time")
完整复现代码
MainActivity
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Since this is a test app, we just assume the overlay and notification permissions are already granted // Don't forget to declare the permissions and the service in the manifest ContextCompat.startForegroundService(this, Intent(this, MyService::class.java)) } }
MyService
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "store") class MyService : LifecycleService(), SavedStateRegistryOwner { companion object { private const val STATUS_NOTIFICATION_ID = 1 private const val NOTIFICATION_CHANNEL_ID = "overlay_indicator" private val TEST = booleanPreferencesKey("test") } private data class Settings(val test: Boolean = true) private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.Main + job) private val savedStateRegistryController = SavedStateRegistryController.create(this) private val time = MutableStateFlow(0L) private lateinit var view: ComposeView override fun onCreate() { super.onCreate() savedStateRegistryController.performAttach() savedStateRegistryController.performRestore(null) view = ComposeView(this).apply { setViewTreeSavedStateRegistryOwner(this@MyService) setViewTreeLifecycleOwner(this@MyService) setContent { CompositionLocalProvider(LocalLifecycleOwner provides this@MyService) { val context = Dispatchers.Main + job val settings by dataStore.data.map { Settings(it[TEST] ?: true) }.collectAsStateWithLifecycle(Settings(), context = context) val time by time.collectAsStateWithLifecycle(context = context) Text("${settings.test} $time") } } } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) startForeground() val layoutParams = WindowManager.LayoutParams( WRAP_CONTENT, WRAP_CONTENT, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { TYPE_APPLICATION_OVERLAY } else { @Suppress("deprecation") TYPE_SYSTEM_ALERT }, FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT, ).apply { gravity = Gravity.BOTTOM } val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager try { windowManager.addView(view, layoutParams) } catch (_: Exception) { } scope.launch { var now = TimeSource.Monotonic.markNow() while (true) { val newNow = TimeSource.Monotonic.markNow() time.value += (newNow - now).inWholeMicroseconds now = newNow delay(timeMillis = 1) } } return START_NOT_STICKY } override fun onDestroy() { val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager.removeView(view) super.onDestroy() job.cancel() } override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry private fun startForeground() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( NOTIFICATION_CHANNEL_ID, "Channel", NotificationManager.IMPORTANCE_MIN ) val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground( STATUS_NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK ) } else { startForeground(STATUS_NOTIFICATION_ID, notification) } } }
内容的提问来源于stack exchange,提问作者E__Man

