[Android組件解讀] 初次接觸Retrofit

說到網絡庫就會想到Google的HttpUrlConnection和Apache的HttpClient。

Google推薦使用HttpUrlConnection,拋棄了HttpClient

HttpClient已在Android6.0被廢棄了(吐槽:明明HttpClient功能更強大啊),

查看為什么拋棄HttpClient,可以看下【Android】Android2.3版本以上谷歌為何推薦使用HttpURLConnection卻棄用 Apache HttpClient

想繼續使用HttpClient的話,可以參考android6.0SDK 刪除HttpClient的相關類的解決方法

除了HttpUrlConnection,還有Google推薦的Volley(沒怎么用過),和square的okhttp,還有就是今天介紹的square的retrofit

這個年頭不會點retrofit + rxjava + mvp...的組合拳,都不好意思說是新時代的Android開發

為什么我用的組件都是square公司出品的呢?只能說明他們做的太好了。


回到正題

關于Retrofit的介紹,引用它在github上的一句話

Type-safe HTTP client for Android and Java by Square
用于Android和Java的類型安全的Http框架

現在市面上使用的是retrofit2.0版本,跟以前的1.9版本有部分API不一樣,可以查看Retrofit 1.9 遷移到 Retrofit 2.0,建議使用新的版本

Retrofit是使用RESETful API的。

這里簡單的介紹下關于RESETful的架構:

(1)每一個URI代表一種資源;
(2)客戶端和服務器之間,傳遞這種資源的某種表現層;
(3)客戶端通過四個HTTP動詞(GET用來獲取資源,POST用來新建資源(也可以用于更新資源),PUT用來更新資源,DELETE用來刪除資源),
對服務器端資源進行操作,實現"表現層狀態轉化"。

更多關于RESET API可以看阮一峰的理解RESTful架構

Retrofit的git地址:https://github.com/square/retrofit

Retrofit的官方地址:http://square.github.io/retrofit/

使用姿勢

  1. 往build.gradle添加依賴,不需要額外添加okhttp,retrofit內部支持

     // retrofit
     compile 'com.squareup.retrofit2:retrofit:2.2.0'
     // gson轉化器
     compile 'com.squareup.retrofit2:converter-gson:2.2.0'
    
  2. 創建接口,聲明API

     // 獲取請求API
     // 請求接口 https://api.github.com
     public interface GitHub {
     @GET("/repos/{owner}/{repo}/contributors")
     Call<List<Contributor>> contributors(
             @Path("owner") String owner,
             @Path("repo") String repo);
     }
     
     // 請求對象類
     public static class Contributor {
         public final String login;
         public final int contributions;
    
         public Contributor(String login, int contributions) {
         this.login = login;
         this.contributions = contributions;
         }
     }
    
  3. 方法調用

     public void request() throws IOException {
    
        // 1.創建Retrofit對象,根據Retrofit模板創建
        Retrofit retrofit = new Retrofit.Builder()
                // 傳入baseurl地址,
                .baseUrl("https://api.github.com")
                // 傳入gson轉化器
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    
        // 2.獲取GitHub的對象
        GitHub github = retrofit.create(GitHub.class);
    
        // 3.傳入參數
            Call<List<Contributor>> call = github.contributors("square", "retrofit");
    
        // 4.異步請求接口
        // 請求接口格式為"https://api.github.com/repos/square/retrofit/contributors"
        call.enqueue(new Callback<List<Contributor>>() {
            @Override
            public void onResponse(Call<List<Contributor>> call, Response<List<Contributor>> response) {
                List<Contributor> contributors = response.body();
                for (Contributor contributor : contributors) {
                    Log.e("MainActivity",contributor.login + " (" + contributor.contributions + ")");
                }
            }
    
            @Override
            public void onFailure(Call<List<Contributor>> call, Throwable t) {
    
            }
        });
     }
    

通過上面的例子,應該能夠理解Retrofit的使用了吧

關于使用,可以參考下官網介紹http://square.github.io/retrofit/

