Bitmap的載入和Cache

cryAllen發表於2017-02-27

由於Bitmap的特殊性以及Android對單個應用所施加的記憶體限制,比如16M,這導致載入Bitmap的時候很容易出現記憶體溢位。比如以下場景:

java.lang.OutofMemoryError:bitmap size exceeds VM budget

Android中常用的快取策略也是很有意思,快取策略一個通用的思想,可以用到很多場景中,比如在實際開發中經常需要用到Bitmap做快取。通過快取策略,我們不需要每次都從網路上請求圖片或者從儲存裝置中載入圖片,這樣就極大地提高了圖片的載入效率以及產品的使用者體驗。目前比較常用的快取策略是LruCache和DiskLruCache,其中LruCache常被用做記憶體快取,而DiskLruCache用做儲存快取。Lru是Least Recently Used的縮寫,即最近最少使用演算法,這種演算法的核心思想:當快取快滿時,會淘汰近期最少使用的快取目標,很顯然Lru演算法的思想是很容易被接受的。

Bitmap的高效載入

Bitmap在Android中指的是一張圖片,可以是png格式也可以是jpg等其他常見的圖片格式。BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分別用於支援從檔案系統、資源、輸入流以及位元組陣列中載入出一個Bitmap物件,其中decodeFile和decodeResource又間接呼叫了decodeStream方法,這四類方法最終是在Android的底層實現的,對應著BitmapFactory類的幾個native方法。

如何高效地載入Bitmap呢,其實核心思想也簡單,那就是採用BitmapFactory.Options來載入所需尺寸的圖片。主要是用到它的inSampleSize引數,即取樣率。當inSampleSize為1時,取樣後的圖片大小為圖片的原始大小,當inSampleSize大於1時,比如為2,那麼取樣後的圖片其寬/寬均為原圖大小的1/2,而畫素數為原圖的1/4,其佔有的記憶體大小也為原圖的1/4。從最新官方文件中指出,inSampleSize的取值應該是2的指數,比如1、2、4、8、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。

LruCache

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,開發人員必備技能——單元測試

相關文章