手记

理解ThumbnailUtils

前言

特别喜欢系统中一些小而精的工具类,有的时候分析一下别有一番味道。
ThumbnailUtils是系统内置的一个生成缩略图的工具类,只有512行代码,网上有很多使用ThumbnailUtils的例子,刚好我个人正在整理Bitmap的相关资料,希望从中也能有所收获。

几个概念

像素规范

系统中对缩略图的像素定义了三种规范:

1
2
3
4
5
// frameworks/base/core/java/android/provider/MediaStoreSaver.java
// Images.Thumbnails
public static final int MINI_KIND = 1; // 512 x 384
public static final int FULL_SCREEN_KIND = 2; // 未定义
public static final int MICRO_KIND = 3; // 160 * 120


对于开发者,只支持MINI_KIND和MICRO_KIND两种类型。为什么是这个像素呢?因为ThumbnailUtils中定义如下:

1
2
3
4
5
6
public class ThumbnailUtils {
    /* Maximum pixels size for created bitmap. */
    private static final int MAX_NUM_PIXELS_THUMBNAIL = 512 * 384;
    private static final int MAX_NUM_PIXELS_MICRO_THUMBNAIL = 160 * 120;
    private static final int UNCONSTRAINED = -1;
}


其中MAX_NUM_PIXELS_MICRO_THUMBNAIL的值之前是128 128,在4.2+版本上被调整为160 120,原因很简单,现在手机拍摄照片比例普遍是4:3,如果不是这个比例生成缩略图的时候需要更多的计算。

尺寸规范

系统中对MINI_KIND和MICRO_KIND两种类型的图片尺寸做了限制,强调一下,是“系统”。

1
2
public static final int TARGET_SIZE_MINI_THUMBNAIL = 320;
public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96;


