Bitmap的加載和Cache --《Android開發藝術探索》閱讀筆記——第十二章

Bitmap,表示位圖,由像素點構成。Bitmap的承載容器是jpg、png等格式的文件,是對bitmap的壓縮。當jpg、png等文件需要展示在手機上的控件時,就會解析成Bitmap並繪製到view上。通常處理圖片時要避免過多的內存使用,畢竟移動設備的內存有限。
那麼加載一張圖片需要佔用多大內存呢?考慮到效率加載圖片時緩存策略是怎樣的呢?

一、Bitmap的加載

1.1 Bitmap的內存佔用

原始計算公式如下:

Bitmap的內存 = 分辨率 * 像素點的大小

  • 圖片分辨率,可能不是原始圖片的分辨率。例如 圖片放在Res中不同dpi的文件夾中,分辨率是原始分辨率轉換後的。比如放hdpi與放xhdpi,轉換後的分辨率是不同的。轉換後的分辨率=原始分辨率*(設備的 dpi / 目錄對應的 dpi)。其他情況,如放在磁盤、文件、流等均按原分辨率處理。另外,這個邏輯是原生系統BitmapFactory的邏輯,如果是直接使用圖片庫,內部邏輯可能不會轉換分辨率,如glide的不轉換Res中圖片的分辨率。
  • 像素點的大小,就是ARGB8888(4B)、RGB565(2B)這幾個。

詳細理解參考Android中一張圖片佔據的內存大小是如何計算

1.2 Bitmap的高效加載

Bitmap的加載,可過系統提供的BitmapFactory四個方法:decodeFile、decodeResource、decodeStream、decodeByteArray,對應處理從 文件、資源、流、字節數組 來加載Bitmap。

如何優化加載呢?由公式可見想要減少圖片加載成Bitmap時佔用的內存,兩個方法:

  • 降低像素點的大小:如可以把圖片格式ARGB8888 換成RGB565,內存佔用就會減少一半,但會降低。但導致不支持透明度,降低圖片質量。開源庫一般也支持更換格式。
  • 降低分辨率:通常圖片的分辨率遠大於控件view的分辨率,加載後view無法顯示原始的分辨率,所以降低分辨率也不會影響圖片的展示效果。

針對第二點,降低分辨率,BitmapFactory.Options也提供了對應的方法,步驟如下:

  1. 把BitmapFactory.Options.inJustDecodeBounds設爲true,並加載圖片。(只加載原始寬高信息,輕量級操作)
  2. 獲取原始寬高信息:options.outWidth、options.outHeight
  3. 設置採樣率options.inSampleSize。採樣率根據 原始寬高信息 和 view的大小計算。
  4. 把BitmapFactory.Options.inJustDecodeBounds設爲false,並加載圖片。

代碼如下:

    private void initView() {
        //R.mipmap.blue放在Res的xxh(480dpi)中,測試手機dpi也是480

        //1、inJustDecodeBounds設爲true,並加載圖片
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.mipmap.blue, options);

        //2、獲取原始寬高信息
        int outWidth = options.outWidth;
        int outHeight = options.outHeight;

        Log.i(TAG, "initView: outWidth="+outWidth+",outHeight="+outHeight);

        //3、原始寬高信息 和 view的大小 計算並設置採樣率
        ViewGroup.LayoutParams layoutParams = ivBitamp.getLayoutParams();
        int inSampleSize = getInSampleSize(layoutParams.width, layoutParams.height, outWidth, outHeight);
        options.inSampleSize = inSampleSize;

        Log.i(TAG, "initView: inSampleSize="+options.inSampleSize);

        //4、inJustDecodeBounds設爲false,並加載圖片
        options.inJustDecodeBounds = false;
        Bitmap bitmap= BitmapFactory.decodeResource(getResources(), R.mipmap.blue, options);

        Log.i(TAG, "initView: size="+bitmap.getByteCount());

        int density = bitmap.getDensity();
        Log.i(TAG, "initView: density="+density);
        Log.i(TAG, "initView: original size="+337*222*4);
        Log.i(TAG, "initView: calculated size="+ (337/inSampleSize) *(222/inSampleSize)* density/480 *4);

        //繪製到view
        ivBitamp.setImageBitmap(bitmap);
    }

    /**
     * 計算採樣率
     * @param width view的寬
     * @param height view的高
     * @param outWidth 圖片原始的寬
     * @param outHeight 圖片原始的高
     * @return
     */
    private int getInSampleSize(int width, int height, int outWidth, int outHeight) {
        int inSampleSize = 1;
        if (outWidth>width || outHeight>height){
            int halfWidth = outWidth / 2;
            int halfHeight = outHeight / 2;
            //保證採樣後的寬高都不小於目標快高,否則會拉伸而模糊
            while (halfWidth/inSampleSize >=width
                    && halfHeight/inSampleSize>=height){
                //採樣率一般取2的指數
                inSampleSize *=2;
            }
        }

        return inSampleSize;
    }
}

