Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android-----XUtils框架之BitmapUtils源碼分析

android-----XUtils框架之BitmapUtils源碼分析

編輯:關於Android編程

這一篇我們從源碼的角度分析下BitmapUtils到底是怎麼一個執行流程的;

先來回顧下之前我們使用BitmapUtils的步驟:

很簡單,就只有兩步:

(1)通過BitmapUtils的構造函數創建對象;

(2)調用BitmapUtils對象的display方法;

好了,我們先從創建BitmapUtils對象開始分析,很自然想到了BitmapUtils的構造函數啦:

 

  public BitmapUtils(Context context) {
        this(context, null);
    }

    public BitmapUtils(Context context, String diskCachePath) {
        if (context == null) {
            throw new IllegalArgumentException("context may not be null");
        }

        this.context = context;
        globalConfig = new BitmapGlobalConfig(context, diskCachePath);
        defaultDisplayConfig = new BitmapDisplayConfig();
    }

    public BitmapUtils(Context context, String diskCachePath, int memoryCacheSize) {
        this(context, diskCachePath);
        globalConfig.setMemoryCacheSize(memoryCacheSize);
    }

    public BitmapUtils(Context context, String diskCachePath, int memoryCacheSize, int diskCacheSize) {
        this(context, diskCachePath);
        globalConfig.setMemoryCacheSize(memoryCacheSize);
        globalConfig.setDiskCacheSize(diskCacheSize);
    }

    public BitmapUtils(Context context, String diskCachePath, float memoryCachePercent) {
        this(context, diskCachePath);
        globalConfig.setMemCacheSizePercent(memoryCachePercent);
    }

    public BitmapUtils(Context context, String diskCachePath, float memoryCachePercent, int diskCacheSize) {
        this(context, diskCachePath);
        globalConfig.setMemCacheSizePercent(memoryCachePercent);
        globalConfig.setDiskCacheSize(diskCacheSize);
    }
可以看到他總共有6個構造函數,但他們都會執行第2個構造函數,那我們就來看看第二個構造函數具體做些什麼吧,第11行看到他使用diskCachePath以及上下文創建了一個BitmapGlobalConfig對象,既然用到BitmapGlobalConfig的構造函數,那我們就該看看裡面是什麼樣子的了:

 

 

public BitmapGlobalConfig(Context context, String diskCachePath) {
        if (context == null) throw new IllegalArgumentException("context may not be null");
        this.mContext = context;
        this.diskCachePath = diskCachePath;
        initBitmapCache();
    }
2---4行只是簡單的判斷context參數以及將參數賦值給BitmapGlobalConfig屬性操作,最關鍵的部分就是initBitmapCache方法啦,來看看具體實現:

 

private void initBitmapCache() {
        new BitmapCacheManagementTask().execute(BitmapCacheManagementTask.MESSAGE_INIT_MEMORY_CACHE);
        new BitmapCacheManagementTask().execute(BitmapCacheManagementTask.MESSAGE_INIT_DISK_CACHE);
    }
可以看到在initBitmapCache()中用到了BitmapCacheManagementTask,他是BitmapGlobalConfig的內部私有類,繼承自PriorityAsyncTask類,第2行調用了BitmapCacheManagementTask的execute方法,並且傳入的參數是BitmapCacheManagementTask.MESSAGE_INIT_MEMORY_CACHE,也就是傳入的參數是0,這個execute方法在BitmapCacheManagementTask中並不存在,所以需要到他的父類PriorityAsyncTask中查找,可以看到有如下方法:

 

 

 public final PriorityAsyncTask execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }
很顯然他調用的是PriorityAsyncTask裡面的executeOnExecutor方法,executeOnExecutor方法有兩個重載實現:

 

   public final PriorityAsyncTask executeOnExecutor(Executor exec,
                                                                               Params... params) {
        return executeOnExecutor(exec, Priority.DEFAULT, params);
    }

   public final PriorityAsyncTask executeOnExecutor(Executor exec,
                                                                               Priority priority,
                                                                               Params... params) {
        if (mExecuteInvoked) {
            throw new IllegalStateException("Cannot execute task:"
                    + " the task is already executed.");
        }

        mExecuteInvoked = true;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(new PriorityRunnable(priority, mFuture));

        return this;
    }
