細細品讀Retrofit的設計之美一

JerryloveEmily發表於2019-02-26

Retrofit是這兩年比較流行的網路通訊庫,之所以流行自然有它的優點,首先是大廠Square公司開源之作,俗話說大廠出品,必須精品。
作為網路通訊框架,基本的組成部分就是三大塊:1. 請求體部分。2. 響應體部分。3. 與UI層回撥部分。


Retrofit的使用這裡就不細說了,我們從構建Retrofit物件談起。

private Retrofit buildRetrofit(){
        return new Retrofit.Builder()
                .baseUrl(AppConst.BASE_URL)      // 設定基礎請求地址
                .client(buildHttpClient())       // 構建自定義的httpClient 物件
                .addConverterFactory(FastJsonConverterFactory.create())  // 新增資料解析工廠
                .build();  // 構建
    }複製程式碼

首先:構建者Builder模式

整體看很明顯有個構建者Builder模式,Builder模式在Android中是很常見的一種設計模式,它的優點很明顯一般情況一個框架都是需要靈活自由的配置引數屬性的,如果不用Builder模式,都改成setter、getter,那初始化一個Retrofit物件就顯得複雜和臃腫了。而這裡Builder模式加上鍊式呼叫方式,為Retrofit框架的引數配置增添了不少靈活和自由,而且程式碼可讀性也增強了。
其實Builder模式的套路很簡單,下面來個簡單的虛擬碼Builder模式:

// Builder模式的套路模板
public class Retrofit{
    final HttpUrl baseUrl;
    final List<Converter.Factory> converterFactories;
    ....//  省略一大坨程式碼

    Retrofit(HttpUrl baseUrl, List<Converter.Factory> converterFactories) {
        this.baseUrl = baseUrl;
        this.converterFactories = unmodifiableList(converterFactories); 
    }
    ....//  省略一大坨程式碼, 其實就是上面引數屬性的一些獲取方法
    public HttpUrl baseUrl() {
      return baseUrl;
    }

    public static final class Builder{
        private HttpUrl baseUrl;
        private final List<Converter.Factory> converterFactories = new ArrayList<>();

        public Builder baseUrl(HttpUrl baseUrl) {
          // ... 省去部分程式碼
          this.baseUrl = baseUrl;
          return this;
        }

        public Builder addConverterFactory(Converter.Factory factory) {
          converterFactories.add(checkNotNull(factory, "factory == null"));
          return this;
        }

        public Retrofit build() {
          // Make a defensive copy of the converters.
          List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

          return new Retrofit(baseUrl, converterFactories);
        }
    }
}複製程式碼

以上就是Builder模式的套路模板,外部Retrofit的物件的構建最終是在build()方法new出來返回。
Retrofit框架內部有好多地方都用到了Builder模式,也是為了方便自由配置引數的。
Builder模式在Android開發中最常見的就是AlertDialog.Builder,可以自由的配置對話方塊的標題、內容、內容設定來源、確認取消等按鈕事件等等。有興趣的可以去了解下AlertDialog的原始碼,基本也是上面模板的套路。

代理模式

構建好Retrofit物件後,大家都知道這個框架網路請求的通訊介面api都是Interface介面中宣告的,框架本身為了與網路請求業務做解耦用了動態代理的方式,為業務通訊介面生成代理物件,當代理物件呼叫業務介面方法api的時候,動態代理類就能監測到並回撥,這時候就可以做網路框架該有的功能:解析通訊業務介面,生成網路請求體便於供應給底層OkHttp做具體的網路請求工作。其實就是框架本身沒有辦法直接使用業務介面,所以請了一個代理中介物件去間接的訪問業務介面,來完成相關的業務功能。

public ApiWrapper() {
        // 構建生成retrofit物件
        Retrofit retrofit = buildRetrofit();
        // ApiService是網路通過create方法建立出的代理物件mApiService
        mApiService = retrofit.create(ApiService.class);
}複製程式碼

來看看create方法是如何建立出ApiService介面的物件的。

public <T> T create(final Class<T> service) {
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {

          @Override public Object invoke(Object proxy, Method method, Object[] args)
              throws Throwable {
           }
    }
}複製程式碼

