You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现Scope Storage后应用无响应问题排查求助

排查Android 11(API30)下Scope Storage导致的ANR问题

你的App在API28运行正常,但到了API30就出现ANR且无Logcat错误,大概率是主线程被频繁的UI更新或后台操作阻塞。结合你的代码,我梳理了几个核心问题点及对应的修复方案:

1. ContentObserver频繁触发主线程任务

ContentObserver的onChange方法默认在主线程执行,你在该方法中直接调用loadPhotosFromExternalStorageIntoRecyclerView(),会频繁启动协程查询媒体库并刷新RecyclerView。在API30+,系统媒体库的变更通知会更频繁(比如相册自动扫描、其他App的媒体操作),这会导致:

  • 大量协程同时执行IO查询,耗尽线程池资源
  • RecyclerView被频繁刷新,主线程忙于UI绘制

修复方案:

给ContentObserver指定后台线程的Handler,并添加防抖机制,避免短时间内重复触发数据加载:

private lateinit var mediaObserverThread: HandlerThread

private fun initContentObserver() {
    // 创建后台线程处理Observer回调
    mediaObserverThread = HandlerThread("MediaObserverThread")
    mediaObserverThread.start()
    val handler = Handler(mediaObserverThread.looper)

    contentObserver = object : ContentObserver(handler) {
        private var debounceRunnable: Runnable? = null

        override fun onChange(selfChange: Boolean) {
            if(readPermissionGranted) {
                // 移除之前的任务,避免重复触发
                debounceRunnable?.let { handler.removeCallbacks(it) }
                // 延迟500ms执行,过滤频繁的变更通知
                debounceRunnable = Runnable {
                    loadPhotosFromExternalStorageIntoRecyclerView()
                }
                debounceRunnable?.let { handler.postDelayed(it, 500) }
            }
        }
    }
    contentResolver.registerContentObserver(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        true,
        contentObserver
    )
}

记得在onDestroy中停止后台线程:

override fun onDestroy() {
    super.onDestroy()
    contentResolver.unregisterContentObserver(contentObserver)
    mediaObserverThread.quitSafely()
}

2. 内部存储图片加载未做压缩优化

loadPhotosFromInternalStorage中,你直接将整个图片文件读入内存并解码成Bitmap,如果图片数量多或分辨率高,会导致IO线程长时间占用,甚至引发内存抖动,间接拖慢主线程。

修复方案:

使用BitmapFactory.Options进行采样压缩,避免加载过大的Bitmap到内存:

private suspend fun loadPhotosFromInternalStorage(): List<InternalStoragePhoto> {
    return withContext(Dispatchers.IO) {
        val files = filesDir.listFiles()
        files?.filter { it.canRead() && it.isFile && it.name.endsWith(".jpg") }?.map {
            val options = BitmapFactory.Options().apply {
                // 先获取图片尺寸,不加载到内存
                inJustDecodeBounds = true
                BitmapFactory.decodeFile(it.path, this)
                // 计算采样率,缩放到512px以内
                val scaleFactor = max(1, max(outWidth / 512, outHeight / 512))
                inJustDecodeBounds = false
                inSampleSize = scaleFactor
                inPreferredConfig = Bitmap.Config.RGB_565 // 减少内存占用
            }
            val bmp = BitmapFactory.decodeFile(it.path, options)
            InternalStoragePhoto(it.name, bmp)
        } ?: listOf()
    }
}

3. 删除图片后未清理临时Uri

在API30+删除图片时,调用MediaStore.createDeleteRequest后,若未清空deletedImageUri,可能导致后续重复执行删除逻辑,加重线程负担。

修复方案:

在intentSenderLauncher的回调中清空临时Uri:

intentSenderLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
    if(it.resultCode == RESULT_OK) {
        if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
            lifecycleScope.launch {
                deletePhotoFromExternalStorage(deletedImageUri ?: return@launch)
            }
        }
        Toast.makeText(this@MainActivity, "Photo deleted successfully", Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(this@MainActivity, "Photo couldn't be deleted", Toast.LENGTH_SHORT).show()
    }
    // 清空临时Uri,避免重复操作
    deletedImageUri = null
}

4. 权限请求逻辑优化(可选)

API30+已废弃WRITE_EXTERNAL_STORAGE权限,可明确移除该版本下的权限请求,避免不必要的弹窗:

private fun updateOrRequestPermissions() {
    val hasReadPermission = ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.READ_EXTERNAL_STORAGE
    ) == PackageManager.PERMISSION_GRANTED
    val hasWritePermission = ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
    ) == PackageManager.PERMISSION_GRANTED
    val minSdk29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q

    readPermissionGranted = hasReadPermission
    writePermissionGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        true // API30+无需WRITE权限
    } else {
        hasWritePermission || minSdk29
    }

    val permissionsToRequest = mutableListOf<String>()
    if(!readPermissionGranted) {
        permissionsToRequest.add(Manifest.permission.READ_EXTERNAL_STORAGE)
    }
    // 仅在API28及以下请求WRITE权限
    if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && !writePermissionGranted) {
        permissionsToRequest.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    }

    if(permissionsToRequest.isNotEmpty()) {
        permissionsLauncher.launch(permissionsToRequest.toTypedArray())
    }
}

额外排查建议

  • 使用Android Studio的Profile工具查看主线程CPU占用,定位阻塞方法
  • 开启StrictMode检测主线程IO操作:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (BuildConfig.DEBUG) {
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()
            .detectDiskWrites()
            .penaltyLog()
            .build())
    }
    // 其他初始化代码...
}

内容的提问来源于stack exchange,提问作者Afaq Khan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 17:24:12