1. 程式人生 > >Android xUtils3原始碼解析之圖片模組

Android xUtils3原始碼解析之圖片模組

本文已授權微信公眾號《非著名程式設計師》原創首發,轉載請務必註明出處。

xUtils3原始碼解析系列

初始化

x.Ext.init(this);

public static void init(Application app) {
    TaskControllerImpl.registerInstance();
    if (Ext.app == null) {
        Ext.app = app;
    }
}

public final class TaskControllerImpl implements TaskController {
    public
static void registerInstance() { if (instance == null) { synchronized (TaskController.class) { if (instance == null) { instance = new TaskControllerImpl(); } } } x.Ext.setTaskController(instance); } }

獲取ApplicationContext,例項化TaskControllerImpl物件,並設定為非同步任務的真正管理類。

初始化ImageOptions

        ImageOptions imageOptions = new ImageOptions.Builder()
                .setSize(DensityUtil.dip2px(120), DensityUtil.dip2px(120))
                .setRadius(DensityUtil.dip2px(5))
                // 如果ImageView的大小不是定義為wrap_content, 不要crop.
                .setCrop
(true) // 很多時候設定了合適的scaleType也不需要它. // 載入中或錯誤圖片的ScaleType //.setPlaceholderScaleType(ImageView.ScaleType.MATRIX) .setImageScaleType(ImageView.ScaleType.CENTER_CROP) .setLoadingDrawableId(R.mipmap.ic_launcher) .setFailureDrawableId(R.mipmap.ic_launcher) .build();

上段程式碼來自xUtils3 sample。運用建造者(builder)模式例項化了一些初始引數,例如:圖片大小、縮放模式、佔位圖、失敗圖等等。最後使用build()方法返回了一個ImageOptions物件。程式碼比較長,而且幾乎都是get/set所以就不貼了。載入圖片的所有設定都在這裡,感興趣的同學還請自行檢視。

繫結圖片的幾種方式見下列程式碼,當然,裡面幾種CallBack是可以依據需求自己設定的:

x.image().bind(imageView, url, imageOptions);

// assets file
x.image().bind(imageView, "assets://test.gif", imageOptions);

// local file
x.image().bind(imageView, new File("/sdcard/test.gif").toURI().toString(), imageOptions);
x.image().bind(imageView, "/sdcard/test.gif", imageOptions);
x.image().bind(imageView, "file:///sdcard/test.gif", imageOptions);
x.image().bind(imageView, "file:/sdcard/test.gif", imageOptions);

x.image().bind(imageView, url, imageOptions, new Callback.CommonCallback<Drawable>() {...});
x.image().loadDrawable(url, imageOptions, new Callback.CommonCallback<Drawable>() {...});
x.image().loadFile(url, imageOptions, new Callback.CommonCallback<File>() {...});

沒錯,上面程式碼片段依舊來自xUtils3 README。下文以sample中的方式進行分析。

x.image().bind(holder.imgItem,
                    imgSrcList.get(position),
                    imageOptions,
                    new CustomBitmapLoadCallBack(holder));

首次載入圖片流程分析

x.image()

public final class x {
    public static ImageManager image() {
        if (Ext.imageManager == null) {
            ImageManagerImpl.registerInstance();
        }
        return Ext.imageManager;
    }
}

和初始化的套路一樣,例項化ImageManagerImpl物件,並設定為圖片載入的管理器。之後呼叫ImageManagerImpl.bind()方法,跟進。

ImageManagerImpl.bind()

public final class ImageManagerImpl implements ImageManager {
    public void bind(final ImageView view, final String url, final ImageOptions options, final Callback.CommonCallback<Drawable> callback) {
        x.task().autoPost(new Runnable() {
            @Override
            public void run() {
                ImageLoader.doBind(view, url, options, callback);
            }
        });
    }
}

bind()方法內部呼叫了x.task().autoPost()。x.task()返回的是TaskController物件,實際上在初始化的時候TaskController被例項化的是TaskControllerImpl,向上轉型的一個過程。所以實際上呼叫的還是TaskControllerImpl.aotoPost()。

TaskControllerImpl.aotoPost()

public final class TaskControllerImpl implements TaskController {
    public void autoPost(Runnable runnable) {
        if (runnable == null) return;
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            runnable.run();
        } else {
            TaskProxy.sHandler.post(runnable);
        }
    }
}

