Android記憶體優化之圖片優化

tinycoder發表於2019-03-03

背景

一般來說,圖片是APP佔用記憶體高的主要原因,所以優化圖片的記憶體佔用是避免OOM的根本手段。對於圖片佔用的記憶體,我們可能總有這樣的誤區:圖片本身所佔的儲存空間越小,佔用的記憶體越小。所以認為只要將圖片進行壓縮,就相當於減小了記憶體佔用。其實這是不對的,圖片佔用的儲存空間的大小與所佔的記憶體大小沒有直接關係。

既然與記憶體沒有關係,那壓縮圖片有什麼意義呢?對於APK而言,壓縮圖片是為了減小APK的體積,而對於需要網路請求的圖片,壓縮則是為了更快的網路響應。

所以優化之前需要清楚2個基本原則:

  • 圖片佔用記憶體的大小與圖片本身的大小沒有直接關係;
  • WebP格式的圖片雖然小,但佔用的記憶體和其他格式無差別;

圖片佔用記憶體的大小

memorySize ≈ width * height * 每個畫素需要的位元組數
複製程式碼

優化策略

既然需要的記憶體公式已得到,那優化就顯而易見了,無非就是減小的這三個引數的值,具體的策略如下:

這裡我們將圖片分為2種情況來探討:

drawable中的圖片

單獨探討這種情況,是因為Android系統會對drawable中的圖片進行縮放,縮放係數與設定的螢幕解析度和drawable所表示的解析度有關,具體的公式如下:

scale = 裝置解析度 / 資源目錄解析度  如:1080x1920的圖片顯示xhdpi中的圖片,scale = 480 / 320 = 1.5
複製程式碼

所以此時圖片佔用的記憶體大小為:

memorySize ≈ (width * scale) * (height * scale) * 每個畫素需要的位元組數
           ≈ width * height * scale ^ 2 * 每個畫素需要的位元組數
複製程式碼

具體的縮放過程可參考Android中Bitmap記憶體優化

這裡我們只探討一下scale係數的影響因素:裝置解析度和資源目錄解析度。至於其他的可變因子會在另一種情況中介紹。裝置解析度我們沒法改變,所以影響因素只有資源目錄解析度,也就是說,同一張圖片,放在不同的drawable中,佔用的記憶體大小不同。從公式可看出,使用同一個裝置時,drawable表示的解析度越高,則圖片佔用的記憶體越小,反之越大。所以,在做圖片的相容性時,如果只想使用一張圖片,則應使用3倍甚至4倍的圖片(3倍是主流機型,但在4倍手機上會被放大,圖片可能失真),這樣在低解析度的手機上,不僅顯示清晰,而且系統會自動進行縮放,從而確保佔用較小的記憶體。

同樣是存放圖片的位置,為什麼mipmap不在這種情況的考慮範圍之內呢?因為mipmap是Android系統為了避免Launcher Icon變形而新增的資源目錄,也就是說,mipmap中的圖片不會被縮放。所以Google也不推薦將除Launcher Icon之外的圖片放在mipmap目錄中。

其他位置的圖片

其他位置的圖片包括mipmap, asset, 本地圖片,網路圖片等。這些位置的圖片都有一個共同點——不會被縮放。所以只需要考慮如何改變圖片解析度和每個畫素需要的位元組數即可。

本地圖片

本地圖片通常都是通過Android提供的BitmapFactory來載入的, 這裡看幾個常用的API:

// 根據路徑載入
public static Bitmap decodeFile(String pathName, Options opts);
// 載入drawable或mipmap中的圖片
public static Bitmap decodeResource(Resources res, int id, Options opts)
// 根據位元組流載入
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
// 根據IO流載入
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
複製程式碼

圖片的優化可通過Options引數來實現(Options的介紹可參考從fresco 看圖片優化):

方式一:inSampleSize

inSampleSize可理解為圖片的縮小比例,若inSampleSize小於1,則當做1處理。設定inSampleSize後,圖片的寬度和高度將變成原來的1/inSampleSize, 其佔用的記憶體空間將是原來的1/(inSampleSize ^ 2)。但是具體如何取值呢,可通過以下程式碼來獲取:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
options.inSampleSize = getSampleSize(options, 100, 100);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.abc, options);
imageView.setImageBitmap(bitmap);

public static int getSampleSize(BitmapFactory.Options options, int viewWidth, int viewHeight) {
	if (viewWidth == 0 || viewHeight == 0 || options == null) {
		return 1;
	}
	int widthScale = options.outWidth / viewWidth;
	int heightScale = options.outHeight / viewHeight;
	Log.i("out", "width==" + widthScale + " heightScale==" + heightScale);
	return widthScale >= heightScale ? heightScale : widthScale;
}
複製程式碼

方式二:inDensity

