1. 程式人生 > >高效顯示Bitmap+listview衝突解決+圖片記憶體快取+硬碟快取

高效顯示Bitmap+listview衝突解決+圖片記憶體快取+硬碟快取

Android高效載入大圖

BitmapFactory提供了一些解碼(decode)的方法(decodeByteArray(), decodeFile(), decodeResource()等),用來從不同
的資源中建立一個Bitmap。 我們應該根據圖片的資料來源來選擇合適的解碼方法。 這些方法在構造點陣圖的時候會嘗試分配內
存,因此會容易導致 OutOfMemory 的異常。每一種解碼方法都可以通過BitmapFactory.Options設定一些附加的標記,以此來
指定解碼選項。設定 i**nJustDecodeBounds 屬性為 true 可以在解碼的時候避免記憶體的分配,它會返回一個 null 的Bitmap,
但是可以獲取到 outWidth, outHeight 與 outMimeType**。該技術可以允許你在構造Bitmap之前優先讀圖片的尺寸與型別。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

為了避免 java.lang.OutOfMemory 的異常,我們需要在真正解析圖片之前檢查它的尺寸(除非你能確定這個資料來源提供了準確
無誤的圖片且不會導致佔用過多的記憶體)。
通過上面的步驟我們已經獲取到了圖片的尺寸,這些資料可以用來幫助我們決定應該載入整個圖片到記憶體中還是載入一個縮小的版本。有下面一些因素需要考慮:

  1. 評估載入完整圖片所需要耗費的記憶體。
  2. 程式在載入這張圖片時可能涉及到的其他記憶體需求。
  3. 呈現這張圖片的控制元件的尺寸大小。
  4. 螢幕大小與當前裝置的螢幕密度。

例如,如果把一個大小為1024x768畫素的圖片顯示到大小為128x96畫素的ImageView上嗎,就沒有必要把整張原圖都載入到記憶體中。
為了告訴解碼器去載入一個縮小版本的圖片到記憶體中,需要在BitmapFactory.Options 中設定 inSampleSize 的值。例如, 一個解析度為2048x1536的圖片,如果設定 inSampleSize 為4,那麼會產出一個大約512x384大小的Bitmap。載入這張縮小的圖片僅僅使用大概0.75MB的記憶體,如果是載入完整尺寸的圖片,那麼大概需要花費12MB(前提都是Bitmap的配置是ARGB_8888)。

下面有一段根據目標圖片大小來計算Sample圖片大小的程式碼示例:

public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
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;
// 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;
}
}
return inSampleSize;
}

Note: 設定inSampleSize為2的冪是因為解碼器最終還是會對非2的冪的數進行向下處理,獲取到最靠近2的冪的數.這是官方給的示例程式碼,我們可以進行如下改造:

public static int calculateInSampleSize(BitmapFactory.Options options,
        int reqWidth, int reqHeight) {
          // 源圖片的高度和寬度
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        // 計算出實際寬高和目標寬高的比率
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        // 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
        // 一定都會大於等於目標的寬和高。
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    return inSampleSize;
}

為了使用該方法,首先需要設定 inJustDecodeBounds 為 true , 把options的值傳遞過來,然後設定 inSampleSize 的值並設定 inJustDecodeBounds 為 false ,之後重新呼叫相關的解碼方法。

public static 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);
    }

使用上面這個方法可以簡單地載入一張任意大小的圖片。如下面的程式碼樣例顯示了一個接近 100x100畫素的縮圖:

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

非UI執行緒處理Bitmap

使用AsyncTask:
AsyncTask 類提供了一個在後臺執行緒執行一些操作的簡單方法,它還可以把後臺的執行結果呈現到UI執行緒中。下面是一個加
載大圖的示例:

class BitmapWorkerTask extends AsyncTask {
        private final WeakReference imageViewReference;
        private int data = 0;

        public BitmapWorkerTask(ImageView imageView) {
            // Use a WeakReference to ensure the ImageView can be garbage
            // collected
            imageViewReference = new WeakReference(imageView);
        }

        // Decode image in background.
        @Override
        protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
        }

        // Once complete, see if ImageView is still around and set bitmap.
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (imageViewReference != null && bitmap != null) {
                final ImageView imageView = imageViewReference.get();
                if (imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    }

為ImageView使用WeakReference確保了AsyncTask所引用的資源可以被垃圾回收器回收。由於當任務結束時不能確保
ImageView仍然存在,因此我們必須在 onPostExecute() 裡面對引用進行檢查。該ImageView在有些情況下可能已經不存在
了,例如,在任務結束之前使用者使用了回退操作,或者是配置發生了改變(如旋轉螢幕等)。
開始非同步載入點陣圖,只需要建立一個新的任務並執行它即可:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

處理併發問題

通常類似ListView與GridView等檢視控制元件在使用上面演示的AsyncTask 方法時,會同時帶來併發的問題。首先為了更高的效
率,ListView與GridView的子Item檢視會在使用者滑動螢幕時被迴圈使用。如果每一個子檢視都觸發一個AsyncTask,那麼就無
法確保關聯的檢視在結束任務時,分配的檢視已經進入迴圈佇列中,給另外一個子檢視進行重用。而且, 無法確保所有的異
步任務的完成順序和他們本身的啟動順序保持一致。
Multithreading for Performance 這篇博文更進一步的討論瞭如何處理併發問題,並且提供了一種解決方法:ImageView儲存
最近使用的AsyncTask的引用,這個引用可以在任務完成的時候再次讀取檢查。使用這種方式, 就可以對前面提到的AsyncTask進行擴充套件。
建立一個專用的Drawable的子類來儲存任務的引用。在這種情況下,我們使用了一個BitmapDrawable,在任務執行的過程
中,一個佔位圖片會顯示在ImageView中:

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask);
        }

        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

