天天看點

Picasso解析(1)-一張圖檔是如何加載出來的前言Picasso.with(context)load(“http://i.imgur.com/DvpvklR.png“)into(imageView)picasso.enqueueAndSubmit(action)

前言

Picasso是JakeWharton大神在github上的一個開源圖檔加載架構,使用起來極其友善,甚至隻需要一行代碼就可以搞定圖檔加載:

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);
           

具體如何使用該架構我就不在這裡贅述了,大家可以自行上官網進行學習。本文主要是針對Picasso中如何通過上面的一句代碼将一張網絡圖檔加載出來的流程進行梳理分析。

Picasso.with(context)

public static Picasso with(@NonNull Context context) {
        if (context == null) {
            throw new IllegalArgumentException("context == null");
        }
        if (singleton == null) {
            synchronized (Picasso.class) {
                if (singleton == null) {
                    singleton = new Builder(context).build();
                }
            }
        }
        return singleton;
    }
           

這是一個典型的雙重檢查鎖定的懶漢式單例模式和建造者模式,我們先來看看單例模式的好處:

(1)使用延遲初始化方式加載對象,保證隻有在使用的時候才加載對象。 (2)使用同步代碼塊來保證就算在多個線程同時調用此方法時,也隻會有唯一的單例對象存在。 (3)通過對singleton是否為null的雙重檢查,保證了代碼的效率。

上述兩點優勢在我們使用RecyclerView、ListView、GridView這樣的控件時尤其重要,因為我們可能會有多個view同時用這一段代碼來加載圖檔,這樣可以充分保證對象的安全性和加載的效率。

在初始化singleton對象時,采用了建造者模式的方式:

public Picasso build() {
            Context context = this.context;

            if (downloader == null) {
                downloader = Utils.createDefaultDownloader(context);
            }
            if (cache == null) {
                cache = new LruCache(context);
            }
            if (service == null) {
                service = new PicassoExecutorService();
            }
            if (transformer == null) {
                transformer = RequestTransformer.IDENTITY;
            }

            Stats stats = new Stats(cache);

            Dispatcher dispatcher = new Dispatcher(context, service, HANDLER,
                    downloader, cache, stats);

            return new Picasso(context, dispatcher, cache, listener,
                    transformer, requestHandlers, stats, defaultBitmapConfig,
                    indicatorsEnabled, loggingEnabled);
}
           

因為我們在建立Picsasso對象時,沒有指定特别的downloader、cache、service和transormer,是以就全部采用了Picasso為我們提供的預設對象。實際上,我們可以在進行建構的時候,用以下方式自定義這些對象參數:

new Picasso.Builder(this).memoryCache(new LruCache(this)).build().load("http://i.imgur.com/DvpvklR.png").into(image);
           

當然,這裡違反了單例模式,如果我們真的需要自己來指定線程池、下載下傳器這些對象,就需要我們自己維護Picasso的單例對象來進行自定義建構。如果後面有這樣的需求,我會單獨再寫一篇文章來對這種方式進行分析。