autoPost()方法中首先判斷是否是主執行緒,如果是,直接執行runnable.run()。如果不是,那麼通過獲取了MainLooper的Handler(sHandler)post到主執行緒中執行。所以不論x.image().bind()在主執行緒還是子執行緒呼叫,其內部呼叫的ImageLoader.doBind(view, url, options, callback)總是會在主執行緒中執行。

ImageLoader.doBind(view, url, options, callback)

為了方便閱讀原始碼,我們以xml設定ImageView的寬高進行閱讀。先閱讀第一次從網路載入流程,之後分析快取載入流程。

/*package*/ final class ImageLoader implements
        Callback.PrepareCallback<File, Drawable>,
        Callback.CacheCallback<Drawable>,
        Callback.ProgressCallback<Drawable>,
        Callback.TypedCallback<Drawable>,
        Callback.Cancelable {
    /*package*/
    static Cancelable doBind(final ImageView view,
                             final String url,
                             final ImageOptions options,
                             final Callback.CommonCallback<Drawable> callback) {

        // check params
        ImageOptions localOptions = options;
        {
            ...
            localOptions.optimizeMaxSize(view);
        }
        if (memDrawable != null) { // has mem cache
            ...
        } else {
            // load from Network or DiskCache
            return new ImageLoader().doLoad(view, url, localOptions, callback);
        }
        return null;
    }
}

ImageLoader實現了五種CallBack(真特麼多哇),這也就意味著等會請求的中間會回撥ImageLoader的各種方法。首先是效驗各種引數,因為是在xml設定了ImageView具體寬高。所以在localOptions.optimizeMaxSize(view)中會根據ImageView的寬高設定ImageOptions中的width、maxWidth屬性的值等於ImageView的寬高。之後會從記憶體快取中查詢圖片,由於是第一次載入所以不會執行裡面的程式碼,之後會再次回到這裡檢視從快取中查詢圖片相關邏輯。

ImageLoader.doLoad(view, url, options, callback)

/*package*/ final class ImageLoader implements ...{
    private Cancelable doLoad(ImageView view,
                              String url,
                              ImageOptions options,
                              Callback.CommonCallback<Drawable> callback) {

        this.viewRef = new WeakReference<ImageView>(view);
        this.options = options;
        this.key = new MemCacheKey(url, options);
        this.callback = callback;
        if (callback instanceof Callback.ProgressCallback) {
            this.progressCallback = (Callback.ProgressCallback<Drawable>) callback;
        }
        ...

        // set loadingDrawable
        Drawable loadingDrawable = null;
        if (options.isForceLoadingDrawable()) {
            loadingDrawable = options.getLoadingDrawable(view);
            view.setScaleType(options.getPlaceholderScaleType());
            view.setImageDrawable(new AsyncDrawable(this, loadingDrawable));
        } else {
            loadingDrawable = view.getDrawable();
            view.setImageDrawable(new AsyncDrawable(this, loadingDrawable));
        }

        // request
        RequestParams params = createRequestParams(url, options);
        if (view instanceof FakeImageView) {
            synchronized (FAKE_IMG_MAP) {
                FAKE_IMG_MAP.put(url, (FakeImageView) view);
            }
        }
        return cancelable = x.http().get(params, this);
    }
}

為了方便在後面請求的時候獲取各種引數的引用,所以首先是各種賦值。之後設定等待載入的佔位圖(LoadingDrawable)。options的forceLoadingDrawable預設為true,placeholderScaleType屬性預設為ImageView.ScaleType.CENTER_INSIDE。最後建立一個RequestParams物件之後開始載入圖片的網路請求。這裡先看下建立請求的過程。

網路請求

請求引數的建立

    private static RequestParams createRequestParams(String url, ImageOptions options) {
        RequestParams params = new RequestParams(url);
        // 設定快取目錄
        params.setCacheDirName(DISK_CACHE_DIR_NAME);
        // 設定超時時間
        params.setConnectTimeout(1000 * 8);
        // 設定優先順序(最低)
        params.setPriority(Priority.BG_LOW);
        // 指定載入圖片的執行緒池
        params.setExecutor(EXECUTOR);
        // 設定立即取消
        params.setCancelFast(true);
        params.setUseCookie(false);
        if (options != null) {
            ImageOptions.ParamsBuilder paramsBuilder = options.getParamsBuilder();
            if (paramsBuilder != null) {
                params = paramsBuilder.buildParams(params, options);
            }
        }
        return params;
    }