對此,作者菌有幾個問題

  • Retrofit提供的這么多注解是怎么運用的?
  • Retrofit有多少種注解
  • Retrofit生成API對象后,怎么發起請求的

源碼解讀

先查看retrofit的代碼構造

.
└── retrofit2
    ├── BuiltInConverters.java --默認的轉化器,支持流,字符串
    ├── Call.java  -- 請求的發送/取消,并獲取當前請求的狀態
    ├── CallAdapter.java
    ├── Callback.java. --回調方法
    ├── Converter.java -- 轉化器,將okhttp中的RequestBody,ResponseBody進行轉化,支持XML,JSON,ProtocolBuf,如GsonConverterFactory,將返回的內容或者發送Body中的內容轉化,
    ├── DefaultCallAdapterFactory.java
    ├── ExecutorCallAdapterFactory.java
    ├── HttpException.java
    ├── OkHttpCall.java --實現了Call的接口,使用okhttp3.0的API
    ├── ParameterHandler.java --參數句柄,處理retrofit頭文件的操作請求
    ├── Platform.java --判斷當前運行環境是Android還是Java8
    ├── RequestBuilder.java
    ├── Response.java 服務端返回內容,可以通過response.body()獲取到返回內容
    ├── Retrofit.java  --`對外方法`
    ├── ServiceMethod.java
    ├── Utils.java
    ├── http --http文件夾中包含的是retrofit所有的注解類,關于Http的操作和上傳字段,以及Header配置
    │   ├── Body.java  用于Post,根據轉換方式將實例對象轉化為對應字符串傳遞參數.比如Retrofit添加GsonConverterFactory則是將body轉化為gson字符串進行傳遞
    │   ├── DELETE.java Delete請求
    │   ├── Field.java  用于Post方式傳遞參數,需要在請求接口方法上添加@FormUrlEncoded,即以表單的方式傳遞參數
    │   ├── FieldMap.java
    │   ├── FormUrlEncoded.java 同@Field使用
    │   ├── GET.java GET請求
    │   ├── HEAD.java
    │   ├── HTTP.java
    │   ├── Header.java 添加http header
    │   ├── HeaderMap.java
    │   ├── Headers.java 跟@Header作用一樣,只是使用方式不一樣,@Header是作為請求方法的參數傳入,@Headers是以固定方式直接添加到請求方法上
    │   ├── Multipart.java 配合@Multipart使用,一般用于文件上傳
    │   ├── OPTIONS.java
    │   ├── PATCH.java
    │   ├── POST.java  Post請求
    │   ├── PUT.java   PUT請求
    │   ├── Part.java 一般用于文件上傳
    │   ├── PartMap.java
    │   ├── Path.java 用于URL上占位符
    │   ├── Query.java 用于Http Get請求傳遞參數
    │   ├── QueryMap.java
    │   ├── QueryName.java  用于Http Get請求傳遞參數
    │   ├── Streaming.java  用于Http Get請求傳遞流形式參數
    │   ├── Url.java 直接賦值URL
    │   └── package-info.java
    └── package-info.java

附上一篇別人寫的###Retrofit注解含義###

講解下Retrofit的使用原理

  1. 首先講解Retrofit初始化
 Retrofit retrofit = new Retrofit.Builder()
            // 傳入baseurl地址,
            .baseUrl("https://api.github.com")
            // 傳入gson轉化器
            .addConverterFactory(GsonConverterFactory.create())
            .build();

此處用了Build機制來初始化,現在很多初始化通過Build機制,如Dialog,OKHttp,Picasso等

Build構建時可以傳入以下配置

