Bitmap的加載和Cache

由于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開啟硬件加速。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容