likes
comments
collection
share

Bitmap面试基础知识

作者站长头像
站长
· 阅读数 7

Bitmap内存位置

加载Bitmap到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java部分的,一部分是C部分的,也就是Native部分,但在各Android版本是有区别的。 Java部分的内存可以被GC垃圾回收器给主动回收掉,但是Native部分是不参与到虚拟机的回收过程的,因此此块无法主动释放,所以必须开发者手动去释放,这就需要调用到recycle()方法来释放内存。

bitmap.recycle()方法会先回收该Bitmap所占用的native内存,接着将bitmap置空,最后使用System.gc()调用一下系统的垃圾回收器进行回收,调用System.gc()并不能保证立即开始进行回收过程,而只是为了加快回收的到来。 但是我们平时开发中并不需要主动调用recycle(),这是涉及到两种方案,一种是在Object#finalize()方法里调用recycle,另一种是使用虚引用和引用队列来实现。

Bitmap面试基础知识

Android1.0~Android2.3

Bitmap的像素数据是分配在Native内存中的,bitmap对象在Dalvik堆中占用的数据是很小的,只有width、height、config和指向堆的引用,这样的结果是Native中bitmap内存无法被GC主动回收,因此在bitmap使用完以后,必须调用recycle释放掉Native的内存

Android3.0~Android7.1

Bitmap的数据结构发生了改变,其中多了以下属性,用来存储像素数据

 private byte[] mBuffer;

至此像素数据就和bitmap对象一块儿都分配在堆中了,一块儿接受GC管理,只要bitmap置为null没有被强引用持有,GC就会把它回收掉,和普通对象同样。因此可以不主动调用recycle

在Object#finalize()方法里调用recycle,来回收native层内存,从而实现主动回收

Android8.0+

Bitmap的像素数据又从新回到native分配了,Bitmap的mBuffer这个属性不见了,取而代之的是private final long mNativePtr,其指向的是native的内存地址。不过为了可以主动recycle,Bitmap引入了NativeAllocationRegistry这样一种辅助自动回收native内存的机制,依然不须要用户主动回收了,当bitmap的Java对象被回收后,NativeAllocationRegistry辅助回收这个对象所申请的native内存。

NativeAllocationRegistry利用虚引用感知Java对象被回收的时机,来回收native层内存,从而实现主动回收

回归Native的原因:

安卓的每一个APP都是运行在单独的虚拟机中的,系统同时会有多个APP同时运行,因此分给每一个虚拟机内存上限不会过高,通常也就几百M,虚拟机启动时内存上限就是定值,一旦达到内存上限就会OOM。可是安卓手机大多数状况下系统仍是有剩余内存可用的,而一个APP中占用内存最多的通常都是Bitmap,因此若是能把系统空余内存空间利用起来,就能大大增长当前APP的可用内存,而把bitmap的像素数据放到native就能解决这个问题,native能够直接使用整个linux系统的内存,不受当前APP所在虚拟机的内存上限控制,这样就能够持续使用内存,直到用完系统的空余内存。

Bitmap内存计算

Bitamp 所占内存大小 = 宽度像素 x 压缩比 x 高度像素 x 压缩比 x 一个像素所占的内存字节大小。

压缩比= (inTargetDensity / inDensity)

inDensity表示目标图片的dpi(放在哪个资源文件夹下),inTargetDensity表示目标屏幕的dpi,所以你可以发现inDensity和inTargetDensity会对Bitmap的宽高进行拉伸,进而改变Bitmap占用内存的大小。

一个像素所占的内存字节大小是变化的,由BitmapFactory.Options的inPreferredConfig决定,为Bitmap.Config类型,一般默认为:ARGB_8888,也就是4byte。

Bitmap两个获取内存占用大小的方法

  • getByteCount() :API12 加入,代表存储 Bitmap 的像素需要的最少内存。
  • getAllocationByteCount() :API19 加入,代表在内存中为 Bitmap 分配的内存大小,代替了 getByteCount() 方法。
  • 不复用 Bitmap 时,getByteCount() 和 getAllocationByteCount 返回的结果是一样的。在通过复用 Bitmap 来解码图片时,那么 getByteCount() 表示新解码图片占用内存的大 小,getAllocationByteCount() 表示被复用 Bitmap 真实占用的内存大小

