Android 大图加载显示
通过本文你能学到什么?
1、普通设置方法设置大图片是否会导致界面崩溃,多大的图片才会导致崩溃
2、如何保证加载大图不发生崩溃
3、Glide设置显示大图是否会发生崩溃
4、大图缩放滑动如何实现
5、大图缩放和滑动框架的使用
这里有个测试上面几个问题的demo,效果图如下:
代码和apk资源: https://download.csdn.net/download/wenzhi20102321/85079242
代码不多也不太难,又兴趣的可以自己运行调试下。
本文主要分析大图加载为啥会崩溃,其他的可以参考demo代码。
下面开始正文内容:
一、ImagerView直接放置一张几十M的图片会崩溃吗?
测试代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bitmap);
Log.d(TAG," onCreate");
ImageView image = findViewById(R.id.image);
image.setImageResource(R.mipmap.test);//22M的图片
Log.d(TAG," show bitmap end");
}
测试的Demo代码和测试的资源都在上面打包的资源里面。
测试效果如下: 在模拟器上Activity直接崩溃了 日志:
Process: com.liwenzhi.bitmapdemo, PID: 10090
java.lang.RuntimeException: Canvas: trying to draw too large(161566192bytes) bitmap.
at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:280)
at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
在华为手机上显示卡了下,后显示白屏,Activity界面未崩溃: 日志如下:
E/BitmapDrawable: Canvas: trying to use a recycled bitmap
2022-03-12 01:04:53.845 31366-31366/com.liwenzhi.bitmapdemo W/System.err: java.lang.RuntimeException: Canvas: trying to draw too large(161566192bytes) bitmap.
2022-03-12 01:04:53.845 31366-31366/com.liwenzhi.bitmapdemo W/System.err: at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:282)
2022-03-12 01:04:53.845 31366-31366/com.liwenzhi.bitmapdemo W/System.err: at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
2022-03-12 01:04:53.845 31366-31366/com.liwenzhi.bitmapdemo W/System.err: at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:549)
2022-03-12 01:04:53.845 31366-31366/com.liwenzhi.bitmapdemo W/System.err: at android.widget.ImageView.onDraw(ImageView.java:1529)
从上面日志也可以很清楚看到,从ImagView到具体报错的类的地方:
ImageView.onDraw -->BitmapDrawable.draw-->BaseRecordingCanvas.drawBitmap-->RecordingCanvas.throwIfCannotDraw是最后报错的方法
在联想平板上测试,整个应用崩溃了:
2022-03-12 01:20:23.443 11461-11461/? D/ShowBitmapActivity: onCreate
2022-03-12 01:20:26.083 11461-11461/? D/ShowBitmapActivity: show bitmap end
2022-03-12 01:20:26.103 1368-1673/? I/InputDispatcher: c5b948a com.liwenzhi.bitmapdemo/com.liwenzhi.bitmapdemo.MainActivity (server) spent 2713ms processing FocusEvent(hasFocus=false)
2022-03-12 01:20:26.109 7367-9126/? W/FMSC::AppStuckDetectionService: Receive App Stuck Event: PID: 11461 Package: com.liwenzhi.bitmapdemo Type: 3 Level: 0 Timestamp: 1647019226109 Duration: 2732152213 JankCnt: 1
2022-03-12 01:20:26.144 11461-11461/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.liwenzhi.bitmapdemo, PID: 11461
java.lang.RuntimeException: Canvas: trying to draw too large(161566192bytes) bitmap.
at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:283)
at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
...//
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:956)
2022-03-12 01:20:26.162 1368-8778/? W/ActivityTaskManager: Force finishing activity com.liwenzhi.bitmapdemo/.ShowBitmapActivity
从上面的日志看,不管是哪个设备,加载到161566192bytes(160M多一点)会发生异常。
尝试添加一张12M的图片,是没问题的,大概一秒后就显示了。
使用联想平板,尝试显示一张18M的图片,应用崩溃了。
2022-03-12 01:40:54.274 12930-12930/com.liwenzhi.bitmapdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.liwenzhi.bitmapdemo, PID: 12930
java.lang.RuntimeException: Canvas: trying to draw too large(120422400bytes) bitmap.
at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:283)
...
从上面日志看,是加载到120M左右就崩溃了。
那么13M的图片能不能正常显示呢?
测试结果是崩溃了。
日志入下:
--------- beginning of crash
2022-03-12 01:48:38.555 13216-13216/com.liwenzhi.bitmapdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.liwenzhi.bitmapdemo, PID: 13216
java.lang.RuntimeException: Canvas: trying to draw too large(194510848bytes) bitmap.
at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:283)
at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
194510848bytes–>194 510 848 = 185.5M
不再去测试了,先总结一下情况:
从上面测试结果看,ImageView大概加载13M以上的图片就会崩溃。
下面找出抛出异常崩溃原因:
Android9.0 api28 RecordingCanvas.java
private static int getPanelFrameSize() {
final int DefaultSize = 100 * 1024 * 1024; // 100 MB;
return Math.max(SystemProperties.getInt("ro.hwui.max_texture_allocation_size", DefaultSize),
DefaultSize);
}
/** @hide */
public static final int MAX_BITMAP_SIZE = getPanelFrameSize();
/** @hide */
@Override
protected void throwIfCannotDraw(Bitmap bitmap) {
super.throwIfCannotDraw(bitmap);
int bitmapSize = bitmap.getByteCount();
if (bitmapSize > MAX_BITMAP_SIZE) {
throw new RuntimeException(
"Canvas: trying to draw too large(" + bitmapSize + "bytes) bitmap.");
}
}
所以结论是:系统从ro.hwui.max_texture_allocation_size属性值和100 * 1024 * 1024(100 MB)取其中一个最大的值, 如果图片的数据值大于这个值就会抛出异常错误。
但是我的联想平板Android11(api 30),看了下并没有属性ro.hwui.max_texture_allocation_size,应该是9.0之后就没这个属性了的。
所以查看一下源码发现: Android11 api30
RecordingCanvas.java
/** @hide */
public static final int MAX_BITMAP_SIZE = 100 * 1024 * 1024; // 100 MB
发现最大加载Bitmap大小是100M,但是为啥12M的图片没事,13M多的图片就会崩溃???
下面就跟大家慢慢分析了:
先公布答案:
图片的像素大小和图片的文件大小并不代表数据大小,图片的数据大小指的是内存数据大小,
具体图片具体内存大小计算方式是每行的像素字节数*图片高度;
注意上面说的是每行的像素字节数,并不是像素数,每个像素占4个字节。
这里可以直接看源码一步步分析,但是我觉得还是先看看报错的实际原因和弄懂基本原理比较好。
下面看看12M 和13M图片的参数情况:
先看会崩溃的13M.jpg
分辨率:13568*3584
其他的位深和分辨率单位那些参数都是不用管的,知道上面这个分辨率参数就行了。
上面的参数表示:图片一行有13568个像素,有3584行
13568*3584大概就40M左右的大小,为啥会导致崩溃呢?
这里就要注意一个概念了,一个像素并不是一个字节大小,而是4个字节, 图片是用一个int(4个字节)记录一个像素大大小的,为啥是4个不是1个或者2个,因为要存储ARGB所以要4个字节。
理解了上面这两句概念,应该就不难理解为啥14M图片直接显示会崩溃了吧。
因为1356843584 内存大小,已经有160M左右的大小了,远超100M,代码直接抛出异常了。
12M.jpg参数情况:
分辨率:5184*3456
518443456 内存大小,大概只有60 M左右大小,未达到抛出异常条件。
所以我们是不是可以先计算图片的内存大小,再决定是否显示这个图片呢?
Bitmap.java 刚好暴露有这个方法是表示内存数据大小的
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight(); //getRowBytes表示行像素的字节数据大小
}
//同样的图片,在不同的设备获取的每行像素值可能不同
//比如我同一张照片华为手机,比如普通平板的高了很多
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mNativePtr);//每行的像素大小和设备相关
}
当然是可以用这种比较low的方法进行判断,防止设置图片界面崩溃。
但是还是有更好的解决方法的,比如固定图片的采样率,以及设置图片像素模式RGB888/565达到减少图片内存数据的目的
二、如何保证加载大图不发生崩溃?
保证图片的加载内存不超过100M即可
可以通过设置Options对象的inSampleSize 减少采样率(达到之前的几分之一) 也可以通过设置Options对象的inPreferredConfig为Bitmap.Config.RGB_565,
比如下面的这段代码:
/**
* 获取Bitmap对象
*
* @param res Resource对象
* @param resId 图片资源ID
* @return 返回Bitmap对象
*/
private Bitmap decodeSampledBitmapFromResource(Resources res, int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
//亮点:不加载像素,不占用内存
options.inJustDecodeBounds = true; //超大图缩放一般都有这样设置,否则在底层占用大量内存
BitmapFactory.decodeResource(res, resId, options); //不加载内存的情况下,无返回值
int height = options.outHeight; //图片的宽
int width = options.outWidth; //图片的高
//做一些其他的判断和处理,根据图片的宽高和View的宽高的情况,设置一些参数
//关键点: Calculate inSampleSize 几分之一
options.inSampleSize = 4;
options.inPreferredConfig = Bitmap.Config.RGB_565; //默认是RGB_888
options.inJustDecodeBounds = false; //设置为false,才会从底层申请到内存
return BitmapFactory.decodeResource(res, resId, options);
}
RGB_888到RGB_565,其实也是减少了采样率,大概是原图的5/8.
所有的大图加载框架都是用到了上面这段代码的思想: 第一次在不占用内存的情况加载测量图片的宽高,根据情况适配参数,第二次真正加载并设置相关参数。
三、Glide设置显示大图是否会发生崩溃
Glide加载大图是不会崩溃的
其关键点就是:
把options.inJustDecodeBounds属性设置为true,
对比图片实际大小和ImageView实际大小的情况,设置对应参数,做出相应的缩放等操作。
options.inJustDecodeBounds
也就是加载两次 第一次读取配置,比如图片原生宽高 第二次结合布局的宽高设置图片宽高,
四、大图缩放滑动如何实现
1、写一个自定义View加载图片
2、设置setImage方法设置图片,测量图片宽高
3、调用requestLayout()重新加载图片
4、在onMeasure(...)方法中对比图片宽高和View的宽高,
5、定义使用区域解码器、手势对象、缩放手势,缩放因子等对象
//区域解码器
private BitmapRegionDecoder mDecode;
//手势对象
private GestureDetector mGestureDetector;
//缩放功能
ScaleGestureDetector mScaleGestureDetector;
//滑动帮助类
private Scroller mScroller;
//需要显示的区域
private Rect mRect;
//图片缩放因子
private float mScale;
6、在onDraw(...)做对应的缩放和区域滑动
具体参考demo项目代码。
demo中手写的大图缩放功能,并不是非常完善, 试了下,部分大图正常显示和放大缩小,宽度较大的图片显示有问题, 谁有需求可以参考大图缩放框架自己完善下。
五、大图缩放和滑动框架的使用
大图处理神器: SubsamplingScaleImageView 框架 简称:Subsampling
用法也是比较简单的:
public class ShowFunctionBitmapActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_function_bitmap);
SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(R.id.image); //自定义View
imageView.setImage(ImageSource.resource(R.mipmap.test_22m)); //使用ImageSource的静态方法转换成ImageSource对象,加载到View中即可
}
}
demo里面没有进行远程依赖,是把SubsamplingScaleImageView框架的几个类复制到demo使用,需要的可以自己进行适配修改。
Subsampling的其他相关介绍:https://blog.csdn.net/zhangphil/article/details/49557549
六、最后总结一下最开始目录学习的内容:
1、普通设置方法设置大图片是否会导致界面崩溃,多大的图片才会导致崩溃
答:会发生异常,加载的图片数据内存大于100M会发生RuntimeException异常,系统框架未处理的情况会发生崩溃
因为Android系统就是这样限制的。
一般来说加载几M的图片没啥问题,但是加载十几M以上的图片就会有异常,出现图片不加载或者界面崩溃或者应用崩溃的现象。
2、如何保证加载大图不发生崩溃
答:可设置options.inSampleSize设置采样率和options.inPreferredConfig降低图片分辨率达到减少内存加载的目的
3、Glide设置显示大图是否会发生崩溃
Glide加载大图不会发生崩溃
Glide里面会加载两次图片
第一次读取配置,比如图片原生宽高,设置options.inJustDecodeBounds属性设置为true,不占用内存
第二次结合布局的宽高设置图片设置理想的采样率比例。
4、大图缩放滑动如何实现
(1)写一个自定义View加载图片
(2)设置setImage方法设置图片,测量图片宽高
(3)调用requestLayout()重新加载图片
(4)在onMeasure(...)方法中对比图片宽高和View的宽高,
(5)定义使用区域解码器、手势对象、缩放手势,缩放因子等对象
(6)在onDraw(...)做对应的缩放和区域滑动
5、大图缩放和滑动框架的使用
大图处理神器: SubsamplingScaleImageView 框架
(1)定义缩放图片对象
SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(R.id.image); //自定义View
(2)缩放图片对象中放入图片数据
imageView.setImage(ImageSource.resource(R.mipmap.test_22m)); //使用ImageSource的静态方法转换成ImageSource对象,加载到View中即可
共勉:自强不息,才是生活的样子。
|