二、Android中的緩存策略

緩存策略在Android中應用廣泛。使用緩存可以節省流量、提高效率

加載圖片時,一般會從網絡加載,然後緩存在存儲設備上,這樣下次就不用請求網絡了。並且通常也會緩存一份到內存中,這樣下次可以直接取內存中的緩存,要比從存儲設備中取快很多。所以一般是先從內存中取,內存沒有就取存儲設備,也沒有才會請求網絡,這就是所謂的“三級緩存”。此策略同樣適用其他文件類型。

緩存策略中的操作有 添加緩存、獲取緩存、刪除緩存。添加和獲取比較好理解,刪除緩存是啥意思?因爲緩存大小是有限制的,像移動設備的 內存 和 設備存儲都是有限的,不能無限制的添加,只能限定一個最大緩存,到達最大時就會刪除一部分緩存。但是刪除哪一部分緩存呢?刪除 緩存創建時間最老的嗎,如果它經常用到呢,好像不太完美,當然這也是一種緩存算法。

目前經典的緩存算法是LRU(Least Recently Used),最近最少使用。具體就是 當緩存滿時,會先刪除那些 近期 最少使用 的緩存。使用LRU算法的緩存有兩種,LruCache和DiskLruCache,LruCache是使用內存緩存,DiskLruCache是實現磁盤緩存。

2.1 LruCache

LruCache是泛型類,使用方法如下:
提供最大緩存容量,創建LruCache實例,並重寫其sizeOf方法來計算緩存對象的大小。最大容量和緩存對象大小單位要一致。

    private void testLruCache() {
        //當前進程的最大內存,單位M
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        //取進程內存的1/8
        int cacheMaxSize = (int) (maxMemory/8);
        //創建Bitmap實例
        mBitmapLruCache = new LruCache<String, Bitmap>(cacheMaxSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //緩存對象bitmap的大小,單位M
                return value.getByteCount()/1024/1024;
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //移除舊緩存時會調用,可以在這裏進行像資源回收的工作。
                //evicted爲true,表示此處移除是因爲快滿了要騰出空間
            }
        };

        //添加緩存
        mBitmapLruCache.put("1",mBitmap);

        //獲取緩存
        Bitmap bitmap = mBitmapLruCache.get("1");
        ivBitamp.setImageBitmap(bitmap);

        //刪除緩存,一般不會用,因爲快滿時會自動刪近期最少使用的緩存,就是它的核心功能
        mBitmapLruCache.remove("1");
    }

可見使用很簡單,那麼LruCache是怎麼完成 刪除“近期最少使用” 的呢?看下LruCache的代碼:

public class LruCache<K, V> {
	//此map以強引用的方式存儲緩存對象
    private final LinkedHashMap<K, V> map;
    //當前緩存的大小(帶單位的)
    private int size;
    //緩存最大容量(帶單位的)
    private int maxSize;
	...
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        //LinkedHashMap是按照 訪問順序 排序的,所以get、put操作都會把要存的k-v放在隊尾
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    
    /**
     * 獲取緩存,同時會把此k-v放在鏈表的尾部
     */
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        //get是線程安全的操作
        synchronized (this) {
        	//LinkedHashMap的get方法中調afterNodeAccess,會移到鏈表尾部
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
        ...
    }
    
