Bitmap的加載和Cache

因爲Bitmap的特殊性以及Android對單個應用所施加的內存限制,好比16M,這致使加載Bitmap的時候很容易出現內存溢出。好比如下場景:html

java.lang.OutofMemoryError:bitmap size exceeds VM budget

Android中經常使用的緩存策略也是頗有意思,緩存策略一個通用的思想,能夠用到不少場景中,好比在實際開發中常常須要用到Bitmap作緩存。經過緩存策略,咱們不須要每次都從網絡上請求圖片或者從存儲設備中加載圖片,這樣就極大地提升了圖片的加載效率以及產品的用戶體驗。目前比較經常使用的緩存策略是LruCache和DiskLruCache,其中LruCache常被用作內存緩存,而DiskLruCache用作存儲緩存。Lru是Least Recently Used的縮寫,即最近最少使用算法,這種算法的核心思想:當緩存快滿時,會淘汰近期最少使用的緩存目標,很顯然Lru算法的思想是很容易被接受的。java

Bitmap的高效加載

Bitmap在Android中指的是一張圖片,能夠是png格式也能夠是jpg等其餘常見的圖片格式。BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分別用於支持從文件系統、資源、輸入流以及字節數組中加載出一個Bitmap對象,其中decodeFile和decodeResource又間接調用了decodeStream方法,這四類方法最終是在Android的底層實現的,對應着BitmapFactory類的幾個native方法。android

如何高效地加載Bitmap呢,其實核心思想也簡單,那就是採用BitmapFactory.Options來加載所需尺寸的圖片。主要是用到它的inSampleSize參數,即採樣率。當inSampleSize爲1時,採樣後的圖片大小爲圖片的原始大小,當inSampleSize大於1時,好比爲2,那麼採樣後的圖片其寬/寬均爲原圖大小的1/2,而像素數爲原圖的1/4,其佔有的內存大小也爲原圖的1/4。從最新官方文檔中指出,inSampleSize的取值應該是2的指數,好比一、二、四、八、16等等。算法

經過採樣率便可有效地加載圖片,那麼到底如何獲取採樣率呢,獲取採樣率也很簡單,循序以下流程:數組

  • 將BitmapFactory.Options的inJustDecodeBounds參數設爲True並加載圖片
  • 從BitmapFactory.Options中取出圖片的原始寬高信息,他們對應於outWidth和outHeight參數
  • 根據採樣率的規則並結合目標View的所需大小計算出採樣率inSampleSize
  • 將BitmapFactory.Options的inJustDecodeBounds參數設爲False,而後從新加載圖片。

通過上面4個步驟,加載出的圖片就是最終縮放後的圖片,固然也有可能不須要縮放。代碼以下:緩存