我們調用的是第一個executeOnExecutor實現,傳入的第一個參數sDefaultExecutor是PriorityExecutor類型的線程池,他實現了Executor接口,默認情況下調用無參構造函數的話生成含有5個線程的線程池,也就是我們這裡的sDefaultExecutor是包含有5個線程的線程池,第一個executeOnExecutor最終也會調用第二個第一個executeOnExecutor實現,只不過將每個線程的優先級設置為默認而已啦,所有我們只需要查看第二個executeOnExecutor即可,該方法中的大部分原理都是來自於AsyncTask的,如果任務正在執行則拋出異常,否則的話設置任務正在執行,接著調用onPreExecute方法,這個方法在PriorityAsyncTask中是並沒有實現的,所以如果我們想要在正式執行加載圖片前提示用戶操作的話可以在子類實現該方法,接著將請求參數賦值給WorkerRunnable類型的對象mWorker的mParams屬性,如果非要跟蹤WorkerRunnable類型的話,需要到AsyncTask源碼中查看,他是AsyncTask的一個抽象靜態內部類,該類繼承了Callable接口,理解到這就可以了,來看看mWorker的具體定義:

 

 mWorker = new WorkerRunnable() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                return postResult(doInBackground(mParams));
            }
        };

可以看到他確實實現了call方法

繼續上面的executeOnExecutor方法講解,第19行調用了線程池的execute將新創建的PriorityRunnable對象加入到了線程池,具體的執行調用的是創建PriorityRunnable的run方法啦,而PriorityRunnable的run方法其實調用的是第二個參數也就是這裡mFuture的run方法,這點在源碼中也體現出來了:

 

 @Override
    public void run() {
        this.obj.run();
    }
這裡的obj就是PriorityRunnable的第二個參數,也即mFuture,他是FutureTask類型的實現了Runnable接口,mFuture具體的定義如下:

 

 

mFuture = new FutureTask(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    LogUtils.w(e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
可以看到創建mFuture的時候傳入了我們剛剛創建的mWorker,那麼調用mFuture的run方法具體做些什麼操作呢?這需要查看FutureTask的run方法啦:

 

 public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
這個方法最核心的一句代碼在第12行,他會調用c的call方法,這裡的c就是上面創建mFuture對象時傳入的mWorker這個對象,也就是調用的mWorker的call方法,這段源碼已經在上面出現過了,直接分析可以看出在call方法的第7行調用了doInBackground方法,這個方法在PriorityAsyncTask中也是一個抽象方法,因此具體的實現還是在實現他的子類BitmapCacheManagementTask中,所以回到了BitmapCacheManagementTask中的doInBackground方法,此處我們傳入的參數標志是MESSAGE_INIT_MEMORY_CACHE,查看case語句可以發現他調用了BitmapCache的initMemoryCache方法,那麼BitmapCache是什麼呢?其實他就是內存緩存、SD卡緩存的初始化、修改等等關於緩存操作的方法集合類,進入他的initMemoryCache方法:

 

 

public void initMemoryCache() {
        if (!globalConfig.isMemoryCacheEnabled()) return;

        // Set up memory cache
        if (mMemoryCache != null) {
            try {
                clearMemoryCache();
            } catch (Throwable e) {
            }
        }
        mMemoryCache = new LruMemoryCache(globalConfig.getMemoryCacheSize()) {
            /**
             * Measure item size in bytes rather than units which is more practical
             * for a bitmap cache
             */
            @Override
            protected int sizeOf(MemoryCacheKey key, Bitmap bitmap) {
                if (bitmap == null) return 0;
                return bitmap.getRowBytes() * bitmap.getHeight();
            }
        };
    }
這個方法是用來初始化內存緩存的,首先第2行判斷是否允許內存緩存,不允許的話直接return,第5---10行如果緩存非空的話,那麼會調用clearMemoryCache來清空緩存,第11行創建新的緩存,其大小是globalConfig.getMemoryCacheSize(),這個大小我們可以在創建BitmapUtils的時候傳入,並且在創建的過程中重寫了每個對象占用字節數大小的siezof方法;

這樣initBitmapCache的第2行代碼分析完畢,其實就是創建了一個內存緩存LruMemoryCache對象,第3行代碼傳入的參數是BitmapCacheManagementTask.MESSAGE_INIT_DISK_CACHE,在經歷和上面一樣的步驟之後會走到BitmapGlobalConfig內部類BitmapCacheManagementTask的doInBackground方法裡面,執行他標志為MESSAGE_INIT_DISK_CACHE的case語句塊:

 

 case MESSAGE_INIT_DISK_CACHE:
      cache.initDiskCache();
      break;
可以看到他執行的是BitmapCache的initDiskCache方法,也就是創建了SD卡緩存對象,我們到這個方法裡面看看:

 

public void initDiskCache() {
        if (!globalConfig.isDiskCacheEnabled()) return;

        // Set up disk cache
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
                File diskCacheDir = new File(globalConfig.getDiskCachePath());
                if (diskCacheDir.exists() || diskCacheDir.mkdirs()) {
                    long availableSpace = OtherUtils.getAvailableSpace(diskCacheDir);
                    long diskCacheSize = globalConfig.getDiskCacheSize();
                    diskCacheSize = availableSpace > diskCacheSize ? diskCacheSize : availableSpace;
                    try {
                        mDiskLruCache = LruDiskCache.open(diskCacheDir, 1, 1, diskCacheSize);
                        mDiskLruCache.setFileNameGenerator(globalConfig.getFileNameGenerator());
                    } catch (Throwable e) {
                        mDiskLruCache = null;
                        LogUtils.e(e.getMessage(), e);
                    }
                }
            }
            isDiskCacheReadied = true;
            mDiskCacheLock.notifyAll();
        }
    }