Builder(Platform platform) {
      this.platform = platform;
      // Add the built-in converter factory first. This prevents overriding its behavior but also
      // ensures correct behavior when using converters that consume all types.
      converterFactories.add(new BuiltInConverters());
    }

    public Builder() {
        // 獲取當前的系統版本
        this(Platform.get());
    }

      /**
       * 配置Client,默認使用okhttp3.OkHttpClient
       * @param client
       * @return
       */
    public Builder client(OkHttpClient client) {
      return callFactory(checkNotNull(client, "client == null"));
    }

      /**
       * 創建CallFactory,用于生成請求Http的Call
       * @param factory
       * @return
       */
    public Builder callFactory(okhttp3.Call.Factory factory) {
      this.callFactory = checkNotNull(factory, "factory == null");
      return this;
    }

      /**
       * 配置URL
       * @param baseUrl
       * @return
       */
    public Builder baseUrl(String baseUrl) {
      checkNotNull(baseUrl, "baseUrl == null");
      HttpUrl httpUrl = HttpUrl.parse(baseUrl);
      if (httpUrl == null) {
        throw new IllegalArgumentException("Illegal URL: " + baseUrl);
      }
      return baseUrl(httpUrl);
    }

    /**
     * 配置URL,下面有幾組實例配置URL
     * <p>
     * <b>Correct:</b><br>
     * Base URL: http://example.com/api/<br>
     * Endpoint: foo/bar/<br>
     * Result: http://example.com/api/foo/bar/
     * <p>
     * <b>Incorrect:</b><br>
     * Base URL: http://example.com/api<br>
     * Endpoint: foo/bar/<br>
     * Result: http://example.com/foo/bar/
     * <p>
     * This method enforces that {@code baseUrl} has a trailing {@code /}.
     * <p>
     * <b>Endpoint values which contain a leading {@code /} are absolute.</b>
     * <p>
     * Absolute values retain only the host from {@code baseUrl} and ignore any specified path
     * components.
     * <p>
     * Base URL: http://example.com/api/<br>
     * Endpoint: /foo/bar/<br>
     * Result: http://example.com/foo/bar/
     * <p>
     * Base URL: http://example.com/<br>
     * Endpoint: /foo/bar/<br>
     * Result: http://example.com/foo/bar/
     * <p>
     * <b>Endpoint values may be a full URL.</b>
     * <p>
     * Values which have a host replace the host of {@code baseUrl} and values also with a scheme
     * replace the scheme of {@code baseUrl}.
     * <p>
     * Base URL: http://example.com/<br>
     * Endpoint: https://github.com/square/retrofit/<br>
     * Result: https://github.com/square/retrofit/
     * <p>
     * Base URL: http://example.com<br>
     * Endpoint: //github.com/square/retrofit/<br>
     * Result: http://github.com/square/retrofit/ (note the scheme stays 'http')
     */
    public Builder baseUrl(HttpUrl baseUrl) {
      checkNotNull(baseUrl, "baseUrl == null");
      List<String> pathSegments = baseUrl.pathSegments();
      if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
        throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
      }
      this.baseUrl = baseUrl;
      return this;
    }

      /**
       * 添加轉換器
       * @param factory
       * @return
       */
    public Builder addConverterFactory(Converter.Factory factory) {
      converterFactories.add(checkNotNull(factory, "factory == null"));
      return this;
    }

    /**
     * Add a call adapter factory for supporting service method return types other than {@link
     * Call}.
     */
    public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
      adapterFactories.add(checkNotNull(factory, "factory == null"));
      return this;
    }

      /**
       * 回調線程
       * @param executor
       * @return
       */
    public Builder callbackExecutor(Executor executor) {
      this.callbackExecutor = checkNotNull(executor, "executor == null");
      return this;
    }

    public Builder validateEagerly(boolean validateEagerly) {
      this.validateEagerly = validateEagerly;
      return this;
    }

    /**
     * Create the {@link Retrofit} instance using the configured values.
     * <p>
     * Note: If neither {@link #client} nor {@link #callFactory} is called a default {@link
     * OkHttpClient} will be created and used.
     */
    public Retrofit build() {
      if (baseUrl == null) {
        throw new IllegalStateException("Base URL required.");
      }

      // 默認使用okhttp3.0的OKHttpClient
      okhttp3.Call.Factory callFactory = this.callFactory;
      if (callFactory == null) {
        callFactory = new OkHttpClient();
      }

      // 若Platform是Android,回調的是主線程
      Executor callbackExecutor = this.callbackExecutor;
      if (callbackExecutor == null) {
        callbackExecutor = platform.defaultCallbackExecutor();
      }

      // Make a defensive copy of the adapters and add the default Call adapter.
      List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
      adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

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

      return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
          callbackExecutor, validateEagerly);
    }