public Bitmap decodeSampledBitmapFromResource(Resources res,
            int resId, int reqWidth, int reqHeight) {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
 
    public int calculateInSampleSize(BitmapFactory.Options options,
            int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        Log.d(TAG, "origin, w= " + width + " h=" + height);
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and
            // keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        Log.d(TAG, "sampleSize:" + inSampleSize);
        return inSampleSize;
    }

Android中的緩存策略

緩存策略在Android中有着普遍的使用場景,尤爲在圖片加載這個場景下,緩存策略變得更爲重要。有一個場景就是批量下載網絡圖片,在PC上是能夠把全部的圖片下載到本地再顯示便可,可是放到移動設備上就不同了。不論是Android仍是IOS設備,流量對於用戶來講都是一種寶貴的資源。安全

如何避免過多的流量消耗呢,那就是緩存。當程序第一次從網絡加載圖片後,就將其緩存到存儲設備上,這樣下次使用這張圖片就不用從網絡上獲取了,這樣就爲用戶節省了流量。不少時候爲了提升用戶的用戶體驗,每每還會把圖片在內存中再緩存一份,這樣當應用打算從網絡上請求一張圖片時,程序首先從內存中去獲取,若是內存中沒有那就從存儲設備中去獲取,若是存儲設備中也沒有,那就從網絡上下載這張圖片。由於從內存中加載圖片比從存儲設備中加載圖片要快,因此這樣既提升了程序的效率又爲用戶節約了沒必要要的流量開銷。性能優化

目前經常使用的一種緩存算法是LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想是當緩存滿時,會優先淘汰那些近期最少使用的緩存對象。採用LRU算法的緩存有兩種:LruCache和DiskLruCache,LruCache用於實現內存緩存,而DiskLruCache則充當了存儲設備緩存,經過這兩者的完美結合,就能夠很方便地實現一個具備很高實用價值的ImageLoader。網絡

LruCacheapp

LruCache是Android 3.1提供的一個緩存類,經過support-v4兼容包能夠兼容到早期的Android版本。它是一個泛型類,它內部採用一個LinkedHashMap,當強引用的方式存儲外界的緩存對象,其提供了get和put方法來完成緩存的獲取和添加操做,當緩存滿時,LruCache會移除較早使用的緩存對象,而後再添加新的緩存對象。

  • 強引用:直接的對象引用
  • 軟引用:當一個對象只有軟引用存在時,系統內存不足時此對象會被gc回收。
  • 弱引用:當一個對象只有弱引用存在時,此對象會隨時被gc回收。

LruCache是線程安全的,由於用到了LinkedHashMap。從Android 3.1開始,LruCache就已是Android源碼的一部分。

DiskLruCache

DiskLruCache用於實現存儲設備緩存,即磁盤存儲,它經過將緩存對象寫入文件系統從而實現緩存的效果。DiskLruCache獲得了Android官方文檔的推薦,但它不屬於Android SDK的一部分。

ImageLoader的實現

通常來講,一個優秀的ImageLoader應該具有以下功能:

  • 圖片的同步加載
  • 圖片的異步加載
  • 圖片壓縮
  • 內存緩存
  • 磁盤緩存
  • 網絡拉取

圖片的同步加載是指可以以同步的方式向調用者提供所加載的圖片,這個圖片多是從內存緩存讀取的,也多是從磁盤緩存中讀取的,還多是從網絡拉取的。

圖片的異步加載是一個頗有用的功能,不少時候調用者不想再單獨的線程中以同步的方式來獲取圖片,這個時候ImageLoader內部須要本身在線程中加載圖片並將圖片設置所需的ImageView。圖片壓縮的做用更須要了,這是下降OOM機率的有效手段,ImageLoader必須合適地處理圖片的壓縮問題。

內存緩存和磁盤緩存是ImageLoader的核心,也是ImageLoader的意義所在,經過這兩級緩存極大地提升了程序的效率而且有效地下降了對用戶所形成的流量消耗,只有當這兩級緩存都不可用時才須要從網絡中拉取圖片。

一個實現ImageLoader的例子:

public class ImageLoader {

    private static final String TAG = "ImageLoader";

    public static final int MESSAGE_POST_RESULT = 1;

    private static final int CPU_COUNT = Runtime.getRuntime()
            .availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;

    private static final int TAG_KEY_URI = R.id.imageloader_uri;
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private boolean mIsDiskLruCacheCreated = false;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
            KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);
    
    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            imageView.setImageBitmap(result.bitmap);
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed, ignored!");
            }
        };
    };

    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;

    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
                        DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * build a new instance of ImageLoader
     * @param context
     * @return a new instance of ImageLoader
     */
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }

    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    /**
     * load bitmap from memory cache or disk cache or network async, then bind imageView and bitmap.
     * NOTE THAT: should run in UI Thread
     * @param uri http url
     * @param imageView bitmap's bind object
     */
    public void bindBitmap(final String uri, final ImageView imageView) {
        bindBitmap(uri, imageView, 0, 0);
    }

    public void bindBitmap(final String uri, final ImageView imageView,
            final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {

            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    /**
     * load bitmap from memory cache or disk cache or network.
     * @param uri http url
     * @param reqWidth the width ImageView desired
     * @param reqHeight the height ImageView desired
     * @return bitmap, maybe null.
     */
    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
            return bitmap;
        }

        try {
            bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
            if (bitmap != null) {
                Log.d(TAG, "loadBitmapFromDisk,url:" + uri);
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
            Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (bitmap == null && !mIsDiskLruCacheCreated) {
            Log.w(TAG, "encounter error, DiskLruCache is not created.");
            bitmap = downloadBitmapFromUrl(uri);
        }

        return bitmap;
    }

    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemCache(key);
        return bitmap;
    }

    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
            int reqHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
        if (snapShot != null) {
            FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
                    reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }

        return bitmap;
    }

    public boolean downloadUrlToStream(String urlString,
            OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),
                    IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            Log.e(TAG, "downloadBitmap failed." + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(out);
            MyUtils.close(in);
        }
        return false;
    }

    private Bitmap downloadBitmapFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),
                    IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap: " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(in);
        }
        return bitmap;
    }

    private String hashKeyFormUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.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();
    }

    public File getDiskCacheDir(Context context, String uniqueName) {
        boolean externalStorageAvailable = Environment
                .getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }

        return new File(cachePath + File.separator + uniqueName);
    }

    @TargetApi(VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    private static class LoaderResult {
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}

優化列表的卡頓現象

在通常ListView或者GridView中,使用照片牆的時候,容易出現滑動卡頓,如何優化呢,有三點建議:

  • 不要在getView中執行耗時操做。好比加載圖片,確定會致使卡頓,由於加載圖片是一個耗時的操做,這種操做必須經過異步的方式來處理。
  • 控制異步任務的執行頻率。好比在異步加載圖片時,用戶刻意地頻繁上下滑動,這就會在一瞬間產生上百個異步任務,這些異步任務會形成線程池的擁堵並隨即帶來大量的UI更新操做,這是沒有意義的。那該如何解決呢,能夠考慮在列表滑動的時候,中止加載圖片,儘管這個過程是異步的,等列表停下來之後在加載圖片仍然能夠得到良好的用戶體驗。
  • 開啓硬件加速能夠解決莫名的卡頓問題,經過設置android:hardwareAccelerated = "true"便可爲Activity開啓硬件加速。

閱讀擴展

源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中能夠看到技術積累的過程。
1,Android系統簡介
2,ProGuard代碼混淆
3,講講Handler+Looper+MessageQueue關係
4,Android圖片加載庫理解
5,談談Android運行時權限理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大組件之 " Activity "
10,Android 四大組件之" Service "
11,Android 四大組件之「 BroadcastReceiver "
12,Android 四大組件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命週期和啓動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工做原理
19,理解 Window 和 WindowManager
20,Activity 啓動過程分析
21,Service 啓動過程分析
22,Android 性能優化
23,Android 消息機制
24,Android Bitmap相關
25,Android 線程和線程池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸摸事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 插件化思考
32,開發人員必備技能——單元測試

相關文章
相關標籤/搜索