同樣首先判斷是否允許SD卡緩存,不允許的話直接return,否則執行後面代碼,7--11行設置SD卡緩存文件的存儲路徑以及緩存大小設置,13行通過LruDiskCache的靜態方法open創建緩存對象,如果你對LruDiskCache不太了解,可以先看看我的另一篇博客:android-----帶你一步一步優化ListView(二),這篇博客詳細介紹了LruDiskCache源碼部分內容;

至此,調用完initBitmapCache方法之後就創建出來內存緩存以及SD卡緩存了,BitmapGlobalConfig構造函數也就執行結束,BitmapUtils包含兩個參數的構造方法中也就創建了BitmapGlobalConfig對象了,接著在BitmapUtils包含兩個參數的構造方法中我們看到穿件了BitmapDisplayConfig對象,這個對象主要用來設置顯示圖片的一些屬性的,比如動畫、旋轉之類的,這個不是重點;

那麼BitmapUtils最重要的兩個參數構造函數源碼就執行結束了,至於其他5個構造函數基本上都是自定義設置緩存大小的一些操作,都可以從參數名字上面體現出來,這裡就不再贅述;

有了BitmapUtils對象,下一步我們就要調用display方法請求網絡圖片或者本地圖片或者assets文件夾下面的圖片顯示到控件上面了,很顯然display將是重要方法:

 

public  void display(T container, String uri) {
        display(container, uri, null, null);
    }

    public  void display(T container, String uri, BitmapDisplayConfig displayConfig) {
        display(container, uri, displayConfig, null);
    }

    public  void display(T container, String uri, BitmapLoadCallBack callBack) {
        display(container, uri, null, callBack);
    }

    public  void display(T container, String uri, BitmapDisplayConfig displayConfig, BitmapLoadCallBack callBack) {
	}

可以看到display共有4個重載實現,但是他們最終都會聚集到第四個實現上面,所以我們只需要分析第四個即可,先來看看他的四個參數個表示什麼:

container:表示所要顯示圖片的控件;

uri:表示請求圖片的地址,注意這個可以是本地路徑或者assets文件夾下面的路徑;

displayConfig:對顯示圖片的一些操作,比如動畫呀,旋轉呀之類的;

callBack:圖片加載的一些回調接口實現對象;

這裡面我們需要看看BitmapLoadCallBack裡面提供了哪些回調方法可以讓我們在加載圖片的過程中進行一些提示用戶操作:

 

public abstract class BitmapLoadCallBack {

    public void onPreLoad(T container, String uri, BitmapDisplayConfig config) {
    }

    public void onLoadStarted(T container, String uri, BitmapDisplayConfig config) {
    }

    public void onLoading(T container, String uri, BitmapDisplayConfig config, long total, long current) {
    }

    public abstract void onLoadCompleted(T container, String uri, Bitmap bitmap, BitmapDisplayConfig config, BitmapLoadFrom from);

    public abstract void onLoadFailed(T container, String uri, Drawable drawable);
}
可以看出BitmapLoadCallBack是抽象類,在這裡我只列出了它裡面的抽象方法,具體每個方法是做什麼的名字上面都可以體現出來的啦!

好啦,開始進入display方法裡面了:

 

