深入Weex系列(七)之Adapter元件原始碼解析

頭條祁同偉發表於2017-11-30

1、前言

在上一篇文章《深入Weex系列(六)Weex渲染流程分析》中我們分析了Weex的渲染流程,但是實際上串起來的還是Module以及Component元件,加上Weex特有的渲染流程。

Module元件和Component元件對Weex Android SDK的意義非常大,屬於沒有這倆寸步難行的關係,包括今天我們要分析的Adapter也依賴於Module、Component元件,尤其是Js引擎與Module、Component互動的部分,Adapter表面上並不涉及,但是實際上息息相關。如果還有疑惑的話非常建議大家回過頭再去看看之前的原始碼分析文章。

本篇文章我們就開始分析Weex中另一個元件Adapter,對Weex的設計理解更深一步。

2、初識Adapter

2.1 Adapter的定位

《Android 擴充套件》中我們可以看到Adapter的定位:

Adapter 擴充套件 Weex 對一些基礎功能實現了統一的介面,可實現這些介面來定製自己的業務。例如:圖片下載等。

此處可以看到:Weex對Adapter的定位是基礎功能的定義,可以實現這些介面自己進行實現。

2.2 Adapter的使用

實際上在我的WeexList專案中已經有了關於Adapter的使用,因為Weex並沒有實現預設的圖片載入功能。

Adapter的註冊:

    InitConfig config = new InitConfig.Builder().setImgAdapter(new WeexImageAdapter()).build();
    WXSDKEngine.initialize(this, config);
複製程式碼

Adapter的實現:

public class WeexImageAdapter implements IWXImgLoaderAdapter {

    @Override
    public void setImage(String url, ImageView view, WXImageQuality quality, WXImageStrategy strategy) {
        Glide.with(view.getContext())
                .load(url)
                .error(R.mipmap.me_image_man)
                .into(view);
    }
}

複製程式碼

可以看到我們實現了Weex的IWXImgLoaderAdapter介面,自己實現了圖片載入的能力。需要注意的是Adapter的註冊和Module、Component的註冊方式是不一樣的。

2.3 Adapter與Module的區別

我們知道Module的定位是非UI性質的功能元件,那和Adapter是不是有點衝突?為什麼還多了一個Adapter元件呢?

不要Adapter元件可不可以呢?其實是可以的,要做的事情都通過Module來做。但是一些基礎功能例如圖片載入、網路請求等不同的應用有自己的實現方式、依賴庫,如果Weex自己再加一套,就顯得冗餘而且這些基礎能力Weex定義好介面讓接入的應用來提供就好了。

這就是Adapter存在的意義,你看Weex團隊是多麼的貼心啊!

備註: 那麼我們此時可以由Adapter和Module的戰略意義不一樣,猜到兩個元件的地位也是不一樣的。通過翻閱原始碼,在InitConfig中Weex使用建造者模式來提供各種各樣的Adapter的自定義,但是卻不支援自己新增Adapter的型別。此處也提現了Adapter的定位:基礎功能實現了統一的介面。

3、Adapter原始碼分析

3.1 Adapter註冊

Adapter註冊時序圖

可以看到:Adapter的註冊實際上非常簡單,只是通過WXSDKEngine初始化了一些配置資訊而已,根本不需要像Module或者Component一樣需要通過JsBridge也讓Js引擎知道自己的存在。

再次看出Adapter的定位:基礎功能實現了統一的介面,具體的互動交給Module或Component來做,然後Module或Component來呼叫我們實現的Adapter。

3.2 Adapter呼叫

關於Adapter的呼叫就不再畫圖分析,因為實在不需要怎麼分析。實際上對於Adapter的呼叫本質就是類的呼叫,因為剛才在註冊的時候設定了各種各樣的Adapter,然後直接呼叫介面即可。

4、特定Adapter分析

