Glide 4.9 原始碼分析(二) —— 取樣壓縮的實現
前言
從 Glide 的一次載入流程中可知, Glide 拿到資料流之後, 使用 Downsampler 進行取樣處理並且反回了一個 Bitmap
public class StreamBitmapDecoder implements ResourceDecoder<InputStream, Bitmap> { private final Downsampler downsampler; public Resource<Bitmap> decode(@NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { ...... try { // 根據請求配置的資料, 對資料流進行取樣壓縮 return downsampler.decode(invalidatingStream, width, height, options, callbacks); } finally { ...... } } }
本次就著重的分析它對資料流的處理
一. 處理資料流
public final class Downsampler { public Resource<Bitmap> decode(InputStream is, int outWidth, int outHeight, Options options) throws IOException { return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS); } @SuppressWarnings({"resource", "deprecation"}) public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight, Options options, DecodeCallbacks callbacks) throws IOException { // 從快取複用池中獲取 byte 資料組 byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class); // 獲取 Bitmap.Options 併為其 BitmapFactory.Options.inTempStorage 分配緩衝區 BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions(); bitmapFactoryOptions.inTempStorage = bytesForOptions; // 獲取解碼的型別, ARGB_8888, RGB_565... DecodeFormat decodeFormat = options.get(DECODE_FORMAT); // 獲取採用壓縮的策略 DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION); // 是否需要將 Bitmap 的寬高固定為請求的尺寸 boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS); // 用於判斷 Bitmap 尺寸是否是可變的 boolean isHardwareConfigAllowed = options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG); try { // 呼叫 decodeFromWrappedStreams 獲取 Bitmap 資料 Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions, downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth, requestedHeight, fixBitmapToRequestedDimensions, callbacks); return BitmapResource.obtain(result, bitmapPool); } finally { ....... // 回收陣列資料 byteArrayPool.put(bytesForOptions); } } private Bitmap decodeFromWrappedStreams(InputStream is, BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth, int requestedHeight, boolean fixBitmapToRequestedDimensions, DecodeCallbacks callbacks) throws IOException { long startTime = LogTime.getLogTime(); // 1. 通過資料流解析圖片的尺寸 int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool); int sourceWidth = sourceDimensions[0]; int sourceHeight = sourceDimensions[1]; ...... // 2. 獲取圖形的旋轉角度等資訊 int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation); boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation); // 3. 獲取目標的寬高 int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth; int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight; // 4. 解析圖片封裝格式, JPEG, PNG, WEBP, GIF... ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool); // 5. 計算 Bitmap 的取樣率存放到 options.inSampleSize 中 calculateScaling(......); // 6. 計算 Bitmap 所需顏色通道, 儲存到 options.inPreferredConfig 中 calculateConfig(.......); // 7. 根據取樣率計算期望的尺寸, boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) { int expectedWidth; int expectedHeight; if (sourceWidth >= 0 && sourceHeight >= 0 && fixBitmapToRequestedDimensions && isKitKatOrGreater) { expectedWidth = targetWidth; expectedHeight = targetHeight; } else { // 計算 density 的比例 float densityMultiplier = isScaling(options) ? (float) options.inTargetDensity / options.inDensity : 1f; int sampleSize = options.inSampleSize; // 計算取樣的寬高 int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize); int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize); // 根據畫素比求出期望的寬高 expectedWidth = Math.round(downsampledWidth * densityMultiplier); expectedHeight = Math.round(downsampledHeight * densityMultiplier); } // 7.1 根據期望的寬高從 BitmapPool 中取可以複用的物件, 存入 Options.inBitmap 中, 減少記憶體消耗 if (expectedWidth > 0 && expectedHeight > 0) { setInBitmap(options, bitmapPool, expectedWidth, expectedHeight); } } // 8. 根據配置好的 options 解析資料流 Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool); callbacks.onDecodeComplete(bitmapPool, downsampled); // 9. 嘗試對圖片進行角度矯正 Bitmap rotated = null; if (downsampled != null) { // 嘗試對圖片進行旋轉操作 downsampled.setDensity(displayMetrics.densityDpi); rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation); // 若返回了一個新的 Bitmap, 則將之前的 Bitmap 新增進享元複用池 if (!downsampled.equals(rotated)) { bitmapPool.put(downsampled); } } return rotated; } }
好的, Downsampler.decode 解析資料流獲取 Bitmap 物件一共有如下幾個步驟
- 通過資料流解析出圖形的原始寬高
- 獲取圖形的旋轉角度等資訊
- 獲取這次圖片請求的目標寬高
-
獲取影象的封裝格式
- JPEG, PNG, WEBP, GIF...
- 計算 Bitmap 縮放方式
- 計算 Bitmap 顏色通道
-
根據取樣率計算期望的尺寸
- 根據期望的寬高從 BitmapPool 中取可以複用的物件, 存入 Options.inBitmap 中, 減少記憶體消耗
-
根據配置好的 options 解析資料流
- 與獲取影象原始寬高的操作一致
- 對影象進行角度矯正
好的, 可見 Glide 解析一次資料流做了很多的操作, 我們對重點的操作進行逐一分析
二. 通過資料流獲取影象寬高
public final class Downsampler { private static int[] getDimensions(InputStream is, BitmapFactory.Options options, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException { options.inJustDecodeBounds = true; decodeStream(is, options, decodeCallbacks, bitmapPool); options.inJustDecodeBounds = false; return new int[] { options.outWidth, options.outHeight }; } private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException { if (options.inJustDecodeBounds) { is.mark(MARK_POSITION); } else { ...... callbacks.onObtainBounds(); } ...... final Bitmap result; TransformationUtils.getBitmapDrawableLock().lock(); try { // 1. 通過 BitmapFactory 來解析 InputStream 將資料儲存在 options 中 result = BitmapFactory.decodeStream(is, null, options); } catch (IllegalArgumentException e) { ...... // 2. 若是因為 BitmapFactory 無法重用 options.inBitmap 這個點陣圖, 則會進入下面分支 if (options.inBitmap != null) { try { is.reset();// 重置 InputStream 的位置 bitmapPool.put(options.inBitmap);// 將 inBitmap 新增到快取池中 // 2.1 將 options.inBitmap 置空後重新解析 options.inBitmap = null; return decodeStream(is, options, callbacks, bitmapPool); } catch (IOException resetException) { ...... } } ...... } finally { TransformationUtils.getBitmapDrawableLock().unlock(); } // 3. 重置 InputStream 流, 供後續使用 if (options.inJustDecodeBounds) { is.reset(); } // 4. 返回解析到的資料 return result; } }
具體的流程如上所示, 其中還是有很多細節值得我們參考和學習
- 在解析 Bitmap 的時候, 通過給 Options 中的 inBitmap 賦值, 讓新解析的 Bitmap 複用這個物件以此來減少記憶體的消耗
- 若無法複用則會在異常處理中, 使用無 inBitmap 的方式再次解析
三. 獲取影象封裝格式
public final class ImageHeaderParserUtils { public static ImageType getType(@NonNull List<ImageHeaderParser> parsers, @Nullable InputStream is, @NonNull ArrayPool byteArrayPool) throws IOException { ...... is.mark(MARK_POSITION); for (int i = 0, size = parsers.size(); i < size; i++) { // 1. 獲取解析器 ImageHeaderParser parser = parsers.get(i); try { // 2. 使用解析器解析輸入流獲取圖片型別 ImageType type = parser.getType(is); if (type != ImageType.UNKNOWN) { return type; } } finally { is.reset(); } } return ImageType.UNKNOWN; } }
好的, 首先是獲取解析器, 這個解析器是 Glide 物件建立時註冊的
public class Glide implements ComponentCallbacks2 { Glide(...) { ...... registry = new Registry(); registry.register(new DefaultImageHeaderParser()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { registry.register(new ExifInterfaceImageHeaderParser()); } ...... } }
Glide 中提供了兩個解析器, 分別為 DefaultImageHeaderParser 和 ExifInterfaceImageHeaderParser, 我們主要關注一下 DefaultImageHeaderParser 這個解析器
public final class DefaultImageHeaderParser implements ImageHeaderParser { @Override public ImageType getType(@NonNull InputStream is) throws IOException { return getType(new StreamReader(Preconditions.checkNotNull(is))); } private static final int GIF_HEADER = 0x474946; private static final int PNG_HEADER = 0x89504E47; static final int EXIF_MAGIC_NUMBER = 0xFFD8; // "RIFF" private static final int RIFF_HEADER = 0x52494646; // "WEBP" private static final int WEBP_HEADER = 0x57454250; // "VP8" null. private static final int VP8_HEADER = 0x56503800; private static final int VP8_HEADER_MASK = 0xFFFFFF00; private static final int VP8_HEADER_TYPE_MASK = 0x000000FF; // 'X' private static final int VP8_HEADER_TYPE_EXTENDED = 0x00000058; // 'L' private static final int VP8_HEADER_TYPE_LOSSLESS = 0x0000004C; private static final int WEBP_EXTENDED_ALPHA_FLAG = 1 << 4; private static final int WEBP_LOSSLESS_ALPHA_FLAG = 1 << 3; private ImageType getType(Reader reader) throws IOException { final int firstTwoBytes = reader.getUInt16(); // 1. 獲取 InputStream 的前兩個 Byte, 若為 0xFFD8 則說明為 JPEG 封裝格式 if (firstTwoBytes == EXIF_MAGIC_NUMBER) { return JPEG; } // 2. 獲取 InputStream 前四個 Byte, 若為 0x89504E47, 則說明為 PNG 封裝格式 final int firstFourBytes = (firstTwoBytes << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF); if (firstFourBytes == PNG_HEADER) { // 2.1 判斷是否為帶 Alpha 通道的 png 圖片 reader.skip(25 - 4); int alpha = reader.getByte(); return alpha >= 3 ? PNG_A : PNG; } // 3. 獲取前三個 Byte, 若為 0x474946, 則說明為 GIF 封裝格式 if (firstFourBytes >> 8 == GIF_HEADER) { return GIF; } // 4. 判斷是否為 Webp 封裝型別 if (firstFourBytes != RIFF_HEADER) { return UNKNOWN; } reader.skip(4);// Bytes [4 - 7] 包含的是長度資訊, 跳過 final int thirdFourBytes = (reader.getUInt16() << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF); if (thirdFourBytes != WEBP_HEADER) { return UNKNOWN; } final int fourthFourBytes = (reader.getUInt16() << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF); if ((fourthFourBytes & VP8_HEADER_MASK) != VP8_HEADER) { return UNKNOWN; } if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_EXTENDED) { // Skip some more length bytes and check for transparency/alpha flag. reader.skip(4); return (reader.getByte() & WEBP_EXTENDED_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP; } if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_LOSSLESS) { reader.skip(4); return (reader.getByte() & WEBP_LOSSLESS_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP; } return ImageType.WEBP; } }
好的, 可以看到它是通過圖片封裝格式中的位元組數來判斷圖片的型別的
- JPEG 的前兩個 Byte 為 0xFFD8
- PNG 的前 4 個 Byte 為 0x89504E47
- GIF 的前 3 個 Byte 為 0x474946
- WEBP 的判定較為複雜可以對照程式碼自行檢視
我們知道平時獲取圖片封裝格式是使用以下的方式
val ops = BitmapFactory.Options() ops.inJustDecodeBounds = true val bitmap = BitmapFactory.decodeResource(resources, R.drawable.wallpaper, ops) Log.e("TAG", ops.outMimeType)
Glide 通過直接解析流的方式獲取圖片的封裝格式, 不需要關注其他資訊, 無疑比通過 BitmapFactory 來的更加高效
四. 計算 Bitmap 縮放方式
Glid 對於 Bitmap 縮放的計算過程比較複雜, 分別有如下幾步
- 計算取樣率
- 計算取樣後圖片的尺寸
- 將取樣後圖片的尺寸調整為目標尺寸
一) 計算取樣率
public final class Downsampler { private static void calculateScaling( ImageType imageType, InputStream is, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool, DownsampleStrategy downsampleStrategy, int degreesToRotate, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight, BitmapFactory.Options options) throws IOException { ...... // 1. 計算取樣率 // 1.1 獲取源圖片尺寸與目標尺寸的精確縮放比 // downsampleStrategy 在構建 Request 時傳入 final float exactScaleFactor; if (degreesToRotate == 90 || degreesToRotate == 270) { // 1.1.1 將寬高倒置計算縮放因子 exactScaleFactor = downsampleStrategy.getScaleFactor(sourceHeight, sourceWidth, targetWidth, targetHeight); } else { // 1.1.2 正常計算縮放因子 exactScaleFactor = downsampleStrategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight); } // 1.2 獲取取樣的型別: MEMORY(節省記憶體), QUALITY(更高質量) SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth, sourceHeight, targetWidth, targetHeight); ...... // 1,3 計算縮放因子 // 1.3.1 計算整型的尺寸(round 操作在原來值的基礎上 + 0.5), 參考 Android 原始碼 int outWidth = round(exactScaleFactor * sourceWidth); int outHeight = round(exactScaleFactor * sourceHeight); // 1.3.2 計算寬高方向上的整型縮放因子 int widthScaleFactor = sourceWidth / outWidth; int heightScaleFactor = sourceHeight / outHeight; // 1.3.3 根據取樣型別, 確定整型縮放因子 scaleFactor // 若為 MEMORY, 則為寬高的最大值 // 若為 QUALITY, 則為寬高的最小值 int scaleFactor = rounding == SampleSizeRounding.MEMORY ? Math.max(widthScaleFactor, heightScaleFactor) : Math.min(widthScaleFactor, heightScaleFactor); // 1.4 根據整型縮放因子, 計算取樣率(即將 scaleFactor 轉為 2 的冪次) int powerOfTwoSampleSize; // 1.4.1 Android 7.0 以下不支援縮放 webp, 縮放因子置為 1 if (Build.VERSION.SDK_INT <= 23 && NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) { powerOfTwoSampleSize = 1; } else { // 1.4.2 將 scaleFactor 轉為 2 的冪次, 若為省記憶體模式, 則嘗試近一步增加取樣率 powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor)); if (rounding == SampleSizeRounding.MEMORY && powerOfTwoSampleSize < (1.f / exactScaleFactor)) { powerOfTwoSampleSize = powerOfTwoSampleSize << 1; } } ...... } }
計算取樣率的過程主要有如下幾步
- 計算精確的縮放因子
-
獲取取樣的型別
- MEMORY: 省記憶體
- QUALITY: 高質量
- 計算整型的縮放因子
-
將整型縮放因子轉為 2 的冪次
- 即轉為 BitmapFactory 可用的取樣率
二) 計算取樣後圖片尺寸
public final class Downsampler { private static void calculateScaling(...) throws IOException { ...... // 2. 根據取樣率, 計算取樣後圖片的尺寸 options.inSampleSize = powerOfTwoSampleSize; int powerOfTwoWidth; int powerOfTwoHeight; // 2.1 處理 JPEG if (imageType == ImageType.JPEG) { // Libjpeg 最高支援單次 8 位的降取樣, 超過 8 次則分步計算 int nativeScaling = Math.min(powerOfTwoSampleSize, 8); powerOfTwoWidth = (int) Math.ceil(sourceWidth / (float) nativeScaling);// 對 float 向上取整 powerOfTwoHeight = (int) Math.ceil(sourceHeight / (float) nativeScaling); // 若 powerOfTwoSampleSize 比 8 大, 則再進行一次取樣, 用於計算出最終的目標值 int secondaryScaling = powerOfTwoSampleSize / 8; if (secondaryScaling > 0) { powerOfTwoWidth = powerOfTwoWidth / secondaryScaling; powerOfTwoHeight = powerOfTwoHeight / secondaryScaling; } //2.2 處理 PNG } else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) { // 對取樣結果向下取整 powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize); powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize); // 2.3 處理 WEBP } else if (imageType == ImageType.WEBP || imageType == ImageType.WEBP_A) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 7.0 以上對取樣結果進行四捨五入 powerOfTwoWidth = Math.round(sourceWidth / (float) powerOfTwoSampleSize); powerOfTwoHeight = Math.round(sourceHeight / (float) powerOfTwoSampleSize); } else { // 7.0 以下, 對取樣結果向下取整 powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize); powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize); } // 2.4 處理其他圖片型別, 並且需要降取樣 } else if ( sourceWidth % powerOfTwoSampleSize != 0 || sourceHeight % powerOfTwoSampleSize != 0) { // 通過 Android 的 BitmapFactory 去獲取尺寸 int[] dimensions = getDimensions(is, options, decodeCallbacks, bitmapPool); powerOfTwoWidth = dimensions[0]; powerOfTwoHeight = dimensions[1]; // 2.5 處理其他圖片型別, 並且不需要降取樣 } else { // 若為其他圖片型別, 並且不需要降取樣 powerOfTwoWidth = sourceWidth / powerOfTwoSampleSize; powerOfTwoHeight = sourceHeight / powerOfTwoSampleSize; } ...... } }
計算取樣尺寸, Glide 並沒有直接將取樣率放入 options.inSampleSize 而是根據規則自行進行了運算, 降低了使用 BitmapFactory 呼叫 native 方法帶來的效能損耗
三) 將取樣後圖片的尺寸調整為目標尺寸
public final class Downsampler { private static void calculateScaling(...) throws IOException { ...... // 3. 將取樣尺寸調整成為目標尺寸 // 3.1 計算取樣尺寸與目標尺寸的縮放因子 double adjustedScaleFactor = downsampleStrategy.getScaleFactor( powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight); // 3.2 通過調整 inTargetDensity 和 inDensity 來完成目標的顯示效果 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // 調整目標的螢幕密度 options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor); // 調整圖片的畫素密度 options.inDensity = getDensityMultiplier(adjustedScaleFactor); } if (isScaling(options)) { options.inScaled = true; } else { options.inDensity = options.inTargetDensity = 0; } } }
可以看到將取樣尺寸調整成為目標尺寸 是通過調整 options 中 inTargetDensity 和 inDensity 的值, 來讓圖片縮放到目標顯示效果尺寸的
好的, 到這裡 Glide 計算 Bitmap 縮放的部分就解析完畢了, 我們光知道 Glide 預設會將圖片載入的尺寸置為 ImageView 的大小, 卻不知道它為了還原的精度, 內部做了如何之多的細節處理, 其縝密性可見一斑
五. 選擇顏色通道
public final class Downsampler { private void calculateConfig( InputStream is, DecodeFormat format, boolean isHardwareConfigAllowed, boolean isExifOrientationRequired, BitmapFactory.Options optionsWithScaling, int targetWidth, int targetHeight) { ...... // 判斷是否有 Alpha 通道 boolean hasAlpha = false; try { hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha(); } catch (IOException e) { ...... } // 若存在 Alpha 通道則使用 RGB_8888, 反之使用 565 optionsWithScaling.inPreferredConfig = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; if (optionsWithScaling.inPreferredConfig == Config.RGB_565) { optionsWithScaling.inDither = true; } } }
好的, Bitmap 顏色通道的選取方式還是非常簡單的
- 對於存在透明通道的圖片, 使用 ARGB_8888 保證圖片不會丟失透明通道
- 對於無透明通道圖片, 使用 RGB_565 保證圖片記憶體佔用量最低
總結
到這裡 Glide 將資料流解析成為 Bitmap 的流程就完成了, 其中提供了非常優秀的將圖片取樣壓縮的實現 和顏色通道的選取策略 , 這都非常值得我們學習和借鑑