Android Studio运行AVD时ImageView位图过大闪退如何无画质损失修复
java.lang.RuntimeException: Canvas: trying to draw too large(115781908bytes) bitmap报错的核心原因是图片解码后的内存占用超过了Android系统的单Canvas绘制阈值。你看到的1.93MB是图片压缩后的磁盘存储体积,Android加载位图时会将JPG/PNG/WebP等压缩格式的图片解码为ARGB_8888格式的像素数组,每个像素占4字节内存,115MB就是解码后位图的实际内存占用,和图片本身的压缩质量没有直接关系。
存在完全不降低原图显示质量的修复方案,具体如下:
1. 将图片放到匹配密度的资源目录,避免系统自动放大
绝大多数这类报错都是因为把高分辨率图片直接放到了无后缀的drawable目录导致的:
- 无后缀的
drawable目录默认对应mdpi(160dpi)密度基准,在高密度AVD/真机上运行时,系统会自动将该目录下的图片按密度比放大,像素量会按密度比的平方级增长,直接导致内存暴增。 - 可以根据图片的原始分辨率,将图片移动到对应密度的资源目录:
- 1440P及以上分辨率原图放到
drawable-xxxhdpi - 1080P分辨率原图放到
drawable-xxhdpi - 720P分辨率原图放到
drawable-xhdpi
- 1440P及以上分辨率原图放到
- 如果不想让系统做密度适配,也可以把图片放到
drawable-nodpi目录,该目录下的资源不会触发系统的自动密度缩放,会按原始像素尺寸加载,不会产生额外的内存占用。
2. 按ImageView实际尺寸按需加载位图
如果ImageView本身尺寸远小于图片原始分辨率,不需要把全尺寸图片加载进内存,可以通过系统自带的BitmapFactory采样加载能力,加载和控件尺寸匹配的位图,这个过程不会损失控件显示下的画质:
- 先配置
inJustDecodeBounds = true读取图片的原始宽高,不把位图加载进内存 - 根据ImageView的实际宽高计算合适的采样率
inSampleSize - 用计算好的采样率加载真正的位图,设置给ImageView即可
核心实现代码如下:
// 计算匹配目标控件尺寸的采样率 public 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; } // 按目标尺寸采样加载位图 public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
调用时传入ImageView的实际宽高即可,比如ImageView宽高为300dp,转换为对应像素值传入,加载出的位图尺寸刚好匹配控件显示需求,不会加载多余像素占用内存。
3. 超大图浏览场景使用分块加载
如果需求是显示长图、4K/8K超高清原图这类本身分辨率极高的图片,不需要一次性把全图加载进内存,可以用BitmapRegionDecoder做分块加载:只解码当前屏幕显示区域的位图内容,滑动时动态更新加载的区域,哪怕是上亿像素的原图也能正常显示,完全不需要压缩原图质量。
不要试图仅通过修改ImageView的scaleType属性修复问题:scaleType只控制位图绘制到控件上的缩放规则,不会改变位图加载到内存中的实际占用,已经加载进内存的超大位图依然会触发报错。
内容的提问来源于stack exchange,提问作者Satyam Raj

