Android ImageLoader 框架之初始配置與請求排程

Mr.Simple的專欄發表於2015-04-05

前言

Android ImageLoader框架之基本架構中我們對SimpleImageLoader框架進行了基本的介紹,今天我們就從原始碼的角度來剖析ImageLoader的設計與實現。
在我們使用ImageLoader前都會通過一個配置類來設定一些基本的東西,比如載入中的圖片、載入失敗的圖片、快取策略等等,SimpleImageLoader的設計也是如此。配置類這個比較簡單,我們直接看原始碼吧。

ImageLoaderConfig配置

/**
 * ImageLoader配置類,
 * 
 * @author mrsimple
 */
public class ImageLoaderConfig {

    /**
     * 圖片快取配置物件
     */
    public BitmapCache bitmapCache = new MemoryCache();

    /**
     * 載入圖片時的loading和載入失敗的圖片配置物件
     */
    public DisplayConfig displayConfig = new DisplayConfig();
    /**
     * 載入策略
     */
    public LoadPolicy loadPolicy = new SerialPolicy();

    /**
     * 
     */
    public int threadCount = Runtime.getRuntime().availableProcessors() + 1;

    /**
     * @param count
     * @return
     */
    public ImageLoaderConfig setThreadCount(int count) {
        threadCount = Math.max(1, count);
        return this;
    }

    public ImageLoaderConfig setCache(BitmapCache cache) {
        bitmapCache = cache;
        return this;
    }

    public ImageLoaderConfig setLoadingPlaceholder(int resId) {
        displayConfig.loadingResId = resId;
        return this;
    }

    public ImageLoaderConfig setNotFoundPlaceholder(int resId) {
        displayConfig.failedResId = resId;
        return this;
    }

    public ImageLoaderConfig setLoadPolicy(LoadPolicy policy) {
        if (policy != null) {
            loadPolicy = policy;
        }
        return this;
    }
}

都是很簡單的setter函式,但是不太一樣的是這些setter都是返回一個ImageLoaderConfig物件的,在這裡也就是返回了自身。這個設計是類似Builder模式的,便於使用者的鏈式呼叫,例如:

 private void initImageLoader() {
        ImageLoaderConfig config = new ImageLoaderConfig()
                .setLoadingPlaceholder(R.drawable.loading)
                .setNotFoundPlaceholder(R.drawable.not_found)
                .setCache(new DoubleCache(this))
                .setThreadCount(4)
                .setLoadPolicy(new ReversePolicy());
        // 初始化
        SimpleImageLoader.getInstance().init(config);
    }

對於Builder模式不太瞭解的同學可以參考 Android原始碼分析之Builder模式。構建好配置物件之後我們就可以通過這個配置物件來初始化SimpleImageLoader了。SimpleImageLoader會根據配置物件來初始化一些內部策略,例如快取策略、執行緒數量等。呼叫init方法後整個ImageLoader就正式啟動了。

SimpleImageLoader的實現

SimpleImageLoader這個類的職責只是作為使用者入口,它的工作其實並沒有那麼多,只是一個門童罷了。我們看看它的原始碼吧。

/**
 * 圖片載入類,支援url和本地圖片的uri形式載入.根據圖片路徑格式來判斷是網路圖片還是本地圖片,如果是網路圖片則交給SimpleNet框架來載入,
 * 如果是本地圖片那麼則交給mExecutorService從sd卡中載入
 * .載入之後直接更新UI,無需使用者干預.如果使用者設定了快取策略,那麼會將載入到的圖片快取起來.使用者也可以設定載入策略,例如順序載入{@see
 * SerialPolicy}和逆向載入{@see ReversePolicy}.
 * 
 * @author mrsimple
 */
public final class SimpleImageLoader {
    /** SimpleImageLoader例項 */
    private static SimpleImageLoader sInstance;

    /** 網路請求佇列  */
    private RequestQueue mImageQueue;
    /** 快取 */
    private volatile BitmapCache mCache = new MemoryCache();

    /** 圖片載入配置物件 */
    private ImageLoaderConfig mConfig;

    private SimpleImageLoader() {
    }

    /**
     * 獲取ImageLoader單例
     * 
     * @return
     */
    public static SimpleImageLoader getInstance() {
        if (sInstance == null) {
            synchronized (SimpleImageLoader.class) {
                if (sInstance == null) {
                    sInstance = new SimpleImageLoader();
                }
            }
        }
        return sInstance;
    }