public  void display(T container, String uri, BitmapDisplayConfig displayConfig, BitmapLoadCallBack callBack) {
        if (container == null) {
            return;
        }

        container.clearAnimation();

        if (callBack == null) {
            callBack = new DefaultBitmapLoadCallBack();
        }

        if (displayConfig == null || displayConfig == defaultDisplayConfig) {
            displayConfig = defaultDisplayConfig.cloneNew();
        }

        // Optimize Max Size
        BitmapSize size = displayConfig.getBitmapMaxSize();
        displayConfig.setBitmapMaxSize(BitmapCommonUtils.optimizeMaxSizeByView(container, size.getWidth(), size.getHeight()));

        callBack.onPreLoad(container, uri, displayConfig);

        if (TextUtils.isEmpty(uri)) {
            callBack.onLoadFailed(container, uri, displayConfig.getLoadFailedDrawable());
            return;
        }

        // find bitmap from mem cache.
        Bitmap bitmap = globalConfig.getBitmapCache().getBitmapFromMemCache(uri, displayConfig);

        if (bitmap != null) {
            callBack.onLoadStarted(container, uri, displayConfig);
            callBack.onLoadCompleted(
                    container,
                    uri,
                    bitmap,
                    displayConfig,
                    BitmapLoadFrom.MEMORY_CACHE);
        } else if (!bitmapLoadTaskExist(container, uri, callBack)) {

            final BitmapLoadTask loadTask = new BitmapLoadTask(container, uri, displayConfig, callBack);

            // load bitmap from uri or diskCache
            PriorityExecutor executor = globalConfig.getBitmapLoadExecutor();
            File diskCacheFile = this.getBitmapFileFromDiskCache(uri);
            boolean diskCacheExist = diskCacheFile != null && diskCacheFile.exists();
            if (diskCacheExist && executor.isBusy()) {
                executor = globalConfig.getDiskCacheExecutor();
            }
            // set loading image
            Drawable loadingDrawable = displayConfig.getLoadingDrawable();
            callBack.setDrawable(container, new AsyncDrawable(loadingDrawable, loadTask));

            Priority priority = displayConfig.getPriority();
            if (priority == null) {
                priority = Priority.DEFAULT;
            }
            loadTask.executeOnExecutor(executor, priority);
        }
    }
第2行首先判斷所要顯示圖片的控件是否存在,不存在直接return,這個很好理解啦,你都沒想要把圖片顯示在哪裡,那整個display函數執行就沒什麼意義了,接著第6行清除掉原先控件上面的所有動畫屬性,第8行判斷我們傳入的第四個參數是否為空,如果為空的話,則執行第9行,創建一個DefaultBitmapLoadCallBack對象出來,那DefaultBitmapLoadCallBack裡面是什麼樣子的呢?

 

@Override
    public void onLoadCompleted(T container, String uri, Bitmap bitmap, BitmapDisplayConfig config, BitmapLoadFrom from) {
        this.setBitmap(container, bitmap);
        Animation animation = config.getAnimation();
        if (animation != null) {
            animationDisplay(container, animation);
        }
    }

    @Override
    public void onLoadFailed(T container, String uri, Drawable drawable) {
        this.setDrawable(container, drawable);
    }

    private void animationDisplay(T container, Animation animation) {
        try {
            Method cloneMethod = Animation.class.getDeclaredMethod("clone");
            cloneMethod.setAccessible(true);
            container.startAnimation((Animation) cloneMethod.invoke(animation));
        } catch (Throwable e) {
            container.startAnimation(animation);
        }
    }

 

可以看到他實現了BitmapLoadCallBack抽象類的裡面的onLoadCompleted和onLoadFailed方法,onLoadCompleted是在加載成功之後回調的方法,第3行調用setBitmap方法將圖片顯示到container上面,接著第4行查看是否設置了animation屬性,如果設置了的話調用animationDisplay方法開啟animation屬性,這個方法會通過反射來克隆一個Animation對象出來,並且調用startAnimation方法來將動畫設置到控件上面;onLoadFailed方法會在加載圖片失敗的時候調用,至於onLoadCompleted和onLoadFailed這兩個方法的調用時機會在後面介紹;

回到display方法,接下來第12行如果display方法的第三個參數displayConfig為空或者等於defaultDisplayConfig的話,則調用cloneNew方法深度拷貝defaultDisplayConfig給displayConfig,第17行獲取需要設置圖片的大小,我們來看看getBitmapMaxSize方法:

 

public BitmapSize getBitmapMaxSize() {
        return bitmapMaxSize == null ? BitmapSize.ZERO : bitmapMaxSize;
    }
如果bitmapMaxSize為空的話,則其大小為(0,0);

回到display方法,第18行首先通過optimizeMaxSizeByView方法來根據View的大小以及bitmapsize的大小來計算出最終設置成圖片的大小,隨後調用setBitmapMaxSize方法將其設置到配置對象上面,來看看optimizeMaxSizeByView方法:

 