Bitmap内存优化

1、Bitmap压缩

  • 质量压缩可用于无法减少加载到内存的bitmap大小,但压缩转化后的 bytes.length 减少,适用于传输。上传大图前的处理。
  • 尺寸压缩可用于减少加载到内存的bitmap大小。可用于生成缩略图
  • 编码压缩
尺寸压缩

尺寸压缩会改变图片的尺寸,即压缩图片的宽度和高度的像素点,但是要注意,如果压缩比太大,也会由于像素点降低导致图片失真严重,最后图片有高清成了马赛克。

BitmapFactory.Options 参数inSampleSize的使用,先把options.inJustDecodeBounds设为true,只是去读取图片的大小,在拿到图片的大小之后和要显示的大小做比较通过calculateInSampleSize()函数计算inSampleSize的具体值,得到值之后。options.inJustDecodeBounds设为false读图片资源。

 public static Bitmap compressSample(String filePath) {
     int inSampleSize = 8;
     BitmapFactory.Options options = new BitmapFactory.Options();
     options.inJustDecodeBounds = false;
     options.inSampleSize = inSampleSize;
     Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
     return bitmap;
 }
质量压缩

Bitmap.compress(CompressFormat format, int quality, OutputStream stream)

第一个表示Bitmap被压缩成的图片格式;第二个表示压缩的质量控制。quality为80,表示压缩为原来80%的质量效果。但是PNG是无损格式,因此设置quality不生效。

 public static Bitmap compressQuality(Bitmap bitmap, int quality) {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
     return BitmapFactory.decodeStream(bais, null, null);
 }
编码压缩

我们通常会使用RGB_565,因为ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。

  • ALPHA_8 每个像素占用1byte内存
  • ARGB_4444 每个像素占用2byte内存
  • ARGB_8888 每个像素占用4byte内存(默认)
  • RGB_565 每个像素占用2byte内存

2、复用内存

当我们使用listview加载多张图片时,我们都知道,listview的Item有复用的特性,必然经常复用bitmap缓存,这时inBitmap的作用尤为重要。inBitmap是在BitmapFactory中的内部类Options的一个变量,简单而言,使用该变量可以复用旧的Bitmap的内存而不用重新分配以及销毁旧Bitmap

  • 4.4之前的版本inBitmap只能够重用相同大小的Bitmap内存区域。简单而言,被重用的Bitmap需要与新的Bitmap规格完全一致,否则不能重用。
  • 4.4之后的版本系统不再限制旧Bitmap与新Bitmap的大小,只要保证旧Bitmap的大小是大于等于新Bitmap大小即可。
  • 除上述规则之外,旧Bitmap必须是mutable的,这点也很好理解,如果一个Bitmap不支持修改,那么其内存自然也重用不了。

3、使用LruCache 和 DiskLruCache

LruCache将资源缓存在内存

DiskLruCache将资源缓存在外部存储磁盘上

Bitmap大图加载

如果要加载的单个图片非常巨大,并且还不允许压缩。

首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中,所以肯定是局部加载。其次,既然屏幕显示不完,那么最起码要添加一个上下左右拖动的手势,让用户可以拖动查看。

分块加载

bitmapRegionDecoder实现分块加载

 //获得宽高
 BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
 tmpOptions.inJustDecodeBounds = true;
 BitmapFactory.decodeStream(inputStream, null, tmpOptions);
 int width = tmpOptions.outWidth;
 int height = tmpOptions.outHeight;
 ​
 //设置显示图片的中心区域
 BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
 BitmapFactory.Options options = new BitmapFactory.Options();
 options.inPreferredConfig = Bitmap.Config.RGB_565;
 Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
 mImageView.setImageBitmap(bitmap);

拖动显示

自定义一个view,重写onTouchEvent,在里面根据用户移动的手势,去更新显示区域的参数。每次更新区域参数后,调用invalidate,onDraw里面去regionDecoder.decodeRegion拿到bitmap,去draw

转载自:https://juejin.cn/post/7003019712015630344
评论
请登录