現在市場上有不少封裝好的第三方庫,對Bitmap內存也是作到了很好的優化,好比Glide、Fresco,每次加載只要直接調用就好,可是除掉第三方庫外,咱們仍是須要去了解一下Bitmap的基本優化手段。java
首先咱們有必要去了解一下Bitmap的基本知識點,在Android3.0以前,Bitmap的對象是放在Java堆中,而Bitmap的像素是放置在Native內存中,這個時候須要手動的去調用recycle,才能去回收Native內存;算法
在Android3.0到Android7.0,Bitmap對象和像素都是放置到Java堆中,這個時候即便不調用recycle,Bitmap內存也會隨着對象一塊兒被回收。雖然Bitmap內存能夠很容易被回收,可是Java堆的內存有很大的限制,也很容易形成GC。緩存
在Android8.0的時候,Bitmap內存又從新放置到了Native中。markdown
Bitmap形成OOM不少時候也是由於對Bitmap的資源沒有獲得很好的利用,同時沒有作到及時的釋放。網絡
對於Bitmap的優化主要分爲針對不一樣密度的設備合理的分配資源,壓縮以及緩存處理三種。app
總所周知,drawable時放置本地圖片資源的地方,從上圖能夠發現,AS將drawable分爲了mdpi,hdpi,xhdpi...不一樣的等級,簡單歸納爲不一樣等級的dpi表明着不一樣的設備密度,它們之間的區別暫時先不論,有必要先去了解一下AS對於drawable的匹配規則.ide
舉個例子,噹噹前的設備密度爲xhdpi,此時代碼中ImageView須要去引用drawable中的圖片,那麼根據匹配規則,系統首先會在drawable-xhdpi文件夾中去搜索,若是須要的圖片存在,那麼直接顯示;若是不存在,那麼系統將會開始從更高dpi中搜索,例如drawable-xxhdpi,drawable-xxxhdpi,若是在高dpi中搜索不到須要的圖片,那麼就會去drawable-nodpi中搜索,有則顯示,無則繼續向低dpi,如drawable-hdpi,drawable-mdpi,drawable-ldpi等文件夾一級一級搜索.優化
當在比當前設備密度低的文件夾中搜到圖片,那麼在ImageView(寬高在wrap_content狀態下)中顯示的圖片將會被放大.圖片放大也就意味着所佔內存也開始增多.這也就是爲何分辨率不高的圖片隨意放置在drawable中也會出現OOM.而在高密度文件夾中搜到圖片,圖片在該設備上將會被縮小,內存也就相應減小.ui
在理想的狀態下,不一樣dpi的文件下應該放置相應dpi的圖片資源,以對不一樣的設備進行適配.但在圖片資源沒有作dpi區分的時候,根據以上所說的匹配規則,將圖片資源放置在高dpi 如drawable-xdpi,drawable-xxdpi文件夾中.是比較好的選擇,在最大程度上減小OOM的概率。this
當裝載圖片的容器例如ImageView只有100*100,而圖片的分辨率爲800 * 800,這個時候將圖片直接放置在容器上,很容易OOM,同時也是對圖片和內存資源的一種浪費。當容器的寬高都很小於圖片的寬高,其實就須要對圖片進行尺寸上的壓縮,將圖片的分辨率調整爲ImageView寬高的大小,一方面不會對圖片的質量有影響,同時也能夠很大程度上減小內存的佔用。
對於尺寸壓縮首先須要去了解一個知識點inSampleSize,
從上圖發現Android官方對它的解釋是,若是inSampleSize 設置的值大於1,則請求解碼器對原始的bitmap進行子採樣圖像,而後返回較小的圖片來減小內存的佔用,例如inSampleSize == 4,則採樣後的圖像寬高爲原圖像的1/4,而像素值爲原圖的1/16,也就是說採樣後的圖像所佔內存也爲原圖所佔內存的1/16;當inSampleSize <=1時,就看成1來處理也就是和原圖同樣大小。另外最後一句還註明,inSampleSize的值一直爲2的冪,如1,2,4,8。任何其餘的值也都是四捨五入到最接近2的冪。
採樣率inSampleSize實際上是一個規定圖片壓縮倍數的一個參數,經過圖片寬高的比較獲得一個新的數值,inSampleSize設置到BitmapFactory中從新去解碼圖片。下面就是利用inSampleSize對圖片進行尺寸上的優化代碼。
/** * 對圖片進行解碼壓縮。 * * @param resourceId 所需壓縮的圖片資源 * @param reqHeight 所需壓縮到的高度 * @param reqWidth 所需壓縮到的寬度 * @return Bitmap */
private Bitmap decodeBitmap(int resourceId, int reqHeight, int reqWidth) {
BitmapFactory.Options options = new BitmapFactory.Options();
//inJustDecodeBounds設置爲true,解碼器將返回一個null的Bitmap,系統將不會爲此Bitmap上像素分配內存。
//只作查詢圖片寬高用。
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), resourceId, options);
//查詢該圖片的寬高。
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
//若是當前圖片的高或者寬大於所需的高或寬,
// 就進行inSampleSize的2倍增長處理,直到圖片寬高符合所須要求。
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while ((halfHeight / inSampleSize >= reqHeight)
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
//inSampleSize獲取結束後,須要將inJustDecodeBounds置爲false。
options.inJustDecodeBounds = false;
//返回壓縮後的Bitmap。
return BitmapFactory.decodeResource(getResources(), resourceId, options);
}
複製代碼
通常狀況下質量壓縮是不推薦的一種優化手法,此手法壓縮後圖片將會失真。但不排除有項目對圖片的清晰度沒有太高的要求。
在開始談如何壓縮以前咱們須要瞭解一下Bitmap的質量等級,在API29中,將Bitmap分爲ALPHA_8, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE六個等級。
每一個等級每一個像素所佔用的字節也都不同,所存儲的色彩信息也不一樣。同一張100像素的圖片,ARGB_8888就佔了400字節,RGB_565才佔200字節,RGB_565在內存上取得了優點,可是Bitmap的色彩值以及清晰度卻不如ARGB_8888模式下的Bitmap。質量壓縮說到底就是用清晰度來換內存。
質量壓縮的具體操做也和上面2.2同樣,只是將options.inPreferredConfig 設置爲所需的圖片質量,以下:
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
複製代碼
不論是從網絡上下載圖片,仍是直接從USB中讀取圖片,緩存對於圖片加載的優化起到了相當重要的做用。當咱們首次從網絡上或者USB讀取圖片,會對圖片進行相應的壓縮處理。在處理事後不加入緩存,下一次請求圖片仍是直接從網絡上或者USB中直接讀取,不只消耗了用戶的流量還重複對圖片進行壓縮處理,佔用多餘內存的同時加載圖片也很緩慢。
對於緩存,目前的策略是內存緩存和存儲設備緩存。當加載一張圖片時,首先會從內存中去讀取,若是沒有就接着在存儲設備中讀,最後才直接從網絡或者USB中讀取。接下來就聊一聊這兩種緩存的具體內容。
LRU是用於實現內存緩存的一種常見算法,LRU也叫作最近最少使用算法,通俗來說就是當緩存滿了的時候,就會優先的去淘汰最近最少使用的緩存對象。接下來就以代碼的方式直觀的分析。
...
private LruCache<Integer,Bitmap> mCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1.初始化LruCache.
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mCache = new LruCache<Integer,Bitmap>(cacheSize){
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
}
//2.從Cache中獲取數據
public Bitmap getDataFromCache(int key) {
if (mCache.size() != 0) {
return mCache.get(key);
}
return null;
}
//3.將數據存儲到Cache中
public void putDataToCache(int key, Bitmap bitmap) {
if (getDataFromCache(key) == null) {
mCache.put(key,bitmap);
}
}
...
複製代碼
從代碼中看首先對LruCache進行初始化,獲取當前進程可用的內存,而後將內存緩存的容量制定爲可用內存的1/8,同時對Bitmap對象進行大小的計算。接着構造出兩個對外的方法,一個是根據Key從Cache中獲取數據,一個是將數據存儲到cache中。簡單的3步也就完成了LruCache的使用。
磁盤緩存所使用的算法爲DiskLruCache,它的使用比內存緩存要複雜一點,可是仍是離不開上面的3步,初始化,查找和添加。一樣的,直接從代碼中開始分析。
private final static int DISK_MAX_SIZE = 20 * 1024 * 1024;
private DiskLruCache mDiskLruCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化DiskLruCache。
File directory = getFile(this,"DiskCache");
if (!directory.exists()) {
directory.mkdirs();
}
try {
mDiskLruCache = DiskLruCache.open(directory, 1, 1, DISK_MAX_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
private File getFile(Context context,String dirName){
String filePath = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
? Objects.requireNonNull(context.getExternalCacheDir()).getPath() : context.getCacheDir().getPath();
return new File(filePath + File.pathSeparator + dirName);
}
複製代碼
DiskLruCache的建立是DiskLruCache.open()來建立,其中會傳入4個參數,第一個參數表示磁盤緩存所要存儲的路徑,通常來講,若是外部設備存在,那麼存儲路徑放置在 /storage/emulated/0/Android/data/package_name/cache 中;反之就放置在 /data/data/package_name/cache 這個目錄下。存儲路徑能夠根據本身的實際要求進行制定,值得注意的是,若是緩存路徑選擇SD卡上的緩存目錄,即 /storage/emulated/0/Android/data/package_name/cache,那麼當應用被卸載時,該目錄也會被刪除。
第二個參數表示應用的版本號,直接設置爲1便可;第三個參數表示單個節點所對應的數據的個數,設置爲1便可;第四個參數表示磁盤緩存的容量大小。
private void addDataToDisk(String url) {
//採用url的md5值做爲key。
String key = hashKeyFromUrl(url);
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadDataFromNet(url, outputStream)) {
//提交至緩存
editor.commit();
} else {
//回退整個操做
editor.abort();
}
mDiskLruCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(url.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
複製代碼
DiskLruCache的添加主要是由DiskLruCache.Editor來完成,首先咱們會採用url的md5值來做爲key,經過.Editor和key獲取一個文件輸出流,下載好圖片經過這個文件輸出流寫入到文件系統中,最後經過editor.commit()的方法將文件提交纔算真正將圖片寫入文件系統。
private Bitmap getDataFromDisk(String url) {
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(0);
return BitmapFactory.decodeStream(inputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
複製代碼
DiskLruCache的添加是經過Editor來完成,而查找是由DiskLruCache.Snapshot來完成的。首先經過url獲取到當前文件的key值,初始化Snapshot後獲取一個文件輸入流,最後經過該文件輸入流來解析出當前緩存的文件。
上面已經分別描述了幾種優化手段,最後再來總結一下。