实现Scope Storage后应用无响应问题排查求助
你的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

