Android中圖片加載和顯示問題的探究

本文通過對Android SDK中DisplayBitmap Case的探究,來理解在Android中如何實現圖片的異步加載、緩存機制等。下面進行具體的分析:

1 工程結構

工程結構
主要包含一個通用的日誌包以及與圖片顯示相關的包。

2 具體的結構圖

靜態結構圖

3 類的具體分析

3.1 ui包

3.1.1 ImageGridActivity.java 類

該類提供了應用加載的主界面。該Activity持有一個Fragment,源碼如下:

protected void onCreate(Bundle savedInstanceState) {
    if (BuildConfig.DEBUG) {
        Utils.enableStrictMode();
    }
    super.onCreate(savedInstanceState);
     //TAG是給Fragment定義的標籤
    if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
        final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.add(android.R.id.content, new ImageGridFragment(), TAG);
        ft.commit();
    }
}

該類很好理解。下面介紹ImageGridFragment.java類。

3.1.2 ImageGridFragment.java 類

首先看在onCreate()方法中幹了什麼?

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //設置選項菜單
    setHasOptionsMenu(true);

    mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size);
    mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing);
    //創建ImageAdapter,用來適配GridView。可以通過getActivity()方法來獲得Fragment依附的Activity(上下文環境)
    mAdapter = new ImageAdapter(getActivity());
    //設置圖片緩存目錄及縮放比
    ImageCache.ImageCacheParams cacheParams =
            new ImageCache.ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
    //設置內存緩存大小,佔應用緩存的25%
    cacheParams.setMemCacheSizePercent(0.25f); 

    // 創建ImageFetcher對象,該對象只專注於實現異步加載圖片
    mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize);
    //設置默認加載圖片
    mImageFetcher.setLoadingImage(R.drawable.empty_photo);
    //設置加載緩存
    mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);
}

創建的ImageAdapter,用來在UI中顯示圖片,具體實現如下:

private class ImageAdapter extends BaseAdapter {

    private final Context mContext;
    private int mItemHeight = 0;//項的高度
    private int mNumColumns = 0;//列數
    private int mActionBarHeight = 0;//動作條(實現導航的)高度
    private GridView.LayoutParams mImageViewLayoutParams;//GridView的佈局參數對象
    //Adapter構造器
    public ImageAdapter(Context context) {
        super();
        mContext = context;
        mImageViewLayoutParams = new GridView.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        // 計算ActionBar的高度
        //TypedValue是動態類型數據值的一個容器,主要用在持有value的Resource對象上
        TypedValue tv = new TypedValue();
        if (context.getTheme().resolveAttribute(
                android.R.attr.actionBarSize, tv, true)) {
            mActionBarHeight = TypedValue.complexToDimensionPixelSize(
                    tv.data, context.getResources().getDisplayMetrics());
        }
    }
    //重載的getCount()方法
    @Override
    public int getCount() {
        // 如果列數沒有確定,就返回 0 .
        if (getNumColumns() == 0) {
            return 0;
        }
        // 數據大小加上頂部的空行,就得到要顯示的總數
        return Images.imageThumbUrls.length + mNumColumns;
    }
    //得到position位置的具體項
    @Override
    public Object getItem(int position) {
        return position < mNumColumns ?
                null : Images.imageThumbUrls[position - mNumColumns];
    }
    @Override
    public long getItemId(int position) {
        return position < mNumColumns ? 0 : position - mNumColumns;
    }
    //返回顯示的View的類型,這兒主要有兩種:一種是顯示圖片的ImageView,另一種是頂部空行的顯示view,故返回2
    @Override
    public int getViewTypeCount() {
        // Two types of views, the normal ImageView and the top row of empty views
        return 2;
    }
    @Override
    public int getItemViewType(int position) {
        return (position < mNumColumns) ? 1 : 0;
    }
    @Override
    public boolean hasStableIds() {
        return true;
    }
    //重載的getView()方法
    @Override
    public View getView(int position, View convertView, ViewGroup container) {
        // 首先檢查是不是頂行
        if (position < mNumColumns) {
            if (convertView == null) {
                convertView = new View(mContext);
            }
            // 設置ActionBar空View的高度
            convertView.setLayoutParams(new AbsListView.LayoutParams(
                    LayoutParams.MATCH_PARENT, mActionBarHeight));
            return convertView;
        }
        // 下面處理主要的ImageView的顯示
        ImageView imageView;
        if (convertView == null) { // 如果沒有被回收,就實例化和初始化
            imageView = new RecyclingImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            imageView.setLayoutParams(mImageViewLayoutParams);
        } else { // 否者重用convertView
            imageView = (ImageView) convertView;
        }
        // 檢驗高度是否和計算的列寬匹配
        if (imageView.getLayoutParams().height != mItemHeight) {
            imageView.setLayoutParams(mImageViewLayoutParams);
        }
        // 異步加載圖片  
        mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView);
        return imageView;
    }