    /**
     * 初始化ImageLoader,啟動請求佇列
     * @param config 配置物件
     */
    public void init(ImageLoaderConfig config) {
        mConfig = config;
        mCache = mConfig.bitmapCache;
        checkConfig();
        mImageQueue = new RequestQueue(mConfig.threadCount);
        mImageQueue.start();
    }

    private void checkConfig() {
        if (mConfig == null) {
            throw new RuntimeException(
                    "The config of SimpleImageLoader is Null, please call the init(ImageLoaderConfig config) method to initialize");
        }

        if (mConfig.loadPolicy == null) {
            mConfig.loadPolicy = new SerialPolicy();
        }
        if (mCache == null) {
            mCache = new NoCache();
        }

    }

    public void displayImage(ImageView imageView, String uri) {
        displayImage(imageView, uri, null, null);
    }

    public void displayImage(final ImageView imageView, final String uri,
            final DisplayConfig config, final ImageListener listener) {
        BitmapRequest request = new BitmapRequest(imageView, uri, config, listener);
        // 載入的配置物件,如果沒有設定則使用ImageLoader的配置
        request.displayConfig = request.displayConfig != null ? request.displayConfig
                : mConfig.displayConfig;
        // 新增對佇列中
        mImageQueue.addRequest(request);
    }

	  // 程式碼省略...

    /**
     * 圖片載入Listener
     * 
     * @author mrsimple
     */
    public static interface ImageListener {
        public void onComplete(ImageView imageView, Bitmap bitmap, String uri);
    }
}

從上述程式碼中我們可以看到SimpleImageLoader的工作比較少,也比較簡單。它就是根據使用者傳遞進來的配置來初始化ImageLoader,並且作為圖片載入入口,使用者呼叫displayImage之後它會將這個呼叫封裝成一個BitmapRequest請求,然後將該請求新增到請求佇列中。

BitmapRequest圖片載入請求

BitmapRequest只是一個儲存了ImageView、圖片uri、DisplayConfig以及ImageListener的一個物件,封裝這個物件的目的在載入圖片時減少引數的個數,***在BitmapRequest的建構函式中我們會將圖片uri設定為ImageView的tag,這樣做的目的是防止圖片錯位顯示。***BitmapRequest類實現了Compare介面,請求佇列會根據它的序列號進行排序,排序策略使用者也可以通過配置類來設定,具體細節在載入策略的章節我們再聊吧。

 public BitmapRequest(ImageView imageView, String uri, DisplayConfig config,
            ImageListener listener) {
        mImageViewRef = new WeakReference<ImageView>(imageView);
        displayConfig = config;
        imageListener = listener;
        imageUri = uri;
        // 設定ImageView的tag為圖片的uri
        imageView.setTag(uri);
        imageUriMd5 = Md5Helper.toMD5(imageUri);
    }

RequestQueue圖片請求佇列

請求佇列我們採用了SImpleNet中一樣的模式,通過封裝一個優先順序佇列來維持圖片載入佇列,mSerialNumGenerator會給每一個請求分配一個序列號,PriorityBlockingQueue會根據BitmapRequest的compare策略來決定BitmapRequest的順序。RequestQueue內部會啟動使用者指定數量的執行緒來從請求佇列中讀取請求,分發執行緒不斷地從佇列中讀取請求,然後進行圖片載入處理,這樣ImageLoader就happy起來了。

/**
 * 請求佇列, 使用優先佇列,使得請求可以按照優先順序進行處理. [ Thread Safe ]
 * 
 * @author mrsimple
 */
public final class RequestQueue {
    /**
     * 請求佇列 [ Thread-safe ]
     */
    private BlockingQueue<BitmapRequest> mRequestQueue = new PriorityBlockingQueue<BitmapRequest>();
    /**
     * 請求的序列化生成器
     */
    private AtomicInteger mSerialNumGenerator = new AtomicInteger(0);

    /**
     * 預設的核心數
     */
    public static int DEFAULT_CORE_NUMS = Runtime.getRuntime().availableProcessors() + 1;
    /**
     * CPU核心數 + 1個分發執行緒數
     */
    private int mDispatcherNums = DEFAULT_CORE_NUMS;
    /**
     * NetworkExecutor,執行網路請求的執行緒
     */
    private RequestDispatcher[] mDispatchers = null;

    /**
     * 
     */
    protected RequestQueue() {
        this(DEFAULT_CORE_NUMS);
    }

    /**
     * @param coreNums 執行緒核心數
     * @param httpStack http執行器
     */
    protected RequestQueue(int coreNums) {
        mDispatcherNums = coreNums;
    }