public static BitmapSize optimizeMaxSizeByView(View view, int maxImageWidth, int maxImageHeight) {
        int width = maxImageWidth;
        int height = maxImageHeight;

        if (width > 0 && height > 0) {
            return new BitmapSize(width, height);
        }

        final ViewGroup.LayoutParams params = view.getLayoutParams();
        if (params != null) {
            if (params.width > 0) {
                width = params.width;
            } else if (params.width != ViewGroup.LayoutParams.WRAP_CONTENT) {
                width = view.getWidth();
            }

            if (params.height > 0) {
                height = params.height;
            } else if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) {
                height = view.getHeight();
            }
        }

        if (width <= 0) width = getImageViewFieldValue(view, "mMaxWidth");
        if (height <= 0) height = getImageViewFieldValue(view, "mMaxHeight");

        BitmapSize screenSize = getScreenSize(view.getContext());
        if (width <= 0) width = screenSize.getWidth();
        if (height <= 0) height = screenSize.getHeight();

        return new BitmapSize(width, height);
    }
這個方法首先獲取到bitmapsize的長寬,如果這兩個值都不為0的話表示這裡的長寬已經使我們所指定的,則執行第6行返回該長寬的BitmapSize對象;如果長或者寬為0的話,則執行9行後面的代碼,首先獲取View的LayoutParams屬性值,若params的寬大於0表示View的大小是固定的,而並不是wrap或者match,那麼直接設置即可,如果寬小於等於0並且寬度不等於View的寬度的話,則設置寬度為View的寬度,高度設置方法與寬度一致,如果10--22行的設置之後width或者height仍然小於等於0,並且設置的圖片顯示控件是ImageView的話,則通過getImageViewFieldValue方法獲取到圖片的寬高,getImageViewFieldValue裡面是通過反射實現獲取的,如果24以及25行之後width或者height仍然小於等於0,那麼就會將BitmapSize設置為是當前窗體的大小了;

 

回到display方法裡面,接著第20行調用callBack的onPreLoad方法,這個方法是我們自己實現的,具體實現在我們調用display方法的第四個參數裡面,22行判斷如果請求地址為空的話,直接調用callBack的onLoadFailed方法顯示出錯信息,並且直接return;接著第28行通過getBitmapFromMemCache方法從內存緩存中讀取對應uri的Bitmap:

 

  public Bitmap getBitmapFromMemCache(String uri, BitmapDisplayConfig config) {
        if (mMemoryCache != null && globalConfig.isMemoryCacheEnabled()) {
            MemoryCacheKey key = new MemoryCacheKey(uri, config);
            return mMemoryCache.get(key);
        }
        return null;
    }
這裡首先根據uri生成key,隨後根據key從內存緩存中讀取對應於key的Bitmap對象;

回到display方法,如果bitmap不為空的話,表示內存緩存中存在對應於uri的Bitmap對象,那麼我們調用callback的onLoadStarted提示開始加載圖片已經結束,以及onLoadCompleted圖片已經把加載完成並且把Bitmap顯示在控件上面;如果bitmap為空,表示當前內存緩存中並不存在對應於uri的bitmap,那麼首先會在38行通過bitmapLoadTaskExist來判斷是否已經存在加載當前uri圖片的任務,我們來看看bitmapLoadTaskExist方法:

 

private static  boolean bitmapLoadTaskExist(T container, String uri, BitmapLoadCallBack callBack) {
        final BitmapLoadTask oldLoadTask = getBitmapTaskFromContainer(container, callBack);

        if (oldLoadTask != null) {
            final String oldUrl = oldLoadTask.uri;
            if (TextUtils.isEmpty(oldUrl) || !oldUrl.equals(uri)) {
                oldLoadTask.cancel(true);
            } else {
                return true;
            }
        }
        return false;
    }
首先通過container以及callBack獲取到異步任務,如果該任務的uri不為空並且等於當前uri的話,則返回true,否則返回false;

回到display方法,可以看到只有當bitmapLoadTaskExist返回false的情況下才會進入if語句塊的,這樣子保證了不存在有兩個異步任務加載同一個uri上面圖片的情況,接下來的40行到56行分別創建了一個指定container, uri, displayConfig, callBack的BitmapLoadTask對象,創建了一個PriorityExecutor類型的線程池對象,設置了正在加載圖片的過程中控件上所顯示的圖片,以及當前異步任務的優先級,最後57行執行了BitmapLoadTask的executeOnExecutor,熟悉AsyncTask源碼的應該都知道執行executeOnExecutor方法最終會進入到BitmapLoadTask的doInBackground方法裡面,注意BitmapLoadTask是BitmapUtils的內部類,我們來看看這裡面的代碼:

 

 @Override
        protected Bitmap doInBackground(Object... params) {

            synchronized (pauseTaskLock) {
                while (pauseTask && !this.isCancelled()) {
                    try {
                        pauseTaskLock.wait();
                        if (cancelAllTask) {
                            return null;
                        }
                    } catch (Throwable e) {
                    }
                }
            }

            Bitmap bitmap = null;

            // get cache from disk cache
            if (!this.isCancelled() && this.getTargetContainer() != null) {
                this.publishProgress(PROGRESS_LOAD_STARTED);
                bitmap = globalConfig.getBitmapCache().getBitmapFromDiskCache(uri, displayConfig);
            }

            // download image
            if (bitmap == null && !this.isCancelled() && this.getTargetContainer() != null) {
                bitmap = globalConfig.getBitmapCache().downloadBitmap(uri, displayConfig, this);
                from = BitmapLoadFrom.URI;
            }

            return bitmap;
        }