請求引數的構造過程註釋比較清晰了。這裡需要注意的是執行緒池Executor EXECUTOR = new PriorityExecutor(10, false),核心執行緒數為10,FILO(first in last out)型別。假設在RecyclerView滑動後加載圖片,首先要載入的肯定是正在展示給使用者的圖片,即最後例項化的runnable,所以這裡是FILO型別。options在這裡的作用是看有沒有自定義的ImageOptions.ParamsBuilder,通過實現ImageOptions.ParamsBuilder介面,可以自己構建請求引數。預設是沒有的,所以這裡不用管。之後就進入了網路載入請求的過程。

網路載入圖片

x.http().get(params, this)
這裡的網路請求流程和 xUtils3原始碼解析之網路模組中差不多,這裡主要講兩者的區別,建議先去看下上篇博文的分析。

由於ImageLoader實現了五種CallBack所以相應的回撥例項會很多。在構造請求引數的過程中指定了EXECUTOR,所以不再使用預設的HTTP_EXEUTOR。在TaskProxy中首先會呼叫progressCallback.onStarted()(主執行緒),接著呼叫HttpTask.doBackground()。在HttpTask.doBackground()中呼叫resolveLoadType(),由於泛型是Drawable,所以loadType為File.class。即相對於普通網路請求例項化的是HttpRequest,但是例項化的Loader為FileLoader。如果這個過程不明白,強烈建議閱讀 xUtils3原始碼解析之網路模組之後再回來看這篇。

FileLoader.load()
與StringLoader不同的是,FileLoader中加入了很多建立檔案、讀寫檔案相關的程式碼。如果只是簡單的首次載入而且不考慮快取的話,FileLoader中從網路中下載圖片,期間呼叫progressHandler.updateProgress(total, current, true)更新進度,最後轉換成Drawable,在ImageLoader.prepare()中壓縮圖片,並在ImageLoader.onSuccess(),將壓縮後的Drawable資源設定給ImageView。

/*package*/ final class ImageLoader implements ... {
    public void onSuccess(Drawable result) {
        if (!validView4Callback(!hasCache)) return;

        if (result != null) {
            setSuccessDrawable4Callback(result);
            if (callback != null) {
                callback.onSuccess(result);
            }
        }
    }

    private void setSuccessDrawable4Callback(final Drawable drawable) {
        final ImageView view = viewRef.get();
        if (view != null) {
            view.setScaleType(options.getImageScaleType());
            if (drawable instanceof GifDrawable) {
                if (view.getScaleType() == ImageView.ScaleType.CENTER) {
                    view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
                }
                view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            }
            if (options.getAnimation() != null) {
                ImageAnimationHelper.animationDisplay(view, drawable, options.getAnimation());
            } else if (options.isFadeIn()) {
                ImageAnimationHelper.fadeInDisplay(view, drawable);
            } else {
                view.setImageDrawable(drawable);
            }
        }
    }
}

這當然不是關注的重點!!!

重點在兩個地方:

  1. 圖片的壓縮
  2. 圖片的快取

圖片的壓縮

無論是從網路中載入圖片還是從磁碟快取中載入圖片(記憶體快取中的圖片已經被壓縮過,所以無需再次壓縮),在正式載入圖片之前。在HttpTask.doBackground()中會呼叫prepareCallback.prepare(rawResult),實際上呼叫的是ImageLoader.prepare()。

    public Drawable prepare(File rawData) {
        if (!validView4Callback(true)) return null;

        try {
            Drawable result = null;
            if (prepareCallback != null) {
                result = prepareCallback.prepare(rawData);
            }
            if (result == null) {
                result = ImageDecoder.decodeFileWithLock(rawData, options, this);
            }
            if (result != null) {
                if (result instanceof ReusableDrawable) {
                    ((ReusableDrawable) result).setMemCacheKey(key);
                    MEM_CACHE.put(key, result);
                }
            }
            return result;
        } catch (IOException ex) {
            IOUtil.deleteFileOrDir(rawData);
            LogUtil.w(ex.getMessage(), ex);
        }
        return null;
    }

這個方法的主要作用有兩個:

  1. 壓縮圖片
  2. 將壓縮後的圖片存入記憶體快取中

這裡我們先看下壓縮圖片相關的程式碼,圖片快取相關在後文會講。

ImageDecoder.decodeFileWithLock()

public final class ImageDecoder {
    static {
        int cpuCount = Runtime.getRuntime().availableProcessors();
        BITMAP_DECODE_MAX_WORKER = cpuCount > 4 ? 2 : 1;
    }

