Android 基础之图片加载(一)

Bitmap 的使用

在Bitmap中,其构造函数:

// called from JNI
    Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets)

通过构造函数的注释,得知这是一个给 native 层调用的方法,因此可以知道 Bitmap 的创建将会涉及到底层库的支持。为了方便从不同来源来创建 Bitmap,Android 中提供了 BitmapFactory 工具类。BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。

// BitmapFactory部分代码:
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeStream(InputStream is)
private static native Bitmap nativeDecodeStream

当我们需要从本地 Resource 中加载一个图片,并展示出来,我们可以通过BitmapFacotry来完成:

Bitmap bitmapDecode = BitmapFactory.decodeResource(getResources(), resId);
imageView.setImageBitmap(bitmapDecode);

Bitmap内存优化

减少内存主要可以通过以下几种方式:

  1. 使用低色彩的解析模式,如RGB565,减少单个像素的字节大小
  2. 资源文件合理放置,高分辨率图片可以放到高分辨率目录下
  3. 图片缩小,减少尺寸

第一种方式,大约能减少一半的内存开销。Android 默认是使用 ARGB8888 配置来处理色彩,占用 4 字节,改用 RGB565,将只占用 2 字节,代价是显示的色彩将相对少,适用于对色彩丰富程度要求不高的场景。
第二种方式,和图片的具体分辨率有关,建议开发中,高分辨率的图像应该放置到合理的资源目录下,注意到Android默认放置的资源目录是对应于160dpi,目前手机屏幕分辨率越来越高,此处能节省下来的开销也是很可观的。理论上,图片放置的资源目录分辨率越高,其占用内存会越小,但是低分辨率图片会因此被拉伸,显示上出现失真。另一方面,高分辨率图片也意味着其占用的本地储存也变大。
第三种方式,理论上根据适用的环境,是可以减少十几倍的内存使用的,它基于这样一个事实:源图片尺寸一般都大于目标需要显示的尺寸,因此可以通过缩放的方式,来减少显示时的图片宽高,从而大大减少占用的内存。

drawable

在 Android 项目当中,drawable 文件夹都是用来放置图片资源的,不管是 jpg、png、还是9.png,都可以放在这里。除此之外,还有像 selector 这样的 xml 文件也是可以放在 drawable 文件夹下面的。
而mipmap文件夹只是用来放置应用程序的 icon 的,将 icon 放置在 mipmap 文件夹还可以让我们程序的 launcher 图标自动拥有跨设备密度展示的能力,比如说一台屏幕密度是 xxhdpi 的设备可以自动加载 mipmap-xxxhdpi 下的 icon 来作为应用程序的 launcher 图标,这样图标看上去就会更加细腻。
对于每种密度下的icon应该设计成什么尺寸其实Android也是给出了最佳建议,建议尺寸如下表:

密度 建议尺寸
mipmap-mdpi 48*48
mipmap-hdpi 72 * 72
mipmap-xhdpi 96 * 96
mipmap-xxhdpi 144 * 144
mipmap-xxxhdpi 192 * 192

怎么才能知道自己手机屏幕的密度呢?你可以使用如下方法先获取到屏幕的 dpi 值:

float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;

参考下面这个表格:

宽度dpi范围 密度
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~640dpi xxhdpi

当我们使用资源 id 来去引用一张图片时,Android 会使用一些规则来去帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用android_logo这张图时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果drawable-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,我们当前的场景就是drawable-xxxhdpi文件夹,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

小结:图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支,而 UI 在设计图片的时候也应该尽量面向高密度屏幕的设备来进行设计。就目前来讲,最佳放置图片资源的文件夹就是drawable-xxhdpi。

高效加载大图片

为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。我们可以通过下面的代码看出每个应用程序最高可用内存是多少。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");

怎样才能对图片进行压缩呢?通过设置 BitmapFactory.Options 中 inSampleSize 的值就可以实现。比如我们有一张 20481536 像素的图片,将 inSampleSize 的值设置为4,就可以把这张图片压缩成 512384 像素。原本加载这张图片需要占用 13M 的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

public static int calculateInSampleSize(BitmapFactory.Options options,
        int reqWidth, int reqHeight) {
    // 源图片的高度和宽度
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        // 计算出实际宽高和目标宽高的比率
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
        // 一定都会大于等于目标的宽和高。
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    return inSampleSize;
}

使用这个方法,首先你要将 BitmapFactory.Options 的 inJustDecodeBounds 属性设置为 true ,解析一次图片。然后将 BitmapFactory.Options 连同期望的宽度和高度一起传递到 calculateInSampleSize 方法中,就可以得到合适的 inSampleSize 值了。之后再解析一次图片,使用新获取到的 inSampleSize 值,并把 inJustDecodeBounds 设置为 false ,就可以得到压缩后的图片了。


public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 调用上面定义的方法计算inSampleSize值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 使用获取到的inSampleSize值再次解析图片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。


mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

你可能感兴趣的:(Android 基础之图片加载(一))