1. 程式人生 > >【Android效能優化】Android圖片載入方案--Bitmap的記憶體管理和優化方案

【Android效能優化】Android圖片載入方案--Bitmap的記憶體管理和優化方案

如果你覺得對你有幫助的話,希望可以star/follow一下喲,我會持續保持更新。

寫在前面:筆者的上一篇博文有提到過,如果不恰當的使用Bitmap,很容易造成OOM。這篇博文就來談談應該如何正確的管理Bitmap的記憶體,以及優化策略。

一、載入按顯示需要的比例縮小的圖片

1、先來說說螢幕密度

  • 在Android中,Imageview控制元件的長寬單位一般設定為dp/dip,而不用px。這樣做的原因,是因為dp/dip與螢幕畫素密度無關,px與螢幕密度有關。在Android中,規定以160dpi為基準,1dip=1px,如果密度是320dpi,則1dip=2px。(所以,同一個imageview,在不同的裝置上所顯示的長寬的畫素是不同的,我們需要根據畫素不同,來按比例壓縮大圖)

  • 如果把一個大小為1024x1024畫素的圖片顯示到大小為512x512畫素的ImageView上嗎,就沒有必要載入整張原圖到Bitmap中。

  • 為了告訴解碼器去載入一個縮小比例是多少的圖片到記憶體中,需要在BitmapFactory.Options 中設定 inSampleSize 的值。例如, 一個解析度為2048x2048的圖片,如果設定 inSampleSize 為4,那麼會產出一個大約512x512大小的Bitmap。

  • 下面的程式碼是動態獲取一個ImageView的長寬畫素:
    注意:返回的是畫素值(px),而不是dp/dip

    int reqHeight = iv.getLayoutParams
().height; int reqWidth = iv.getLayoutParams().width;

2、壓縮圖片

  • 一般來說,載入本地資源圖片才需要壓縮,載入網路圖片,應該讓伺服器按需壓縮,一方面節省流量,另一方面提高載入流暢度
  • 可以通過下面的程式碼計算inSampleSize的值,就是需要壓縮的倍數:
//reqWidth和reqHeight是需要顯示的Imageview的width和height
public static int calculateInSampleSize(
            BitmapFactory.Options options, int
reqWidth, int reqHeight) { // height和width圖片長寬的畫素 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
  • 通過下面的方法可以獲得壓縮後的Bitmap:
    注意:可以通過設定 inJustDecodeBounds 屬性為true可以在解碼的時候避免記憶體的分配,它會返回一個null的Bitmap,但是可以獲取到 outWidth, outHeight 與 outMimeType。確定好壓縮比例後,再將inJustDecodeBounds設定為false。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

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

    // 根據inSampleSize壓縮圖片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

二、Bitmap快取

1、快取類LruCache介紹

  • 在Android3.0之後,一般使用LruCache來快取Bitmap,它使用一個強引用的LinkedHashMap儲存最近引用的物件,並且在快取超出設定大小的時候剔除(evict)最少使用到的物件。
  • LinkedHashMap會根據LRU演算法來排列物件的順序,新加入的物件新增到頭部,剛被使用過的物件也被移動到頭部,所以在連結串列尾部的物件是最久沒有被使用過的,一旦連結串列滿了,有新物件加入,就會刪除連結串列尾部的物件。

2、如何給LruCache設定一個合適的大小?

  • LruCache太大的話,容易造成OOM或者分配給應用的剩餘記憶體不夠用,LruCache大小的設定,應該考慮下面的因素:
    • (1)應用允許的最大記憶體是多少?剩下了多少可用的記憶體?
    • (2)多少張圖片會同時顯示在螢幕上?
    • (3)裝置的螢幕密度是多少?顯示圖片的畫素是多少?
    • (4)可以根據訪問頻率給Bitmap分組,為不同的Bitmap組設定不同大小的LruCache物件。

3、一個LruCache使用的完整例子

  • 程式碼參考了一個大神的程式碼,並修真了一些寫的不對和不好的地方。
public class PhotoAdapter extends BaseAdapter implements AbsListView.OnScrollListener {

    // 從network下載圖片的執行緒集合
    private List<ImageDownloadTask> mDownloadTaskList;
    private LruCache<String, Bitmap> mLruCache;

    // 引用外部的變數
    private WeakReference<GridView> mGridView;
    private WeakReference<List<String>> urls;
    private WeakReference<Context> mContext;


    // 可見項的第一項的index
    private int mFirstVisibleIndex;

    // 可見項的個數
    private int mVisibleItemCount;

    // 是不是第一次開啟Activity
    private boolean isFirstOpen = true;

    public PhotoAdapter(Context context, GridView mGridView, List<String> urls) {
        this.mContext = new WeakReference<Context>(context);
        this.urls = new WeakReference<List<String>>(urls);
        this.mGridView = new WeakReference<GridView>(mGridView);
        this.mGridView.get().setOnScrollListener(this);
        mDownloadTaskList = new ArrayList<>();
        // 初始化圖片快取池
        initCache();
    }

    private void initCache() {

        // 獲取應用的max heap size
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // Android官方教學文件推薦LruCache的size為heap size的1/8
        int cacheSize = maxMemory / 8;

        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                if (bitmap != null) {
                    return bitmap.getByteCount() / 1024;
                }
                return 0;
            }
        };
    }

    @Override
    public int getCount() {
        return urls.get().size();
    }

    @Override
    public Object getItem(int position) {
        return urls.get().get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        viewHolder holder = null;

        if (convertView == null) {
            convertView = LayoutInflater.from(mContext.get()).inflate(R.layout.layout_item, parent, false);
            holder = new viewHolder();
            holder.mImageView = (ImageView) convertView.findViewById(R.id.imageView);
            holder.mTextView = (TextView) convertView.findViewById(R.id.textView);
            convertView.setTag(holder);
        } else {
            holder = (viewHolder) convertView.getTag();
        }

        String url = urls.get().get(position);
        //imageview與url繫結,防止錯亂顯示
        holder.mImageView.setTag(MD5Tools.decodeString(url));
        holder.mTextView.setText("第" + position + "項");

        if (!holder.mImageView.getTag().equals(url)) {
            showImageView(holder.mImageView, url);
        }

        return convertView;
    }

    /**
     * convertView複用
     */
    private class viewHolder {
        ImageView mImageView;
        TextView mTextView;
    }

    /**
     * 給ImageView設定Bitmap
     */
    private void showImageView(ImageView imageView, String url) {

        // 對url進行md5編碼
        String key = MD5Tools.decodeString(url);
        // 先從cache中找bitmap快取
        Bitmap bitmap = get(key);

        if (bitmap != null) {
            // 如果快取命中
            imageView.setImageBitmap(bitmap);
        } else {
            // 如果cache miss
            imageView.setBackgroundResource(R.color.color_five);
        }
    }

    /**
     * 將Bitmap put 到 cache中
     */
    private void put(String key, Bitmap bitmap) {

        if (get(key) == null) {
            mLruCache.put(key, bitmap);
        }
    }

    /**
     * 在Cache中查詢bitmap,如果miss則返回null
     */
    private Bitmap get(String key) {
        return mLruCache.get(key);
    }

    /**
     * 從網路下載圖片
     */
    private Bitmap loadBitmap(String urlStr) {

        HttpURLConnection connection = null;
        Bitmap bitmap = null;
        try {
            URL url = new URL(urlStr);
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);
            connection.setDoInput(true);
            connection.connect();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                InputStream mInputStream = connection.getInputStream();
                bitmap = BitmapFactory.decodeStream(mInputStream);
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
        return bitmap;
    }

    /**
     * 取消所有的下載任務
     */
    public void cancelAllTask() {

        if (mDownloadTaskList != null) {
            for (int i = 0; i < mDownloadTaskList.size(); i++) {
                mDownloadTaskList.get(i).cancel(true);
            }
        }
    }

    /**
     * 載入可見項的圖片
     */
    private void loadVisibleBitmap(int mFirstVisibleItem, int mVisibleItemCount) {

        for (int i = mFirstVisibleItem; i < mFirstVisibleItem + mVisibleItemCount; i++) {
            final String url = urls.get().get(i);
            String key = MD5Tools.decodeString(url);
            Bitmap bitmap = get(key);
            ImageView mImageView;
            if (bitmap != null) {
                //快取中存在該圖片的話就設定給ImageView
                mImageView = (ImageView) mGridView.get().findViewWithTag(MD5Tools.decodeString(url));
                if (mImageView != null) {
                    mImageView.setImageBitmap(bitmap);
                }
            } else {
                //不存在的話就開啟一個非同步執行緒去下載
                ImageDownloadTask task = new ImageDownloadTask(this);
                mDownloadTaskList.add(task);
                task.execute(url);
            }
        }
    }

    /**
     * 從網路下載圖片的非同步task
     */
    static class ImageDownloadTask extends AsyncTask<String, Void, Bitmap> {

        private String url;
        private WeakReference<PhotoAdapter> photoAdapter;

        public ImageDownloadTask(PhotoAdapter photoAdapter) {
            this.photoAdapter = new WeakReference<PhotoAdapter>(photoAdapter);
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            //在後臺開始下載圖片
            url = params[0];
            Bitmap bitmap = photoAdapter.get().loadBitmap(url);
            if (bitmap != null) {
                //把下載好的圖片放入LruCache中
                String key = MD5Tools.decodeString(url);
                photoAdapter.get().put(key, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            //把下載好的圖片顯示出來
            ImageView mImageView = (ImageView) photoAdapter.get().mGridView.get().findViewWithTag(MD5Tools.decodeString(url));
            if (mImageView != null && bitmap != null) {
                mImageView.setImageBitmap(bitmap);
                photoAdapter.get().mDownloadTaskList.remove(this);//把下載好的任務移除
            }
        }
    }

    /**
     * 監聽GridView的滑動狀態
     */
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

        //GridView停止滑動時,去載入可見項的圖片
        if (scrollState == SCROLL_STATE_IDLE) {
            loadVisibleBitmap(mFirstVisibleIndex, mVisibleItemCount);
        } else {
            //GridView開始滑動時,取消所有載入任務
            cancelAllTask();
        }
    }

    /**
     * 監聽並更新GridView滑動過程中的可見項
     */
    @Override
    public void onScroll(AbsListView view, int firstVisibleIndex, int visibleItemCount, int totalItemCount) {

        mFirstVisibleIndex = firstVisibleIndex;
        mVisibleItemCount = visibleItemCount;

        // 第一次開啟,載入可見項
        if (isFirstOpen && visibleItemCount > 0) {
            loadVisibleBitmap(mFirstVisibleIndex, mVisibleItemCount);
            isFirstOpen = false;
        }
    }
}