通過Proxy.newProxyInstance方法動態建立了代理物件,也就是上文create方法返回的mApiService物件。是不是很懵逼,為什麼Proxy.newProxyInstance方法就能建立出代理物件,而且又正好是ApiService.class這個介面物件呢,是怎麼建立出來的?帶著這些問題,我們來聊聊“代理模式”。

要搞清楚上面的這些問題,就得明白代理模式的套路,最重要就是區分清楚角色:
角色一:目標介面
角色二:目標物件
角色三:代理物件

這裡先舉個簡單的例子,有個這樣的場景:

Jerry是個程式設計師年紀大了眼看就要30歲了還是單身,家裡人非常的著急,於是找到了隔壁老王(老王認識的妹紙比較多,為人熱情)叫著幫忙介紹妹子給Jerry認識。

上面這個場景來區分下角色:
角色一:目標介面,大齡單身汪找妹子(要乾的事情)
角色二:目標物件,單身程式設計師Jerry(需要找妹子的目標人物)
角色三:代理物件,皮條客隔壁老王(代表Jerry去找妹子)

這裡建立三個檔案:目標介面 IFindGirl、目標類Jerry、代理類ProxyLaoWan

// IService.java
/**
 * 目標介面
 * Created by Administrator on 2017/9/17 0017.
 */
public interface IService {
    /**
     * 找妹子
     * @param name  名字
     * @param age   年齡
     */
    void findGirl(String name, int age);
}

// Jerry.java
/**
 * 目標物件:單身汪Jerry
 * Created by Administrator on 2017/9/17 0017.
 */
public class Jerry implements IService {
    private static final String TAG = "Jerry";

    @Override
    public void findGirl(String name, int age) {
        Log.e(TAG, name + " 說願意做Jerry的女朋友");
    }
}

// ProxyLaoWan.java
/**
 * 代理物件:找物件的代理人老王
 * Created by Administrator on 2017/9/17 0017.
 */
public class ProxyLaoWan implements IService {

    private IService service;

    public ProxyLaoWan(IService service) {
        this.service = service;
    }

    @Override
    public void findGirl(String name, int age) {
        // 老王找到妹子後,再這告訴Jerry
        service.findGirl(name, age);
    }
}複製程式碼

使用的時候很簡單:

// jerry
IService service = new Jerry();
// 建立代理人, 然後把Jerry委託給老王
ProxyLaoWan laoWan = new ProxyLaoWan(service);
// 老王幫Jerry去找妹子
laoWan.findGirl("Tom", 22);複製程式碼

這個例子中Jerry沒有直接去找妹子“Tom”,而是通過了老王,這是一個典型的靜態代理模式,Jerry把找妹子的事情委託代理給了老王,同樣Jerry如果還有其它的事情,比如買最新的腎phone手機,可是國行的很貴,剛好老王要去香港,又委託老王買港版的iPhone X,於是就要IService目標介面中加入新的要乾的事情buyPhone(),同樣老王的類、Jerry類都需要實現相應的方法。如果Jerry不斷的有新的事情要做,新的功能要擴充套件那需要修改的地方就比較多了不利於專案的擴充套件和團隊開發。為此這樣的需求就產生了動態代理模式。
先來看看套路模板:

// 動態代理
    public void testDynamicProxy(){
        // 目標物件jerry
        final IService jerryService = new Jerry();
        // 代理物件老王
        IService proxyLaoWan = (IService) Proxy.newProxyInstance(
                jerryService.getClass().getClassLoader(),
                jerryService.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 呼叫目標介面方法的時候,就呼叫invoke方法
                        long currentTime = System.currentTimeMillis();
                        Object returnValue = method.invoke(jerryService, args);
                        long calledMethodTime = System.currentTimeMillis();
                        long invokeMethodTime = calledMethodTime - currentTime;
                        // 介面方法的執行時間,便於檢測效能
                        Log.e("InvocationHandler", "方法執行效能時間:" + invokeMethodTime);
                        return returnValue;
                    }
                });

        // 老王幫忙找妹子,妹子叫Tom  22歲
        proxyLaoWan.findGirl("Tom", 22);

        // 老王幫Jerry買了價值8288元的iPhone X手機
        proxyLaoWan.buyPhone("iPhone X", 8288);
    }複製程式碼