    static Drawable decodeFileWithLock(final File file,
                                       final ImageOptions options,
                                       final Callback.Cancelable cancelable) throws IOException {
        ...
        Drawable result = null;
        if (!options.isIgnoreGif() && isGif(file)) {
            ...
        } else {
            Bitmap bitmap = null;
            { // decode with lock
                try {
                    synchronized (bitmapDecodeLock) {
                    ...
                    if (bitmap == null) {
                        bitmap = decodeBitmap(file, options, cancelable);
                        if (bitmap != null && options.isCompress()) {
                            final Bitmap finalBitmap = bitmap;
                            THUMB_CACHE_EXECUTOR.execute(new Runnable() {
                                @Override
                                public void run() {
                                    saveThumbCache(file, options, finalBitmap);
                                }
                            });
                        }
                        ...
                    }
                } finally {
                    ...
                }
            }
            if (bitmap != null) {
                result = new ReusableBitmapDrawable(x.app().getResources(), bitmap);
            }
        }
        return result;
    }

}

首先通過decodeBitmap()方法解析下載的圖片檔案,接著將解析出來的bitmap包裝成ReusableBitmapDrawable物件返回。之後還會儲存縮圖,這個過程跟下文儲存磁碟快取相同,這裡先不分析這些。感興趣的同學還請自行檢視。跟進decodeBitmap()。

    public static Bitmap decodeBitmap(File file, ImageOptions options, Callback.Cancelable cancelable) throws IOException {
        // check params
        ...
        Bitmap result = null;
        try {
            final BitmapFactory.Options bitmapOps = new BitmapFactory.Options();
            bitmapOps.inJustDecodeBounds = true;
            bitmapOps.inPurgeable = true;
            bitmapOps.inInputShareable = true;
            BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOps);
            bitmapOps.inJustDecodeBounds = false;
            bitmapOps.inPreferredConfig = options.getConfig();
            int rotateAngle = 0;
            int rawWidth = bitmapOps.outWidth;
            int rawHeight = bitmapOps.outHeight;
            int optionWith = options.getWidth();
            int optionHeight = options.getHeight();
            ...
            bitmapOps.inSampleSize = calculateSampleSize(
                    rawWidth, rawHeight,
                    options.getMaxWidth(), options.getMaxHeight());
            // decode file
            Bitmap bitmap = null;

            if (bitmap == null) {
                bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOps);
            }
            // 旋轉、縮放、圓角等效果的處理
            ...
            result = bitmap;
        } catch (IOException ex) {
            throw ex;
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
            result = null;
        }

        return result;
    }

首先將inJustDecodeBounds屬性設定為true,這樣在解析圖片檔案的時候,只是獲取了圖片的寬高等引數,並不會真正的將圖片載入進來。之後將inJustDecodeBounds屬性設定為false,下次解析的時候,將會真正的將檔案載入到記憶體中。之後獲取圖片的寬高和ImageView的寬高,calculateSampleSize()方法根據這四個引數獲取inSampleSize屬性的值。inSampleSize代表壓縮比例。例如,inSampleSize = 1,那麼圖片寬高都不會被壓縮,inSampleSize = 2,那麼圖片的寬高都會被壓縮至原來的1/2,即圖片大小變為原來的1/2 * 1/2 = 1/4。

public final class ImageDecoder {
    public static int calculateSampleSize(final int rawWidth, final int rawHeight,
                                          final int maxWidth, final int maxHeight) {
        int sampleSize = 1;

        if (rawWidth > maxWidth || rawHeight > maxHeight) {
            if (rawWidth > rawHeight) {
                sampleSize = Math.round((float) rawHeight / (float) maxHeight);
            } else {
                sampleSize = Math.round((float) rawWidth / (float) maxWidth);
            }

            if (sampleSize < 1) {
                sampleSize = 1;
            }

            final float totalPixels = rawWidth * rawHeight;

            final float maxTotalPixels = maxWidth * maxHeight * 2;

            while (totalPixels / (sampleSize * sampleSize) > maxTotalPixels) {
                sampleSize++;
            }
        }
        return sampleSize;
    }
 }

通過while迴圈計算出恰當的壓縮取樣倍數。寬高都小於ImageView的圖片不需要壓縮,即sampleSize=1。大圖片經過這個壓縮取樣倍數的壓縮正好比ImageView寬高小。什麼叫恰當?恰當就是當sampleSize減一,圖片的寬/高會比ImageView的寬/高大。