最終使用下面這行代碼完成圖片的異步加載,由於加載圖片是耗時操作,所以一定不能在UI線程中加載圖片。

mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView);

接着創建了一個緩存參數對象,並設置了相應的屬性,包括緩存目錄和緩存大小。然後創建了ImageFetcher對象,主要用來關注於異步加載圖片。
接下來分析onCreateView()方法:

public View onCreateView(
        LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    //加載佈局view
    final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
    //找到GridView對象
    final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
    //設置適配器
    mGridView.setAdapter(mAdapter);
    //設置項點擊事件
    mGridView.setOnItemClickListener(this);
    //設置滑動監聽事件
    mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView absListView, int scrollState) {
            // 當滑動的時候暫停加載,以使滑動更流暢
            if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
                 if (!Utils.hasHoneycomb()) {
                    mImageFetcher.setPauseWork(true);
                }
            } else {
                mImageFetcher.setPauseWork(false);
            }
        }

上面完成了加載網格佈局對象,註冊適配器,並設置了監聽器。
下面看看其他幾個生命週期中的任務:

public void onResume() {
    super.onResume();
    mImageFetcher.setExitTasksEarly(false);
    mAdapter.notifyDataSetChanged();
}
public void onPause() {
    super.onPause();
    mImageFetcher.setPauseWork(false);
    mImageFetcher.setExitTasksEarly(true);
    mImageFetcher.flushCache();
}
public void onDestroy() {
    super.onDestroy();
    mImageFetcher.closeCache();
}

上面處理的主要是伴隨生命週期有關的資源的暫停和釋放。

3.2 util包

3.2.1 分析 AsyncTask.java類

AsyncTask類是對https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java的一個修改類。
首先它持有一個ThreadFactory類的引用,具體實現:

private static final ThreadFactory  sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);
    //實際就是開闢了一個新的線程
    public Thread newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

通過該工廠對象的工廠方法newThread(Runnable r)來創建線程,其返回一個線程對象。線程Thread類的構造方法的第二個參數代表線程名字。
下面是一個Runnable類型的隊列, 並且限制了大小爲10。

private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(10);

下面是一個Executor對象的引用,用來執行具體的任務

public static final Executor THREAD_POOL_EXECUTOR
        = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
        TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
        new ThreadPoolExecutor.DiscardOldestPolicy());

可以看出它的構造方法含有7個參數。它們分別是核心池的大小,池中線程的最大數量,池中線程保持活躍狀態的數量,時間單元以秒計 ,活躍線程隊列,線程工廠對象,以及一個策略對象。它們中的一些在一開始就被初始化了。如下:

private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;

接下來就是其他用途的Excutor對象,以及一些對象和狀態變量的初始化,其中包括一個Handler,如下:

public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
        Executors.newSingleThreadExecutor(sThreadFactory);

public static final Executor DUAL_THREAD_EXECUTOR =
        Executors.newFixedThreadPool(2, sThreadFactory);

private static final int MESSAGE_POST_RESULT = 0x1;
private static final int MESSAGE_POST_PROGRESS = 0x2;

private static final InternalHandler sHandler = new InternalHandler();

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;

private volatile Status mStatus = Status.PENDING;

private final AtomicBoolean mCancelled = new AtomicBoolean();
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

該類中還包括了一個SerialExecutor子類,可以從上面的代碼中看到針對版本問題的。一個內部InternalHandler類,其繼承自Handler類,來看看它都做了什麼:

private static class InternalHandler extends Handler {
    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult result = (AsyncTaskResult) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

InternalHandler類的實例化工作,在前面的代碼中已經看到。下面看看是何處發送了激活Handler的消息的呢?有兩處:
其一

protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

其二

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

一個是publishProgress()一個是postResult(),兩個方法的功能顯而易見,都構建了一個Message消息對象,並調用了sendToTarget(),發送出去,激活handler及其他操作。
從這兒可以看出,實際上可以根據需求來定義自己的AsyncTask類(不是指繼承自系統的AsyncTask,而是自己來重新構造一個這樣的類)。同時如果要在異步線程中執行長時間的操作,上面的類是不滿足要求的,這是就需要自己定義類來實現,可以參考java.util.concurrent包中的一些API,比如這些類:

java.util.concurrent.Executor
java.util.concurrent.ThreadPoolExecutor 
java.util.concurrent.FutureTask

其實這些類在上面也用到。
總結起來AsyncTask實際上就是結合線程池技術,來完成異步任務,並封裝了Handler,使得感覺好像跨越了異步線程,而直接可以修改UI界面。其實不能在子線程中修改UI界面是始終保持的,這兒只不過將這部分工作封裝了起來。

3.2.2 DiskLruCache.java類

明白該類首先要明白LRU是什麼。LRU(Leasted Recently Used ) “最近最少使用”的意思。而LRU緩存也就使用了這樣一種思想,LRU緩存把最近最少使用的數據移除,讓給最新讀取的數據。而往往最常讀取的,也就是使用次數最多的。所以利用LRU緩存可以提高系統的性能。要實現LRU,就要用到一個LinkedHashMap。LinkedHashMap有什麼特性呢?具體的可以參考JDK來了解。這兒簡要的說明一下,該類繼承自HashMap,由Map提供的集合通常是雜亂無章的,而LinkedHashMap與HashMap不同的是,它維護了一個雙重鏈接表。此鏈接表維護了迭代順序。通常該迭代順序是插入順序。然而其也提供了特殊的構造方法來創建鏈接哈希映射,可以按照訪問順序來排序。該構造方法API如下:

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder)

構造一個帶指定初始容量、加載因子和排序模式的空 LinkedHashMap 實例。
參數:
initialCapacity - 初始容量
loadFactor - 加載因子
accessOrder - 排序模式 - 對於訪問順序,爲 true;對於插入順序,則爲 false
拋出:
IllegalArgumentException - 如果初始容量爲負或者加載因子爲非正
按照訪問順序來排序不正是LRU想要的結果嗎!這種映射很適合構建 LRU 緩存。下面來詳細看一下該類的具體實現:

static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TMP = "journal.tmp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final int IO_BUFFER_SIZE = 8 * 1024;

上面是定義的一些常量,比如備忘文件名、版本、字符集,還有輸入輸出流的緩存大小8k。

private final File directory;
private final File journalFile;
private final File journalFileTmp;
private final int appVersion;
private final long maxSize;
private final int valueCount;
private long size = 0;
private Writer journalWriter;
private final LinkedHashMap<String, Entry> lruEntries
        = new LinkedHashMap<String, Entry>(0, 0.75f, true);
private int redundantOpCount;

定義了一些File對象,當然還有最重要的LinkedHashMap對象,lruEntries實例。注意構造器的第三個參數是true,說明是訪問順序。
從Reader中讀取,並以字符串形式返回。

public static String readFully(Reader reader) throws IOException {
    try {
        StringWriter writer = new StringWriter();
        char[] buffer = new char[1024];
        int count;
        while ((count = reader.read(buffer)) != -1) {
            writer.write(buffer, 0, count);
        }
        return writer.toString();
    } finally {
        reader.close();
    }
}

從InputStream輸入流中讀取ASCII行數據(但不包括”\r\n”或”\n”),以字符串形式返回:

public static String readAsciiLine(InputStream in) throws IOException {
    StringBuilder result = new StringBuilder(80);
    while (true) {
        int c = in.read();
        if (c == -1) {
            throw new EOFException();
        } else if (c == '\n') {
            break;
        }
        result.append((char) c);
    }
    int length = result.length();
    if (length > 0 && result.charAt(length - 1) == '\r') {
        result.setLength(length - 1);
    }
    return result.toString();
}

刪除目錄中的內容:

public static void deleteContents(File dir) throws IOException {
    File[] files = dir.listFiles();
    if (files == null) {
        throw new IllegalArgumentException("not a directory: " + dir);
    }
    for (File file : files) {
        if (file.isDirectory()) {
            deleteContents(file);
        }
        if (!file.delete()) {
            throw new IOException("failed to delete file: " + file);
        }
    }
}