    /**
     * 啟動RequestDispatcher
     */
    private final void startDispatchers() {
        mDispatchers = new RequestDispatcher[mDispatcherNums];
        for (int i = 0; i < mDispatcherNums; i++) {
            mDispatchers[i] = new RequestDispatcher(mRequestQueue);
            mDispatchers[i].start();
        }
    }

    public void start() {
        stop();
        startDispatchers();
    }

    /**
     * 停止RequestDispatcher
     */
    public void stop() {
        if (mDispatchers != null && mDispatchers.length > 0) {
            for (int i = 0; i < mDispatchers.length; i++) {
                mDispatchers[i].interrupt();
            }
        }
    }

    /**
     * 不能重複新增請求
     * @param request
     */
    public void addRequest(BitmapRequest request) {
        if (!mRequestQueue.contains(request)) {
            request.serialNum = this.generateSerialNumber();
            mRequestQueue.add(request);
        } else {
            Log.d("", "### 請求佇列中已經含有");
        }
    }

    private int generateSerialNumber() {
        return mSerialNumGenerator.incrementAndGet();
    }
}

RequestDispatcher請求分發

請求Dispatcher,繼承自Thread,從網路請求佇列中迴圈讀取請求並且執行,也比較簡單,直接上原始碼吧。

final class RequestDispatcher extends Thread {

    /**
     * 網路請求佇列
     */
    private BlockingQueue<BitmapRequest> mRequestQueue;

    /**
     * @param queue 圖片載入請求佇列
     */
    public RequestDispatcher(BlockingQueue<BitmapRequest> queue) {
        mRequestQueue = queue;
    }

    @Override
    public void run() {
        try {
            while (!this.isInterrupted()) {
                final BitmapRequest request = mRequestQueue.take();
                if (request.isCancel) {
                    continue;
                }
				// 解析圖片schema
                final String schema = parseSchema(request.imageUri);
                // 根據schema獲取對應的Loader
                Loader imageLoader = LoaderManager.getInstance().getLoader(schema);
                // 載入圖片
                imageLoader.loadImage(request);
            }
        } catch (InterruptedException e) {
            Log.i("", "### 請求分發器退出");
        }
    }

	/**
	 * 這裡是解析圖片uri的格式,uri格式為: schema:// + 圖片路徑。
	 */
    private String parseSchema(String uri) {
        if (uri.contains("://")) {
            return uri.split("://")[0];
        } else {
            Log.e(getName(), "### wrong scheme, image uri is : " + uri);
        }

        return "";
    }

}

第一個重點就是run函式了,不斷地從佇列中獲取請求,然後解析到圖片uri的schema,從schema的格式就可以知道它是儲存在哪裡的圖片。例如網路圖片物件的schema是http或者https,sd卡儲存的圖片對應的schema為file。根據schema我們從LoaderManager中獲取對應的Loader來載入圖片,這個設計保證了SimpleImageLoader可載入圖片型別的可擴充套件性,這就是為什麼會增加loader這個包的原因。使用者只需要根據uri的格式來構造圖片uri,並且實現自己的Loader類,然後將Loader物件注入到LoaderManager即可,後續我們會再詳細說明。

這裡的另一個重點是parseSchema函式,它的職責是解析圖片uri的格式,uri格式為: schema:// + 圖片路徑,例如網路圖片的格式為http://xxx.image.jpg,而本地圖片的uri為file:///sdcard/xxx/image.jpg。如果你要實現自己的Loader來載入特定的格式,那麼它的uri格式必須以schema://開頭,否則解析會錯誤,例如可以為drawable://image,然後你註冊一個schema為”drawable”的Loader到LoaderManager中,SimpleImageLoader在載入圖片時就會使用你註冊的Loader來載入圖片,這樣就可以應對使用者的多種多樣的需求。如果不能擁抱變化那就不能稱之為框架了,應該叫功能模組。

本章總結

最後我們來整理一下這個過程吧,SimpleImageLoader根據使用者的配置來配置、啟動請求佇列,請求佇列又會根據使用者配置的執行緒數量來啟動幾個分發執行緒。這幾個執行緒不斷地從請求佇列(執行緒安全)中讀取圖片載入請求,並且執行圖片載入請求。這些請求是使用者通過呼叫SimpleImageLoader的displayImage函式而產生的,內部會把這個呼叫封裝成一個BitmapRequest物件,並且將該物件新增到請求佇列中。這樣就有了生產者(使用者)和消費者(分發執行緒),整個SimpleImageLoader就隨著CPU跳動而熱血沸騰起來了!

Github倉庫連結

SimpleImageLoader倉庫地址

相關文章