Android性能优化—Bitmap的内存管理与长图加载

1.如何计算Bitmap所占内存?
2.Bitmap在内存中的缓存管理
3.长图加载需要注意的地方

Bitmap是App中内存使用的 “大户”,如何更好的使用Bitmap,减少其对App内存的使用,是我们开发中不可回避的问题。

如何得到 bitmap 对象?

Bitmap 是 Android 系统中的图像处理中最重要类之一。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。

有两种方法可以创建 Bitmap 对象,分别是通过 Bitmap.createBitmap() 和 BitmapFactory 的 decode 系列静态方法创建 Bitmap 对象。

下面我们主要介绍 BitmapFactory 的 decode 方式创建 Bitmap 对象。

  • decodeFile() 从文件系统中加载
    • 通过 Intent 打开本地图片或照片
    • 根据 uri 获取图片的路径
    • 根据路径解析 Bitmap:Bitmap bm = BitmapFactory.decodeFile(path);
  • decodeResource() 以 R.drawable.xxx 的形式从本地资源中加载
    • Bitmap bm = BitmapFactory.decodeResource(getResources(),R.drawable.icon);
  • decodeStream() 从输入流加载
    • Bitmap bm = BitmapFactory.decodeStream(stream);
  • decodeByteArray() 从字节数组中加载
    • Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);

BitmapFactory.Options

  • inSampleSize:采样率,这是表示采样大小。用于将图片缩小加载出来的,以免占用太大内存,适合缩略图。

  • inJustDecodeBounds:当 inJustDecodeBounds 为 true 时,执行 decodexxx 方法时,BitmapFactory 只会解析图片的原始宽高信息,并不会真正的加载图片

  • inPreferredConfig:用于配置图片解码方式,对应的类型 Bitmap.Config。如果非null,则会使用它来解码图片。默认值为是 Bitmap.Config.ARGB_8888

  • inBitmap:在 Android 3.0 开始引入了 inBitmap 设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存,避免再次创建一个Bitmap。在 Android4.4,新增了允许 inBitmap 设置的图片与需要加载的图片的大小不同的情况,只要 inBitmap 的图片比当前需要加载的图片大就好了。

  • inDensity
    表示这个bitmap的的像素密度,这个值跟这张图片的放置的drawable目录有关。
    inDensity赋值:
    drawable-ldpi 120
    drawable-mdpi 160
    drawable-hdpi 240
    drawable-xhdpi 320
    drawable-xxhdpi 480

  • inTargetDensity
    表示要被画出来时的目标(屏幕)的像素密度,
    代码中获取的方式:getResources().getDisplayMetrics().densityDpi

    扫描二维码关注公众号,回复: 12013765 查看本文章
  • inScreenDensity

    /**
     * Decode a new Bitmap from an InputStream. This InputStream was obtained from
     * resources, which we pass to be able to scale the bitmap accordingly.
     * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
     *         is {@link android.graphics.Bitmap.Config#HARDWARE}
     *         and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
     *         is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
     *         function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
     */
    @Nullable
    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
    
    
        validate(opts);
        if (opts == null) {
    
    
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
    
    
        	//value就是读取资源文件时,资源文件的一些数据信息
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
    
    
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
    
    
            	//inDensity赋值的是资源所在文件夹对应的密度
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
    
    
        	//inTargetDensity赋值的是手机屏幕的像素密度densityDpi
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

通过 BitmapFactory.Options 的这些参数,我们就可以按一定的采样率来加载缩小后的图片,然后在 ImageView 中使用缩小的图片这样就会降低内存占用,避免OOM,提高了 Bitamp 加载时的性能。

这其实就是我们常说的图片尺寸压缩。尺寸压缩是压缩图片的像素,一张图片所占内存大小的计算方式: 图片的像素的色彩类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。

Bitmap.Config

public static enum Config {
    
    
    ALPHA_8,//每个像素占用1byte内存
    RGB_565,//每个像素占用2byte内存
    ARGB_4444,//每个像素占用2byte内存
    ARGB_8888;//每个像素占用4byte内存;默认模式
}

Bitmap内存占用计算

Bitmap作为位图,需要读入图片在每个像素点上的数据,其主要占据内存的地方,也就是这些像素数据。一张图片像素数据的总大小为,图片的像素大小 * 每个像素点的字节大小,通常你就可以把这个值理解为Bitmap对象所占内存的大小。而图片的像素大小为横向像素值 * 纵向像素值。所以就有了下面这个公式:

Bitmap内存 ≈ 像素数据总大小 = 横向像素值 * 纵向像素值 * 每个像素的内存

没有持有Bitmap对象,计算drawable资源目录中的图片加载为Bitmap后占用多少内存

BitmapFactory.decodeResource加载图片到内存时,生成的Bitmap的宽和高并不是等于原始图片的宽和高,而是会根据当前屏幕的密度进行缩放的。

Bitmap 在内存当中占用的大小取决于:

  • 色彩格式,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
  • 原始图片文件存放的资源目录
  • 目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)
int realWidth = (int) (rawWidth * targetDensity / (float) rawDensity + 0.5f)
int realHeight = (int) (rawHeight * targetDensity / (float) rawDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

rawWidth就是资源图片的原始宽度
targetDensity就是当前屏幕的density(Android源代码中就是inTargetDensity变量)
rawDensity就是资源图片所在的资源文件夹对应的density(Android源代码中就是inDensity变量)
bytes_for_current_colorMode就是当前色彩格式下每个像素对应的字节数

比如:
ARGB_8888色彩格式:
ARGB各占8位,则每个像素占用内存(8bit + 8bit + 8bit + 8bit)/8=4Byte,所以Bitmap占用内存为 realWidth * realHeight * 4
RGB_565色彩格式:
R5位,G6位,B5位,则每个像素占用内存(5bit + 6bit + 5bit)/8=2Byte,所以Bitmap占用内存为 realWidth * realHeight * 2

对应到Android源代码中的变量:

int realWidth = (int) (rawWidth * inTargetDensity / (float) inDensity + 0.5f)
int realHeight = (int) (rawHeight * inTargetDensity / (float) inDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

我们通常的理解方式是 直接拿图片的宽乘以高,再乘以当前Bitmap格式下单个像素占用的内存大小 。 这种算法忽视了两点:
1、Android设备加载Bitmap时本身会对存放在drawable-hdpi、drawable-xhdpi、drawable-xxhdpi… 等这种目录下的图片进行缩放,所以这里需要拿图片的原始宽高进行缩放计算。

2、如果考虑到第1点,最后计算的出来bitmap占用内存大小与bitmap.getByteCount()有微小的差异。 这个差异就是因为 “(rawWidth * inTargetDensity / (float) inDensity + 0.5f)” 这样计算的结果是float类型, 而图片的像素个数必须是整数。 所以这里有一个 四舍五入的过程,误差来源于这里。

理解了上述原理后,我们可以得出以下结论:
1.在同一台设备上,图片文件存放的资源目录对应的密度(dpi)越小,则加载后的Bitmap宽和高越大,占用的内存也越大。同理,图片所在资源目录的dpi越大,生成的bitmap尺寸越小。
2.设备屏幕的像素密度越大,生成的bitmap尺寸越大
3.res/drawable目录对应的density值和res/drawable-mdpi目录一样,等于1,dpi值为160。
4.如果图片文件存放的资源目录的像素密度与设备屏幕的像素密度相同,则生成的bitmap不会缩放,尺寸是原始大小。
因此,之前的bitmap内存的计算公式可以演化成:
bitmap内存 ≈ 像素数据总大小 = 图片的像素宽 * 图片的像素高 * (设备屏幕的像素密度/图片文件存放的资源目录的像素密度)^2 * 每个像素的内存 = 图片的像素宽 * 图片的像素高 * 每个像素的内存

Bitmap的内存优化:
从上面的公式,不难看出,Bitmap的内存优化,主要有三种方式:

  • 加载Bitmap时,选择低色彩的质量参数(Bitmap.Config),如RGB_5665,这样相比默认的ARGB_8888,占用内存缩小一半。适用于对色彩多样性要求比较低的场景。
  • 将图片放在合理的资源目录下,尽可能保持和屏幕密度一致。但也不要全都放在最高密度的资源目录下,资源目录的像素密度高于屏幕密度,加载的Bitmap尺寸会小于原始尺寸,甚至小于显示区域的尺寸,这也不能满足有些需求。
  • 根据目标控件的尺寸,在加载图片时,对bitmap的尺寸进行缩放。比如在像素密度为480dpi的屏幕上,width为300dp,height为200dp的ImageView,能显示的无缩放的图片分辨率为900*600,如果图片分辨率大于这个尺寸,解析时就要考虑按比例缩小。

已有Bitmap对象,计算Bitmap所占内存大小

通过调用Bitmap的getByteCount()方法即可。

getByteCount():返回可用于存储此位图像素的最小字节数。它内部的计算方式: 每一行的字节大小 * 总行数(即高度)

    /**
     * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
     *
     * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
     * no longer be used to determine memory usage of a bitmap. See {@link
     * #getAllocationByteCount()}.</p>
     */
    public final int getByteCount() {
    
    
        if (mRecycled) {
    
    
            Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
            return 0;
        }
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

Bitmap内存压缩

不同格式的相同宽高的图片的内存占用一样吗

我们在使用图片的时候,选择 jpg、png或者webp,对内存会不会有影响呢?
只要图片的原始宽和高一样,并且放在同一个文件夹,则加载后Bitmap占用的内存是一样的。比如:R.drawable.icon_mv_jpg,R.drawable.icon_mv_png,R.drawable.icon_mv_webp三张图片的宽和高都一样,且放在同一个文件夹,因此
BitmapFactory.decodeResource加载三张图片在同一个手机屏幕上的宽和高都是一样的,而且BitmapFactory.Options配置默认的色彩质量参数都是ARGB_8888,
因此这三张图片载到内存中时占用内存的大小是一样的,虽然这三张图片占用的磁盘大小不一样。

图片压缩实现

主要用到的BitmapFactory.options的参数:
inJustDecodeBounds
为true时,decoder将返回null,但是会解析出 outxxx 字段

inPreferredConfig
设置图片解码后的像素格式,如ARGB_8888/RGB_565

inSampleSize
设置图片解码缩放比,如值为2,则加载图片的宽高是原来的 1/2,
整个图片所占内存的大小就是原图的 1/4

/**
 * 图片压缩
 */
public class ImageResize {
    
    


    /**
     * 返回压缩图片
     *
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @param hasAlpha
     * @return
     */
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha, Bitmap reusable) {
    
    

        Resources resources = context.getResources();

        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true后,再去解析,就只解析 out 参数
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(resources, id, options);

        int w = options.outWidth;
        int h = options.outHeight;


        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
    
    
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        options.inJustDecodeBounds = false;

        // 复用, inMutable 为true 表示易变
        options.inMutable = true;
        options.inBitmap = reusable;


        return BitmapFactory.decodeResource(resources, id, options);

    }

    /**
     * 计算 缩放系数
     *
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return
     */
    private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
    
    

        int inSampleSize = 1;

        if (w > maxW && h > maxH) {
    
    
            inSampleSize = 2;

            while (w / inSampleSize > maxW && h / inSampleSize > maxH) {
    
    
                inSampleSize *= 2;
            }

        }

        return inSampleSize;
    }
}

Bitmap内存优化—内存复用

Bitmap自带的inBitmap内存复用机制,主要就是指的复用内存块,不需要在重新给这个bitmap申请一块新的内存,避免了一次内存的分配和回收,从而避免OOM和内存抖动,改善了运行效率。

inBitmap类似对象池的技术原理,避免内存的频繁的创建和销毁带来性能的损耗。使用inBitmap能高提升bitmap的循环使用效率。

在Google发布的第二季性能优化都有提到inBitmap技术 https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE(需翻墙)

使用inBitmap前,每创建一个bitmap需要独占一块内存
在这里插入图片描述
使用inBitmap后,多个bitmap会复用同一块内存
在这里插入图片描述
所以使用inBitmap能够大大提高内存的利用效率,但是它也有几个限制条件:
1.inBitmap只能在SDK 11(Android 3.0)以后使用。Android 2.3上,bitmap的数据是存储在native的内存区域,并不是在Dalvik的内存堆上。
2.在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。
从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。
3.新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是ARGB_8888的,如果前面的bitmap是ARGB_8888,那么就不能支持ARGB_4444与RGB_565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。

这里最好的方法就是使用LRUCache来缓存bitmap,后面缓存新的bitmap时,可以从cache中按照api版本找到最适合重用的bitmap,来重用它的内存区域。

google官方的Bitmap相关教程:
http://developer.android.com/training/displaying-bitmaps/manage-memory.html
http://developer.android.com/training/displaying-bitmaps/index.html

需要内存复用的Bitmap不能调用recycle()回收内存

如下图:

在这里插入图片描述

LruCache 移除图片的时候回调 entryRemoved 方法,在这个方法中我们应该分情况处理:

  1. 能够复用的时候(oldValue.isMutable() 就是判断能不能复用),我们就通过复用池来复用
  2. 如果不能复用,我们就直接调用 recycle() 回收

而上图这样处理,就是不管什么情况,都会调用recycle()去回收我们的bitmap,释放占用的内存 ,这就导致我们后面从复用池中取出该Bitmap进行复用时发现该Bitmap的内存已经被回收了,所以报错。

所以上面应该改成下面这样:

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
    
    
				if (oldValue.isMutable()) {
    
    
				    // < 3.0  bitmap 内存在 native
				    // >= 3.0 在 java
				    // >= 8.0 又变为 native
				    // 如果从内存缓存中移除,将其放入复用池
				    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
				} else {
    
    
				    oldValue.recycle();
				}
			}

Bitmap内存缓存

使用LruCache 缓存工具类:android.util.LruCache

lru(least recently used),即最近最少使用,核心思想是:最近使用过的数据在将来被使用的概率也更高。

实现方式:
利用链表+HashMap(即LinkedHashMap)。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

在这里插入图片描述

Bitmap磁盘缓存

使用DiskLruCache
https://github.com/JakeWharton/DiskLruCache

Bitmap长图加载

使用:android.graphics.BitmapRegionDecoder

使用方式:
InputStream is = getAssets().open(“big.png”);

// 长图使用BigmapRegionDecoder 加载
// true:输入流共享,关闭输入流就用不了了,所以这个地方用false
BigmapRegionDecoder decoder = BigmapRegionDecoder.newInstance(is,false);

// 获取指定 Rect 区域的图片,相当于从大图上截取一块下来
// options 设置为null
Bitmap bitmap = decoder.decodeRegion(Rect,null);

比如:右边的长图怎么加载到手机中
在这里插入图片描述
如果直接放,会导致里面的内容看不清:
在这里插入图片描述

正确方案:
将长图的缩放放大到和屏幕一样宽,然后长度进行等比缩放,显示长图的一块区域,然后滑动时不断移动长图的区域。
在这里插入图片描述

参考:
【Android 内存优化】Bitmap 内存占用计算 ( Bitmap 图片内存占用分析 | Bitmap 内存占用计算 | Bitmap 不同像素密度间的转换 )
Android性能优化:Bitmap详解&你的Bitmap占多大内存?
图片加载和Bitmap的内存优化
Bitmap占用内存大小的准确计算公式

猜你喜欢

转载自blog.csdn.net/yzpbright/article/details/109209028