Android 13及以上图片压缩Bitmap宽高为0报错修复咨询
适配全Android版本的图片压缩修复方案
问题根源
- Scoped Storage 权限限制:Android 10+(含Android X)的会话范围文件访问模式下,
File对象仅为逻辑占位路径,无法通过FileInputStream读取真实图片数据,导致BitmapFactory.decodeStream解码失败,返回的Bitmap宽高为0。 - EXIF 读取未适配:原代码直接通过文件路径读取EXIF信息,在Scoped Storage下会因权限问题读取失败。
- 输入方式单一:仅支持
File作为输入源,未处理会话范围访问返回的Content Uri。
修复步骤与核心代码修改
1. 重构解码方法,兼容Content Uri与File
替换原 decodeFile 方法,改为通过 ContentResolver 获取流,同时支持两种输入方式:
private static final int IMAGE_QUALITY = 99; private static final int MAX_SIZE_PIXELS = 1200; public static Bitmap decodeImage(Context context, Uri uri, int maxSize) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 先读取图片尺寸 try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { BitmapFactory.decodeStream(inputStream, null, options); } catch (IOException e) { e.printStackTrace(); return null; } // 计算合适的采样率 int scale = calculateInSampleSize(options, maxSize, maxSize); // 解码图片 BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); decodeOptions.inSampleSize = scale; try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { return BitmapFactory.decodeStream(inputStream, null, decodeOptions); } catch (IOException e) { e.printStackTrace(); return null; } } private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
2. 修改压缩主方法,适配Scoped Storage
核心修改点:
- 接收
Uri作为输入(会话范围访问返回的是Uri) - 统一通过
MediaStore写入文件,规避权限问题 - 适配不同Android版本的EXIF读取方式
public static Uri compressImage(Context context, Uri imageUri, String displayName, String mimeType) { Bitmap source = decodeImage(context, imageUri, MAX_SIZE_PIXELS); if (source == null || source.getWidth() == 0 || source.getHeight() == 0) { Log.e(ImageCompress.class.getSimpleName(), "Bitmap解码失败,宽高为0"); return null; } // 确定压缩格式 Bitmap.CompressFormat compressFormat; if (mimeType.contains("png")) { compressFormat = Bitmap.CompressFormat.PNG; } else if (mimeType.contains("webp")) { compressFormat = Bitmap.CompressFormat.WEBP; } else if (mimeType.contains("gif")) { return imageUri; // GIF格式不压缩 } else { compressFormat = Bitmap.CompressFormat.JPEG; } // 缩放图片 Bitmap resizedBmp = source; if (source.getWidth() > MAX_SIZE_PIXELS || source.getHeight() > MAX_SIZE_PIXELS) { resizedBmp = resizeBitmap(source, MAX_SIZE_PIXELS); } // 写入压缩后的图片到MediaStore ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Constants.MULTIMEDIA_CHATROOM_11); Uri resultUri = null; OutputStream outputStream = null; try { resultUri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); if (resultUri != null) { outputStream = context.getContentResolver().openOutputStream(resultUri); resizedBmp.compress(compressFormat, IMAGE_QUALITY, outputStream); } } catch (IOException e) { e.printStackTrace(); if (resultUri != null) { context.getContentResolver().delete(resultUri, null, null); } resultUri = null; } finally { try { if (outputStream != null) { outputStream.flush(); outputStream.close(); } source.recycle(); if (resizedBmp != source) { resizedBmp.recycle(); } } catch (IOException e) { e.printStackTrace(); } } // 复制EXIF方向信息 if (resultUri != null) { copyExifData(context, imageUri, resultUri); } return resultUri; } private static void copyExifData(Context context, Uri sourceUri, Uri destUri) { try { ExifInterface sourceExif; // Android 9及以上支持直接从InputStream创建ExifInterface if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { try (InputStream inputStream = context.getContentResolver().openInputStream(sourceUri)) { sourceExif = new ExifInterface(inputStream); } } else { // 低版本用缓存目录临时文件中转 File tempFile = createTempFileFromUri(context, sourceUri); if (tempFile == null) return; sourceExif = new ExifInterface(tempFile.getAbsolutePath()); tempFile.delete(); } String orientation = sourceExif.getAttribute(ExifInterface.TAG_ORIENTATION); if (orientation == null) return; ExifInterface destExif; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { try (InputStream inputStream = context.getContentResolver().openInputStream(destUri)) { destExif = new ExifInterface(inputStream); } } else { File tempDestFile = createTempFileFromUri(context, destUri); if (tempDestFile == null) return; destExif = new ExifInterface(tempDestFile.getAbsolutePath()); tempDestFile.delete(); } destExif.setAttribute(ExifInterface.TAG_ORIENTATION, orientation); destExif.saveAttributes(); } catch (IOException e) { e.printStackTrace(); } } private static File createTempFileFromUri(Context context, Uri uri) { try { InputStream inputStream = context.getContentResolver().openInputStream(uri); File tempFile = File.createTempFile("temp_exif", null, context.getCacheDir()); FileOutputStream outputStream = new FileOutputStream(tempFile); byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); } inputStream.close(); outputStream.close(); return tempFile; } catch (IOException e) { e.printStackTrace(); return null; } } private static Bitmap resizeBitmap(Bitmap source, int maxSizePixels) { int targetWidth, targetHeight; double aspectRatio; if (source.getWidth() > source.getHeight()) { targetWidth = maxSizePixels; aspectRatio = (double) source.getHeight() / (double) source.getWidth(); targetHeight = (int) (targetWidth * aspectRatio); } else { targetHeight = maxSizePixels; aspectRatio = (double) source.getWidth() / (double) source.getHeight(); targetWidth = (int) (targetHeight * aspectRatio); } return Bitmap.createScaledBitmap(source, targetWidth, targetHeight, false); }
3. 调用方式调整
会话范围访问图库获取到 Uri 后,直接传入压缩方法:
// 示例:从相册选择图片后的回调处理 Uri selectedImageUri = data.getData(); String displayName = getFileNameFromUri(context, selectedImageUri); String mimeType = context.getContentResolver().getType(selectedImageUri); Uri compressedUri = ImageCompress.compressImage(context, selectedImageUri, displayName, mimeType);
补充获取文件名的工具方法:
public static String getFileNameFromUri(Context context, Uri uri) { String fileName = null; Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); if (cursor != null && cursor.moveToFirst()) { int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); if (nameIndex != -1) { fileName = cursor.getString(nameIndex); } cursor.close(); } return fileName != null ? fileName : "compressed_image_" + System.currentTimeMillis() + ".jpg"; }
关键适配说明
- 统一用ContentResolver处理流:彻底解决Scoped Storage下File无法访问真实数据的问题
- EXIF跨版本适配:Android 9+直接从流读取EXIF,低版本用缓存目录临时文件中转,无需外部存储权限
- 返回Uri而非File:符合Scoped Storage的访问规范,避免File路径无效的问题
- 资源安全回收:确保Bitmap和流正确关闭,避免内存泄漏
内容的提问来源于stack exchange,提问作者DEEPAK MAURYA
相关产品推荐
相关产品推荐

