[轉]教你寫Android ImageLoader框架之初始配置與請求調度(二)

本文轉自Mr.Simple的博客,如侵刪


前言

教你寫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跳動而熱血沸騰起來了!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容