對於Adapter的呼叫分為Component呼叫和Module呼叫(Adapter處於呼叫鏈的下端

4.1 IWXImgLoaderAdapter(Component呼叫)

我們來介紹下實現圖片載入能力的Adapter,這個Adapter沒有被Weex預設實現。之前總結過元件的互動是通過JsBridge然後來呼叫Component被註解修飾的方法。

對於ImageView它在Weex裡對應了WXImage這個Component。我們在Vue程式碼裡寫的src屬性既然是需要通過Adapter實現的,那在WXImage中必定有方法對應src屬性,果然我們發現了它。

    @Component(lazyload = false)
    public class WXImage extends WXComponent<ImageView> {
        @WXComponentProp(name = Constants.Name.SRC)
        public void setSrc(String src) {
            if (src == null) {
                return;
            }
            this.mSrc = src;
            WXSDKInstance instance = getInstance();
            Uri rewrited = instance.rewriteUri(Uri.parse(src), URIAdapter.IMAGE);

            if (Constants.Scheme.LOCAL.equals(rewrited.getScheme())) {
                setLocalSrc(rewrited);
            } else {
                int blur = 0;
                if (getDomObject() != null) {
                    String blurStr = getDomObject().getStyles().getBlur();
                    blur = parseBlurRadius(blurStr);
                }
                setRemoteSrc(rewrited, blur);
            }
        }
        
    private void setRemoteSrc(Uri rewrited, int blurRadius) {
        
        ·······
        
        IWXImgLoaderAdapter imgLoaderAdapter = getInstance().getImgLoaderAdapter();
        if (imgLoaderAdapter != null) {
            imgLoaderAdapter.setImage(rewrited.toString(), getHostView(),
                    getDomObject().getAttrs().getImageQuality(), imageStrategy);
        }
    }
}
複製程式碼

IWXImgLoaderAdapter呼叫時序圖

總結:

  • Js引擎通過JsBridge傳送訊息給客戶端,最終呼叫到相關Component的具體方法;
  • 對於WXImage來說,被呼叫setSrc方法之後,會呼叫設定的IWXImgLoaderAdapter的setImage方法;

4.2 IWXHttpAdapter(Module呼叫)

對於網路請求比較常用,Weex就做了預設的實現,但是之前在《Weex系列(四)之Module元件原始碼解析》分析過,實現比較簡陋,最好重新實現。

我們先看下平時寫Vue程式碼的時候使用網路請求是怎麼做的:

    var stream = weex.requireModule('stream')
    stream.fetch
複製程式碼

既然require的是名叫"stream"的Module,那麼我們就在WXSDKEngine中找一下,果然在register()方法中找到了:

    registerModule("stream", WXStreamModule.class);
複製程式碼

接下來,我們不用猜測,堅信WXStreamModule類中必定存在一個fetch方法;

  @JSMethod(uiThread = false)
  public void fetch(String optionsStr, final JSCallback callback, JSCallback progressCallback){

    JSONObject optionsObj = null;
    try {
      optionsObj = JSON.parseObject(optionsStr);
    }catch (JSONException e){
      WXLogUtils.e("", e);
    }

    boolean invaildOption = optionsObj==null || optionsObj.getString("url")==null;
    if(invaildOption){
      if(callback != null) {
        Map<String, Object> resp = new HashMap<>();
        resp.put("ok", false);
        resp.put(STATUS_TEXT, Status.ERR_INVALID_REQUEST);
        callback.invoke(resp);
      }
      return;
    }
    
    //此處可以看到Http請求的那些引數最終是被怎麼取的,這樣即便是文件沒寫的一些設定我們也可以根據取引數的方法反推出來怎麼設定。
    String method = optionsObj.getString("method");
    String url = optionsObj.getString("url");
    JSONObject headers = optionsObj.getJSONObject("headers");
    String body = optionsObj.getString("body");
    String type = optionsObj.getString("type");
    int timeout = optionsObj.getIntValue("timeout");

    if (method != null) method = method.toUpperCase();
    Options.Builder builder = new Options.Builder()
            .setMethod(!"GET".equals(method)
                    &&!"POST".equals(method)
                    &&!"PUT".equals(method)
                    &&!"DELETE".equals(method)
                    &&!"HEAD".equals(method)
                    &&!"PATCH".equals(method)?"GET":method)
            .setUrl(url)
            .setBody(body)
            .setType(type)
            .setTimeout(timeout);

    extractHeaders(headers,builder);
    final Options options = builder.createOptions();
    
    // 真正請求網路去了,裡面會呼叫IWXHttpAdapter
    sendRequest(options, new ResponseCallback() {
      @Override
      public void onResponse(WXResponse response, Map<String, String> headers) {
        if(callback != null) {
          Map<String, Object> resp = new HashMap<>();
          if(response == null|| "-1".equals(response.statusCode)){
            resp.put(STATUS,-1);
            resp.put(STATUS_TEXT,Status.ERR_CONNECT_FAILED);
          }else {
            int code = Integer.parseInt(response.statusCode);
            resp.put(STATUS, code);
            resp.put("ok", (code >= 200 && code <= 299));
            if (response.originalData == null) {
              resp.put("data", null);
            } else {
              String respData = readAsString(response.originalData,
                      headers != null ? getHeader(headers, "Content-Type") : ""
              );
              try {
                resp.put("data", parseData(respData, options.getType()));
              } catch (JSONException exception) {
                WXLogUtils.e("", exception);
                resp.put("ok", false);
                resp.put("data","{'err':'Data parse failed!'}");
              }
            }
            resp.put(STATUS_TEXT, Status.getStatusText(response.statusCode));
          }
          resp.put("headers", headers);
          callback.invoke(resp);
        }
      }
    }, progressCallback);
  }
  
  
  private void sendRequest(Options options,ResponseCallback callback,JSCallback progressCallback){
    WXRequest wxRequest = new WXRequest();
    wxRequest.method = options.getMethod();
    wxRequest.url = mWXSDKInstance.rewriteUri(Uri.parse(options.getUrl()), URIAdapter.REQUEST).toString();
    wxRequest.body = options.getBody();
    wxRequest.timeoutMs = options.getTimeout();

    if(options.getHeaders()!=null)
    if (wxRequest.paramMap == null) {
      wxRequest.paramMap = options.getHeaders();
    }else{
      wxRequest.paramMap.putAll(options.getHeaders());
    }

    IWXHttpAdapter adapter = (mAdapter==null && mWXSDKInstance != null) ? mWXSDKInstance.getWXHttpAdapter() : mAdapter;
    if (adapter != null) {
    // 呼叫了IWXHttpAdapter的sendRequest方法
      adapter.sendRequest(wxRequest, new StreamHttpListener(callback,progressCallback));
    }else{
      WXLogUtils.e("WXStreamModule","No HttpAdapter found,request failed.");
    }
  }
複製程式碼

然後我們看下DefaultWXHttpAdapter的預設網路請求實現;

public class DefaultWXHttpAdapter implements IWXHttpAdapter {

  private static final IEventReporterDelegate DEFAULT_DELEGATE = new NOPEventReportDelegate();
  private ExecutorService mExecutorService;

  private void execute(Runnable runnable){
    if(mExecutorService==null){
      mExecutorService = Executors.newFixedThreadPool(3);
    }
    mExecutorService.execute(runnable);
  }

  @Override
  public void sendRequest(final WXRequest request, final OnHttpListener listener) {
    if (listener != null) {
      listener.onHttpStart();
    }
    execute(new Runnable() {
      @Override
      public void run() {
        WXResponse response = new WXResponse();
        IEventReporterDelegate reporter = getEventReporterDelegate();
        try {
          HttpURLConnection connection = openConnection(request, listener);
          reporter.preConnect(connection, request.body);
          Map<String,List<String>> headers = connection.getHeaderFields();
          int responseCode = connection.getResponseCode();
          if(listener != null){
            listener.onHeadersReceived(responseCode,headers);
          }
          reporter.postConnect();

          response.statusCode = String.valueOf(responseCode);
          if (responseCode >= 200 && responseCode<=299) {
            InputStream rawStream = connection.getInputStream();
            rawStream = reporter.interpretResponseStream(rawStream);
            response.originalData = readInputStreamAsBytes(rawStream, listener);
          } else {
            response.errorMsg = readInputStream(connection.getErrorStream(), listener);
          }
          if (listener != null) {
            listener.onHttpFinish(response);
          }
        } catch (IOException|IllegalArgumentException e) {
          e.printStackTrace();
          response.statusCode = "-1";
          response.errorCode="-1";
          response.errorMsg=e.getMessage();
          if(listener!=null){
            listener.onHttpFinish(response);
          }
          if (e instanceof IOException) {
            reporter.httpExchangeFailed((IOException) e);
          }
        }
      }
    });
  }
}
複製程式碼

可以看到Weex預設的網路請求是基於HttpURLConnection,一個核心池和最大池都是3的FixThreadPool。對於網路請求來說缺點顯而易見:

  • 沒有Https的實現;
  • 執行緒池使用可以更優;

但是對Weex來說實際上只是提供預設的簡單實現,也沒錯,需要自己去重新定義。

接下來看圖總結下:

Module呼叫Adapter的時序圖

總結:

  • Js引擎發訊息來執行網路請求;
  • 呼叫到了WXStreamModule,呼叫其fetch方法;
  • WXStreamModule裡會呼叫IWXHttpAdapter的sendRequest方法,實現真正的網路請求;

5、問題

Weex除了使用網路圖片之外可以使用別的型別圖片嗎,例如直接使用drawable資料夾裡的圖片?

在剛開始接觸到Weex的時候我內心的答案也是NO,畢竟drawable裡的圖片在常規的安卓開發中都是需要使用R檔案來呼叫的。原始碼面前,了無祕密!我們就來看下WXImage的實現吧,在setSrc方法中有一個判斷:

    if (Constants.Scheme.LOCAL.equals(rewrited.getScheme())) {
      setLocalSrc(rewrited);// 以local 開頭的話則走到了這裡
    } else {
      int blur = 0;
      if(getDomObject() != null) {
        String blurStr = getDomObject().getStyles().getBlur();
        blur = parseBlurRadius(blurStr);
      }
      setRemoteSrc(rewrited, blur);
    }

複製程式碼

最終會走到這裡:

  public static Drawable getDrawableFromLoaclSrc(Context context, Uri rewrited) {
    Resources resources = context.getResources();
    List<String> segments = rewrited.getPathSegments();
    if (segments.size() != 1) {
      WXLogUtils.e("Local src format is invalid.");
      return null;
    }
    int id = resources.getIdentifier(segments.get(0), "drawable", context.getPackageName());
    return id == 0 ? null : ResourcesCompat.getDrawable(resources, id, null);
  }
複製程式碼

老司機們已經明白了吧:通過資源名生成uri,然後還是拿到了資源對應的id,獲取的圖片。

備註:

  • 如果不是仔細跟蹤Weex原始碼的話,我們很容易給Weex貼上一個不能載入本地圖片的標籤。
  • 實際上,Weex也支援別的型別的圖片呼叫方式,老司機們可以自己探索實現下。

6、Adapter總結

  • Module和Component類是理解Adapter的前提;與Js引擎互動的部分被Module和Component做了,但是對理解很重要;
  • Adapter的定位是擴充套件Weex對一些基礎功能實現了統一的介面,可實現這些介面來定製自己的業務;
  • Module自身的原始碼其實很簡單,複雜的是上面與Module、Component相關的呼叫鏈;
  • 通過細讀原始碼,可以發現很多問題的答案;帶著問題去讀,更加事半功倍;

歡迎持續關注Weex原始碼分析專案:Weex-Analysis-Project

歡迎關注微信公眾號:定期分享Java、Android乾貨!

歡迎關注

相關文章