Jerry如果還要委託老王給買手機,只要給目標介面加入buyPhone方法,然後Jerry實現這個方法,而代理者老王,不需要管都有什麼具體的目標介面,通過Proxy.newProxyInstance建立的代理物件,就可以呼叫目標介面的方法。
介紹下Proxy.newProxyInstance方法:

// Proxy.java
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h);複製程式碼

第一個引數:目標物件的類載入器。因為這個代理物件是執行時才建立的,沒有編譯時候預先準備的位元組碼檔案提供,所以需要一個類載入器來載入產生Proxy代理裡的類型別,便於建立代理物件。
第二個引數:interfaces是目標介面陣列
第三個引數:是代理物件當呼叫目標介面方法的時候,會先回撥InvocationHandler介面的實現方法invoke。

到目前為止還是看不出來Proxy.newProxyInstance是怎麼給我們建立代理物件的,下面分析下它的原始碼實現:

動態代理的原始碼實現

class Proxy{
  private final static Class[] constructorParams = { InvocationHandler.class };

  public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        if (h == null) {  throw new NullPointerException(); }
        // 獲取到Proxy類的 類型別Class
        Class<?> cl = getProxyClass0(loader, interfaces);
        // 通過Proxy類的類型別物件獲取InvocationHandler作為引數的構造方法
        try {
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            //  通過構造方法物件建立一個代理物件
            return newInstance(cons, h);
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString());
        }
    }

    private static Object newInstance(Constructor<?> cons, InvocationHandler h) {
        return cons.newInstance(new Object[] {h} );
    }
}

// Constructor.java
class Constructor{
  public T newInstance(Object... args) throws InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (serializationClass == null) {
            // 看最終的實現是Native方法,使用底層NDK來實現建立的代理例項物件
            return newInstance0(args);
        } else {
            return (T) newInstanceFromSerialization(serializationCtor, serializationClass);
        }
    }

    // 底層NDK實現建立代理物件
    private static native Object newInstanceFromSerialization(Class<?> ctorClass, Class<?> allocClass)
        throws InstantiationException, IllegalArgumentException, InvocationTargetException;

    // 底層NDK實現建立代理物件
    private native T newInstance0(Object... args) throws InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}複製程式碼

從上面程式碼中我們可以看出,最終的代理物件的建立是底層NDK來建立返回的,具體就不去看底層的實現了,大體瞭解到動態代理物件是通過這個構造方法來建立的。

protected Proxy(InvocationHandler h) {
  this.h = h;
}複製程式碼

經過上門對動態代理模式的一番學習和解釋,現在回過頭來看

mApiService = retrofit.create(ApiService.class);

public <T> T create(final Class<T> service) {
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {

          @Override public Object invoke(Object proxy, Method method, Object[] args)
              throws Throwable {
           }
    }
}複製程式碼

create方法建立返回的正是ApiService介面的代理物件,每當代理物件呼叫目標介面裡的方法時,動態代理物件就會回撥InvocationHandler介面的invoke實現方法。
在Retrofit中,動態代理模式的角色劃分:
角色一:目標介面(委託方法),ApiService介面方法
角色二:目標物件(委託方),ApiService.class
角色三:代理物件,create建立的mApiService物件。

至此就把Retrofit中,動態代理業務的網路通訊介面講清楚了,好處就是非入侵的方式,把網路通訊api呼叫代理出來,然後在呼叫回撥的invoke方法裡統一處理和準備網路框架需要構建的請求體,作為後續加入到請求佇列任務池中進行具體的網路請求。動態代理模式也是AOP的一種實現方式,切片思想的一種。做過Java EE服務端開發的對於Spring的AOP應該深有體會,動態代理與Annotation的結合真是完美,這一點在Retrofit的各種請求方式、引數、url路徑等等的註解就體現了。


文章有些長,這是第一篇,後面還會持續更新關於Retrofit的解讀,不單單是讓你懂的Retrofit的原理,還讓你學會感受設計模式的美妙。感謝你的閱讀。

相關文章