我們挑重點來看,這裡面第19行if語句塊首先查看當前異步任務是否被暫停並且當前想要顯示圖片的控件是否存在,條件滿足的話進入if語句塊中,首先執行publishProgress方法,這個方法最終會執行到onProgressUpdate裡面,因為publishProgress傳入的參數是PROGRESS_LOAD_STARTED,所以查看onProgressUpdate的case標志值為PROGRESS_LOAD_STARTED的語句知道:

 

 

 case PROGRESS_LOAD_STARTED:
       callBack.onLoadStarted(container, uri, displayConfig);
       break;
他會執行callback的onLoadStarted,這個方法也是我們在定義BitmapLoadCallBack對象的時候自己實現的;

接著第21行調用getBitmapFromDiskCache方法從SD卡緩存中獲取Bitmap對象,我們來看看這個方法:

 

public Bitmap getBitmapFromDiskCache(String uri, BitmapDisplayConfig config) {
        if (uri == null || !globalConfig.isDiskCacheEnabled()) return null;
        synchronized (mDiskCacheLock) {
            while (!isDiskCacheReadied) {
                try {
                    mDiskCacheLock.wait();
                } catch (Throwable e) {
                }
            }
            if (mDiskLruCache != null) {
                LruDiskCache.Snapshot snapshot = null;
                try {
                    snapshot = mDiskLruCache.get(uri);
                    if (snapshot != null) {
                        Bitmap bitmap = null;
                        if (config == null || config.isShowOriginal()) {
                            bitmap = BitmapDecoder.decodeFileDescriptor(
                                    snapshot.getInputStream(DISK_CACHE_INDEX).getFD());
                        } else {
                            bitmap = BitmapDecoder.decodeSampledBitmapFromDescriptor(
                                    snapshot.getInputStream(DISK_CACHE_INDEX).getFD(),
                                    config.getBitmapMaxSize(),
                                    config.getBitmapConfig());
                        }

                        bitmap = rotateBitmapIfNeeded(uri, config, bitmap);
                        addBitmapToMemoryCache(uri, config, bitmap, mDiskLruCache.getExpiryTimestamp(uri));
                        return bitmap;
                    }
                } catch (Throwable e) {
                    LogUtils.e(e.getMessage(), e);
                } finally {
                    IOUtils.closeQuietly(snapshot);
                }
            }
            return null;
        }
    }
這段代碼最核心的部分是從第13行開始的,通過uri查看當前SD卡緩存中是否存在指定uri的值,如果存在的話進入14行處的語句塊調用BitmapDecoder的decodeFileDescriptor或者decodeSampledBitmapFromDescriptor方法來獲取到Bitmapt圖片,並且在第27行將該Bitmap圖片加入到內存緩存中,加入方法其實挺簡單的:

 

 

 private void addBitmapToMemoryCache(String uri, BitmapDisplayConfig config, Bitmap bitmap, long expiryTimestamp) throws IOException {
        if (uri != null && bitmap != null && globalConfig.isMemoryCacheEnabled() && mMemoryCache != null) {
            MemoryCacheKey key = new MemoryCacheKey(uri, config);
            mMemoryCache.put(key, bitmap, expiryTimestamp);
        }
    }
就僅僅是先通過uri生成key值,隨後將其通過put方法加入內存緩存即可,因為內存緩存是通過map實現的,所以在這裡你見到了put和get操作;

上面對於SD卡緩存講的不怎麼詳細,因為之前的博客已經分析過這方面的源碼了包括圖片壓縮技術,有興趣的可以看看:android-----帶你一步一步優化ListView(二)以及android-----解決Bitmap內存溢出的一種方法(圖片壓縮技術),

