android 圖片佔用記憶體大小及載入解析
*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
在講解圖片佔用記憶體前,我們先問自己幾個問題:
- 我們在對手機進行螢幕適時,常想可不可以只切一套圖適配所有的手機呢?
- 一張圖片載入到手機中,佔用記憶體到底有多少?
- 圖片佔用記憶體跟哪些東西有關?跟手機有關係麼?同一張圖片放在不同的dpi資料夾下記憶體佔用會變化麼?
如果是網路圖片,載入到手機中,佔用記憶體跟手機螢幕有關係麼?
帶著這些問題我們來一層層解析。我們先看看載入本地資源,不同手機所佔記憶體情況:
一、載入本地資源,不同手機佔記憶體情況
我們如果載入app內圖片,想知道它佔用多少記憶體,可先將此資源轉成bitmap進行檢視。
1. 從資源中獲取bitmap
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.mipmap.testxh);
獲取到bitmap,我們還需要知道此bitmap在記憶體佔多少空間,具體方法如下。
2. 獲取圖片大小
public int getBitmapSize(Bitmap bitmap){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ //API 19
return bitmap.getAllocationByteCount();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1){//API 12
return bitmap.getByteCount();
} else {
return bitmap.getRowBytes() * bitmap.getHeight(); //earlier version
}
}
接下來就來測試,不同的手機、同一張圖片放在不同的密度資料夾下,佔用記憶體情況。
3. 同一圖片在不同螢幕的手機、不同的螢幕密度資料夾下佔用記憶體大小
(1) 經測試同一張圖片分別放在不同的mipmap資料夾(mipmap-hdpi, mipmap-xhdpi, mipmap-xxhdpi)下或是drawable資料夾(drawable-hdpi, drawable-xhdpi, drawable-xxhdpi)下,相同的dpi下的資料夾下加載出來的圖片,bitmap佔用記憶體大小一樣;
(2) 對於同一張圖片,放在不同手機、不同的螢幕密度資料夾下佔用記憶體情況又是如何呢,這裡我們以一張大小為1024*731 = 748544B, 大小為485.11K 的圖片為例,下面是測試手機佔用的記憶體情況。
從上表可以看出不同螢幕密度的手機載入圖片,如果圖片放在與自己螢幕密度相同的資料夾下,佔用的記憶體都是2994176B,與圖片本身大小748544B存在一個4倍關係,因為圖片採用的ARGB-888色彩格式,每個畫素點佔用4個位元組。
從上述測試可以得出,bitmap佔用記憶體大小,與手機的螢幕密度、圖片所放資料夾密度、圖片的色彩格式有關。
這裡總結一下獲取Bitmap圖片大小的程式碼:
手機在載入圖片時,會先查詢自己本密度的文夾下是否存在資源,不存在則會向上查詢,再向下查詢,並對圖片進行相應倍數的縮放:
如果在與自己螢幕密度相同的資料夾下存在此資源,會原樣顯示出來,佔用記憶體正好是: 圖片的解析度*色彩格式佔用位元組數;
若自己螢幕密度相同的資料夾下不存在此檔案,而在大於自己螢幕密度的資料夾下存在此資源,會進行縮小相應的倍數的平方;
若在大於自己螢幕密度的資料夾下沒找到此資源,則會向小於自己螢幕密度的資料夾下查詢,如果存在,則會進行放大相應的倍數的平方,這兩種情況圖片佔用記憶體為:
佔用記憶體=圖片寬度 X 圖片高度/((資原始檔夾密度/手機螢幕密度)^2) * 色彩格式每一個畫素佔用位元組數
4. 圖片佔用記憶體與圖片的色彩格式的關係
我們在計算bitmap大小時,是通過計算getRowBytes * bitmap.getHeight()得來的,後面的乘數就是圖片的高度,而第一個乘數getRowBytes是什麼呢?我們根進Bitmap程式碼檢視getRowBytes函式:
/**
* 返回點陣圖畫素的行的位元組數,由點陣圖儲存的畫素值有關,它會根據Color類進行打包
*/
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mNativePtr);
}
該方法最終呼叫的是Bitmap中的native方法:
private static native int nativeRowBytes(long nativeBitmap);
我們再檢視對應的Bitmap.cpp裡的nativeRowBytes方法
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
return static_cast<jint>(bitmap->rowBytes());
}
我們可以看到這裡的bitmap形式是以SkBitmap物件展現的,這個Bitmap就和圖片展示的色彩格式有關,我們再看看SkBitmap裡是怎麼計算rowBytes的:
size_t SkBitmap::ComputeRowBytes(Config c, int width) {
return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
SkImageInfo.h
static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0, // Unknown
1, // Alpha_8
2, // RGB_565
2, // ARGB_4444
4, // RGBA_8888
4, // BGRA_8888
1, // kIndex_8
};
SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
size_mismatch_with_SkColorType_enum);
SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
return gSize[ct];
}
static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
return width * SkColorTypeBytesPerPixel(ct);
}
可以看到,圖片的寬乘以了一個SkColorTypeBytesPerPixel(ct)變數,對於不同色彩格式,每個畫素佔用的位元組數就是在SkColorTypeBytesPerPixel中定義的。這就是為什麼上面得出的bitmap大小,在自己螢幕密度的資料夾下圖片佔用的記憶體大小都被乘以了4,因為bitmap載入預設採用的是RGBA_8888編碼格式。
5. 圖片佔用記憶體與手機螢幕密度、圖片所在資料夾密度的關係
那麼手機怎麼載入圖片時,為什麼同樣的圖片在不同的螢幕解析度的手機上、不同的螢幕密度資料夾下佔用記憶體會相差這麼大呢?
在載入資源圖片時,我們一般會藉助於BitmapFactory的decodeResource方法,此方法的原始碼如下:
/**
* @param res 包含圖片資源的Resources物件,一般通過getResources()即可獲取
* @param id 資原始檔id, 如R.mipmap.ic_laucher
* @param opts 可為空,控制取樣或圖片是否需要完全解碼還是隻需要獲取圖片大小
* @return 解碼的bitmap
*/
public static Bitmap decodeResource(Resources res, int id, Options opts) {
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
//1.讀取資源返回資料流格式,最終會呼叫AssetManager的openNonAsset方法進行讀取資源
is = res.openRawResource(id, value);
//2. 根據資料流格式進行解碼,在直接載入res資源時,一般opts為空
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
// Ignore
}
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return bm;
}
我們再來看看BitmapFactory的decodeResourceStream方法
/**
* 根據輸入的資料流確碼成一個新的bitmap, 資料流是從資源處獲取,在這裡可以根據規則對圖片進行一些縮放操作
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {//如果沒有設定Options,系統會新建立一個Options物件
opts = new Options();
}
//若沒有設定opts,inDensity就是初始值0,它代表圖片資源密度
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) { //如果density等於0,則採用預設值160
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {//如果沒有設定資源密度,則圖片不會被縮放
opts.inDensity = density;//這裡density的值對應的就是資源密度值
}
}
//此時inTargetDensity預設也為0
if (opts.inTargetDensity == 0 && res != null) {
//將手機的螢幕密度值賦值給最終圖片顯示的密度
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
可以看到這裡呼叫了native decodeStream方法:
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
......
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
//資源本身的密度
const int density = env->GetIntField(options, gOptions_densityFieldID);
//最終載入的圖片的密度
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
//手機的螢幕密度
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
//如果資源密度不為0,手機螢幕密度也不為0, 資源的密度與螢幕密度不相等時,圖片縮放比例=螢幕密度/資源密度,如對於三星手機螢幕密度為640,如果圖片放在資料夾為xhdpi 320下,則scale=2,會對圖片長寬均放大2倍
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}
const bool willScale = scale != 1.0f;//判斷是否需要縮放
......
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
//這裡這個deodingBitmap就是解碼出來的bitmap,大小是圖片原始的大小
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);//這裡+0.5是保證在圖片縮小時,可能會出小數,這裡加0.5是為了讓除後的數向上取整
scaledHeight = int(scaledHeight * scale + 0.5f);
}
if (willScale) {
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// 設定解碼圖片的colorType
SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
//設定圖片的寬高
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
if (outputAllocator != &javaAllocator) {
outputBitmap->eraseColor(0);
}
SkPaint paint;
paint.setFilterLevel(SkPaint::kLow_FilterLevel);
SkCanvas canvas(*outputBitmap);
canvas.scale(sx, sy);//根據縮放比畫出影象
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);//將圖片畫到畫布上
}
......
}
inDensity,inTargetDensity,inScreenDensity, inScaled三者關係
通過追查程式碼,我們可以看到圖片資源通過資料流解碼時,會根據inDensity,inTargetDensity,inScreenDensity三個值和是否被縮放標識inScaled
- inDensity:圖片本身的畫素密度(其實就是圖片資源所在的哪個密度資料夾下,如在xxhdpi下就是480,如果在asstes、手機記憶體/sd卡下,預設是160);
- inTargetDensity:圖片最終在bitmap裡的畫素密度,如果沒有賦值,會將inTargetDensity設定成inScreenDensity;
- inScreenDensity:手機本身的螢幕密度,如我們測試的三星手機dpi=640, 如果inDensity與inTargetDensity不相等時,就需要對圖片進行縮放,inScaled = inTargetDensity/inDensity。
我們上面研究了載入應用程式的圖片佔用記憶體大小與手機螢幕密碼和圖片所放的密度資料夾、圖片的編碼格式有關,那如果載入的是網路圖片或是本地圖片,在不同的手機上佔用記憶體又是否一樣呢?
二、載入sd卡下的資源或是網路圖片解析
手機無論是載入sd卡圖片,assets路徑下還是網路圖片,都需要先把圖片讀成資料流格式,再呼叫相應的decodeStream方法,將資料流轉成bitmap形式,在呼叫decodeStream如果不設定Options的話,通過以上三款手機打印出圖片所佔記憶體大小均為:2994176B,也就是跟手機的螢幕密度沒有關係。那如果設定Options中的引數,圖片佔用的記憶體會不會與手機的螢幕密度有關係呢?我在測試中發現單獨手動設定圖片密度inDensity或是inTargetDensity,並不起作用,圖片佔用記憶體一直都是圖片本身大小。為什麼沒起作用呢,這需要我們從資源載入的源頭看起。
1. 根據手機本地圖片路徑獲取Bitmap
我們先來看一下BitmapFactory的decodeFile函式:
//讀取手機本地的圖片資源
public static Bitmap decodeFile(String pathName, Options opts) {
Bitmap bm = null;
InputStream stream = null;
try {
stream = new FileInputStream(pathName);
//呼叫decodeStream將資料流轉成bitmap
bm = decodeStream(stream, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
*/
Log.e("BitmapFactory", "Unable to decode stream: " + e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// do nothing here
}
}
}
return bm;
}
2. 根據網路地址獲取圖片Bitmap
/**
* 從網路中獲取圖片,先獲取資料流,再轉成Bitmap
* @return
*/
public Bitmap getBitmapByPicUrl(String picurl) throws IOException {
InputStream inputStream = null;
URL url = new URL(picurl); //伺服器地址
if (url != null) {
//開啟連線
HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();
httpURLConnection.setConnectTimeout(3000);//設定網路連線超時的時間為3秒
httpURLConnection.setRequestMethod("GET"); //設定請求方法為GET
httpURLConnection.setDoInput(true); //開啟輸入流
int responseCode = httpURLConnection.getResponseCode(); // 獲取伺服器響應值
if (responseCode == HttpURLConnection.HTTP_OK) { //正常連線
inputStream = httpURLConnection.getInputStream(); //獲取輸入流
}
}
return BitmapFactory.decodeStream(inputStream);
}
可以看到通過路徑載入圖片,最終還是會呼叫BitmapFactory裡的decodeStream方法,我們再來看看decodeStream方法。
3. 將資料流轉成Bitmap
/**
*根據輸入的資料流確碼成一個新的bitmap
*
* @param is 從源資料獲取的輸入數居流,用於解碼成bitmap
* @param outPadding 如果不為空,返回bitmap的邊距,這個會加入到圖片所佔記憶體大小裡
* @param opts 可以為空; 用來控制圖片的取樣率和圖片是否需要完全解碼,還是隻需要獲取圖片大小
* @return 解碼後的圖片
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
if (is == null) {
return null;
}
Bitmap bm = null;
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {//如果資料流來自資源,則直接呼叫native方法
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}
return bm;
}
如果資料流來自於資源,則呼叫BitmapFactory的nativeDecodeAsset,
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
否則呼叫decodeStreamInternal方法:
/**
* is不得為空,會根據流的需要提供一個緩衝區
*/
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
// ASSERT(is != null);
byte [] tempStorage = null;
if (opts != null) tempStorage = opts.inTempStorage;
//如果Options沒有提供inTempStorage引數會預設提供一個16M的緩衝區
if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
return nativeDecodeStream(is, tempStorage, outPadding, opts);
}
此方法會呼叫native的nativeDecodeStream方法:
Rect padding, Options opts);
4. native層的資料流解析
(1)nativeDecodeStream/nativeDecodeAsset
通過追蹤上述兩種nativeDecodeStream方法和nativeDecodeAsset方法,它們最終都會呼叫nativeDecodeStreamScaled或是nativeDecodeAssetScaled方法,它們會新增兩個引數,一個是false,一個是1.0f,這兩個引數具體代表什麼呢?
//解碼Asset資源的資料流
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jint native_asset,
jobject padding, jobject options) {
return nativeDecodeAssetScaled(env, clazz, native_asset, padding, options, false, 1.0f);
}
//解碼純資料流
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
return nativeDecodeStreamScaled(env, clazz, is, storage, padding, options, false, 1.0f);
}
(2) nativeDecodeStreamScaled/nativeDecodeAssetScaled
nativeDecodeAssetScaled或是nativeDecodeStreamScaled方法中最後兩個引數,分別是applyScale, sclae,一個是是否申請縮放,一個是縮放比例,也就是從這種資料流載入的圖片,預設都不會進縮放。我們注意到,這兩個函式最終都會走到doDecode方法裡,我們直接看nativeDecodeStreamScaled方法,發現此方法只是對輸入流進行了轉換,轉成SkStream型別。
static jobject nativeDecodeStreamScaled(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options, jboolean applyScale, jfloat scale) {
jobject bitmap = NULL;
SkStream* stream = CreateJavaInputStreamAdaptor(env, is, storage, 0);
if (stream) {
// for now we don't allow purgeable with java inputstreams
bitmap = doDecode(env, stream, padding, options, false, false, applyScale, scale);
stream->unref();
}
return bitmap;
}
(3)doDecode
我們來看最終的doDecode函式:
static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false,
bool applyScale = false, float scale = 1.0f) {
int sampleSize = 1;
SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode;
SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;//直接採用ARGB_8888的色彩格式
bool doDither = true;
bool isMutable = false;
bool willScale = applyScale && scale != 1.0f;//上面傳的引數applyScale為false,所以willScale為false
bool isPurgeable = !willScale &&
(forcePurgeable || (allowPurgeable && optionsPurgeable(env, options)));
bool preferQualityOverSpeed = false;
jobject javaBitmap = NULL;
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);//獲取取樣率
if (optionsJustBounds(env, options)) {//是否只加載圖片邊界,而不解碼
mode = SkImageDecoder::kDecodeBounds_Mode;
}
//省略初始化程式碼
}
//省略一堆程式碼
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
int scaledWidth = decoded->width();//獲取解碼圖片的寬度
int scaledHeight = decoded->height();//獲取解碼後圖片的調節度
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {//由於willScale為false,這裡不會執行
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
// update options (if any)
if (options != NULL) {
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID,
getMimeTypeString(env, decoder->getFormat()));
}
if (mode == SkImageDecoder::kDecodeBounds_Mode) {//如果只獲取圖片大小,這裡不會返回bitmap
return NULL;
}
if (padding) {//如果設定了padding,則會把邊矩算進去
if (peeker.fPatchIsValid) {
GraphicsJNI::set_jrect(env, padding,
peeker.fPatch->paddingLeft, peeker.fPatch->paddingTop,
peeker.fPatch->paddingRight, peeker.fPatch->paddingBottom);
} else {
GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
}
}
SkPixelRef* pr;
if (isPurgeable) {
pr = installPixelRef(bitmap, stream, sampleSize, doDither);
} else {
pr = bitmap->pixelRef();
}
return GraphicsJNI::createBitmap(env, bitmap, javaAllocator.getStorageObj(),
isMutable, ninePatchChunk);//建立bitmap
}
通過上面分析直至native中的decode函式,我們發現options裡的引數只提取了sampleSize、optionsJustBounds,但是沒有見到inDensity,inTargetDensity,inScreenDensity等引數的提取。如果我在載入流前,設定ops.inDensity和ops.inTargetDensity引數如下,圖片佔用記憶體大小放大到原來的4倍
BitmapFactory.Options ops = new BitmapFactory.Options();
int targetDensity = getResources().getDisplayMetrics().densityDpi;
ops.inDensity = 240;
ops.inTargetDensity = 480;
Bitmap assetsbmp = BitmapFactory.decodeStream(stream, null, ops);
但是如果只設置inDensity或是inTargetDensity引數,是完全不起作用,感覺是因為只設置了一個引數,另一個引數預設為0, 前面咱們判斷過,只要有一個引數為0, 就不會計算縮放比。所以預設還是顯示原來圖片尺寸大小,只有兩個引數均設定,都不為0, 才會去計算縮放比。
通過上面的分析,我們可以回答最開始的問題了。
結論:
在對手機進行螢幕適時,可以只切一套圖適配所有的手機。
但是如果只切一套小圖,那在高螢幕密度手機上,會對圖片進行放大,這樣圖片佔用的記憶體往往比切相應圖片放在高密度資料夾下,佔用的記憶體還要大。
那如果只切一套大圖放在高幕資料夾下,在小螢幕密度手機上,會縮小顯示,按道理是行得通的。但系統在對圖片進行縮放時,會進行大量計算,會對手機的效能有一定的影響。同時如果圖片縮放比較狠,可能導致圖片出現抖動或是毛邊。
- 所以最好切出不同比便的圖片放在不同幕度的資料夾下,對於效能要求不大高的圖片,可以只切一套大圖;
一張圖片佔用記憶體=圖片長 * 圖片寬 / (資源圖片檔案密度/手機螢幕密度)^2 * 每一象素佔用位元組數,所以圖片佔用記憶體跟圖片本身大小、手機螢幕密度、圖片所在的資料夾密度,圖片編碼的色彩格式有關;
對於網路圖片,在不同螢幕密度的手機上加載出來,佔用記憶體是一樣的。
對於網路或是assets/手機本地圖片載入,如果想通過設定Options裡的
inDensity或是inTargetDensity引數來調整圖片的縮放比,必須兩個引數均設定才能起作用,只設置一個,不會起作用。drawable和mipmap資料夾存放圖片的區別,首先圖片放在drawable-xhdpi和mipmap-xhdpi下,兩者佔用的記憶體是一樣的,
Mipmaps早在Android2.2+就可以用了,但是直到4.3 google才強烈建議使用。把圖片放到mipmaps可以提高系統渲染圖片的速度,提高圖片質量,減少GPU壓力。其他並沒有什麼區別。