【優化篇】不使用第三方庫,Bitmap的優化策略

現在市場上有不少封裝好的第三方庫,對Bitmap內存也是作到了很好的優化,好比Glide、Fresco,每次加載只要直接調用就好,可是除掉第三方庫外,咱們仍是須要去了解一下Bitmap的基本優化手段。java

1、Bitmap內存進程

首先咱們有必要去了解一下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的資源沒有獲得很好的利用,同時沒有作到及時的釋放。網絡

2、優化策略

對於Bitmap的優化主要分爲針對不一樣密度的設備合理的分配資源,壓縮以及緩存處理三種。app

2.1.drawable的合理分配

總所周知,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

2.2.尺寸優化

當裝載圖片的容器例如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);
    }
複製代碼

2.3.質量優化

通常狀況下質量壓縮是不推薦的一種優化手法,此手法壓縮後圖片將會失真。但不排除有項目對圖片的清晰度沒有太高的要求。

在開始談如何壓縮以前咱們須要瞭解一下Bitmap的質量等級,在API29中,將Bitmap分爲ALPHA_8, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE六個等級。

  • ALPHA_8:不存儲顏色信息,每一個像素佔1個字節;
  • RGB_565:僅存儲RGB通道,每一個像素佔2個字節,對Bitmap色彩沒有高要求,可使用該模式;
  • ARGB_4444:已棄用,用ARGB_8888代替;
  • ARGB_8888:每一個像素佔用4個字節,保持高質量的色彩保真度,默認使用該模式;
  • RGBA_F16:每一個像素佔用8個字節,適合寬色域和HDR;
  • HARDWARE:一種特殊的配置,減小了內存佔用同時也加快了Bitmap的繪製。

每一個等級每一個像素所佔用的字節也都不同,所存儲的色彩信息也不一樣。同一張100像素的圖片,ARGB_8888就佔了400字節,RGB_565才佔200字節,RGB_565在內存上取得了優點,可是Bitmap的色彩值以及清晰度卻不如ARGB_8888模式下的Bitmap。質量壓縮說到底就是用清晰度來換內存。

質量壓縮的具體操做也和上面2.2同樣,只是將options.inPreferredConfig 設置爲所需的圖片質量,以下:

options.inPreferredConfig = Bitmap.Config.ARGB_8888;
複製代碼

2.4.緩存

不論是從網絡上下載圖片,仍是直接從USB中讀取圖片,緩存對於圖片加載的優化起到了相當重要的做用。當咱們首次從網絡上或者USB讀取圖片,會對圖片進行相應的壓縮處理。在處理事後不加入緩存,下一次請求圖片仍是直接從網絡上或者USB中直接讀取,不只消耗了用戶的流量還重複對圖片進行壓縮處理,佔用多餘內存的同時加載圖片也很緩慢。

對於緩存,目前的策略是內存緩存和存儲設備緩存。當加載一張圖片時,首先會從內存中去讀取,若是沒有就接着在存儲設備中讀,最後才直接從網絡或者USB中讀取。接下來就聊一聊這兩種緩存的具體內容。

2.4.1.內存緩存

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的使用。

2.4.2.磁盤緩存

磁盤緩存所使用的算法爲DiskLruCache,它的使用比內存緩存要複雜一點,可是仍是離不開上面的3步,初始化,查找和添加。一樣的,直接從代碼中開始分析。

2.4.2.1.DiskLruCache的初始化
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便可;第四個參數表示磁盤緩存的容量大小。

2.4.2.2.DiskLruCache的添加
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()的方法將文件提交纔算真正將圖片寫入文件系統。

2.4.2.3.DiskLruCache的查找
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後獲取一個文件輸入流,最後經過該文件輸入流來解析出當前緩存的文件。

3、總結

上面已經分別描述了幾種優化手段,最後再來總結一下。

  1. 根據不一樣的密度的設備將圖片資源放置再不一樣的drawable文件夾中;
  2. 利用inSampleSize對圖片進行尺寸上的壓縮;
  3. 利用inPreferredConfig對圖片進行質量上的壓縮;
  4. 利用三級緩存,依次從內存緩存(LruCache)磁盤緩存(DiskLruCache)網絡上獲取圖片;
相關文章
相關標籤/搜索