在執行BitmapWorkerTask 之前,你需要建立一個AsyncDrawable並且將它繫結到目標控制元件ImageView中:

    public void loadBitmap(int resId, ImageView imageView) {
        if (cancelPotentialWork(resId, imageView)) {
            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            final AsyncDrawable asyncDrawable = new AsyncDrawable(
                    getResources(), mPlaceHolderBitmap, task);
            imageView.setImageDrawable(asyncDrawable);
            task.execute(resId);
        }
    }

在上面的程式碼示例中, cancelPotentialWork 方法檢查是否有另一個正在執行的任務與該ImageView關聯了起來,如果的確是
這樣,它通過執行 cancel() 方法來取消另一個任務。在少數情況下, 新建立的任務資料可能會與已經存在的任務相吻合,這
樣的話就不需要進行下一步動作了。下面是 cancelPotentialWork 方法的實現:

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            if (bitmapData == 0 || bitmapData != data) {
                // Cancel previous task
                bitmapWorkerTask.cancel(true);
            } else {
                // The same work is already in progress
                return false;
            }
        }
        // No task associated with the ImageView, or an existing task was
        // cancelled
        return true;
    }

在上面的程式碼中有一個輔助方法: getBitmapWorkerTask() ,它被用作檢索AsyncTask是否已經被分配到指定的ImageView:

    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
        if (imageView != null) {
            final Drawable drawable = imageView.getDrawable();
            if (drawable instanceof AsyncDrawable) {
                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
                return asyncDrawable.getBitmapWorkerTask();
            }
        }
        return null;
    }