    /**
     * 緩存key-value,value會存在 隊尾
     * @return 之前也是這個key存的value
     */
    public final V put(K key, V value) {
        if (key == null || value == null) {
        	//不允許 null key、null value
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        //可見put操作是線程安全的
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            //強引用存入map(不會被動地被系統回收),其因爲是LinkedHashMap,會放在隊尾
            previous = map.put(key, value);
            if (previous != null) {
            	//如果前面已這個key,那麼替換後調整下當前緩存大小
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //重新調整大小
        trimToSize(maxSize);
        return previous;
    }

    /**
     * 比較 當前已緩存的大小 和最大容量,決定 是否刪除
     */
    private void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize) {
                	//大小還沒超過最大值
                    break;
                }
                
                //已經達到最大容量
                
                //因爲是訪問順序,所以遍歷的最後一個就是最近沒有訪問的,那麼就可以刪掉它了!
                Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
			//因爲是爲了騰出空間,所以這個回調第一個參數是true
            entryRemoved(true, key, value, null);
        }
    }

    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

    ...
}

由以上代碼及註釋,可見LruCache的算法實現是依靠 設置了訪問順序的LinkedHashMap。因爲是訪問順序模式,get、put操作都會調整k-v到鏈表尾部。在緩存將滿時,遍歷LinkedHashMap,因爲是訪問順序模式,所以遍歷的最後一個就是最近沒有使用的,然後刪除即可。

2.2 DiskLruCache

DiskLruCache是實現磁盤緩存,所以需要設備存儲的讀寫權限;一般是從網絡請求圖片後緩存到磁盤中,所以還需要網絡權限。

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

DiskLruCache,不是官方提供,所以需要引入依賴:

implementation 'com.jakewharton:disklrucache:2.0.2'
  • DiskLruCache的創建,不是通過new,而是open方法,需要傳入緩存目錄、最大緩存容量。
  • 緩存的添加,是通過Editor,緩存對象的編輯器。傳入圖片url的key 調用DiskLruCache的edit方法獲取Editor(如果緩存正在被編輯就會返回null),可以從Editor得到文件輸出流,這樣就可以寫入到文件系統了。
  • 緩存的獲取,傳入圖片url的key 調用DiskLruCache的get方法 得到SnapShot,可從SnapShoty獲取文件輸入流,這樣就用BitmapFactory得到bitmap了。
  • 緩存的刪除,DiskLruCache的remove方法可以刪除key對應的緩存。

通過查看源碼,發現LinkedHashMap內部也是維護了訪問順序的LinkedHashMap,原理上和LruCache是一致的。只是使用上有點點複雜,畢竟涉及文件的讀寫。

具體使用及注意點如下代碼:

    private void testDiskLruCache(String urlString) {
        long maxSize = 50*1024*1024;

        try {
            //一、創建DiskLruCache
            //第一個參數是要存放的目錄,這裏選擇外部緩存目錄(若app卸載此目錄也會刪除);
            //第二個是版本一般設1;第三個是緩存節點的value數量一般也是1;
            //第四個是最大緩存容量這裏取50M
            mDiskLruCache = DiskLruCache.open(getExternalCacheDir(), 1, 1, maxSize);

            //二、緩存的添加:1、通過Editor,把圖片的url轉成key,通過edit方法得到editor,然後獲取輸出流,就可以寫到文件系統了。
            DiskLruCache.Editor editor = mDiskLruCache.edit(hashKeyFormUrl(urlString));
            if (editor != null) {
                //參數index取0(因爲上面的valueCount取的1)
                OutputStream outputStream = editor.newOutputStream(0);
                boolean downSuccess = downloadPictureToStream(urlString, outputStream);
                if (downSuccess) {
                    //2、編輯提交,釋放編輯器
                    editor.commit();
                }else {
                    editor.abort();
                }
                //3、寫到文件系統,會檢查當前緩存大小,然後寫到文件
                mDiskLruCache.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        //三、緩存的獲取
        try {
            String key = hashKeyFormUrl(urlString);
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            FileInputStream inputStream = (FileInputStream)snapshot.getInputStream(0);
//            Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
//            mIvBitamp.setImageBitmap(bitmap);

            //注意,一般需要採樣加載,但文件輸入流是有序的文件流,採樣時兩次decodeStream影響文件流的文職屬性,導致第二次decode是獲取是null
            //爲解決此問題,可用文件描述符
            FileDescriptor fd = inputStream.getFD();

            //採樣加載(就是前面講的bitmap的高效加載)
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds=true;
            BitmapFactory.decodeFileDescriptor(fd,null,options);
            ViewGroup.LayoutParams layoutParams = mIvBitamp.getLayoutParams();
            options.inSampleSize = getInSampleSize(layoutParams.width, layoutParams.height, options.outWidth, options.outHeight);
            options.inJustDecodeBounds = false;
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);

            //存入內容緩存,繪製到view。(下次先從內存緩存獲取,沒有就從磁盤緩存獲取,在沒有就請求網絡--"三級緩存")
            mBitmapLruCache.put(key,bitmap);

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mIvBitamp.setImageBitmap(mBitmapLruCache.get(key));
                }
            });

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 下載圖片到文件輸入流
     */
    private boolean downloadPictureToStream(String urlString, OutputStream outputStream) {
        URL url = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        try {
            url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream());
            out = new BufferedOutputStream(outputStream);

            int b;
            while ((b=in.read()) != -1) {
                //寫入文件輸入流
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (in != null) {in.close();}
                if (out != null) {out.close();}
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 圖片的url轉成key,使用MD5
     */
    private String hashKeyFormUrl(String url) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            return byteToHexString(digest.digest(url.getBytes()));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String byteToHexString(byte[] bytes) {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0XFF & bytes[i]);
            if (hex.length()==1) {
                stringBuffer.append(0);
            }
            stringBuffer.append(hex);
        }
        return stringBuffer.toString();
    }