圖片的快取

圖片的快取分為磁碟快取和記憶體快取。它們都採用LRU(Least recently used,最近最少使用)演算法。

磁碟快取

在HttpTask.doBackground()中,起初我以為this.request.save2Cache()是磁碟快取的程式碼,跟進後發現,是個空實現。註釋already saved by diskCacheFile#commit。中間作者可能處於某種原因更改了磁碟快取的時機。diskCacheFile#commit()在FileLoader.load()中呼叫。

   if (diskCacheFile != null) {
       targetFile = diskCacheFile.commit();
   }

在首次下載之後,tempSaveFilePath、targetFile和diskCacheFile雖然物件型別不同,但是都指向同一個路徑。

例如:/storage/emulated/0/Android/data/org.xutils.sample/cache/xUtils_img/3c62a6255c4910034613d999a508cf23.tmp。

diskCacheFile是個DiskCacheFile物件,在File類的基礎上增加了DiskCacheEntity屬性,DiskCacheEntity是個ORM的實體類,採用xUtils資料庫註解實現,可以理解為DiskCacheEntity是資料庫中的一張表,其中的每個屬性對應資料表中的一個欄位。root過的裝置可以開啟data/data/package name/database/xUtils_http_cache.db中的disk_cache表檢視具體內容,這裡就不再贅述。 跟進。

DiskCacheFile.commit()

public final class DiskCacheFile extends File implements Closeable {

    public DiskCacheFile commit() throws IOException {
        return getDiskCache().commitDiskCacheFile(this);
    }

    public LruDiskCache getDiskCache() {
        // SD card adnroid/data/package/xutil_img
        String dirName = this.getParentFile().getName();
        return LruDiskCache.getDiskCache(dirName);
    }
}

xUtils根據不同的dirName例項化不同的LruDiskCache,目前我們用到的只有/storage/emulated/0/Android/data/org.xutils.sample/cache/xUtils_img/對應的LruDiskCache例項。“xUtils_img”資料夾在ImageLoader.createRequestParams()時設定。

LruDiskCache.commitDiskCacheFile()

public final class LruDiskCache {

    private long diskCacheSize = LIMIT_SIZE;
    private static final int LIMIT_COUNT = 5000; // 限制最多5000條資料
    private static final long LIMIT_SIZE = 1024L * 1024L * 100L; // 限制最多100M檔案

    /*package*/ DiskCacheFile commitDiskCacheFile(DiskCacheFile cacheFile) throws IOException {
        ...
        DiskCacheFile result = null;
        DiskCacheEntity cacheEntity = cacheFile.cacheEntity;
        if (cacheFile.getName().endsWith(TEMP_FILE_SUFFIX)) { // is temp file
            ProcessLock processLock = null;
            DiskCacheFile destFile = null;
            try {
                String destPath = cacheEntity.getPath();
                processLock = ProcessLock.tryLock(destPath, true, LOCK_WAIT);
                if (processLock != null && processLock.isValid()) { // lock
                    destFile = new DiskCacheFile(cacheEntity, destPath, processLock);
                    if (cacheFile.renameTo(destFile)) {
                        try {
                            result = destFile;
                            cacheDb.replace(cacheEntity);
                        } catch (DbException ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }

                        trimSize();
                    } else {
                        throw new IOException("rename:" + cacheFile.getAbsolutePath());
                    }
                } else {
                    throw new FileLockedException(destPath);
                }
            } catch (InterruptedException ex) {
                result = cacheFile;
                LogUtil.e(ex.getMessage(), ex);
            } finally {
                ...
            }
        } else {
            result = cacheFile;
        }

        return result;
    }