該緩存使用後臺的一個單線程來驅動實例:

private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
        60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());     

初始化DiskLruCaxhe緩存,注意私有。並不能直接使用構造器來實例化該類:

private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
    this.directory = directory;
    this.appVersion = appVersion;
    this.journalFile = new File(directory, JOURNAL_FILE);
    this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
    this.valueCount = valueCount;
    this.maxSize = maxSize;
}

打開緩存,如果不存在就創建:

  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }
        //DiskLruCache緩存 
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");
                cache.delete();
            }
        }

        // create a new empty cache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }

讀取備忘文件夾:

private void readJournal() throws IOException {
    InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
    try {
        String magic = readAsciiLine(in);
        String version = readAsciiLine(in);
        String appVersionString = readAsciiLine(in);
        String valueCountString = readAsciiLine(in);
        String blank = readAsciiLine(in);
        if (!MAGIC.equals(magic)
                || !VERSION_1.equals(version)
                || !Integer.toString(appVersion).equals(appVersionString)
                || !Integer.toString(valueCount).equals(valueCountString)
                || !"".equals(blank)) {
            throw new IOException("unexpected journal header: ["
                    + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
        }

        while (true) {
            try {
                readJournalLine(readAsciiLine(in));
            } catch (EOFException endOfJournal) {
                break;
            }
        }
    } finally {
        closeQuietly(in);
    }
}

讀取備忘行:

private void readJournalLine(String line) throws IOException {
    String[] parts = line.split(" ");
    if (parts.length < 2) {
        throw new IOException("unexpected journal line: " + line);
    }

    String key = parts[1];
    if (parts[0].equals(REMOVE) && parts.length == 2) {
        lruEntries.remove(key);
        return;
    }

    Entry entry = lruEntries.get(key);
    if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, entry);
    }

    if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
        entry.readable = true;
        entry.currentEditor = null;
        entry.setLengths(copyOfRange(parts, 2, parts.length));
    } else if (parts[0].equals(DIRTY) && parts.length == 2) {
        entry.currentEditor = new Editor(entry);
    } else if (parts[0].equals(READ) && parts.length == 2) {
        // this work was already done by calling lruEntries.get()
    } else {
        throw new IOException("unexpected journal line: " + line);
    }
}

上面兩種方法,與前面的兩種寫的形式相對應。
對備忘目錄進行處理:

private void processJournal() throws IOException {
    deleteIfExists(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
        Entry entry = i.next();
        if (entry.currentEditor == null) {
            for (int t = 0; t < valueCount; t++) {
                size += entry.lengths[t];
            }
        } else {
            entry.currentEditor = null;
            for (int t = 0; t < valueCount; t++) {
                deleteIfExists(entry.getCleanFile(t));
                deleteIfExists(entry.getDirtyFile(t));
            }
            i.remove();
        }
    }
}

計算初始大小,垃圾收部分緩存,以及一些髒數據。

private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
        journalWriter.close();
    }

    Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
    writer.write(MAGIC);
    writer.write("\n");
    writer.write(VERSION_1);
    writer.write("\n");
    writer.write(Integer.toString(appVersion));
    writer.write("\n");
    writer.write(Integer.toString(valueCount));
    writer.write("\n");
    writer.write("\n");

    for (Entry entry : lruEntries.values()) {
        if (entry.currentEditor != null) {
            writer.write(DIRTY + ' ' + entry.key + '\n');
        } else {
            writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
        }
    }
    writer.close();
    journalFileTmp.renameTo(journalFile);
    journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}

構建一個新的備忘錄,代替當前存在的備忘文件。

public synchronized Snapshot get(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null) {
        return null;
    }
    if (!entry.readable) {
        return null;
    }
    InputStream[] ins = new InputStream[valueCount];
    try {
        for (int i = 0; i < valueCount; i++) {
            ins[i] = new FileInputStream(entry.getCleanFile(i));
        }
    } catch (FileNotFoundException e) {
        // 如果手動刪除了,就返回null
        return null;
    }
    redundantOpCount++;
    journalWriter.append(READ + ' ' + key + '\n');
    if (journalRebuildRequired()) {
        executorService.submit(cleanupCallable);
    }
    return new Snapshot(key, entry.sequenceNumber, ins);
}

根據鍵得到Snapshot數據快照對象。

public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null || entry.currentEditor != null) {
        return false;
    }

    for (int i = 0; i < valueCount; i++) {
        File file = entry.getCleanFile(i);
        if (!file.delete()) {
            throw new IOException("failed to delete " + file);
        }
        size -= entry.lengths[i];
        entry.lengths[i] = 0;
    }

    redundantOpCount++;
    journalWriter.append(REMOVE + ' ' + key + '\n');
    lruEntries.remove(key);

    if (journalRebuildRequired()) {
        executorService.submit(cleanupCallable);
    }
    return true;
}

根據鍵移除實例。
下面是一個entries實例的數據快照:

public final class Snapshot implements Closeable {
    private final String key;
    private final long sequenceNumber;
    private final InputStream[] ins;

    private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
        this.key = key;
        this.sequenceNumber = sequenceNumber;
        this.ins = ins;
    }
    public Editor edit() throws IOException {
        return DiskLruCache.this.edit(key, sequenceNumber);
    }
    /**
     * 返回爲緩存的流
     */
    public InputStream getInputStream(int index) {
        return ins[index];
    }
    /**
     * 返回index代表的String值
     */
    public String getString(int index) throws IOException {
        return inputStreamToString(getInputStream(index));
    }
    @Override public void close() {
        for (InputStream in : ins) {
            closeQuietly(in);
        }
    }
}

上面就是該類的一些主要實現。總結:
其實該類中有很多值得學習的地方。比如文件讀取,緩存機制等。LRU緩存機制的具體實現是應該着重關注的。

3.2.3 ImageCache.java類

圖片緩存類,包括內存緩存和Disk緩存,以及對緩存的一些控制。下面看具體實現:

private static final String TAG = "ImageCache";
// 默認的內存緩存大小
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5k

// 默認的disk緩存大小
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
// 緩存圖片到Disk時的壓縮格式
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
private static final int DISK_CACHE_INDEX = 0;
// 常量,用來容易的控制各種緩存的開關
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskLruCache;
private LruCache<String, BitmapDrawable> mMemoryCache;
private ImageCacheParams mCacheParams;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private Set<SoftReference<Bitmap>> mReusableBitmaps;

這兒聲明類一些該類需要使用的狀態變量和引用。注意該類中使用了兩種Lru緩存,一種在Disk磁盤上DiskLruCache類型的mDiskLruCache,一個在內存裏 LruCache類型的mMemoryCache,以及一個若引用對象。 默認的內存緩存大小是5K,默認的Disk緩存是10MB。private final Object mDiskCacheLock = new Object();作爲同步鎖的監視對象。圖片默認的壓縮格式JPEG。
其構造方法如下,同樣它並沒有將構造方法暴露給其他用戶,

private ImageCache(ImageCacheParams cacheParams) {
    init(cacheParams);
}

而是通過getInstance()方法來獲得實例。那是因爲IamgeCache的構造不僅與自身有關,還與Fragment有關。即這樣構造實例是有條件的構造實例,這正是工廠方法的好處之一(不熟悉工廠方法的,可以參考設計模式中的工廠方法)。

public static ImageCache getInstance(
        FragmentManager fragmentManager, ImageCacheParams cacheParams) {
    // 找到或創建以個非UI線程的RetainFragment實例
    final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
    ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
    if (imageCache == null) {
        imageCache = new ImageCache(cacheParams);
        mRetainFragment.setObject(imageCache);
    }
    return imageCache;
}

注意這兒使用的是單例模式,只有當IamgeCache不存在時,纔會創建。
看下面這段初始化代碼:

private void init(ImageCacheParams cacheParams) {
    mCacheParams = cacheParams;
    // 開始內存緩存
    if (mCacheParams.memoryCacheEnabled) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
        }
        if (Utils.hasHoneycomb()) {
            mReusableBitmaps =
                    Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
        }
        mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
             //通知移除緩存實例,不再使用 
            @Override
            protected void entryRemoved(boolean evicted, String key,
                    BitmapDrawable oldValue, BitmapDrawable newValue) {
                if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
                    ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
                } else {
                    if (Utils.hasHoneycomb()) {
                        mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
                    }
                }
            }
            @Override
            protected int sizeOf(String key, BitmapDrawable value) {
                final int bitmapSize = getBitmapSize(value) / 1024;
                return bitmapSize == 0 ? 1 : bitmapSize;
            }
        };
    }

首先檢查內存緩存是否可用。如果可用,在檢查是否在Honeycomb版本以上,如果是則創建一個可重用的set集合。然後在內存中創建一個LRU機制的緩存。由於IamgeCache默認並不初始化一個Disk緩存,因此提供了initDiskCache()方法。

public void initDiskCache() {
    // 開始Disk緩存
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
            File diskCacheDir = mCacheParams.diskCacheDir;
            if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
                if (!diskCacheDir.exists()) {
                    diskCacheDir.mkdirs();
                }
                if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
                    try {
                        mDiskLruCache = DiskLruCache.open(
                                diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "Disk cache initialized");
                        }
                    } catch (final IOException e) {
                        mCacheParams.diskCacheDir = null;
                        Log.e(TAG, "initDiskCache - " + e);
                    }
                }
            }
        }
        mDiskCacheStarting = false;
        mDiskCacheLock.notifyAll();
    }
}

下面這個方法將圖片添加到內存緩存區和磁盤緩存區:

public void addBitmapToCache(String data, BitmapDrawable value) {
    if (data == null || value == null) {
        return;
    }
    // 添加內存緩存
    if (mMemoryCache != null) {
        if (RecyclingBitmapDrawable.class.isInstance(value)) {
            //移除回收實例
            ((RecyclingBitmapDrawable) value).setIsCached(true);
        }
        mMemoryCache.put(data, value);
    }

    synchronized (mDiskCacheLock) {
        // 添加到Disk緩存
        if (mDiskLruCache != null) {
            final String key = hashKeyForDisk(data);
            OutputStream out = null;
            try {
                DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
                if (snapshot == null) {
                    final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                    if (editor != null) {
                        out = editor.newOutputStream(DISK_CACHE_INDEX);
                        value.getBitmap().compress(
                                mCacheParams.compressFormat, mCacheParams.compressQuality, out);
                        editor.commit();
                        out.close();
                    }
                } else {
                    snapshot.getInputStream(DISK_CACHE_INDEX).close();
                }
            } catch (final IOException e) {
                Log.e(TAG, "addBitmapToCache - " + e);
            } catch (Exception e) {
                Log.e(TAG, "addBitmapToCache - " + e);
            } finally {
                try {
                    if (out != null) {
                        out.close();
                    }
                } catch (IOException e) {}
            }
        }
    }
}

與添加相對應的是獲取,如下:
從內存中獲取:

public BitmapDrawable getBitmapFromMemCache(String data) {
    BitmapDrawable memValue = null;
    if (mMemoryCache != null) {
        memValue = mMemoryCache.get(data);
    }
    if (BuildConfig.DEBUG && memValue != null) {
        Log.d(TAG, "Memory cache hit");
    }
    return memValue;
}

從磁盤中獲取:

public Bitmap getBitmapFromDiskCache(String data) {

    final String key = hashKeyForDisk(data);
    Bitmap bitmap = null;

    synchronized (mDiskCacheLock) {
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            InputStream inputStream = null;
            try {
                final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
                if (snapshot != null) {
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "Disk cache hit");
                    }
                    inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
                    if (inputStream != null) {
                        FileDescriptor fd = ((FileInputStream) inputStream).getFD();
                        // 解碼圖片
                        bitmap = ImageResizer.decodeSampledBitmapFromDescriptor(
                                fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
                    }
                }
            } catch (final IOException e) {
                Log.e(TAG, "getBitmapFromDiskCache - " + e);
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                } catch (IOException e) {}
            }
        }
        return bitmap;
    }
}

通過對該demo的學習,應該很好地學習到:
1. 如何去自定義異步任務,從demo中可以學到如何來定製滿足項目需求的AsyncTask的技巧。
2. 緩存機制,包括Lru、使用LinkedHashMap實現Lru機制等
3. 異步加載圖片。有許多的第三方庫具有加載圖片的功能,但在具體項目中,也許只需要這樣一個功能,如果將整個第三方庫都加載進來,這是不和理的。會導致應用佔用的內存增大,影響用戶體驗,也許用戶在查看內存佔用情況時,發現該應用佔用的內存很大,顯然會毫不猶豫的先卸載它。
4. 要注意資源的釋放問題。
5. 文件讀取,流的控制。
完整的demo可以看:http://github.com/Luise-li

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章