最後一步是在BitmapWorkerTask的 onPostExecute() 方法裡面做更新操作:

    class BitmapWorkerTask extends AsyncTask {
                  ~~~~~~~
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (isCancelled()) {
                bitmap = null;
            }
            if (imageViewReference != null && bitmap != null) {
                final ImageView imageView = imageViewReference.get();
                final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
                if (this == bitmapWorkerTask && imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    }

下面是快取技術:

參考:

總結:

大圖片壓縮處理只要是計算inSampleSize 並重寫decode…()方法
然後就是非UI執行緒下載圖片了,為了解決Listview圖片亂序的問題:
我們有幾種解決方法:(參考http://blog.csdn.net/guolin_blog/article/details/45586553

  • 使用findviewByTag(url) //當然之前在getview中要為Imageview setTag(url)
  • 使用若引用即上面 講解的這種方法
  • 第三方圖片非同步載入庫 如 volley的NetworkImageView 、Android-Universal-Image-Loader 還有FaceBook的Fresco等
    快取就用LruCache和DiskCache即可,當然第三方的類庫有的早已經整合好了。
    下面貼一個從網路下載圖片+LruCache+DiskCache+findviewByTag的listview Adapter的程式碼:
package com.lnu.fang.lru.adapter;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;

import libcore.io.DiskLruCache;
import libcore.io.DiskLruCache.Snapshot;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.lnu.fang.lru.lactivity.R;

/**
 * GridView的介面卡,負責非同步從網路上下載圖片展示在照片牆上。
 */
public class PhotoWallAdapter extends ArrayAdapter<String> {

    /**
     * 記錄所有正在下載或等待下載的任務。
     */
    private Set<BitmapWorkerTask> taskCollection;

    /**
     * 圖片快取技術的核心類,用於快取所有下載好的圖片,在程式記憶體達到設定值時會將最少最近使用的圖片移除掉。
     */
    private LruCache<String, Bitmap> mMemoryCache;

    /**
     * 圖片硬碟快取核心類。
     */
    private DiskLruCache mDiskLruCache;

    /**
     * GridView的例項
     */
    private GridView mPhotoWall;

    /**
     * 記錄每個子項的高度。
     */
    private int mItemHeight = 0;

    public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects,
            GridView photoWall) {
        super(context, textViewResourceId, objects);
        mPhotoWall = photoWall;
        taskCollection = new HashSet<BitmapWorkerTask>();
        // 獲取應用程式最大可用記憶體
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        // 設定圖片快取大小為程式最大可用記憶體的1/8
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount();
            }
        };
        try {
            // 獲取圖片快取路徑
            File cacheDir = getDiskCacheDir(context, "thumb");
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            // 建立DiskLruCache例項,初始化快取資料
            mDiskLruCache = DiskLruCache
                    .open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final String url = getItem(position);
        View view;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);
        } else {
            view = convertView;
        }
        final ImageView imageView = (ImageView) view.findViewById(R.id.photo);

        if (imageView.getLayoutParams().height != mItemHeight) {
            imageView.getLayoutParams().height = mItemHeight;
        }

        // 給ImageView設定一個Tag,保證非同步載入圖片時不會亂序 等會通過findviewByTag尋找
        imageView.setTag(url);
        imageView.setImageResource(R.drawable.empty_photo);
        loadBitmaps(imageView, url);
        return view;
    }

    /**
     * 將一張圖片儲存到LruCache中。
     * 
     * @param key
     *            LruCache的鍵,這裡傳入圖片的URL地址。
     * @param bitmap
     *            LruCache的鍵,這裡傳入從網路上下載的Bitmap物件。
     */
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 從LruCache中獲取一張圖片,如果不存在就返回null。
     * 
     * @param key
     *            LruCache的鍵,這裡傳入圖片的URL地址。
     * @return 對應傳入鍵的Bitmap物件,或者null。
     */
    public Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }

    /**
     * 載入Bitmap物件。此方法會在LruCache中檢查所有螢幕中可見的ImageView的Bitmap物件,
     * 如果發現任何一個ImageView的Bitmap物件不在快取中,就會開啟非同步執行緒去下載圖片。
     */
    public void loadBitmaps(ImageView imageView, String imageUrl) {
        try {
            Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
            if (bitmap == null) {
                BitmapWorkerTask task = new BitmapWorkerTask();
                taskCollection.add(task);
                task.execute(imageUrl);
            } else {
                if (imageView != null && bitmap != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 取消所有正在下載或等待下載的任務。
     */
    public void cancelAllTasks() {
        if (taskCollection != null) {
            for (BitmapWorkerTask task : taskCollection) {
                task.cancel(false);
            }
        }
    }

    /**
     * 根據傳入的uniqueName獲取硬碟快取的路徑地址。
     */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();

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

    /**
     * 獲取當前應用程式的版本號。
     */
    public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(),
                    0);
            return info.versionCode;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    /**
     * 設定item子項的高度。
     */
    public void setItemHeight(int height) {
        if (height == mItemHeight) {
            return;
        }
        mItemHeight = height;
        notifyDataSetChanged();
    }

    /**
     * 使用MD5演算法對傳入的key進行加密並返回。
     */
    public String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.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();
    }

    /**
     * 將快取記錄同步到journal檔案中。
     */
    public void fluchCache() {
        if (mDiskLruCache != null) {
            try {
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 非同步下載圖片的任務。
     * 
     * @author guolin
     */
    class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {

        /**
         * 圖片的URL地址
         */
        private String imageUrl;

        @Override
        protected Bitmap doInBackground(String... params) {
            imageUrl = params[0];
            FileDescriptor fileDescriptor = null;
            FileInputStream fileInputStream = null;
            Snapshot snapShot = null;
            try {
                // 生成圖片URL對應的key
                final String key = hashKeyForDisk(imageUrl);
                // 查詢key對應的快取
                snapShot = mDiskLruCache.get(key);
                if (snapShot == null) {
                    // 如果沒有找到對應的快取,則準備從網路上請求資料,並寫入快取
                    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                    if (editor != null) {
                        OutputStream outputStream = editor.newOutputStream(0);
                        if (downloadUrlToStream(imageUrl, outputStream)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    // 快取被寫入後,再次查詢key對應的快取
                    snapShot = mDiskLruCache.get(key);
                }
                if (snapShot != null) {
                    fileInputStream = (FileInputStream) snapShot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                }
                // 將快取資料解析成Bitmap物件
                Bitmap bitmap = null;
                if (fileDescriptor != null) {
                    bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                }
                if (bitmap != null) {
                    // 將Bitmap物件新增到記憶體快取當中
                    addBitmapToMemoryCache(params[0], bitmap);
                }
                return bitmap;
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fileDescriptor == null && fileInputStream != null) {
                    try {
                        fileInputStream.close();
                    } catch (IOException e) {
                    }
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            // 根據Tag找到相應的ImageView控制元件,將下載好的圖片顯示出來。
            ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            taskCollection.remove(this);
        }

        /**
         * 建立HTTP請求,並獲取Bitmap物件。
         * 
         * @param imageUrl
         *            圖片的URL地址
         * @return 解析後的Bitmap物件
         */
        private 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(), 8 * 1024);
                out = new BufferedOutputStream(outputStream, 8 * 1024);
                int b;
                while ((b = in.read()) != -1) {
                    out.write(b);
                }
                return true;
            } catch (final IOException e) {
                e.printStackTrace();
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                try {
                    if (out != null) {
                        out.close();
                    }
                    if (in != null) {
                        in.close();
                    }
                } catch (final IOException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }

    }



}