回到我們的doInBackground方法,第25行判斷從SD卡緩存中獲取的Bitmap是否為空,以及當前異步任務是否暫停、顯示圖片的控件是否存在,如果滿足if條件的話,表示SD卡緩存上面也不存在當前uri對應的bitmap,那麼我們只能從網絡中獲取了,也就是這裡的downloadBitmap方法了:

 

 public Bitmap downloadBitmap(String uri, BitmapDisplayConfig config, final BitmapUtils.BitmapLoadTask task) {

        BitmapMeta bitmapMeta = new BitmapMeta();

        OutputStream outputStream = null;
        LruDiskCache.Snapshot snapshot = null;

        try {

            Bitmap bitmap = null;
            // try download to disk
            if (globalConfig.isDiskCacheEnabled()) {
                synchronized (mDiskCacheLock) {
                    // Wait for disk cache to initialize
                    while (!isDiskCacheReadied) {
                        try {
                            mDiskCacheLock.wait();
                        } catch (Throwable e) {
                        }
                    }

                    if (mDiskLruCache != null) {
                        try {
                            snapshot = mDiskLruCache.get(uri);
                            if (snapshot == null) {
                                LruDiskCache.Editor editor = mDiskLruCache.edit(uri);
                                if (editor != null) {
                                    outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
                                    bitmapMeta.expiryTimestamp = globalConfig.getDownloader().downloadToStream(uri, outputStream, task);
                                    if (bitmapMeta.expiryTimestamp < 0) {
                                        editor.abort();
                                        return null;
                                    } else {
                                        editor.setEntryExpiryTimestamp(bitmapMeta.expiryTimestamp);
                                        editor.commit();
                                    }
                                    snapshot = mDiskLruCache.get(uri);
                                }
                            }
                            if (snapshot != null) {
                                bitmapMeta.inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
                                bitmap = decodeBitmapMeta(bitmapMeta, config);
                                if (bitmap == null) {
                                    bitmapMeta.inputStream = null;
                                    mDiskLruCache.remove(uri);
                                }
                            }
                        } catch (Throwable e) {
                            LogUtils.e(e.getMessage(), e);
                        }
                    }
                }
            }

            // try download to memory stream
            if (bitmap == null) {
                outputStream = new ByteArrayOutputStream();
                bitmapMeta.expiryTimestamp = globalConfig.getDownloader().downloadToStream(uri, outputStream, task);
                if (bitmapMeta.expiryTimestamp < 0) {
                    return null;
                } else {
                    bitmapMeta.data = ((ByteArrayOutputStream) outputStream).toByteArray();
                    bitmap = decodeBitmapMeta(bitmapMeta, config);
                }
            }

            if (bitmap != null) {
                bitmap = rotateBitmapIfNeeded(uri, config, bitmap);
                if (config != null && config.getImageFactory() != null) {
                    bitmap = config.getImageFactory().createBitmap(bitmap);
                }
                addBitmapToMemoryCache(uri, config, bitmap, bitmapMeta.expiryTimestamp);
            }
            return bitmap;
        } catch (Throwable e) {
            LogUtils.e(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(outputStream);
            IOUtils.closeQuietly(snapshot);
        }

        return null;
    }
這個方法同樣比較長,我們也只挑重點進行分析,第12行首先判斷是否允許SD卡緩存,允許的話進入if語句塊,接著24行相當於查看當前SD卡緩存中是否存在指定於uri的對象,這一步個人認為是屬於檢查操作的,為了防止調用downloadBitmap方法前的檢查失誤,接下來的就是LruDiskCache的標准操作步驟了,獲得Editor對象,初始化OutputStream對象,接著通過downloadToStream方法從網絡中獲取指定uri的輸出流,downloadToStream這個方法是在Downloader中定義的,但是具體實現是在DefaultDownloader中,具體實現如下:

 

 

@Override
    public long downloadToStream(String uri, OutputStream outputStream, final BitmapUtils.BitmapLoadTask task) {

        if (task == null || task.isCancelled() || task.getTargetContainer() == null) return -1;

        URLConnection urlConnection = null;
        BufferedInputStream bis = null;

        OtherUtils.trustAllSSLForHttpsURLConnection();

        long result = -1;
        long fileLen = 0;
        long currCount = 0;
        try {
            if (uri.startsWith("/")) {
                FileInputStream fileInputStream = new FileInputStream(uri);
                fileLen = fileInputStream.available();
                bis = new BufferedInputStream(fileInputStream);
                result = System.currentTimeMillis() + this.getDefaultExpiry();
            } else if (uri.startsWith("assets/")) {
                InputStream inputStream = this.getContext().getAssets().open(uri.substring(7, uri.length()));
                fileLen = inputStream.available();
                bis = new BufferedInputStream(inputStream);
                result = Long.MAX_VALUE;
            } else {
                final URL url = new URL(uri);
                urlConnection = url.openConnection();
                urlConnection.setConnectTimeout(this.getDefaultConnectTimeout());
                urlConnection.setReadTimeout(this.getDefaultReadTimeout());
                bis = new BufferedInputStream(urlConnection.getInputStream());
                result = urlConnection.getExpiration();
                result = result < System.currentTimeMillis() ? System.currentTimeMillis() + this.getDefaultExpiry() : result;
                fileLen = urlConnection.getContentLength();
            }

            if (task.isCancelled() || task.getTargetContainer() == null) return -1;

            byte[] buffer = new byte[4096];
            int len = 0;
            while ((len = bis.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
                currCount += len;
                if (task.isCancelled() || task.getTargetContainer() == null) return -1;
                task.updateProgress(fileLen, currCount);
            }
            outputStream.flush();
        } catch (Throwable e) {
            result = -1;
            LogUtils.e(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(bis);
        }
        return result;
    }
正是在這個方法裡面你就會發現為什麼之前說我們可以通過BitmapUtils加載本地圖片以及網絡圖片以及assets文件夾下面圖片的原因了,downloadToStream會根據圖片來源進行不同的處理,15行判斷如果圖片來自SD卡的話,則直接讀取就可以了,20行判斷如果來自於assets文件夾的話,同樣也是直接讀取的,如果來自網絡的話,則從26行代碼開始到33行從網絡中獲取圖片流,第40到45行將圖片流寫入到輸出流中,同時在寫入的過程中第44行還要調用updateProgress來和用戶界面發生一定聯系,具體怎麼聯系呢?就是你可以用來顯示下載圖片的進度條了,具體怎麼做的呢?第44行調用updateProgress之後實際上執行的是BitmapLoadTask裡面的updateProgress,這個方法的代碼:

 

 

public void updateProgress(long total, long current) {
            this.publishProgress(PROGRESS_LOADING, total, current);
        }
也就是說這個方法會調用publishProgress方法並且傳入的標志是PROGRESS_LOADING,轉而就會執行onProgressUpdate裡面的標志為PROGRESS_LOADING的case語句塊:

 

 

 case PROGRESS_LOADING:
      if (values.length != 3) return;
           callBack.onLoading(container, uri, displayConfig, (Long) values[1], (Long) values[2]);
           break;
這個語句塊中的callback會調用onLoading方法,這個方法是我們在定義callback的時候自己實現的,具體可以在該方法裡面進行更新進度條的操作等等;

回到downloadBitmap方法中,第56行判斷如果bitmap為空的話,則需要進入if語句塊中,那麼什麼情況下會出現bitmap為空呢?SD卡不存在或者不可用的情況下,進入if語句塊之後同樣也會在第58行執行downloadToStream方法區網絡中獲取圖片,接著在第67行判斷上面獲得的bitmap是否為空,如果非空的話會在第72行將其添加到內存緩存中,addBitmapToMemoryCache方法前面已經介紹過了,不再贅述,那麼downloadBitmap方法大致分析結束了;

downloadBitmap結束之後doInBackground方法也就執行結束啦,最後返回我們獲取到的bitmap對象;

到現在,我們獲取到了bitmap對象對象,但是如果我們是通過異步任務獲取的bitmap對象的話,我們將其設置到控件上面是什麼時候呢?

在BitmapUtils裡面我們找到了onPostExecute方法,這個方法就是就是我們在doInBackground執行結束之後會回調的方法啦,AsyncTask源碼中已經分析過了:

 

  @Override
        protected void onPostExecute(Bitmap bitmap) {
            final T container = this.getTargetContainer();
            if (container != null) {
                if (bitmap != null) {
                    callBack.onLoadCompleted(
                            container,
                            this.uri,
                            bitmap,
                            displayConfig,
                            from);
                } else {
                    callBack.onLoadFailed(
                            container,
                            this.uri,
                            displayConfig.getLoadFailedDrawable());
                }
            }
        }
這段代碼在控件非空以及返回的圖片非空的情況下會調用callback的onLoadCompleted方法,這個方法是我們在創建callback的時候自己實現的,如果bitmap為空的話,調用callback的onLoadFailed方法,這個方法同樣也是在創建callback的時候自己實現的,這樣的話就能夠方便的在圖片加載成功或者失敗之後我們對用戶界面進行不同的操作提示了;

 

至此,BitmapUtils源碼答題分析結束了,希望有錯的地方大家可以糾正,贈人玫瑰,手留余香!!!!!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved