Android截图保存到相册:攻克小米MIUI兼容性难题的完整方案

📅 2026/8/17 9:52:06
Android截图保存到相册:攻克小米MIUI兼容性难题的完整方案
1. 项目背景与核心挑战最近在做一个社区类App里面有个高频需求用户想把某个精彩的帖子或者个人主页分享出去但直接分享链接不够直观产品经理希望用户能“一键生成带内容的精美截图”并直接保存到手机相册方便他们分享到微信、微博等社交平台。这个需求听起来简单不就是截图加保存吗但真做起来尤其是在Android这个“碎片化”的生态里坑是一个接一个。最典型的就是在小米MIUI系统上你按照Google官方文档写的保存图片到相册的代码在其他手机上跑得好好的一到小米手机上相册里死活找不到刚保存的图片或者提示“保存失败”。用户反馈接踵而至体验直接降级。这背后的核心矛盾在于Android的存储权限和文件访问机制变迁。从Android 10API 29开始作用域存储Scoped Storage被强制执行应用不能像以前一样随意在公共目录如DCIM、Pictures下新建文件了。虽然官方提供了MediaStore API来向公共媒体库插入内容但各大厂商特别是国内深度定制的UI如MIUI、EMUI对这套机制的实现和权限弹窗策略各有各的“理解”导致兼容性问题层出不穷。小米手机在这方面尤为“个性”其相册扫描机制、权限管理策略与原生Android存在差异需要特别适配。所以这个项目的目标非常明确实现一个高兼容性、稳定可靠的Android截图保存到相册功能并重点攻克小米机型的适配难题。它不仅仅是调用系统截图或画布绘制更是一套涵盖视图渲染、图片处理、存储策略和厂商适配的完整解决方案。2. 功能实现的核心技术栈与选型在动手写代码之前我们先得把技术路线定下来。一个完整的“截图并保存到相册”功能可以拆解为几个核心环节每个环节都有不同的技术选项。2.1 视图截取如何拿到“屏幕”的Bitmap这里说的“屏幕”不是物理屏幕而是我们想要截取的特定视图View。通常有两种主流方式View.draw(Canvas)方法这是最通用、最可靠的方式。原理是创建一个与目标View宽高相同的空白Bitmap然后以此为画布调用View的draw方法将其内容绘制上去。这种方式能捕获View的所有子视图内容包括通过Canvas自定义绘制的内容。fun captureView(view: View): Bitmap { val bitmap Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) view.draw(canvas) return bitmap }为什么选它兼容性极佳从古老的Android版本到最新的Android 14都能稳定工作。它是我们方案的基石。View.getDrawingCache()方法这个方法在更早的教程里很常见它需要先启用绘图缓存view.setDrawingCacheEnabled(true)然后获取。但这个方法在Android API 28Pie后被标记为deprecated官方不建议使用因为其内存管理和性能不如draw(Canvas)可控。为了长远兼容我们直接放弃这个选项。注意如果要截取整个屏幕包括状态栏和导航栏需要用到MediaProjectionAPI这需要动态申请权限流程复杂且通常用于录屏或高级截图工具。我们当前场景是截取应用内特定视图因此View.draw(Canvas)完全够用。2.2 图片处理优化与美化拿到原始的Bitmap只是第一步直接保存往往效果不佳我们需要处理质量压缩原始Bitmap如果是ARGB_8888格式每个像素占4字节一张1080x1920的截图就近8MB。直接保存太大分享和上传都慢。我们需要用Bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)进行有损压缩通常quality设置在80-90之间能在画质和文件大小间取得很好平衡。添加水印或边框产品可能要求在截图角落加上App Logo或用户昵称。这需要在绘制原始Bitmap到新Canvas后再用Canvas.drawBitmap()或Canvas.drawText()叠加水印图层。处理滚动视图如RecyclerView这是另一个常见需求。对于长内容需要截取整个滚动视图。方法是将RecyclerView或ScrollView的LayoutManager或子View的高度进行测量然后创建一个足够大的Bitmap通过循环或调整视图布局的方式分块绘制。这部分逻辑较复杂本文重点在保存故不展开但方案核心是Canvas.translate()配合分块绘制。2.3 保存到相册兼容性的主战场这是最棘手的部分也是适配小米的关键。方案演进如下古老方式Android 9及以前直接通过FileAPI在Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)路径下创建图片文件然后通过MediaScannerConnection扫描通知系统相册。在Android 10及以上此方法在无特殊权限下完全失效。现代标准方式Android 10使用MediaStoreAPI。构建ContentValues设置文件名DISPLAY_NAME、MIME类型MIME_TYPE、存储位置RELATIVE_PATH例如Pictures/YourAppName等。通过ContentResolver插入contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)这会返回一个该媒体文件的Uri。通过Uri打开输出流contentResolver.openOutputStream(uri)然后将压缩后的Bitmap数据写入这个流。 这个方法将文件管理的职责交给了系统应用不需要申请WRITE_EXTERNAL_STORAGE权限在Android 10上该权限作用已变。然而问题就出在这里部分小米机型MIUI 12/13某些版本的相册应用对MediaStore的响应不及时或者其媒体扫描服务MediaScanner有延迟或bug导致文件写入后相册里无法立即看到甚至永远不显示。2.4 小米适配的核心策略针对小米的“特殊性”我们需要在标准方案上增加一个“双保险”策略首要路径依然使用MediaStoreAPI进行插入和写入。这是符合Android规范的正道。备用路径针对小米等特定机型在MediaStore写入成功后额外使用MediaScannerConnection.scanFile()主动扫描我们刚刚写入的文件路径。这个API会直接广播一个媒体扫描的Intent强制系统媒体库立即更新。虽然从Android 10开始官方建议优先使用MediaStore但scanFile在兼容旧设备和触发某些厂商相册刷新时依然有效。文件路径的获取如何获得通过MediaStore写入的文件的实际路径我们可以通过ContentResolver和返回的Uri使用DocumentsContract或直接查询MediaStore来获取其_data字段注意_data字段在Android Q后部分情况不可直接访问但通过Uri可以获取文件描述符进行读取。更简单的方法是在插入时我们可以将文件先保存到应用的私有缓存目录然后再将其字节流写入MediaStore提供的OutputStream这样我们就始终拥有源文件可以用来触发扫描。这个“标准MediaStore写入 备用主动扫描”的双轨制是解决小米机型兼容性问题实践下来最有效的手段。3. 分步实现与代码详解接下来我们进入实战环节一步步构建这个功能模块。我将使用Kotlin进行演示并融入必要的错误处理和兼容性判断。3.1 第一步获取截图Bitmap我们创建一个通用的截图工具函数。这里需要考虑View可能还没有被完全测量和布局的情况。import android.graphics.Bitmap import android.graphics.Canvas import android.view.View object ScreenshotUtils { /** * 捕获一个View的截图 * param view 需要截图的View * return 截图的Bitmap可能为null */ fun captureView(view: View): Bitmap? { // 确保View已经完成布局 if (view.width 0 || view.height 0) { view.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) view.layout(0, 0, view.measuredWidth, view.measuredHeight) } val bitmap Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) val canvas Canvas(bitmap) // 设置画布背景色防止透明背景变成黑色 canvas.drawColor(Color.WHITE) // 根据你的UI背景色调整 view.draw(canvas) return bitmap } }实操心得view.draw(canvas)默认会保留View原有的背景。如果你的View背景是透明的绘制到Bitmap上后透明部分在保存为JPEG时会变成黑色JPEG不支持透明通道。因此如果最终要存为JPEG最好先在Canvas上绘制一个白色背景。如果保存为PNG则无需此步骤。3.2 第二步处理与压缩Bitmap获取Bitmap后我们将其压缩并转换为字节数组准备写入。import android.graphics.Bitmap import java.io.ByteArrayOutputStream fun processAndCompressBitmap(originalBitmap: Bitmap, format: Bitmap.CompressFormat Bitmap.CompressFormat.JPEG, quality: Int 85): ByteArray { val outputStream ByteArrayOutputStream() // 参数格式质量0-100输出流 if (originalBitmap.compress(format, quality, outputStream)) { outputStream.flush() outputStream.close() return outputStream.toByteArray() } else { throw IOException(Failed to compress bitmap.) } }3.3 第三步保存到相册兼容小米的核心实现这是最核心的类。我们创建一个ImageSaver类它封装了完整的保存逻辑。import android.content.ContentResolver import android.content.ContentValues import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore import android.text.format.DateFormat import java.io.ByteArrayInputStream import java.io.IOException import java.io.OutputStream import java.util.* class ImageSaver(private val context: Context) { companion object { // 定义一个MIUI的标识用于机型判断可扩展其他品牌 private const val MANUFACTURER_XIAOMI Xiaomi private const val BRAND_XIAOMI Xiaomi } /** * 保存Bitmap到公共图片目录 * param bitmap 要保存的位图 * param displayName 图片显示名称不含后缀 * param mimeType 图片类型如 image/jpeg * param relativeSubDir 在Pictures下的子目录如 MyApp/Screenshots * return 保存成功后图片的Uri失败返回null */ fun saveBitmapToGallery( bitmap: Bitmap, displayName: String Screenshot_${DateFormat.format(yyyyMMdd_HHmmss, Date())}, mimeType: String image/jpeg, relativeSubDir: String MyApp ): Uri? { val contentResolver context.contentResolver val contentValues ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, $displayName.jpg) put(MediaStore.MediaColumns.MIME_TYPE, mimeType) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 使用RELATIVE_PATH指定子目录 put(MediaStore.MediaColumns.RELATIVE_PATH, ${Environment.DIRECTORY_PICTURES}/$relativeSubDir) // 设置文件为待处理状态写入完成后需要更新IS_PENDING状态可选但更规范 put(MediaStore.MediaColumns.IS_PENDING, 1) } else { // Android 9及以下需要WRITE_EXTERNAL_STORAGE权限 // 文件路径由系统决定我们通过MediaStore插入后获取 Suppress(DEPRECATION) val filePath ${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)}/$relativeSubDir // 注意实际路径可能不完全如此此处仅为说明。在旧版本中更可靠的做法是插入后通过DATA字段获取路径。 } } var imageUri: Uri? null var outputStream: OutputStream? null var needScanForXiaomi false // 标记是否需要为小米机型额外扫描 try { // 1. 插入MediaStore记录获取Uri imageUri contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) if (imageUri null) { throw IOException(Failed to create new MediaStore record.) } // 2. 打开输出流并写入图片数据 outputStream contentResolver.openOutputStream(imageUri) if (outputStream null) { throw IOException(Failed to open output stream for $imageUri) } // 将Bitmap压缩后写入 if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outputStream)) { throw IOException(Failed to save bitmap to stream.) } outputStream.flush() // 3. 写入完成更新IS_PENDING状态Android Q if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) contentResolver.update(imageUri, contentValues, null, null) } // 4. 判断是否为小米机型决定是否启用备用扫描策略 needScanForXiaomi isXiaomiDevice() return imageUri } catch (e: Exception) { e.printStackTrace() // 如果发生异常尝试删除已插入的Uri记录 imageUri?.let { uri - try { contentResolver.delete(uri, null, null) } catch (deleteEx: Exception) { deleteEx.printStackTrace() } } return null } finally { outputStream?.close() // 5. 如果是小米设备执行额外的媒体扫描在IO操作完成后 if (needScanForXiaomi imageUri ! null) { scanFileForXiaomi(context, imageUri) } } } /** * 判断当前设备是否为小米/Redmi系列 */ private fun isXiaomiDevice(): Boolean { val manufacturer Build.MANUFACTURER.lowercase(Locale.getDefault()) val brand Build.BRAND.lowercase(Locale.getDefault()) return manufacturer.contains(xiaomi) || brand.contains(xiaomi) || brand.contains(redmi) } /** * 针对小米设备的额外媒体扫描 * 注意从Android 10开始通过MediaStore插入的文件系统理应自动扫描。 * 此方法作为兼容性兜底方案特别是针对某些MIUI版本。 */ private fun scanFileForXiaomi(context: Context, uri: Uri) { try { // 尝试通过Uri获取实际文件路径此方法在Android Q可能失效或返回null val filePath getFilePathFromUri(context, uri) filePath?.let { path - // 使用MediaScannerConnection强制扫描 android.media.MediaScannerConnection.scanFile( context, arrayOf(path), arrayOf(image/jpeg), null // 可以传入回调监听扫描完成 ) // 日志输出便于调试 android.util.Log.d(ImageSaver, [Xiaomi Adapt] Force scanned file: $path) } } catch (e: Exception) { android.util.Log.e(ImageSaver, [Xiaomi Adapt] scanFile failed, e) } } /** * 尝试从Uri解析出文件路径兼容性方法不一定总是成功 */ private fun getFilePathFromUri(context: Context, uri: Uri): String? { if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // 在Android Q及以上直接访问_data字段可能受限。 // 更安全的方式是通过ContentResolver打开文件描述符或输入流但这无法直接得到路径。 // 这里我们采用一个折中方案查询MediaStore的DATA字段但需要READ_EXTERNAL_STORAGE权限。 // 注意此方法仅作演示在生产环境中需谨慎处理权限和兼容性。 return try { val projection arrayOf(MediaStore.Images.Media.DATA) context.contentResolver.query(uri, projection, null, null, null)?.use { cursor - if (cursor.moveToFirst()) { val columnIndex cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) cursor.getString(columnIndex) } else { null } } } catch (e: SecurityException) { // 无权限 null } catch (e: Exception) { null } } else { // Android 9及以下可以使用Uri.getPath()或通过Cursor查询_DATA return uri.path } } }代码逻辑解读与避坑点IS_PENDING标志的使用在Android Q及以上插入媒体文件时设置IS_PENDING 1表示文件正在写入。写入完成后必须将其更新为IS_PENDING 0。这个标志能防止其他应用在文件写入完成前访问到不完整的文件。很多开发者会忽略这一步导致在某些系统上文件不可见。小米机型判断我们通过Build.MANUFACTURER和Build.BRAND来判断。注意要转换为小写进行比较因为不同设备返回值大小写可能不一致。备用扫描策略scanFileForXiaomi函数是关键。它尝试从Uri反推文件路径然后调用MediaScannerConnection.scanFile。这里有个大坑从Android Q开始通过MediaStore插入的文件应用可能无法直接获取其物理路径_data字段可能为空或需要权限。我们的getFilePathFromUri函数在Q上可能返回null。但这没关系因为我们的主逻辑MediaStore插入已经完成了。备用扫描是“尽力而为”的兜底策略有则锦上添花无则不影响主流程。实测中即使路径获取失败这个扫描广播有时也能触发MIUI相册刷新。异常处理与清理如果在写入过程中发生异常如磁盘已满我们尝试删除之前插入的MediaStore记录避免在相册中留下一个0字节的“僵尸”条目。3.4 第四步权限申请与AndroidManifest配置Android 9 (API 28) 及以下需要在AndroidManifest.xml中声明WRITE_EXTERNAL_STORAGE权限并且可能需要运行时申请。uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- 限制在API 28及以下生效 --Android 10 (API 29) 及以上如果仅使用MediaStoreAPI向Pictures或Downloads等公共目录写入媒体文件不需要声明WRITE_EXTERNAL_STORAGE权限。但如果你的getFilePathFromUri函数在Q上需要查询DATA字段则可能需要READ_EXTERNAL_STORAGE权限。为了简化我们可以选择不依赖路径查询因此可以不要这个权限。更优雅的权限处理对于需要兼容旧版本的应用可以动态判断版本并申请权限。// 在Activity或Fragment中 private fun checkAndRequestStoragePermission() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10使用MediaStore写入Pictures目录不需要权限直接执行保存操作 performSaveImage() } else { // Android 9及以下需要申请WRITE_EXTERNAL_STORAGE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ! PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_STORAGE_PERMISSION ) } else { performSaveImage() } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Arrayout String, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_CODE_STORAGE_PERMISSION) { if (grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { performSaveImage() } else { // 权限被拒绝提示用户 Toast.makeText(this, 存储权限被拒绝无法保存图片, Toast.LENGTH_SHORT).show() } } }4. 完整流程串联与UI交互现在我们把所有模块组合起来并在一个按钮点击事件中触发整个流程。// 在Activity中 class MainActivity : AppCompatActivity() { private lateinit var imageSaver: ImageSaver private val REQUEST_CODE_STORAGE_PERMISSION 1001 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) imageSaver ImageSaver(this) val buttonScreenshot findViewByIdButton(R.id.btn_screenshot) val targetView findViewByIdConstraintLayout(R.id.layout_to_capture) // 要截图的根布局 buttonScreenshot.setOnClickListener { // 1. 截图 val bitmap ScreenshotUtils.captureView(targetView) if (bitmap ! null) { // 2. 检查权限并保存 checkAndRequestStoragePermission(bitmap) } else { Toast.makeText(this, 截图失败, Toast.LENGTH_SHORT).show() } } } private fun checkAndRequestStoragePermission(bitmap: Bitmap) { // ... 权限检查逻辑同上 ... // 假设权限已 granted调用 performSaveImage(bitmap) } private fun performSaveImage(bitmap: Bitmap) { // 使用协程或子线程执行IO操作避免阻塞UI lifecycleScope.launch(Dispatchers.IO) { val savedUri imageSaver.saveBitmapToGallery( bitmap, displayName MyApp_Share_${System.currentTimeMillis()}, relativeSubDir MyApp/Screenshots ) withContext(Dispatchers.Main) { if (savedUri ! null) { Toast.makeText(thisMainActivity, 图片已保存到相册, Toast.LENGTH_LONG).show() // 可选通知系统分享Sheet让用户立即分享 val shareIntent Intent().apply { action Intent.ACTION_SEND putExtra(Intent.EXTRA_STREAM, savedUri) type image/jpeg addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } startActivity(Intent.createChooser(shareIntent, 分享截图)) } else { Toast.makeText(thisMainActivity, 保存失败请检查存储空间和权限, Toast.LENGTH_SHORT).show() } } // 记得回收Bitmap防止内存泄漏 bitmap.recycle() } } }4.1 关于分享功能的补充保存成功后我们通常希望用户能立即分享。注意分享Intent中的FLAG_GRANT_READ_URI_PERMISSION标志它临时授权接收分享的应用如微信读取我们通过MediaStore创建的Uri所指向的文件。这是Android文件系统安全机制的一部分。4.2 处理Configuration Change如屏幕旋转如果截图保存操作耗时较长用户可能旋转屏幕。使用lifecycleScope需要依赖androidx.lifecycle:lifecycle-runtime-ktx可以自动管理生命周期避免内存泄漏。如果使用普通线程或AsyncTask需要在Activity销毁时妥善取消任务。5. 针对不同MIUI版本的深度适配与测试经验即使有了上述双保险策略在五花八门的小米机型上测试依然可能遇到“玄学”问题。以下是我在实际项目中总结的几点深度适配经验5.1 MIUI的“优化”与“省电”策略部分MIUI版本特别是国内版有激进的后台进程管理和媒体扫描抑制策略。这可能导致MediaScannerConnection.scanFile的广播被延迟甚至丢弃。对策一加入延迟重试。在调用scanFile后可以延迟几秒再查询一次MediaStore如果还是找不到可以尝试再扫描一次。或者在保存成功后引导用户手动下拉刷新相册。对策二将图片同时写入应用私有缓存目录并分享。这是一个“终极”备选方案。如果检测到是小米手机且通过MediaStore保存后一段时间如3秒相册仍不可见可以降级处理将图片保存到getExternalFilesDir(Environment.DIRECTORY_PICTURES)应用专属外部存储无需权限然后分享这个文件。缺点是文件不在公共相册用户管理起来不方便但能保证分享功能可用。这需要更复杂的逻辑和UI引导。5.2 文件名与特殊字符MediaStore的DISPLAY_NAME字段不支持某些特殊字符如\ / : * ? |且不同厂商对文件名的处理可能有差异。务必对传入的文件名进行过滤只保留字母、数字、下划线和连字符避免因文件名非法导致插入失败。fun sanitizeFileName(name: String): String { return name.replace(Regex([\\\\/:*?\|]), _) }5.3 测试矩阵建议要确保功能稳定建议在以下类型的设备上进行测试小米/Redmi手机至少覆盖MIUI 12、13、14国内版和国际版。原生或类原生Android手机如Pixel覆盖Android 10、11、12、13、14。其他国内品牌手机如华为、OPPO、vivo它们的系统也可能有类似定制可以作为广义上的兼容性测试。5.4 日志与用户反馈在ImageSaver的关键步骤如插入Uri成功/失败、开始扫描、扫描完成添加详细的Log输出并上传到你的错误分析平台如Firebase Crashlytics。当用户反馈“保存不了”时这些日志是定位问题的最直接依据。你甚至可以根据日志判断是MediaStore插入失败还是小米扫描失败从而采取不同的用户提示策略。6. 进阶优化与扩展思路当基础功能稳定后可以考虑以下优化点提升用户体验和专业度6.1 添加加载状态与进度提示保存图片是IO操作可能在低端机上耗时几百毫秒到几秒。在按钮点击后应该显示一个ProgressDialog或Snackbar提示“正在保存...”操作完成后消失。避免用户以为没反应而多次点击。6.2 支持PNG透明背景如果UI需要透明背景比如截取一个自定义形状的View保存格式应选用PNG。在saveBitmapToGallery函数中需要根据Bitmap是否有透明通道自动选择CompressFormat.PNG并设置正确的mimeType为image/png。注意JPEG强制不支持透明会填满黑色背景。6.3 截图内容的预处理与美化在调用captureView之前可以对目标View进行临时美化例如隐藏不必要的控件如分享按钮本身、浮动按钮等。临时添加装饰在截图Bitmap生成后用Canvas绘制一个精美的边框、应用水印或底部标语。拼接多张截图对于长内容可以截取多个部分然后将多个Bitmap垂直拼接成一张长图。6.4 错误处理的用户友好提示不要仅仅在Logcat打印错误。根据异常类型给用户明确的提示SecurityException可能是权限问题Android 9及以下。IOException可能是存储空间不足或文件系统错误。返回的Uri为null可能是MediaStore插入失败系统相册数据库异常。 用Toast或Dialog提示用户具体的失败原因如“存储空间不足请清理后重试”比一句模糊的“保存失败”更有帮助。6.5 考虑使用FileProvider进行更安全的分享虽然我们通过MediaStore获得了Uri但某些旧版本系统或特定应用可能对content://media/...格式的Uri支持不佳。为了获得最广泛的分享兼容性可以考虑使用FileProvider来分享一个位于应用私有缓存目录的文件副本。但这会增加一次文件拷贝的开销需要权衡。实现一个健壮且兼容性良好的Android截图保存功能尤其是要过小米这一关确实需要多花些心思。核心思路就是坚守Android标准MediaStore同时对国内生态的“特性”做好兜底主动扫描。把上述代码和策略融入到你的项目中再结合充分的真机测试相信这个“分享截图到相册”的功能就能稳定流畅地运行在绝大多数用户手机上了。