4、Andorid3.0之前的Bitmap管理方法(參考自xiaanming的部落格)

  • 在Android3.0之前,Bitmap物件與資料是分開儲存的,Bitmap物件儲存在Java heap中,而畫素資料儲存在Native Memory中,Java虛擬機器的垃圾回收機制不會主動回收Native Memory中的物件,需要在Bitmap不需要使用的時候,主動呼叫recycle()方法來釋放,而在Android3.0之後,Bitmap的畫素資料和Bitmap物件都存放在Java Heap中,所以不需要手動呼叫recycle()來釋放,垃圾收集器會處理。
  • 應該在什麼時候去呼叫recycle()方法呢?可以用引用計數演算法,用一個變數來記錄Bitmap顯示情況,如果Bitmap繪製在View上面displayRefCount加一, 否則就減一, 只有在displayResCount為0且Bitmap不為空且Bitmap沒有呼叫過recycle()的時候,才呼叫recycle(),下面用BitmapDrawable類來包裝下Bitmap物件,程式碼如下:
public class RecycleBitmapDrawable extends BitmapDrawable {
    private int displayResCount = 0;
    private boolean mHasBeenDisplayed;

    public RecycleBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }


    /**
     * @param isDisplay
     */
    public void setIsDisplayed(boolean isDisplay){
        synchronized (this) {
            if(isDisplay){
                mHasBeenDisplayed = true;
                displayResCount ++;
            }else{
                displayResCount --;
            }
        }

        checkState();
    }

    /**
     * 檢查圖片的一些狀態,判斷是否需要呼叫recycle
     */
    private synchronized void checkState() {
        if (displayResCount <= 0 && mHasBeenDisplayed
                && hasValidBitmap()) {
            getBitmap().recycle();
        }
    }


    /**
     * 判斷Bitmap是否為空且是否呼叫過recycle()
     * @return
     */
    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }

}
  • 還需要一個自定義的ImageView,重寫了setImageDrawable()方法,在這個方法中我們先獲取ImageView上面的圖片,然後通知之前顯示在ImageView的Drawable不在顯示了,Drawable會判斷是否需要呼叫recycle(),程式碼如下:
public class RecycleImageView extends ImageView {

    public RecycleImageView(Context context) {  
        super(context);  
    }  

    public RecycleImageView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  

    public RecycleImageView(Context context, AttributeSet attrs, int defStyle) {  
        super(context, attrs, defStyle);  
    }  

    @Override  
    public void setImageDrawable(Drawable drawable) {  
        Drawable previousDrawable = getDrawable();  
        super.setImageDrawable(drawable);  

        //顯示新的drawable  
        notifyDrawable(drawable, true);  

        //回收之前的圖片  
        notifyDrawable(previousDrawable, false);  
    }  

    @Override  
    protected void onDetachedFromWindow() {  
        //當View從視窗脫離的時候,清除drawable  
        setImageDrawable(null);  
        super.onDetachedFromWindow();  
    }  

    /** 
     * 通知該drawable顯示或者隱藏 
     *  
     * @param drawable 
     * @param isDisplayed 
     */  
    public static void notifyDrawable(Drawable drawable, boolean isDisplayed) {  
        if (drawable instanceof RecycleBitmapDrawable) {  
            ((RecycleBitmapDrawable) drawable).setIsDisplayed(isDisplayed);  
        } else if (drawable instanceof LayerDrawable) {  
            LayerDrawable layerDrawable = (LayerDrawable) drawable;  
            for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {  
                notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);  
            }  
        }  
    }  

}
  • 具體的使用方法如下
ImageView imageView = new ImageView(context);  
        imageView.setImageDrawable(new RecycleBitmapDrawable(context.getResource(), bitmap));