Q: 一張大小爲 55KB, 分辨率爲 1080 * 480 的 PNG 圖片,它加載近內存時所佔的大小是多少呢?html
圖片佔用內存大小 = 分辨率 * 像素點大小java
其中數據格式不一樣像素點大小也不一樣:android
如今回過頭來看上面的問題,在電腦上顯示 55KB 的圖片,png 只是這張圖片的容器,他們是通過相對應的壓縮算法將原圖的每一個像素點信息轉換爲另外一種數據格式。c++
在通常狀況下,這張圖片佔用的內容應該是:1080 * 480 * 4B = 1.98 M。git
每種設備都會有所差別,以 android 爲例,咱們將同一張圖片放在不一樣 dpi 的 res/drawable 目錄下,佔用的內存也不同。github
這是由於在 android 中 Bitmap.decodeResource()
會根據圖片存放的目錄作一次寬高的轉換,具體公式以下:算法
轉換後高度 = 原圖高度 * (設備的 dpi /目錄對應的 dpi )shell
轉換後寬度 = 原圖寬度 * (設備的 dip / 目錄對應的 dpi)api
假設你的手機 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
簡單瞭解下BitmapOption的幾個相關屬性:
android 系統提供了相應的 api 能夠按比例壓縮圖片 BitmapFactory.Options.inSampleSize
inSampleSzie 值越大,壓縮比例越高
android 系統默認以 ARGB_8888 格式處理圖片,那麼每一個像素點就須要佔用 4B 大小,能夠將格式改成 RGB_565
咱們使用 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
對外提供的方法 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 爲例
接下來分析 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);
複製代碼
在 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,能夠實現不少圖片效果,好比圓角,高斯模糊,黑白。