三、ImageLoader

前面說的 Bitmap的高效加載、LruCache、DiskLruCache,是一個圖片加載框架必備的功能點。下面就來封裝一個ImageLoader。首先羅列 實現的要點

  • 圖片壓縮,就是採樣加載
  • 內存緩存,LruCache
  • 磁盤緩存,DiskLruCache
  • 網絡獲取,請求網絡url
  • 同步加載,外部子線程同步執行
  • 異步加載,ImageLoader內部線程異步執行

說明,

  1. 三級緩存“的邏輯:加載時 先從內存緩存獲取,有就返回bitmap繪製圖片到view,若沒有就從磁盤緩存獲取;磁盤緩存有就返回bitmap並緩存到內存緩存,沒有就請求網絡;網絡請求回來,就緩存到磁盤緩存,然後從磁盤緩存獲取返回。
  2. 同步加載,是在外部的子線程中執行,同步加載方法內部沒有開線程,所以加載過程是耗時的 會阻塞 外部的子線程,加載完成後 需要自行切到主線程繪製到view。
  3. 異步加載,外部可在任意線程執行,因爲內部實現是在子線程(線程池)加載,並且內部會通過Handler切到主線程,只需要傳入view,內部就可直接繪製Bitmap到view。

詳細如下

public class ImageLoader {

    private static final String TAG = "ImageLoader";

    private static final long KEEP_ALIVE_TIME = 10L;

    private static final int CPU_COUNT =  Runtime.getRuntime().availableProcessors();

    private static final int CORE_THREAD_SIZE = CPU_COUNT + 1;

    private static final int THREAD_SIZE = CPU_COUNT * 2 + 1;

    private static final int VIEW_TAG_URL = R.id.view_tag_url;

    private static final Object object = new Object();


    private ThreadPoolExecutor mExecutor;

    private Handler mMainHandler;


    private Context mApplicationContext;

    private static volatile ImageLoader mImageLoader;

    private LruCache<String, Bitmap> mLruCache;

    private DiskLruCache mDiskLruCache;

    /**
     * 磁盤緩存最大容量,50M
     */
    private static final long DISK_LRU_CACHE_MAX_SIZE = 50 * 1024 * 1024;

    /**
     * 當前進程的最大內存,取進程內存的1/8
     */
    private static final long MEMORY_CACHE_MAX_SIZE = Runtime.getRuntime().maxMemory() / 8;


    public ImageLoader(Context context) {
        if (context == null) {
            throw new RuntimeException("context can not be null !");
        }
        mApplicationContext = context.getApplicationContext();

        initLruCache();
        initDiskLruCache();
        initAsyncLoad();
    }

    public static ImageLoader with(Context context){
        if (mImageLoader == null) {
            synchronized (object) {
                if (mImageLoader == null) {
                    mImageLoader = new ImageLoader(context);
                }
            }
        }
        return mImageLoader;
    }