当然这两个字段是@hide的,是专门系统用的。
如果图片缩略图,MINI_KIND则等比例缩到360,MICRO_KIND则缩放为96 x 96的正方形(实现方法参考下面的#最合适的缩略图)
如果视频缩略图,MINI_KIND则等比例缩到512(这个512是写死在代码里的magic number),MICRO_KIND则缩放为96 x 96的正方形(实现方法参考下面的#最合适的缩略图)

Exif格式

Exif是一种图像文件格式,它的数据存储与JPEG格式是完全相同的。实际上Exif格式就是在JPEG格式头部插入了数码照片的信息,包括拍摄时的光圈、快门、白平衡、ISO、焦距、日期时间等各种和拍摄条件以及相机品牌、型号、色彩编码、拍摄时录制的声音以及GPS全球定位系统数据、缩略图等。

具体元信息,可参考f/b/media/java/android/media/ExifInterface.java
这里我特别指出ExifInterface的两点,在大家工作中很有可能会碰到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * This is a class for reading and writing Exif tags in a JPEG file.
 */
public class ExifInterface {
    // 1. 方向,也就是旋转角度
    public static final String TAG_ORIENTATION = "Orientation";

    // 2. 从Exif中获取缩略图, 如果没有则返回null
    public byte[] getThumbnail() {
        synchronized (sLock) {
            return getThumbnailNative(mFilename);
        }
    }
}


最合适的缩略图

等比例缩放只需要按Bitmap.createBitmap即可,但是Thumbnail的缩略图生成算法中为了从中间截图最合适的部分,包含了裁剪的逻辑。主要分两步:

  1. 先缩放:按照填满的思想缩放到目标大小

  2. 再裁剪:从中间裁剪目标大小的区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
 * 把原始图片转化为目标大小的图片,从中间截图
 * 注意:这里我把放大的一个逻辑处理删除了,那段逻辑永远不会执行
 */
private static Bitmap transform(Matrix scaler,
        Bitmap source,
        int targetWidth,
        int targetHeight,
        int options) {
    // 是否回收原始Bitmap
    boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0;

    // 计算是按宽度还是高度计算缩放比例
    // 这里通过高宽比计算缩放的方法,可以用填满的思维去想象一下
    float bitmapWidthF = source.getWidth();
    float bitmapHeightF = source.getHeight();

    float bitmapAspect = bitmapWidthF / bitmapHeightF;
    float viewAspect   = (float) targetWidth / targetHeight;

    if (bitmapAspect > viewAspect) {
        float scale = targetHeight / bitmapHeightF;
        if (scale < .9F || scale > 1F) {
            scaler.setScale(scale, scale);
        } else {
            scaler = null;
        }
    } else {
        float scale = targetWidth / bitmapWidthF;
        if (scale < .9F || scale > 1F) {
            scaler.setScale(scale, scale);
        } else {
            scaler = null;
        }
    }

    // 调用Bitmap.createBitmap方法按上面算出的缩放比例等比例缩小
    Bitmap b1;
    if (scaler != null) {
        // this is used for minithumb and crop, so we want to filter here.
        b1 = Bitmap.createBitmap(source, 0, 0,
                source.getWidth(), source.getHeight(), scaler, true);
    } else {
        b1 = source;
    }

    if (recycle && b1 != source) {
        source.recycle();
    }

    // 从中间裁剪最合适部分
    int dx1 = Math.max(0, b1.getWidth() - targetWidth);
    int dy1 = Math.max(0, b1.getHeight() - targetHeight);

    Bitmap b2 = Bitmap.createBitmap(
            b1,
            dx1 / 2,
            dy1 / 2,
            targetWidth,
            targetHeight);

    if (b2 != b1) {
        if (recycle || b1 != source) {
            b1.recycle();
        }
    }

    return b2;
}

基于上面的算法,ThumbnailUtils对外提供了如下接口生成缩略图:

1
2
3
// options主要用于是否回收原始Bitmap
public static Bitmap extractThumbnail(Bitmap source, int width, int height, int options)
public static Bitmap extractThumbnail(Bitmap source, int width, int height)


视频缩略图

使用MediaMetadataRetriever读取视频第一帧Bitmap,然后据此再生成缩略图。
如果kind为Thumbnails.MINI_KIND,就等比例生成最大宽或者高为512的小图。
如果king为Thumbnails.MICRO_KIND,就使用上面讲的最合适的缩略图算法,生成96 x 96的正方形小图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static Bitmap createVideoThumbnail(String filePath, int kind) {
    Bitmap bitmap = null;
    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    try {
        retriever.setDataSource(filePath);
        bitmap = retriever.getFrameAtTime(-1);
    } catch (IllegalArgumentException ex) {
        // Assume this is a corrupt video file
    } catch (RuntimeException ex) {
        // Assume this is a corrupt video file.
    } finally {
        try {
            retriever.release();
        } catch (RuntimeException ex) {
            // Ignore failures while cleaning up.
        }
    }

    if (bitmap == null) return null;

    if (kind == Images.Thumbnails.MINI_KIND) {
        // Scale down the bitmap if it's too large.
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        int max = Math.max(width, height);
        if (max > 512) {
            float scale = 512f / max;
            int w = Math.round(scale * width);
            int h = Math.round(scale * height);
            bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
        }
    } else if (kind == Images.Thumbnails.MICRO_KIND) {
        bitmap = extractThumbnail(bitmap,
                TARGET_SIZE_MICRO_THUMBNAIL,
                TARGET_SIZE_MICRO_THUMBNAIL,
                OPTIONS_RECYCLE_INPUT);
    }
    return bitmap;
}


内部方法

ThumbnailUtils其实对外的方法就上面三个演示的三个方法,除此之外,内部还有两部分,一部分是生成图片文件的缩略图,另外一部分就是未使用的无用代码。

计算SampleSize

系统中新加入一张图,就要生成缩略图了,最重要的就是计算SampleSize了,ThumbnailUtils提供了两种算法:

按目标最小边(minSideLength)

定义最小边的缩放比例
(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength))

按目标像素(maxNumOfPixels)

定义像素的缩放比例
(int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels))

具体实现

