關於圖片的那點事兒
Q: 一張大小為 55KB, 解析度為 1080 * 480 的 PNG 圖片,它載入近記憶體時所佔的大小是多少呢?
圖片記憶體大小
圖片佔用記憶體大小 = 解析度 * 畫素點大小
其中資料格式不同畫素點大小也不同:
- ALPHA_8: 1B
- RGB_565: 2B
- ARGB_4444: 2B
- ARGB_8888: 4B
- RGBA_F16: 8B
現在回過頭來看上面的問題,在電腦上顯示 55KB 的圖片,png 只是這張圖片的容器,他們是經過相對應的壓縮演算法將原圖的每個畫素點資訊轉換為另一種資料格式。
在一般情況下,這張圖片佔用的內容應該是:1080 * 480 * 4B = 1.98 M。
每種裝置都會有所差異,以 android 為例,我們將同一張圖片放在不同 dpi 的 res/drawable 目錄下,佔用的記憶體也不一樣。
這是因為在 android 中 Bitmap.decodeResource()
會根據圖片存放的目錄做一次寬高的轉換,具體公式如下:
轉換後高度 = 原圖高度 * (裝置的 dpi /目錄對應的 dpi )
轉換後寬度 = 原圖寬度 * (裝置的 dip / 目錄對應的 dpi)
假設你的手機 dpi 是 320(對應 xhdpi),你將上述的圖片放在 xhdpi 目錄下:
圖片佔用記憶體 = 1080 * (320 / 320) * 480 * (320 / 320) * 4B = 1.98 M
同樣的手機,將上述圖片放到 hdpi (240 dpi) 目錄下:
圖片佔用記憶體 = 1080 * (320 / 240) * 480 * (320 / 240) * 4B = 3.52 M
如果需要檢視手機 density 相關配置,可以使用如下命令:
adb shell cat system/build.prop|grep density
該命令可得到手機的 dpi,平常我們在佈局中的單位都是 dp,那 1 dp 等於多少 px 呢。
根據官方轉換公式在 160 dpi 手機下 1 dp 等於 1 px,如果手機 dpi 為 440 dpi,則 1 dp = 2.75 px
如何降低一張圖片佔用的記憶體
Bitmap 相關屬性說明
簡單瞭解下BitmapOption的幾個相關屬性:
- inBitmap——在解析Bitmap時重用該Bitmap,不過必須等大的Bitmap而且inMutable須為true
- inMutable——配置Bitmap是否可以更改,比如:在Bitmap上隔幾個畫素加一條線段
- inJustDecodeBounds——為true僅返回Bitmap的寬高等屬性
- inSampleSize——須>=1,表示Bitmap的壓縮比例,如:inSampleSize=4,將返回一個是原始圖的1/16大小的
- Bitmap
- inPreferredConfig——Bitmap.Config.ARGB_8888等
- inDither——是否抖動,預設為false
- inPremultiplied——預設為true,一般不改變它的值
- inDensity——Bitmap的畫素密度
- inTargetDensity——Bitmap最終的畫素密度
- inScreenDensity——當前螢幕的畫素密度
- inScaled——是否支援縮放,預設為true,當設定了這個,Bitmap將會以inTargetDensity的值進行縮放
- inPurgeable——當儲存Pixel的記憶體空間在系統記憶體不足時是否可以被回收
- inInputShareable——inPurgeable為true情況下才生效,是否可以共享一個InputStream
- inPreferQualityOverSpeed——為true則優先保證Bitmap質量其次是解碼速度
- outWidth——返回的Bitmap的寬
- outHeight——返回的Bitmap的高
- inTempStorage——解碼時的臨時空間,建議16*1024
降低解析度
android 系統提供了相應的 api 可以按比例壓縮圖片 BitmapFactory.Options.inSampleSize
inSampleSzie 值越大,壓縮比例越高
改變資料格式
android 系統預設以 ARGB_8888 格式處理圖片,那麼每個畫素點就需要佔用 4B 大小,可以將格式改為 RGB_565
Glide 中的圖片壓縮
圖片載入的簡單過程
我們使用 Glide 載入圖片的最後一步是 #into(ImageView)
我們直接定位到 RequestBuilder#into(ImageView)
方法:
BaseRequestOptions<?> requestOptions = this;
... // 根據 ImageView 原生的 scale type 構建 Glide 的 scale type
Request = buildRequest(target, targetListener, options) // 這裡最終呼叫的是 SingleRequest.obtain() 來建立 request
requestManager.track(target, request); //從這裡開始請求 URL 載入圖片
複製程式碼
在 tarck() 方法中執行了 targetTracker.track(target),而這行程式碼就是用來跟蹤生命週期的
如果我們是從網路載入圖片,當圖片下載成功後會回撥 SingleRequest#onResourceReady(Resource<?> resource, DataSource dataSource)
方法。
而圖片的下載及解碼起始於 SingleRequest#onSizeReady
,然後呼叫 Engine#load()
開始下載及解碼:
... //省略分別從記憶體,disk 讀取圖片程式碼
EnginJob<R> engineJob = engineJobFactory.build();
DecodeJob<R> decodeJob = decodeJobFacotry.build();
josbs.put(key, enginJob);
engineJob.addCallback(cb);
engineJob.start(decodeJob); //開始解碼工作
複製程式碼
最後呼叫 DecodePath#decodeResourceWithList()
,關鍵程式碼:
Resource<ResourceType> result = null;
for (int i = 0, size = decoders.size(); i < size; i++) {
ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
result = decoder.decode(data, width, height, options);
}
return result;
複製程式碼
圖片解碼
接下來分析圖片的解碼過程。
首先我們需要搞清楚 decoders 是怎麼來的,原來在初始化 Glide 時會將 Glide 支援的所有 Decoder 註冊到 decoderRegistry 中,最終呼叫 ResourceDecoderRegistry#getDecoders()
方法來獲取所需要的 decoders:
public synchronized <T, R> List<ResourceDecoder<T, R>> getDecoders(@NonNull Class<T> dataClass,
@NonNull Class<R> resourceClass) {
List<ResourceDecoder<T, R>> result = new ArrayList<>();
for (String bucket : bucketPriorityList) {
List<Entry<?, ?>> entries = decoders.get(bucket);
if (entries == null) {
continue;
}
for (Entry<?, ?> entry : entries) {
if (entry.handles(dataClass, resourceClass)) {
result.add((ResourceDecoder<T, R>) entry.decoder);
}
}
}
// TODO: cache result list.
return result;
}
複製程式碼
在Glide中 ResourceDecoder 的實現類有很多,如下圖所示
Glide 根據圖片的資源型別會呼叫不同的 Decoder 進行解碼,現在我們以最常見的場景,載入網路圖片來說明。載入網路圖片(PNG格式)呼叫的是 ByteBufferBitmapDecoder。
不管是載入網路圖片還是載入本地資源,都是通過 ByteBufferBitmapDecoder 類進行解碼
public class ByteBufferBitmapDecoder implements ResourceDecoder<ByteBuffer, Bitmap> {
private final Downsampler downsampler;
public ByteBufferBitmapDecoder(Downsampler downsampler) {
this.downsampler = downsampler;
}
@Override
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
return downsampler.handles(source);
}
@Override
public Resource<Bitmap> decode(@NonNull ByteBuffer source, int width, int height,
@NonNull Options options)
throws IOException {
InputStream is = ByteBufferUtil.toStream(source);
return downsampler.decode(is, width, height, options);
}
}
複製程式碼
該類很簡單,最主要的是呼叫Downsampler#decode
方法,Downsampler 直譯向下取樣器,接下來就重點看下該類。
Downsampler
首先來看 Downsampler
對外提供的方法 decode
方法
public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
Options options, DecodeCallbacks callbacks) throws IOException {
Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
+ " mark()");
/* 開始構建 BitmpFactory.Options */
byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
bitmapFactoryOptions.inTempStorage = bytesForOptions;
DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
boolean isHardwareConfigAllowed =
options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);
try {
Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
requestedHeight, fixBitmapToRequestedDimensions, callbacks);
return BitmapResource.obtain(result, bitmapPool);
} finally {
releaseOptions(bitmapFactoryOptions);
byteArrayPool.put(bytesForOptions);
}
}
複製程式碼
該方法首先為 BitmapFactory.Options 設定所需要的引數
-
inTempStorage
Temp storage to use for decoding. Suggest 16K or so. Glide 在這裡用的是 64k
-
decodeFormat
解碼格式, glide 中的圖片主要為兩種模式 ARGB_8888, RGB_565
-
fixBitmapToRequestedDimensions
預設為 false(暫時不太理解這個屬性的含義,也無法設定成 true)
-
isHardwareConfigAllowed
預設禁用
boolean isHardwareConfigSafe = dataSource == DataSource.RESOURCE_DISK_CACHE || decodeHelper.isScaleOnlyOrNoTransform(); Boolean isHardwareConfigAllowed = options.get(Downsampler.ALLOW_HARDWARE_CONFIG); 複製程式碼
接下來通過 decodeFromWrappedStream 獲取 bitmap,該方法主要邏輯如下:
int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool); //獲取原始圖片的寬高
int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
calculateScaling(); //設定 inSampleSize 縮放(取樣)比例
calculateConfig();
Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
callbacks.onDecodeComplete(bitmapPool, downsampled);
複製程式碼
我們先來理清這幾個size,以 width 為例
- sourceWidth: 即你從網路下載的原始圖片的寬
- requestedWidth: 預設為 ImageView 的寬
- targeWidth: 最終生成的 bitmap 的寬
接下來分析 calculateScaling 方法
由於都是計算相關,所以舉個栗子,假設圖片的sourceWidth 為 1000, targetWidth為 200, sourceHeight為 1200, targetWidth 為 300
final float exactScaleFactor = downsampleStrategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight); //假設向下取樣策略為 CenterOutside 實現,則exactScaleFactor 等於 0.25
SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth,
sourceHeight, targetWidth, targetHeight); //rouding 為 QUALITY
int outWidth = round(exactScaleFactor * sourceWidth); //outWidth = 0.25*1000 + 0.5 = 250
int outHeight = round(exactScaleFactor * sourceHeight); // outHeight = 0.25*1200 + 0.5 = 300
int widthScaleFactor = sourceWidth / outWidth; //widthScaleFactor = 1000/250 = 4
int heightScaleFactor = sourceHeight / outHeight; //heightScalFactor = 1200/300 = 4
int scaleFactor = rounding == SampleSizeRounding.MEMORY //scaleFactor = 4
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor)); //powerOfTowSampleSize = 4,且只可能是 1,2,4,8,16 ...
if (rounding == SampleSizeRounding.MEMORY
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
options.inSampleSize = powerOfTwoSampleSize;
// 這裡暫時還不太理解,看演算法這裡的 inTragetDesity 和 inDensity 的比值永遠為 1
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;
}
複製程式碼
我們簡單看下 CenterOutside
類,程式碼很簡單:
public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth,
int requestedHeight) {
float widthPercentage = requestedWidth / (float) sourceWidth;
float heightPercentage = requestedHeight / (float) sourceHeight;
return Math.max(widthPercentage, heightPercentage);
}
@Override
public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight,
int requestedWidth, int requestedHeight) {
return SampleSizeRounding.QUALITY; // 返回值有 QUALITY 和 MEMORY,其中 MEMORY 相比較 QUALITY 會佔用更少記憶體
}
}
複製程式碼
接下來通過呼叫calculateConfig為 options 設定其他屬性
if (hardwareConfigState.setHardwareConfigIfAllowed(
targetWidth,
targetHeight,
optionsWithScaling,
format,
isHardwareConfigAllowed,
isExifOrientationRequired)) {
return;
}
// Changing configs can cause skewing on 4.1, see issue #128.
if (format == DecodeFormat.PREFER_ARGB_8888
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
return;
}
boolean hasAlpha = false;
try {
hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot determine whether the image has alpha or not from header"
+ ", format " + format, e);
}
}
optionsWithScaling.inPreferredConfig =
hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
optionsWithScaling.inDither = true;
}
複製程式碼
最終呼叫 ecodeStream
方法,該方法通過對 android api BitmapFactory#decodeStream
對圖片進行壓縮獲得了 bitmap 物件
特別注意的是我們在使用 Glide 時載入的網路圖片時,預設都是根據 ImageView 的尺寸大小進行了一定比例的,詳細的計算過程在上文中也已經提到。但在實際應用中會有希望讓使用者看到原圖場景,這個時候我們可以這樣操作
ImgurGlide.with(vh.imageView)
.load(image.link)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) // 硬碟快取儲存原圖
.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) // 過載 requestSize,避免 bitmap 被壓縮
.into(vh.imageView);
複製程式碼
Skia 庫
在 android 中, BitmapFactory.decodeStream
呼叫的是 natvie 方法,該函式最終呼叫的是 skia 庫中的encodeStream函式來對圖片進行壓縮編碼。接下來大致介紹一下skia庫。
Skia 是一個 c++實現的程式碼庫,在android 中以擴充套件庫的形式存在,目錄為external/skia/。總體來說skia是個相對簡單的庫,在android中提供了基本的畫圖和簡單的編解碼功能。另外,skia 同樣可以掛接其他第3方編碼解碼庫或者硬體編解碼庫,例如libpng和libjpeg。在Android中skia就是這麼做的,\external\skia\src\images資料夾下面,有幾個SkImageDecoder_xxx.cpp檔案,他們都是繼承自SkImageDecoder.cpp類,並利用第三方庫對相應型別檔案解碼,最後再通過SkTRegistry註冊,程式碼如下所示
static SkTRegistry<SkImageDecoder*, SkStream*> gDReg(sk_libjpeg_dfactory);
static SkTRegistry<SkImageDecoder::Format, SkStream*> gFormatReg(get_format_jpeg);
static SkTRegistry<SkImageEncoder*, SkImageEncoder::Type> gEReg(sk_libjpeg_efactory);
複製程式碼
Android編碼儲存圖片就是通過Java層函式——Native層函式——Skia庫函式——對應第三方庫函式(例如libjpeg),這一層層呼叫做到的。
最後推薦一個第三方庫glide-transformations,可以實現很多圖片效果,比如圓角,高斯模糊,黑白。