    private void initAsyncLoad() {
        mExecutor = new ThreadPoolExecutor(CORE_THREAD_SIZE, THREAD_SIZE,
                KEEP_ALIVE_TIME, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {
            private final AtomicInteger count = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable runnable) {
                return new Thread(runnable, "load bitmap thread "+ count.getAndIncrement());
            }
        });

        mMainHandler = new Handler(Looper.getMainLooper()){
            @Override
            public void handleMessage(@NonNull Message msg) {
                LoadResult result = (LoadResult) msg.obj;
                ImageView imageView = result.imageView;
                Bitmap bitmap = result.bitmap;
                String url = result.url;
                if (imageView == null || bitmap == null) {
                    return;
                }

                //此判斷是 避免 ImageView在列表中複用導致圖片錯位的問題
                if (url.equals(imageView.getTag(VIEW_TAG_URL))) {
                    imageView.setImageBitmap(bitmap);
                }else {
                    Log.w(TAG, "handleMessage: set image bitmap,but url has changed,ignore!");
                }
            }
        };
    }

    private void initLruCache() {

        mLruCache = new LruCache<String, Bitmap>((int) MEMORY_CACHE_MAX_SIZE){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //緩存對象bitmap的大小,單位要和MEMORY_CACHE_MAX_SIZE一致
                return value.getByteCount();
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //移除舊緩存時會調用,可以在這裏進行像資源回收的工作。
            }
        };

    }

    private void initDiskLruCache() {

        File externalCacheDir = mApplicationContext.getExternalCacheDir();
        if (externalCacheDir != null) {
            long usableSpace = externalCacheDir.getUsableSpace();
            if (usableSpace < DISK_LRU_CACHE_MAX_SIZE){
                //剩餘空間不夠了
                Log.e(TAG, "initDiskLruCache: "+"UsableSpace="+usableSpace+" , not enough(target 50M),cannot creat diskLruCache!");
                return;
            }
        }

        //一、創建DiskLruCache
            //第一個參數是要存放的目錄,這裏選擇外部緩存目錄(若app卸載此目錄也會刪除);
            //第二個是版本一般設1;第三個是緩存節點的value數量一般也是1;
            //第四個是最大緩存容量這裏取50M
        try {
            this.mDiskLruCache = DiskLruCache.open(mApplicationContext.getExternalCacheDir(), 1, 1, DISK_LRU_CACHE_MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "initDiskLruCache: "+e.getMessage());
        }
    }

    /**
     * 緩存bitmap到內存
     * @param url url
     * @param bitmap bitmap
     */
    private void addBitmapMemoryCache(String url, Bitmap bitmap) {
        String key = UrlKeyTransformer.transform(url);
        if (mLruCache.get(key) == null && bitmap != null) {
            mLruCache.put(key,bitmap);
        }
    }

    /**
     * 從內存緩存加載bitmap
     * @param url url
     * @return
     */
    private Bitmap loadFromMemoryCache(String url) {
        return mLruCache.get(UrlKeyTransformer.transform(url));
    }


    /**
     * 從磁盤緩存加載bitmap(並添加到內存緩存)
     * @param url url
     * @param requestWidth 要求的寬
     * @param requestHeight 要求的高
     * @return bitmap
     */
    private Bitmap loadFromDiskCache(String url, int requestWidth, int requestHeight) throws IOException {
        if (Looper.myLooper()==Looper.getMainLooper()) {
            Log.w(TAG, "loadFromDiskCache from Main Thread may cause block !");
        }

        if (mDiskLruCache == null) {
            return null;
        }
        DiskLruCache.Snapshot snapshot = null;
        String key = UrlKeyTransformer.transform(url);
        snapshot = mDiskLruCache.get(key);
        if (snapshot != null) {
            FileInputStream inputStream = (FileInputStream)snapshot.getInputStream(0);

            //Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

            //一般需要採樣加載,但文件輸入流是有序的文件流,採樣時兩次decodeStream影響文件流的位置屬性,
            //導致第二次decode是獲取是null,爲解決此問題,可用文件描述符。
            FileDescriptor fd = inputStream.getFD();
            Bitmap bitmap = BitmapSampleDecodeUtil.decodeFileDescriptor(fd, requestWidth, requestHeight);
            addBitmapMemoryCache(url,bitmap);
            return bitmap;
        }

        return null;
    }


    /**
     * 從網路加載圖片 到磁盤緩存(然後再從磁盤中採樣加載)
     * @param urlString urlString
     * @param requestWidth 要求的寬
     * @param requestHeight 要求的高
     * @return Bitmap
     */
    private Bitmap loadFromHttp(String urlString, int requestWidth, int requestHeight) throws IOException {
        //線程檢查,不能是主線程
        if (Looper.myLooper()==Looper.getMainLooper()) {
            throw new RuntimeException("Do not loadFromHttp from Main Thread!");
        }

        if (mDiskLruCache == null) {
            return null;
        }

        DiskLruCache.Editor editor = null;
        editor = mDiskLruCache.edit(UrlKeyTransformer.transform(urlString));
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(0);
            if (downloadBitmapToStreamFromHttp(urlString, outputStream)) {
                editor.commit();
            }else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }

        return loadFromDiskCache(urlString, requestWidth, requestHeight);
    }

    /**
     * 從網絡下載圖片到文件輸入流
     * @param urlString
     * @param outputStream
     * @return
     */
    private boolean downloadBitmapToStreamFromHttp(String urlString, OutputStream outputStream) {
        URL url = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        try {
            url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream());
            out = new BufferedOutputStream(outputStream);

            int b;
            while ((b=in.read()) != -1) {
                //寫入文件輸入流
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "downloadBitmapToStreamFromHttp,failed : "+e.getMessage());
        }finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            IoUtil.close(in);
            IoUtil.close(out);
        }
        return false;
    }

    /**
     * 從網絡直接下載bitmap(無緩存、無採樣)
     * @param urlString
     * @return
     */
    private Bitmap downloadBitmapFromUrlDirectly(String urlString) {
        URL url;
        HttpURLConnection urlConnection = null;
        BufferedInputStream bufferedInputStream = null;
        try {
            url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            bufferedInputStream = new BufferedInputStream(urlConnection.getInputStream());
            return BitmapFactory.decodeStream(bufferedInputStream);
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "downloadBitmapFromUrlDirectly,failed : "+e.getMessage());
        }finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            IoUtil.close(bufferedInputStream);
        }
        return null;
    }

    public Bitmap loadBitmap(String url){
        return loadBitmap(url,0,0);
    }

    /**
     * 同步 加載bitmap
     *
     * 不能在主線程執行。加載時 先從內存緩存獲取,有就返回bitmap,若沒有就從磁盤緩存獲取;
     * 磁盤緩存有就返回bitmap並緩存到內存緩存,沒有就請求網絡;
     * 網絡請求回來,就緩存到磁盤緩存,然後從磁盤緩存獲取返回。
     *
     * @return Bitmap
     */
    public Bitmap loadBitmap(String url, int requestWidth, int requestHeight){

        Bitmap bitmap = loadFromMemoryCache(url);
        if (bitmap != null) {
            Log.d(TAG, "loadBitmap: loadFromMemoryCache, url:"+url);
            return bitmap;
        }

        try {
            bitmap = loadFromDiskCache(url, requestWidth, requestHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap != null) {
            Log.d(TAG, "loadBitmap: loadFromDiskCache, url:"+url);
            return bitmap;
        }

        try {
            bitmap = loadFromHttp(url, requestWidth, requestHeight);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap != null){
            Log.d(TAG, "loadBitmap: loadFromHttp, url:"+url);
            return bitmap;
        }

        if (mDiskLruCache == null) {
            Log.d(TAG, "loadBitmap: diskLruCache is null,load bitmap from url directly!");
            bitmap = downloadBitmapFromUrlDirectly(url);
        }

        return bitmap;
    }

    public void loadBitmapAsync(final String url, final ImageView imageView){
        loadBitmapAsync(url,imageView,0,0);
    }

    /**
     * 異步 加載bitmap
     * 外部可在任意線程執行,因爲內部實現是在子線程(線程池)加載,
     * 並且內部會通過Handler切到主線程,只需要傳入view,內部就可直接繪製Bitmap到view。
     * @param url
     * @param imageView
     * @param requestWidth
     * @param requestHeight
     */
    public void loadBitmapAsync(final String url, final ImageView imageView, final int requestWidth, final int requestHeight){
        if (url == null || url.isEmpty() || imageView == null) {
            return;
        }

        // 標記當前imageView要繪製圖片的url
        imageView.setTag(VIEW_TAG_URL, url);

        mExecutor.execute(new Runnable() {
            @Override
            public void run() {

                Bitmap loadBitmap = loadBitmap(url, requestWidth, requestHeight);

                Message message = Message.obtain();
                message.obj = new LoadResult(loadBitmap, url, imageView);
                mMainHandler.sendMessage(message);
            }
        });
    }

}