inDensity相當於上面說的資源目錄解析度,前面說了,這裡考慮的情況,圖片不會被縮放,其原因就是inDensity和裝置解析度的取值是一致的,因為inDensity=裝置解析度,所以scale=1, 如果將inDensity設定為大於裝置解析度的值,那麼圖片就會被縮小。例如,當前的手機1dp=2px, 即2X螢幕,此時的inDensity為320, 如果將inDensity修改為480, scale=320f/480f=2/3, 那麼圖片所佔用的記憶體將變成原來的4/9。

方式三:inPreferredConfig

inPreferredConfig的取值為Bitmap.Config型別(這裡只考慮以下幾種情況),它是一個列舉型別,用來設定每個畫素需要的位元組數:

ALPHA_8:佔1個位元組
RGB_565:佔2個位元組
ARGB_4444:佔2個位元組,已廢棄,不推薦使用
ARGB_8888:32位真彩色,帶透明度,佔4個位元組
複製程式碼

顯示圖片時預設都是ARGB_8888,所以我們可通過inPreferredConfig的值進行記憶體優化。但實際上inPreferredConfig的取值對記憶體的影響並不是簡單的Bitmap.Config.ALPHA_8佔1個位元組,ARGB_4444和RGB_565佔2個位元組,ARGB_8888佔4個位元組,而是與具體的圖片格式有關:

  • inPreferredConfig對jpeg和gif格式的圖片無作用,無論inPreferredConfig的值取什麼,jpeg格式的圖片每個畫素始終佔用4個位元組,而gif格式的圖片始終站1個位元組;
  • 對於webp格式的圖片,inPreferredConfig取值為RGB_565的時候,每個畫素佔用2個位元組,其餘的取值每個畫素仍然佔4個位元組;
  • 對於png格式的圖片,需要分png8, png24, png32三種情況來說。png8格式的圖片每個畫素佔用的位元組數隨inPreferredConfig的取值而變化,取值為ARGB_ALPHA時佔用一個位元組,取值為RGB_565時佔用2個位元組,取值為ARGB_4444或ARGB_8888時佔用4個位元組。png24格式的圖片,當inPreferredConfig的取值為RGB_565時,每個畫素佔用2個位元組,取其他的值(ARGB_ALPHA, ARGB_4444和ARGB_8888)每個畫素都佔用4個位元組。而對於png32格式的圖片,inPreferredConfig的取值(ARGB_ALPHA, RGB_565, ARGB_4444或ARGB_8888)對每個畫素佔用的位元組數無影響。

所以,如果通過inPreferredConfig來優化圖片的記憶體佔用,就需要webp或png24格式的圖片,png24與png32相比,也就是不支援透明度而已,對於大多數圖片來說,兩者沒有明顯的差別。當然,作為一種新的圖片格式,web可認為是一種不錯的選擇。

注意:
9patch圖雖然在使用時會根據View的尺寸進行放大,但其畫素仍然不變,可視為普通圖片來處理;

網路圖片

網路圖片通常我們都是使用開源庫進行載入(這裡順便推薦一個好用的圖片載入庫ImageSet), inPreferredConfig的值通常可在初始化時進行配置,至於縮放,可讓後臺進行實現。即:根據圖片的請求引數返回合適的尺寸。最大也只需要控制元件的大小即可,再大也沒意義,不僅浪費流量,還佔用記憶體。如果你的APP中有很多圖片,那麼可對圖片的寬高根據裝置的記憶體情況進行適當的縮小:

// 根據記憶體大小設定縮放係數
public static float getDefaultScale() {
    float scale = 1.0f;
    int totalMemorySize = AndroidPlatformUtil.getTotalMemorySize();
    if (totalMemorySize >= 4) {
        scale = 1.0f;
    } else if (totalMemorySize >= 2 && totalMemorySize < 4) {
        scale = 0.8f;
    } else {
        scale = 0.6f;
    }

    return scale;
}

// 獲取裝置的記憶體大小,返回值單位為G
public static int getTotalMemorySize(){
	String path = "/proc/meminfo";
	String firstLine = null;
	FileReader fileReader = null;
	BufferedReader bufferedReader = null;
	try{
		fileReader = new FileReader(path);
		bufferedReader = new BufferedReader(fileReader,8192);
		firstLine = bufferedReader.readLine().split("\s+")[1];
	} catch (Exception e){
		e.printStackTrace();
	} finally {
		try {
			if (bufferedReader != null) {
				bufferedReader.close();
			}
		} catch (Exception e) {
			e.printStackTrace(System.out);
		}

		try {
			if (fileReader != null) {
				fileReader.close();
			}
		} catch (Exception e) {
			e.printStackTrace(System.out);
		}
	}

	if(TextUtils.isEmpty(firstLine)){
		return (int)Math.ceil((new Float(Float.valueOf(firstLine) / (1024 * 1024)).doubleValue()));
	}

	return 0;
}
複製程式碼

總結

對於一個多圖片的APP來說,圖片所佔記憶體的優化是一項必不可少的工作。總的來說,其優化也就是通過縮放和指定Bitmap.Config的值來實現的,只是不同位置,不同格式的圖片有所差異而已。

相關文章