    private void trimSize() {
        trimExecutor.execute(new Runnable() {
            @Override
            public void run() {
                if (available) {

                    long current = System.currentTimeMillis();
                    if (current - lastTrimTime < TRIM_TIME_SPAN) {
                        return;
                    } else {
                        lastTrimTime = current;
                    }

                    // trim expires
                    deleteExpiry();

                    // trim db
                    try {
                    // 超找DiskCacheEntity資料表中一共多少行
                        int count = (int) cacheDb.selector(DiskCacheEntity.class).count();
                        if (count > LIMIT_COUNT + 10) {
                        // 依據lastAccess和hits排序,查詢前count - LIMIT_COUNT條資料
                            List<DiskCacheEntity> rmList = cacheDb.selector(DiskCacheEntity.class)
                                    .orderBy("lastAccess").orderBy("hits")
                                    .limit(count - LIMIT_COUNT).offset(0).findAll();
                            if (rmList != null && rmList.size() > 0) {
                                // delete cache files
                                for (DiskCacheEntity entity : rmList) {
                                    try {
                                        // delete db entity
                                        cacheDb.delete(entity);
                                        // delete cache files
                                        String path = entity.getPath();
                                        if (!TextUtils.isEmpty(path)) {
                                            deleteFileWithLock(path);
                                            deleteFileWithLock(path + TEMP_FILE_SUFFIX);
                                        }
                                    } catch (DbException ex) {
                                        LogUtil.e(ex.getMessage(), ex);
                                    }
                                }

                            }
                        }
                    } catch (DbException ex) {
                        LogUtil.e(ex.getMessage(), ex);
                    }

                    // trim disk
                    try {
                        while (FileUtil.getFileOrDirSize(cacheDir) > diskCacheSize) {
                            List<DiskCacheEntity> rmList = cacheDb.selector(DiskCacheEntity.class)
                                    .orderBy("lastAccess").orderBy("hits").limit(10).offset(0).findAll();
                            if (rmList != null && rmList.size() > 0) {
                                // delete cache files
                                for (DiskCacheEntity entity : rmList) {
                                    try {
                                        // delete db entity
                                        cacheDb.delete(entity);
                                        // delete cache files
                                        String path = entity.getPath();
                                        if (!TextUtils.isEmpty(path)) {
                                            deleteFileWithLock(path);
                                            deleteFileWithLock(path + TEMP_FILE_SUFFIX);
                                        }
                                    } catch (DbException ex) {
                                        LogUtil.e(ex.getMessage(), ex);
                                    }
                                }
                            }
                        }
                    } catch (DbException ex) {
                        LogUtil.e(ex.getMessage(), ex);
                    }
                }
            }
        });
    }

}

港真,我沒弄明白commitDiskCacheFile中將引數cacheFile轉換成destFile儲存的意義在哪裡,它倆除了一個以.tmp結尾一個沒有後綴之外,好像沒什麼區別。更新下資料表中的資訊還是很有必要的,無論下次查詢快取檔案還是刪除的時候查詢檔案,都是通過資料表中的path列來查詢的。每次新增新的檔案之後,都會去呼叫trimSize()方法檢查是否需要重新設定。在trimSize()方法中trimExecutor是個核心執行緒數為1的執行緒池,FIFO型別。available屬性自從LruDiskCache例項化之後就一直為true。這裡有兩個try程式碼塊,對應於上面兩個上限:

  1. 快取資料表不得超過5000(實際按5010判斷)條資料
  2. 快取檔案容量不得超過100M

達到上述任意條件之一,都會執行相應的try程式碼塊。其實這兩個try程式碼塊就是LRU演算法的具體實現。這裡涉及到一些xUtils3資料庫API的一些用法,被我在註釋說明了。第一個try程式碼塊的作用:DiskCacheEntity資料表中超過5000行,刪除按照LRU排序出來的資料及對應的快取檔案。第二個try程式碼塊和第一個邏輯相同,只是查詢條件不一樣。這裡提點小小的瑕疵,兩個try程式碼塊查找出來之後,執行的操作都是一樣的,完全可以抽成一個方法。看來大神也喜歡CV,哈哈~

記憶體快取

圖片的壓縮中提到:無論是從網路中載入圖片還是從磁碟快取中載入圖片(記憶體快取中的圖片已經被壓縮過,所以無需再次壓縮),在正式載入圖片之前。在HttpTask.doBackground()中會呼叫prepareCallback.prepare(rawResult),實際上呼叫的是ImageLoader.prepare()。記憶體的快取就是在這個prepare()中。跟進。