同时支持不指定限制,也做了一个默认值处理,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 计算缩放比例
private static int computeInitialSampleSize(BitmapFactory.Options options,
        int minSideLength, int maxNumOfPixels) {
    double w = options.outWidth;
    double h = options.outHeight;

    int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
        (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
    int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
        (int) Math.min(Math.floor(w / minSideLength),
                Math.floor(h / minSideLength));

    if (upperBound < lowerBound) {
        // return the larger one when there is no overlapping zone.
        return lowerBound;
    }

    if ((maxNumOfPixels == UNCONSTRAINED) &&
            (minSideLength == UNCONSTRAINED)) {
        return 1;
    } else if (minSideLength == UNCONSTRAINED) {
        return lowerBound;
    } else {
        return upperBound;
    }
}


但是上面的缩放比例不是标准的2的次放,不符合BitmapFactory的规范,再封装一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 规范化上面的sampleSize为2的次方或者8的倍数
// 据说这是BitmapFactory的要求,可以避免OOM?注释里说的。
private static int computeSampleSize(BitmapFactory.Options options,
        int minSideLength, int maxNumOfPixels) {
    int initialSize = computeInitialSampleSize(options, minSideLength,
            maxNumOfPixels);

    int roundedSize;
    if (initialSize <= 8) {
        // 如果小于8,转化为2的次方(通过位移来转化,可以借鉴一下)
        roundedSize = 1;
        while (roundedSize < initialSize) {
            roundedSize <<= 1;
        }
    } else {
        // 如果大于8,转化为8的倍数
        roundedSize = (initialSize + 7) / 8 * 8;
    }

    return roundedSize;
}


从EXIF中选取缩略图

只支持JPG中读取EXIF信息。
这里不是说EXIF有缩略图就用这个缩略图,而是会先用高宽算出文件本身的TargetSize对应的缩略图,和EXIF中缩放到TargetSize对应的缩略图比较,哪个大取哪个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image.
 * The functions returns a SizedThumbnailBitmap,
 * which contains a downsampled bitmap and the thumbnail data in EXIF if exists.
 */
private static void createThumbnailFromEXIF(String filePath, int targetSize,
        int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) {
    if (filePath == null) return;

    ExifInterface exif = null;
    byte [] thumbData = null;
    try {
        exif = new ExifInterface(filePath);
        thumbData = exif.getThumbnail();
    } catch (IOException ex) {
        Log.w(TAG, ex);
    }

    BitmapFactory.Options fullOptions = new BitmapFactory.Options();
    BitmapFactory.Options exifOptions = new BitmapFactory.Options();
    int exifThumbWidth = 0;
    int fullThumbWidth = 0;

    // Compute exifThumbWidth.
    if (thumbData != null) {
        exifOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions);
        exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels);
        exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize;
    }

    // Compute fullThumbWidth.
    fullOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filePath, fullOptions);
    fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels);
    fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize;

    // Choose the larger thumbnail as the returning sizedThumbBitmap.
    if (thumbData != null && exifThumbWidth >= fullThumbWidth) {
        int width = exifOptions.outWidth;
        int height = exifOptions.outHeight;
        exifOptions.inJustDecodeBounds = false;
        sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0,
                thumbData.length, exifOptions);
        if (sizedThumbBitmap.mBitmap != null) {
            sizedThumbBitmap.mThumbnailData = thumbData;
            sizedThumbBitmap.mThumbnailWidth = width;
            sizedThumbBitmap.mThumbnailHeight = height;
        }
    } else {
        fullOptions.inJustDecodeBounds = false;
        sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions);
    }
}


图片文件缩略图