/**
 * Bitmap採樣壓縮加載工具
 * @author hufeiyang
 */
public class BitmapSampleDecodeUtil {

    private static final String TAG = "BitmapSampleDecodeUtil";

    /**
     * 對資源圖片的採樣
     * @param resources resources
     * @param resourcesId 資源id
     * @param requestWidth view的寬
     * @param requestHeight view的高
     * @return 採樣後的bitmap
     */
    public static Bitmap decodeSampleResources(Resources resources, int resourcesId, int requestWidth, int requestHeight){
        if (resources == null || resourcesId<=0) {
            return null;
        }

        //1、inJustDecodeBounds設爲true,並加載圖片
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources, resourcesId, options);

        //2、獲取原始寬高信息
        int outWidth = options.outWidth;
        int outHeight = options.outHeight;

        //3、原始寬高信息 和 view的大小 計算並設置採樣率
        options.inSampleSize = getInSampleSize(requestWidth, requestHeight, outWidth, outHeight);

        //4、inJustDecodeBounds設爲false,並加載圖片
        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeResource(resources, resourcesId, options);
    }

    /**
     * 對文件描述符的採樣加載
     * @param fileDescriptor fileDescriptor
     * @param requestWidth view的寬
     * @param requestHeight view的高
     * 注意,文件輸入流是有序的文件流,採樣時兩次decodeStream影響文件流的文職屬性,導致第二次decode是獲取是null。
     * 爲解決此問題,可用本方法對文件流的文件描述符 加載。
     */
    public static Bitmap decodeFileDescriptor(FileDescriptor fileDescriptor, int requestWidth, int requestHeight){

        if (fileDescriptor == null) {
            return null;
        }

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds=true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
        options.inSampleSize = getInSampleSize(requestWidth, requestHeight, options.outWidth, options.outHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

    /**
     * 計算採樣率
     * @param width view的寬
     * @param height view的高
     * @param outWidth 圖片原始的寬
     * @param outHeight 圖片原始的高
     * @return
     */
    private static int getInSampleSize(int width, int height, int outWidth, int outHeight) {
        int inSampleSize = 1;

        if (width==0 || height ==0){
            return inSampleSize;
        }

        if (outWidth>width || outHeight>height){
            int halfWidth = outWidth / 2;
            int halfHeight = outHeight / 2;
            //保證採樣後的寬高都不小於目標快高,否則會拉伸而模糊
            while (halfWidth/inSampleSize >=width
                    && halfHeight/inSampleSize>=height){
                inSampleSize *=2;
            }
        }

        Log.d(TAG, "getInSampleSize: inSampleSize="+inSampleSize);
        return inSampleSize;
    }
}
/**
 * 圖片的url轉成key
 * @author hufeiyang
 */
public class UrlKeyTransformer {

    /**
     * 圖片的url轉成key
     * @param url
     * @return MD5轉換後的key
     */
    public static String transform(String url) {
        if (url == null || url.isEmpty()) {
            return null;
        }
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            return byteToHexString(digest.digest(url.getBytes()));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String byteToHexString(byte[] bytes) {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0XFF & bytes[i]);
            if (hex.length()==1) {
                stringBuffer.append(0);
            }
            stringBuffer.append(hex);
        }
        return stringBuffer.toString();
    }
}
public class LoadResult {

    public Bitmap bitmap;

    /**
     * bitmap對應的url
     */
    public String url;

    public ImageView imageView;


    public LoadResult(Bitmap bitmap, String url, ImageView imageView) {
        this.bitmap = bitmap;
        this.url = url;
        this.imageView = imageView;
    }
}

具體代碼gitHub地址:SimpleImageLoader

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章