/*package*/ final class ImageLoader ...{

    private final static int MEM_CACHE_MIN_SIZE = 1024 * 1024 * 4; // 4M

    static {
        int memClass = ((ActivityManager) x.app()
                .getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();

        // Use 1/8th of the available memory for this memory cache.
        int cacheSize = 1024 * 1024 * memClass / 8;
        if (cacheSize < MEM_CACHE_MIN_SIZE) {
            cacheSize = MEM_CACHE_MIN_SIZE;
        }
        MEM_CACHE.resize(cacheSize);
    }

    private final static LruCache<MemCacheKey, Drawable> MEM_CACHE =
            new LruCache<MemCacheKey, Drawable>(MEM_CACHE_MIN_SIZE) {
                private boolean deepClear = false;

                @Override
                protected int sizeOf(MemCacheKey key, Drawable value) {
                    if (value instanceof BitmapDrawable) {
                        Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
                        return bitmap == null ? 0 : bitmap.getByteCount();
                    } else if (value instanceof GifDrawable) {
                        return ((GifDrawable) value).getByteCount();
                    }
                    return super.sizeOf(key, value);
                }

                @Override
                public void trimToSize(int maxSize) {
                    if (maxSize < 0) {
                        deepClear = true;
                    }
                    super.trimToSize(maxSize);
                    deepClear = false;
                }

                @Override
                protected void entryRemoved(boolean evicted, MemCacheKey key, Drawable oldValue, Drawable newValue) {
                    super.entryRemoved(evicted, key, oldValue, newValue);
                    if (evicted && deepClear && oldValue instanceof ReusableDrawable) {
                        ((ReusableDrawable) oldValue).setMemCacheKey(null);
                    }
                }
            };

    @Override
    public Drawable prepare(File rawData) {
        try {
            ...
            if (result == null) {
                result = ImageDecoder.decodeFileWithLock(rawData, options, this);
            }
            if (result != null) {
                if (result instanceof ReusableDrawable) {
                    ((ReusableDrawable) result).setMemCacheKey(key);
                    MEM_CACHE.put(key, result);
                }
            }
            return result;
        } catch (IOException ex) {
            ...
        }
        return null;
    }
}

ImageLoader中定義了一個LruCache物件MEM_CACHE,預設使用1/8可用記憶體,如果1/8記憶體小於4M,記憶體大小則定義成4M。有趣的是這裡的LruCache類是作者從android.util.LruCache類中拷貝出來的。LruCache的使用非常簡單,上面的例項化是個固定的套路。MEM_CACHE.put(key, result)這裡的key是個全域性屬性,在doLoad()方法中呼叫this.key = new MemCacheKey(url, options)例項化。需要注意一個小細節,記憶體快取中的圖片都是經過壓縮過的,而磁碟快取的圖片是原圖。記憶體快取就此完結。

圖片的各種載入途徑順序

這個問題其實有點弱雞,先不看程式碼,按照載入速度,猜想一下也應該是:記憶體快取–>硬碟快取–>網路載入。不過本著嚴謹的精神,還是檢視下相關程式碼。

從網路載入圖片

參見首次載入圖片流程分析

從記憶體快取載入圖片

/*package*/ final class ImageLoader implements ...{
    static Cancelable doBind(...) {
        ...
        // load from Memory Cache
        Drawable memDrawable = null;
        if (localOptions.isUseMemCache()) {
            memDrawable = MEM_CACHE.get(key);
            if (memDrawable instanceof BitmapDrawable) {
                Bitmap bitmap = ((BitmapDrawable) memDrawable).getBitmap();
                if (bitmap == null || bitmap.isRecycled()) {
                    memDrawable = null;
                }
            }
        }
        if (memDrawable != null) { // has mem cache
            boolean trustMemCache = false;
            try {
                // hit mem cache
                view.setScaleType(localOptions.getImageScaleType());
                view.setImageDrawable(memDrawable);
                ...
            } catch (Throwable ex) {
                ...
            } finally {
                ...
            }
        } else {
            // load from Network or DiskCache
            return new ImageLoader().doLoad(view, url, localOptions, callback);
        }
        ...
    }                        
}

首先是查詢記憶體快取的,記憶體快取沒有命中才去從網路或者磁碟快取中查詢。這點在作者的註釋上也能體現出來~

從磁碟快取中載入圖片

在HttpTask.doBackground()中會首先嚐試從磁碟快取中查詢圖片,程式碼如下:

public class HttpTask<ResultType> extends AbsTask<ResultType> implements ProgressHandler {
    protected ResultType doBackground() throws Throwable {
        ...
        // 檢查快取
        Object cacheResult = null;
        if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {
            // 嘗試從快取獲取結果, 併為請求頭加入快取控制引數.
            try {
                clearRawResult();
                LogUtil.d("load cache: " + this.request.getRequestUri());
                // 從磁碟快取中查詢圖片
                rawResult = this.request.loadResultFromCache();
            } catch (Throwable ex) {
                LogUtil.w("load disk cache error", ex);
            }
            if (rawResult != null) {
                if (prepareCallback != null) {
                    try {
                        // 壓縮查詢到的圖片
                        cacheResult = prepareCallback.prepare(rawResult);
                    } catch (Throwable ex) {
                        ...
                    }
                } 

                if (cacheResult != null) {
                    // 同步等待是否信任快取
                    this.update(FLAG_CACHE, cacheResult);
                    synchronized (cacheLock) {
                        while (trustCache == null) {
                            try {
                                cacheLock.wait();
                            } catch (InterruptedException iex) {
                                throw new Callback.CancelledException("cancelled before request");
                            } catch (Throwable ignored) {
                            }
                        }
                    }

                    // 處理完成
                    if (trustCache) {
                        return null;
                    }
                }
            }
        }
        ...
    }
}

從磁碟快取中查詢的過程等下再說,現在假設從磁碟快取中命中了相應的圖片(實際上也是這樣)。之後壓縮圖片,並新增進記憶體快取,壓縮過程在分析記憶體快取的過程中已經分析過了。最後在呼叫this.update(FLAG_CACHE, cacheResult)之後,鎖住了HttpTask類的繼續執行。this.update()會通過sHandler(例項化時傳入MainLooper)呼叫HttpTask.onUpdate(),其中又在呼叫ImageLoader.onCache()之後,繼續執行HttpTask類相關方法(其實是返回了null)。重點看下ImageLoader.onCache()。

ImageLoader.onCache()

/*package*/ final class ImageLoader implements ... {

        @Override
        public boolean onCache(Drawable result) {
            if (!validView4Callback(true)) return false;

            if (result != null) {
                hasCache = true;
                setSuccessDrawable4Callback(result);
                if (cacheCallback != null) {
                    return cacheCallback.onCache(result);
                } else if (callback != null) {
                    callback.onSuccess(result);
                    return true;
                }
                return true;
            }

            return false;
        }

        private void setSuccessDrawable4Callback(final Drawable drawable) {
        final ImageView view = viewRef.get();
        if (view != null) {
            view.setScaleType(options.getImageScaleType());
            if (drawable instanceof GifDrawable) {
                if (view.getScaleType() == ImageView.ScaleType.CENTER) {
                    view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
                }
                view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            }
            if (options.getAnimation() != null) {
                ImageAnimationHelper.animationDisplay(view, drawable, options.getAnimation());
            } else if (options.isFadeIn()) {
                ImageAnimationHelper.fadeInDisplay(view, drawable);
            } else {
                view.setImageDrawable(drawable);
            }
        }
    }
}

真相大白。現在還差從磁碟快取查詢圖片的過程,即rawResult = this.request.loadResultFromCache()的過程。request對應的HttpRequest,跟進。

HttpRequest.loadResultFromCache()

public class HttpRequest extends UriRequest {
    public Object loadResultFromCache() throws Throwable {
        isLoading = true;
        DiskCacheEntity cacheEntity = LruDiskCache.getDiskCache(params.getCacheDirName())
                .setMaxSize(params.getCacheSize())
                .get(this.getCacheKey());

        if (cacheEntity != null) {
            if (HttpMethod.permitsCache(params.getMethod())) {
                Date lastModified = cacheEntity.getLastModify();
                if (lastModified.getTime() > 0) {
                    params.setHeader("If-Modified-Since", toGMTString(lastModified));
                }
                String eTag = cacheEntity.getEtag();
                if (!TextUtils.isEmpty(eTag)) {
                    params.setHeader("If-None-Match", eTag);
                }
            }
            return loader.loadFromCache(cacheEntity);
        } else {
            return null;
        }
    }
}

public class FileLoader extends Loader<File> {
    @Override
    public File loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable {
        return LruDiskCache.getDiskCache(params.getCacheDirName()).getDiskCacheFile(cacheEntity.getKey());
    }
}

根據cacheKey(其實就是url)獲取到對應資料表中的實體類。之後通過實體類中的path查詢對應的圖片檔案。一切真相大白。

總結

xUtils3圖片模組採用二級快取(記憶體LRU+磁碟LRU)+執行緒池(10核心+FILO)實現。記憶體佔1/8可用記憶體,最少佔用4M。磁碟快取最多快取5000條或者100M資料。記憶體中的圖片已經被壓縮過,磁碟中儲存原圖。圖片載入優先順序:記憶體–>磁碟–>網路。