如果是MINI_KIND,尺寸最小边缩放到320左右,像素缩放到512 x 387。否则就是MICRO_KIND,尺寸最大边缩放到96,像素所放到160 x 120。
如果图片是JPG,参考上面的方法从EXIF中选取缩略图。否则,用decodeFileDescriptor()老老实实等比例生成缩略图。
最终成功后,如果是MICRO_KIND,还要裁剪为96 x 96的正方形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public static Bitmap createImageThumbnail(String filePath, int kind) {
    boolean wantMini = (kind == Images.Thumbnails.MINI_KIND);
    int targetSize = wantMini
        ? TARGET_SIZE_MINI_THUMBNAIL
        : TARGET_SIZE_MICRO_THUMBNAIL;
    int maxPixels = wantMini
        ? MAX_NUM_PIXELS_THUMBNAIL
        : MAX_NUM_PIXELS_MICRO_THUMBNAIL;
    SizedThumbnailBitmap sizedThumbnailBitmap = new SizedThumbnailBitmap();
    Bitmap bitmap = null;
    MediaFileType fileType = MediaFile.getFileType(filePath);
    if (fileType != null && fileType.fileType == MediaFile.FILE_TYPE_JPEG) {
        createThumbnailFromEXIF(filePath, targetSize, maxPixels, sizedThumbnailBitmap);
        bitmap = sizedThumbnailBitmap.mBitmap;
    }

    if (bitmap == null) {
        FileInputStream stream = null;
        try {
            stream = new FileInputStream(filePath);
            FileDescriptor fd = stream.getFD();
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inSampleSize = 1;
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFileDescriptor(fd, null, options);
            if (options.mCancel || options.outWidth == -1
                    || options.outHeight == -1) {
                return null;
            }
            options.inSampleSize = computeSampleSize(
                    options, targetSize, maxPixels);
            options.inJustDecodeBounds = false;

            options.inDither = false;
            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
            bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
        } catch (IOException ex) {
            Log.e(TAG, "", ex);
        } catch (OutOfMemoryError oom) {
            Log.e(TAG, "Unable to decode file " + filePath + ". OutOfMemoryError.", oom);
        } finally {
            try {
                if (stream != null) {
                    stream.close();
                }
            } catch (IOException ex) {
                Log.e(TAG, "", ex);
            }
        }

    }

    if (kind == Images.Thumbnails.MICRO_KIND) {
        // now we make it a "square thumbnail" for MICRO_KIND thumbnail
        bitmap = extractThumbnail(bitmap,
                TARGET_SIZE_MICRO_THUMBNAIL,
                TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT);
    }
    return bitmap;
}


这里你可能注意到了,如果从EXIF的代码中获取本身文件缩略图用的是decodeFile(),而后面非JPG图片获取缩略图用decodeFileDescriptor(),为什么呢?
不知道,也许是开发者“Ray Chen”忘记了,只改了一部分,另外一部分为了稳定性也没改。
据网上资料看,decodeFileDescriptor()比decodeFile()更省内存,没有论证,仅供参考。

未使用的无用代码

在ThumbnailUtils有一些私有方法,但是自己又没有去调用,暂且把这些方法定位无用代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels.
 * The image data will be read from specified pfd if it's not null, otherwise
 * a new input stream will be created using specified ContentResolver.
 *
 * Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A
 * new BitmapFactory.Options will be created if options is null.
 */
private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
        Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,
        BitmapFactory.Options options) {
    Bitmap b = null;
    try {
        if (pfd == null) pfd = makeInputStream(uri, cr);
        if (pfd == null) return null;
        if (options == null) options = new BitmapFactory.Options();

        FileDescriptor fd = pfd.getFileDescriptor();
        options.inSampleSize = 1;
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        if (options.mCancel || options.outWidth == -1
                || options.outHeight == -1) {
            return null;
        }
        options.inSampleSize = computeSampleSize(
                options, minSideLength, maxNumOfPixels);
        options.inJustDecodeBounds = false;

        options.inDither = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        b = BitmapFactory.decodeFileDescriptor(fd, null, options);
    } catch (OutOfMemoryError ex) {
        Log.e(TAG, "Got oom exception ", ex);
        return null;
    } finally {
        closeSilently(pfd);
    }
    return b;
}

private static void closeSilently(ParcelFileDescriptor c) {
    if (c == null) return;
    try {
        c.close();
    } catch (Throwable t) {
        // do nothing
    }
}

private static ParcelFileDescriptor makeInputStream(
        Uri uri, ContentResolver cr) {
    try {
        return cr.openFileDescriptor(uri, "r");
    } catch (IOException ex) {
        return null;
    }
}


小结

通过学习ThumbnailUtils生成缩略图的方方面面,结合自己的经验实践,从此生成缩略图无忧。
零零散散写的有点乱,但基本上能运行到的每行代码都覆盖到了,对于理解ThumbnailUtils这个类来说,应该够了。

原文链接:http://www.apkbus.com/blog-705730-60862.html

0人推荐
随时随地看视频
慕课网APP