此處注意的是baseUrl,正確寫法

* Base URL: http://example.com/api/
* Endpoint: foo/bar/
* Result: http://example.com/api/foo/bar/

Platform判斷當前運行環境是Android還是JAVA8

private static final Platform PLATFORM = findPlatform();

  static Platform get() {
    return PLATFORM;
  }

  private static Platform findPlatform() {
    try {
      Class.forName("android.os.Build");
      if (Build.VERSION.SDK_INT != 0) {
        return new Android();
      }
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("java.util.Optional");
      return new Java8();
    } catch (ClassNotFoundException ignored) {
    }
    return new Platform();
  }

2.獲取GitHub的對象,傳入參數

GitHub github = retrofit.create(GitHub.class);
//傳入參數
Call<List<Contributor>> call = github.contributors("square", "retrofit");

通過代理模式來處理接口

public <T> T create(final Class<T> service) {
        // 校驗提供的是接口實現的類
        Utils.validateServiceInterface(service);
        if (validateEagerly) {
            eagerlyValidateMethods(service);
        }
        // 代理模式
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service},
                new InvocationHandler() {
                    private final Platform platform = Platform.get();

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args)
                            throws Throwable {
                        // If the method is a method from Object then defer to normal invocation.
                        if (method.getDeclaringClass() == Object.class) {
                            return method.invoke(this, args);
                        }
                        if (platform.isDefaultMethod(method)) {
                            return platform.invokeDefaultMethod(method, service, proxy, args);
                        }
                        // 加載ServiceMethod,進行URL組裝
                        ServiceMethod<Object, Object> serviceMethod =
                                (ServiceMethod<Object, Object>) loadServiceMethod(method);
                        OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
                        return serviceMethod.callAdapter.adapt(okHttpCall);
                    }
                });
    }

3.調用call.enqueue(new Callback...);

使用的是OKHttpCall,使用了OKHttp的Call,然后進行Http請求。

Call的接口定義

public interface Call<T> extends Cloneable {
  /**
   * 同步發送請求,返回結果
   * @return
   * @throws IOException
   */
  Response<T> execute() throws IOException;

  /**
   * 異步發送請求,通過Callback回調返回
   * @param callback
   */
  void enqueue(Callback<T> callback);

  /**
   * 是否正在執行發送
   * @return
   */
  boolean isExecuted();

  /**
   * 取消發送的請求,若請求還未執行
   */
  void cancel();

  /**
   * 請求是否被取消
   * @return
   */
  boolean isCanceled();

  /**
   * Create a new, identical call to this one which can be enqueued or executed even if this call
   * has already been.
   */
  Call<T> clone();

  /** The original HTTP request. */
  Request request();
}

4.其他:如何構建Http請求,主要實在ServiceMethod.java這個類中

以@Path為例

 else if (annotation instanceof Path) {
        if (gotQuery) {
          throw parameterError(p, "A @Path parameter must not come after a @Query.");
        }
        if (gotUrl) {
          throw parameterError(p, "@Path parameters may not be used with @Url.");
        }
        if (relativeUrl == null) {
          throw parameterError(p, "@Path can only be used with relative url on @%s", httpMethod);
        }
        gotPath = true;

        Path path = (Path) annotation;
        String name = path.value();
        validatePathName(p, name);

        Converter<?, String> converter = retrofit.stringConverter(type, annotations);
        return new ParameterHandler.Path<>(name, converter, path.encoded());
        }

通過剛才的代理模式調用ServiceMethod,將接口中的注解生成完整的Http請求

相關資料

你真的會用Retrofit2嗎?Retrofit2完全教程

Retrofit各個注解的含義及作用

Android 網絡框架 Retrofit2.0介紹、使用和封裝

理解RESTful架構

RESTful API 設計指南

jdk動態代理實現原理


期待下4月新番-夏目友人帳第六季
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容