Android 大图加载显示

Posted 峥嵘life

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 大图加载显示相关的知识,希望对你有一定的参考价值。

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中即可

共勉:自强不息,才是生活的样子。

以上是关于Android 大图加载显示的主要内容,如果未能解决你的问题,请参考以下文章

Android 高清加载长图或大图方案

学习笔记-android大图加载详解

Android_性能优化之ViewPager加载成百上千高清大图oom解决方案

Android大图绘制——硬件加速限制分析与方案

Android大图绘制——硬件加速限制分析与方案

Android 加载大图