load(“http://i.imgur.com/DvpvklR.png“)

public RequestCreator load(@Nullable String path) {
  if (path == null) {
    return new RequestCreator(this, null, );
  }
  if (path.trim().length() == ) {
    throw new IllegalArgumentException("Path must not be empty.");
  }
  return load(Uri.parse(path));
}

public RequestCreator load(@Nullable Uri uri) {
  return new RequestCreator(this, uri, );
}
           

通過我們指定的path,傳回了一個RequestCreator對象,這裡要提的是,當我們傳回的一個對象為null的時候,Picasso對象會為我們建立一個uri為null,resourceId為0的RequestCreator。而如果我們傳進來的字元串為空,則會抛出異常,這裡針對path為null或者為空的情況做了兩種不同的處理。其實我并沒有想明白為什麼要對這兩種情況分别做不同處理,如果有人知道請告訴我。一切正常的話,我們會得到一個由path初始化的uri的RequestCreator對象。

into(imageView)

public void into(ImageView target, Callback callback) {
    //采用nanoTime()來得到開始的時間,比currentTimeMillis()更加準确。因為前者是精确到微秒為機關的,但是不能用這個來計算日期
        long started = System.nanoTime();
    //檢查是否在主線程,不在主線程的話會抛出異常
        checkMain();

        if (target == null) {
            throw new IllegalArgumentException("Target must not be null.");
        }
    //建立RequestCreator對象時,我們會用uri和resourceId來建構一個Request.Builder對象,隻有uri和resouceid都為空的時候,data.hasImage()會傳回為false,然後取消這次加載圖檔的請求任務,将placeholder設定到ImageView中
        if (!data.hasImage()) {
            picasso.cancelRequest(target);
            if (setPlaceholder) {
                setPlaceholder(target, getPlaceholderDrawable());
            }
            return;
        }
    //當我們建構Picasso對象,設定了fit時,deferred為true,會将圖檔填充整個目标view,這裡我們沒有設定,是以不會走到該邏輯
        if (deferred) {
            if (data.hasSize()) {
                throw new IllegalStateException(
                        "Fit cannot be used with resize.");
            }
            int width = target.getWidth();
            int height = target.getHeight();
            if (width ==  || height ==  || target.isLayoutRequested()) {
                if (setPlaceholder) {
                    setPlaceholder(target, getPlaceholderDrawable());
                }
                picasso.defer(target, new DeferredRequestCreator(this, target,
                        callback));
                return;
            }
            data.resize(width, height);
        }
    //根據前面得到的開始時間建立Request對象
        Request request = createRequest(started);
    //根據Request對象建立requestKey
        String requestKey = createKey(request);

    //判斷是否需要從緩存中取值,目前我所分析的2.5.2的代碼對于memoryPolicy指派的方法已經被标為@Deprecated,是以預設我就認為所有的任務都應該從緩存中先取值
        if (shouldReadFromMemoryCache(memoryPolicy)) {
            Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      //因為我們是首次加載,是以從緩存中取得的值為null
            if (bitmap != null) {
                picasso.cancelRequest(target);
                setBitmap(target, picasso.context, bitmap, MEMORY, noFade,
                        picasso.indicatorsEnabled);
                if (picasso.loggingEnabled) {
                    log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from "
                            + MEMORY);
                }
                if (callback != null) {
                    callback.onSuccess();
                }
                return;
            }
        }

    //預設情況下setPlaceholder為true,我們可以通過placeholder來設定加載中的圖檔,如果不設定的話在加載的過程中就什麼也不顯示
        if (setPlaceholder) {
            setPlaceholder(target, getPlaceholderDrawable());
        }

    //最終經過千辛萬苦我們終于可以建立出來一個ImageViewAction對象
        Action action = new ImageViewAction(picasso, target, request,
                memoryPolicy, networkPolicy, errorResId, errorDrawable,
                requestKey, tag, callback, noFade);

        //将ImageViewAction對象送出給Picasso對象進行處理
        picasso.enqueueAndSubmit(action);
    }
           

該方法主要是用來建立ImageViewAction對象,并送出由Picasso來進行處理。當然,如果緩存的話,就不用建立該對象,直接就可以顯示出來了。總的來說,我們可以總結為下圖所示:

picasso.enqueueAndSubmit(action)

我們調用的一行代碼已經分析完了,但似乎我們還是沒有看到一張網絡圖檔是如何加載進來的。其實,在分析上一段into方法時,最後的一步我們看到了該方法将建立出來的ImageViewAction對象通過enqueueAndSubmit方法交給了Picasso對象來進行處理,那麼我們來看看這一步它具體做了什麼。

void enqueueAndSubmit(Action action) {
        Object target = action.getTarget();
        if (target != null && targetToAction.get(target) != action) {
            // This will also check we are on the main thread.
            cancelExistingRequest(target);
            targetToAction.put(target, action);
        }
        submit(action);
    }

    void submit(Action action) {
        dispatcher.dispatchSubmit(action);
    }
           

可以看到,當我們傳進來的ImageViewAction沒有問題的時候,Picasso就将該action傳給Dispatcher類來進行處理。那麼我們接下來再看看Dispatcher做了什麼:

void dispatchSubmit(Action action) {
        handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
    }
           

改行代碼裡通過發送了一個REQUEST_SUBMIT消息到DispatchHandler對象中:

case REQUEST_SUBMIT : {
        Action action = (Action) msg.obj;
        dispatcher.performSubmit(action);
        break;
    }
    void performSubmit(Action action) {
        performSubmit(action, true);
    }

    void performSubmit(Action action, boolean dismissFailed) {
    //如果action.getTag存在于pausedTags中的話,就放進pausedActions中,并取消此次請求
        if (pausedTags.contains(action.getTag())) {
            pausedActions.put(action.getTarget(), action);
            if (action.getPicasso().loggingEnabled) {
                log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
                        "because tag '" + action.getTag() + "' is paused");
            }
            return;
        }

    //擷取BitmapHunter(圖檔獵人?好像挺好玩的樣子)對象,我們是第一次加載圖檔,是以對象為空
        BitmapHunter hunter = hunterMap.get(action.getKey());
        if (hunter != null) {
            hunter.attach(action);
            return;
        }

    //如果線程池已經關閉的話就直接return
        if (service.isShutdown()) {
            if (action.getPicasso().loggingEnabled) {
                log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(),
                        "because shut down");
            }
            return;
        }

    //建立BitmapHunter對象,并放入線程池中執行,存入hunterMap中
        hunter = forRequest(action.getPicasso(), this, cache, stats, action);
        hunter.future = service.submit(hunter);
        hunterMap.put(action.getKey(), hunter);
        if (dismissFailed) {
            failedActions.remove(action.getTarget());
        }

        if (action.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
        }
    }
           

在DisPatchHandler中,又通過performSubmit方法層層調用,最終建立出來了BitmapHunter對象,并放入線程池裡執行。那麼也就是說,我們的核心加載邏輯應該就在BitmapHunter的run方法中:

public void run() {
        try {
      //更新線程名
            updateThreadName(data);

            if (picasso.loggingEnabled) {
                log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
            }
            //擷取圖檔
            result = hunt();
            ........
           

我們可以看到,最關鍵的result,也就是Bitmap對象,是通過hunt方法來擷取的:

Bitmap hunt() throws IOException {
        Bitmap bitmap = null;
    //首先嘗試從緩存當中讀取圖檔,如果緩存中有圖檔的話就直接讀出來并傳回
        if (shouldReadFromMemoryCache(memoryPolicy)) {
            bitmap = cache.get(key);
            if (bitmap != null) {
                stats.dispatchCacheHit();
                loadedFrom = MEMORY;
                if (picasso.loggingEnabled) {
                    log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
                }
                return bitmap;
            }
        }

    //這裡的retyCount是由RequestHandler在BitmapHunter構造函數中指派而來的,因為是網絡請求,是以用的是NetWorkRequestHandler,所指派為2也就是說它會重試兩次
        data.networkPolicy = retryCount == 
                ? NetworkPolicy.OFFLINE.index
                : networkPolicy;
    //通過NetWorkRequestHandler從網絡中加載圖檔
        RequestHandler.Result result = requestHandler.load(data, networkPolicy);
                    .......
           

至此我們終于拿到了我們想要的圖檔了,而在BitmapHunter的run方法中我們拿到了圖檔以後則接着通過Dispatcher類分發完成事件最終通過ImageViewAction将Bitmap設定在我們的ImageView上。

//BitmapHunter.java
            ......
            if (result == null) {
                dispatcher.dispatchFailed(this);
            } else {
                dispatcher.dispatchComplete(this);
            }

//Dispatcher.java
void performComplete(BitmapHunter hunter) {
    ......
        batch(hunter);
    ......
    }


  private void batch(BitmapHunter hunter) {
      ......
        handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH,
            BATCH_DELAY);
      ......
}


void performBatchComplete() {
  ......
  mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(
      HUNTER_BATCH_COMPLETE, copy));
  ......
}


void complete(BitmapHunter hunter) {
      ......
            deliverAction(result, from, single);
      ...
}

//Picasso.java
private void deliverAction(Bitmap result, LoadedFrom from, Action action) {
      ......
            action.complete(result, from);
      ......
    }

//ImageViewAction.java
  public void complete(Bitmap result, Picasso.LoadedFrom from) {
        if (result == null) {
            throw new AssertionError(String.format(
                    "Attempted to complete action with no result!\n%s", this));
        }

        ImageView target = this.target.get();
        if (target == null) {
            return;
        }

        Context context = picasso.context;
        boolean indicatorsEnabled = picasso.indicatorsEnabled;
        PicassoDrawable.setBitmap(target, context, result, from, noFade,
                indicatorsEnabled);

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

終于,我們将辛辛苦苦得到的Bitmap設定到了我們的ImageView中了(上述代碼過長,設計到多次分發跳轉,是以我隻是截取了裡面關鍵的幾行代碼,有興趣的朋友可以去對應的代碼中去檢視)。下面再梳理一下這個流程:

最後通過這樣的一個過程,圖檔就顯示在了我們的ImageView中。

整個過程下來我們發現,加載一張圖檔遠不是我們所看到的一行代碼那麼簡單,中間還是經過了很多複雜的過程。此次分析主要是針對整個流程進行梳理,後面我會對其中每個流程中的一些關鍵技術做出詳細的分析。

繼續閱讀