教你寫Android ImageLoader架構之初始配置與請求調度,androidimageloader
前言 在教你寫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, threadCount); 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倉庫地址
如果你覺得我寫得還行,那就幫我頂個帖吧!Thanks~