前言

  图片是移动端开发中司空见惯的内容,开发者多数时候都会涉及图片加载的功能。Bitmap是图片在Android中的一种承载方式,它可以被加载到需要展示图片的地方,比如ImageView或是View的background。随着移动设备的更新换代,图片的分辨率越来越高,加载Bitmap的时候就不得不考虑OOM的问题了。

BitmapFactory

  Android中Bitmap的加载一般是通过BitmapFactory类来实现的。BitmapFactory类提供多种加载方式,常用的几个加载方式:

文件

public static Bitmap decodeFile(String pathName, Options opts) {
    ...
}

资源

public static Bitmap decodeResource(Resources res, int id, Options opts) {
    ...
}

字节数组

public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
    ...
}

数据流

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    ...
}

Problem

  通常,直接调用BitmapFactory的上述几个方法,传递相对应的参数就可以加载Bitmap了。Bitmap对象在Android应用程序中占用的内存是非常高:

一张480x800的图片,Bitmap占用内存计算:

类型 内存计算 内存占用(KB)
ARGB_8888 480×800×4÷1024 1500
ARGB_4444 480×800×2÷1024 750
ARGB_565 480×800×2÷1024 750
ARGB_8 480×800×1÷1024 375

  不作任何处理,直接调用BitmapFactory的decode方法来加载Bitmap会有几个问题:

  • 图片分辨率高,Bitmap加载耗时长
  • 图片分辨率高,Bitmap占用内存很高,可能出现OOM
  • 图片数量很多,Bitmap占用内存很高,可能出现OOM

Solution

  不知道大家注意到没有,上述几个decode方法都有一个共同的参数——Options,它其实是BitmapFactory的一个内部类,主要提供Bitmap加载配置参数。我们今天要讨论的优化点就是从Options配置参数出发,减少Bitmap加载过程中的内存消耗,降低Bitmap加载耗时,尽量降低OOM出现的风险。

Options参数源码

  提取Options关键配置参数,看看源码如何描述的:

/**
 * If set to true, the decoder will return null (no bitmap), but
 * the out... fields will still be set, allowing the caller to query
 * the bitmap without having to allocate the memory for its pixels.
 */
public boolean inJustDecodeBounds;

/**
 * If set to a value > 1, requests the decoder to subsample the original
 * image, returning a smaller image to save memory. The sample size is
 * the number of pixels in either dimension that correspond to a single
 * pixel in the decoded bitmap. For example, inSampleSize == 4 returns
 * an image that is 1/4 the width/height of the original, and 1/16 the
 * number of pixels. Any value <= 1 is treated the same as 1. Note: the
 * decoder uses a final value based on powers of 2, any other value will
 * be rounded down to the nearest power of 2.
 */
public int inSampleSize;

Options参数分析

/**
 * 如果为true,不会执行decode操作,返回bitmap为null,因此不会分配内存,但是会返回图片的分辨率参数
 */
public boolean inJustDecodeBounds;

/**
 * 图片采样率,影响图片长宽和像素密度
 */
public int inSampleSize;

  结合两个参数的含义,要降低Bitmap内存占用,需要根据期望图片尺寸和原图尺寸来计算最佳图片采样率,使得图片在不失真的情况下内存占用尽量小,然后使用计算出的最佳图片采样率来decode图片bitmap,达到降低Bitmap内存占用及减少decode操作耗时的目的。

  那么,如何能够快速又不消耗内存地得到原图尺寸呢?答案就是利用inJustDecodeBounds参数!设置Options的inJustDecodeBounds参数为true,此时执行一次decode操作即可在不消耗内存的条件小拿到原图尺寸。

解决方案

  结合上述Options参数分析,我们可以写出一个简单的解决方案:

1. 创建BitmapFactory.Options对象options
2. 设置options参数inJustDecodeBounds为true
3. 执行一次decode操作,获取到原图尺寸options.outWidth和options.outHeight
4. 按照原图长宽比,计算得出期望图片尺寸
5. 根据原图尺寸和期望尺寸,计算出最佳图片采样率
6. 设置options参数inJustDecodeBounds为false,设置options参数为最佳图片采样率
7. 再次执行decode操作,得到优化内存占用后Bitmap对象

计算期望图片尺寸

  如何计算呢?

  假设我们最终要decode的bitmap尺寸是长(maxWidth)和宽(maxHeight),然后根据原图尺寸的长宽比来按比例缩放计算,得到缩放后的期望尺寸desiredWidth和desiredHeight。为啥要这么做呢?因为预设的maxWidth和maxHeight,它们的比例并不一定跟原图比例相符,按比例缩放后的期望尺寸才能保证最终图片不变形。

  具体算法如下:

private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary) {
    // If no dominant value at all, just return the actual.
    if (maxPrimary == 0 && maxSecondary == 0) {
        return actualPrimary;
    }

    // If primary is unspecified, scale primary to match secondary's scaling ratio.
    if (maxPrimary == 0) {
        double ratio = (double) maxSecondary / (double) actualSecondary;
        return (int) (actualPrimary * ratio);
    }

    if (maxSecondary == 0) {
        return maxPrimary;
    }

    double ratio = (double) actualSecondary / (double) actualPrimary;
    int resized = maxPrimary;
    if (resized * ratio > maxSecondary) {
        resized = (int) (maxSecondary / ratio);
    }
    return resized;
}

计算最佳图片采样率

  根据前面对inSampleSize参数的解析,它的最终取值为2的指数倍。结合期望尺寸和原图尺寸的比值,我们可以计算出一个最接近该比值的采样率数值,把它作为最佳采样率。

Powers of 2 : 2^n
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
2^7 = 128
2^8 = 256
2^9 = 512
2^10 = 1024

  具体算法如下:

/**
 * 计算最佳图片采样率
 *
 * @param actualWidth   原图长度
 * @param actualHeight  原图宽度
 * @param desiredWidth  目标长度
 * @param desiredHeight 目标宽度
 * @return  best sample ratio.
 */
private static int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
    double wr = (double) actualWidth / desiredWidth;
    double hr = (double) actualHeight / desiredHeight;
    double ratio = Math.min(wr, hr);
    float n = 1.0f;
    while ((n * 2) <= ratio) {
        n *= 2;
    }
    return (int) n;
}

最终实现

  上述的解决方案经过我们的分解之后,一步一步实现了,最终的代码实现如下:

public static boolean compress(Context context, BitmapCompressOptions compressOptions) {
    String filePath = !TextUtils.isEmpty(compressOptions.sourceFilePath) ? compressOptions.sourceFilePath : getFilePath(context, compressOptions.sourceUri);
    if (TextUtils.isEmpty(filePath)) {
        return false;
    }

    BitmapFactory.Options options = new BitmapFactory.Options();

    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filePath, options);
    int actualWidth = options.outWidth;
    int actualHeight = options.outHeight;
    int desiredWidth = getResizedDimension(compressOptions.maxWidth, compressOptions.maxHeight, actualWidth, actualHeight);
    int desiredHeight = getResizedDimension(compressOptions.maxHeight, compressOptions.maxWidth, actualHeight, actualWidth);

    options.inJustDecodeBounds = false;
    options.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);

    Bitmap bitmap;
    Bitmap destBitmap = BitmapFactory.decodeFile(filePath, options);

    // If necessary, scale down to the maximal acceptable size.
    if (destBitmap.getWidth() > desiredWidth || destBitmap.getHeight() > desiredHeight) {
        bitmap = Bitmap.createScaledBitmap(destBitmap, desiredWidth, desiredHeight, true);
        destBitmap.recycle();
    } else {
        bitmap = destBitmap;
    }

    return null != compressOptions.destFile && compressBitmap(compressOptions, bitmap);
}

结语

  随着Android系统越来越高度成熟,各种开源项目也层出不穷,在图片加载这块上就已经有很多优秀的开源项目了,比如早期的Android-Universal-Image-Loader,FackBook开源的Fresco以及Google开发人员开源的Glide(现已被Google采用)。

  今天分享的Bitmap加载性能优化方案,也可以运用到项目工程中,比如图片上传功能模块,既可以减少选择图片后的等待耗时,又可以上传压缩后的图片文件,达到性能和体验双收目的。