深入淺出 Retrofit,這么牛逼的框架你們還不來看看?

mspe9393 7年前發布 | 21K 次閱讀 Retrofit Android開發 移動開發

Android 開發中,從原生的 HttpUrlConnection 到經典的 Apache 的 HttpClient ,再到對前面這些網絡基礎框架的封裝,比如 Volley 、 Async Http Client ,Http 相關開源框架的選擇還是很多的,其中由著名的 Square 公司開源的 Retrofit 更是以其簡易的接口配置、強大的擴展支持、優雅的代碼結構受到大家的追捧。也正是由于 Square 家的框架一如既往的簡潔優雅,所以我一直在想,Square 公司是不是只招 處女座 的程序員?

1、初識 Retrofit

單從 Retrofit 這個單詞,你似乎看不出它究竟是干嘛的,當然,我也看不出來 :)逃。。

Retrofitting refers to the addition of new technology or features to older systems.

–From Wikipedia

于是我們就明白了,冠以 Retrofit 這個名字的這個家伙,應該是某某某的 『Plus』 版本了。

1.1 Retrofit 概覽

Retrofit 是一個 RESTful 的 HTTP 網絡請求框架的封裝。注意這里并沒有說它是網絡請求框架,主要原因在于網絡請求的工作并不是 Retrofit 來完成的。 Retrofit 2.0 開始內置 OkHttp ,前者專注于接口的封裝,后者專注于網絡請求的高效,二者分工協作,宛如古人的『你耕地來我織布』,小日子別提多幸福了。

我們的應用程序通過 Retrofit 請求網絡,實際上是使用 Retrofit 接口層封裝請求參數、Header、Url 等信息,之后由 OkHttp 完成后續的請求操作,在服務端返回數據之后, OkHttp 將原始的結果交給 Retrofit ,后者根據用戶的需求對結果進行解析的過程。

講到這里,你就會發現所謂 Retrofit ,其實就是 Retrofitting OkHttp 了。

1.2 Hello Retrofit

多說無益,不要來段代碼陶醉一下。使用 Retrofit 非常簡單,首先你需要在你的 build.gradle 中添加依賴:

compile 'com.squareup.retrofit2:retrofit:2.0.2'

你一定是想要訪問 GitHub 的 api 對吧,那么我們就定義一個接口:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

接口當中的 listRepos 方法,就是我們想要訪問的 api 了:

https://api.github.com/users/{user}/repos

其中,在發起請求時, {user} 會被替換為方法的第一個參數 user 。

好,現在接口有了,我們要構造 Retrofit 了:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

這里的 service 就好比我們的快遞哥,還是往返的那種哈~

Call<List<Repo>> repos = service.listRepos("octocat");

發請求的代碼就像前面這一句,返回的 repos 其實并不是真正的數據結果,它更像一條指令,你可以在合適的時機去執行它:

// 同步調用
List<Repo> data = repos.execute(); 

// 異步調用
repos.enqueue(new Callback<List<Repo>>() {
            @Override
            public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
                List<Repo> data = response.body();
            }

            @Override
            public void onFailure(Call<List<Repo>> call, Throwable t) {
                t.printStackTrace();
            }
        });

啥感覺?有沒有突然覺得請求接口就好像訪問自家的方法一樣簡單?吶,前面我們看到的,就是 Retrofit 官方的 demo 了。你以為這就夠了?噗~怎么可能。。

1.3 Url 配置

Retrofit 支持的協議包括 GET / POST / PUT / DELETE / HEAD / PATCH ,當然你也可以直接用 HTTP 來自定義請求。這些協議均以注解的形式進行配置,比如我們已經見過 GET 的用法:

@GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);

這些注解都有一個參數 value,用來配置其路徑,比如示例中的 users/{user}/repos ,我們還注意到在構造 Retrofit 之時我們還傳入了一個 baseUrl("https://api.github.com/") ,請求的完整 Url 就是通過 baseUrl 與注解的 value (下面稱 “ path ” ) 整合起來的,具體整合的規則如下:

  • path 是絕對路徑的形式: path = "/apath" , baseUrl = "http://host:port/a/b" Url = "http://host:port/apath"

  • path 是相對路徑, baseUrl 是目錄形式: path = "apath" , baseUrl = "http://host:port/a/b/" Url = "http://host:port/a/b/apath"

  • path 是相對路徑, baseUrl 是文件形式: path = "apath" , baseUrl = "http://host:port/a/b" Url = "http://host:port/a/apath"

  • path 是完整的 Url: path = "http://host:port/aa/apath" , baseUrl = "http://host:port/a/b" Url = "http://host:port/aa/apath"

建議采用第二種方式來配置,并盡量使用同一種路徑形式。如果你在代碼里面混合采用了多種配置形式,恰好趕上你哪天頭暈眼花,信不信分分鐘寫一堆 bug 啊哈哈。

1.4 參數類型

發請求時,需要傳入參數, Retrofit 通過注解的形式令 Http 請求的參數變得更加直接,而且類型安全。

1.4.1 Query & QueryMap

@GET("/list")
Call<ResponseBody> list(@Query("page") int page);

Query 其實就是 Url 中 ‘?’ 后面的 key-value,比如:

http://www.println.net/?cate=android

這里的 cate=android 就是一個 Query ,而我們在配置它的時候只需要在接口方法中增加一個參數,即可:

interface PrintlnServer{
    @GET("/")
    Call<String> cate(@Query("cate") String cate);
}

這時候你肯定想,如果我有很多個 Query ,這么一個個寫豈不是很累?而且根據不同的情況,有些字段可能不傳,這與方法的參數要求顯然也不相符。于是,打群架版本的 QueryMap 橫空出世了,使用方法很簡單,我就不多說了。

1.4.2 Field & FieldMap

其實我們用 POST 的場景相對較多,絕大多數的服務端接口都需要做加密、鑒權和校驗, GET 顯然不能很好的滿足這個需求。使用 POST 提交表單的場景就更是剛需了,怎么提呢?

@FormUrlEncoded
   @POST("/")
   Call<ResponseBody> example(
       @Field("name") String name,
       @Field("occupation") String occupation);

其實也很簡單,我們只需要定義上面的接口就可以了,我們用 Field 聲明了表單的項,這樣提交表單就跟普通的函數調用一樣簡單直接了。

等等,你說你的表單項不確定個數?還是說有很多項你懶得寫? Field 同樣有個打群架的版本—— FieldMap ,趕緊試試吧~~

1.4.3 Part & PartMap

這個是用來上傳文件的。話說當年用 HttpClient 上傳個文件老費勁了,一會兒編碼不對,一會兒參數錯誤(也怪那時段位太低吧TT)。。。可是現在不同了,自從有了 Retrofit ,媽媽再也不用擔心文件上傳費勁了~~~

public interface FileUploadService {  
    @Multipart
    @POST("upload")
    Call<ResponseBody> upload(@Part("description") RequestBody description,
                              @Part MultipartBody.Part file);
}

如果你需要上傳文件,和我們前面的做法類似,定義一個接口方法,需要注意的是,這個方法不再有 @FormUrlEncoded 這個注解,而換成了 @Multipart ,后面只需要在參數中增加 Part 就可以了。也許你會問,這里的 Part 和 Field 究竟有什么區別,其實從功能上講,無非就是客戶端向服務端發起請求攜帶參數的方式不同,并且前者可以攜帶的參數類型更加豐富,包括數據流。也正是因為這一點,我們可以通過這種方式來上傳文件,下面我們就給出這個接口的使用方法:

//先創建 service
FileUploadService service = retrofit.create(FileUploadService.class);

//構建要上傳的文件
File file = new File(filename);
RequestBody requestFile =
        RequestBody.create(MediaType.parse("application/otcet-stream"), file);

MultipartBody.Part body =
        MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);

String descriptionString = "This is a description";
RequestBody description =
        RequestBody.create(
                MediaType.parse("multipart/form-data"), descriptionString);

Call<ResponseBody> call = service.upload(description, body);
call.enqueue(new Callback<ResponseBody>() {
  @Override
  public void onResponse(Call<ResponseBody> call,
                         Response<ResponseBody> response) {
    System.out.println("success");
  }

  @Override
  public void onFailure(Call<ResponseBody> call, Throwable t) {
    t.printStackTrace();
  }
});

在實驗時,我上傳了一個只包含一行文字的文件:

Visit me: http://www.println.net

那么我們去服務端看下我們的請求是什么樣的:

HEADERS

Accept-Encoding: gzip
Content-Length: 470
Content-Type: multipart/form-data; boundary=9b670d44-63dc-4a8a-833d-66e45e0156ca
User-Agent: okhttp/3.2.0
X-Request-Id: 9d70e8cc-958b-4f42-b979-4c1fcd474352
Via: 1.1 vegur
Host: requestb.in
Total-Route-Time: 0
Connection: close
Connect-Time: 0

FORM/POST PARAMETERS

description: This is a description

RAW BODY

--9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21

This is a description
--9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name="aFile"; filename="uploadedfile.txt"
Content-Type: application/otcet-stream
Content-Length: 32

Visit me: http://www.println.net
--9b670d44-63dc-4a8a-833d-66e45e0156ca--

我們看到,我們上傳的文件的內容出現在請求當中了。如果你需要上傳多個文件,就聲明多個 Part 參數,或者試試 PartMap 。

1.5 Converter,讓你的入參和返回類型豐富起來

1.5.1 RequestBodyConverter

1.4.3 當中,我為大家展示了如何用 Retrofit 上傳文件,這個上傳的過程其實。。還是有那么點兒不夠簡練,我們只是要提供一個文件用于上傳,可我們前后構造了三個對象:

天哪,肯定是哪里出了問題。實際上, Retrofit 允許我們自己定義入參和返回的類型,不過,如果這些類型比較特別,我們還需要準備相應的 Converter,也正是因為 Converter 的存在, Retrofit 在入參和返回類型上表現得非常靈活。

下面我們把剛才的 Service 代碼稍作修改:

public interface FileUploadService {  
    @Multipart
    @POST("upload")
    Call<ResponseBody> upload(@Part("description") RequestBody description,
        //注意這里的參數 "aFile" 之前是在創建 MultipartBody.Part 的時候傳入的
        @Part("aFile") File file);
}

現在我們把入參類型改成了我們熟悉的 File ,如果你就這么拿去發請求,服務端收到的結果會讓你哭了的。。。

RAW BODY

--7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21

This is a description
--7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name="aFile"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35

// 注意這里!!之前是文件的內容,現在變成了文件的路徑
{"path":"samples/uploadedfile.txt"} 
--7d24e78e-4354-4ed4-9db4-57d799b6efb7--

服務端收到了一個文件的路徑,它肯定會覺得

好了,不鬧了,這明顯是 Retrofit 在發現自己收到的實際入參是個 File 時,不知道該怎么辦,情急之下給 toString 了,而且還是個 JsonString (后來查證原來是使用了 GsonRequestBodyConverter。。)。

接下來我們就自己實現一個 FileRequestBodyConverter ,

static class FileRequestBodyConverterFactory extends Converter.Factory {
    @Override
    public Converter<File, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return new FileRequestBodyConverter();
    }
  }

  static class FileRequestBodyConverter implements Converter<File, RequestBody> {

    @Override
    public RequestBody convert(File file) throws IOException {
      return RequestBody.create(MediaType.parse("application/otcet-stream"), file);
    }
  }

在創建 Retrofit 的時候記得配置上它:

addConverterFactory(new FileRequestBodyConverterFactory())

這樣,我們的文件內容就能上傳了。來,看下結果吧:

RAW BODY

--25258f46-48b0-4a6b-a617-15318c168ed4
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21

This is a description
--25258f46-48b0-4a6b-a617-15318c168ed4
//注意看這里,filename 沒了
Content-Disposition: form-data; name="aFile"
//多了這一句
Content-Transfer-Encoding: binary
Content-Type: application/otcet-stream
Content-Length: 32

Visit me: http://www.println.net
--25258f46-48b0-4a6b-a617-15318c168ed4--

文件內容成功上傳了,當然其中還存在一些問題,這個目前直接使用 Retrofit 的 Converter 還做不到,原因主要在于我們沒有辦法通過 Converter 直接將 File 轉換為 MultiPartBody.Part ,如果想要做到這一點,我們可以對 Retrofit 的源碼稍作修改,這個我們后面再談。

1.5.2 ResponseBodyConverter

前面我們為大家簡單示例了如何自定義 RequestBodyConverter ,對應的, Retrofit 也支持自定義 ResponseBodyConverter 。

我們再來看下我們定義的接口:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

返回值的類型為 List<Repo> ,而我們直接拿到的原始返回肯定就是字符串(或者字節流),那么這個返回值類型是怎么來的呢?首先說明的一點是,GitHub 的這個 api 返回的是 Json 字符串,也就是說,我們需要使用 Json 反序列化得到 List<Repo> ,這其中用到的其實是 GsonResponseBodyConverter 。

問題來了,如果請求得到的 Json 字符串與返回值類型不對應,比如:

接口返回的 Json 字符串:

{"err":0, "content":"This is a content.", "message":"OK"}

返回值類型

class Result{
    int code;//等價于 err
    String body;//等價于 content
    String msg;//等價于 message
}

哇,這時候肯定有人想說,你是不是腦殘,偏偏跟服務端對著干?哈哈,我只是示例嘛,而且在生產環境中,你敢保證這種情況不會發生??

這種情況下, Gson 就是再牛逼,也只能默默無語倆眼淚了,它哪兒知道字段的映射關系怎么這么任性啊。好,現在讓我們自定義一個 Converter 來解決這個問題吧!

static class ArbitraryResponseBodyConverterFactory extends Converter.Factory{
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
      return super.responseBodyConverter(type, annotations, retrofit);
    }
  }

  static class ArbitraryResponseBodyConverter implements Converter<ResponseBody, Result>{

    @Override
    public Result convert(ResponseBody value) throws IOException {
      RawResult rawResult = new Gson().fromJson(value.string(), RawResult.class);
      Result result = new Result();
      result.body = rawResult.content;
      result.code = rawResult.err;
      result.msg = rawResult.message;
      return result;
    }
  }

  static class RawResult{
    int err;
    String content;
    String message;
  }

當然,別忘了在構造 Retrofit 的時候添加這個 Converter,這樣我們就能夠愉快的讓接口返回 Result 對象了。

注意!! Retrofit 在選擇合適的 Converter 時,主要依賴于需要轉換的對象類型,在添加 Converter 時,注意 Converter 支持的類型的包含關系以及其順序。

2、Retrofit 原理剖析

前一個小節我們把 Retrofit 的基本用法和概念介紹了一下,如果你的目標是學會如何使用它,那么下面的內容你可以不用看了。

不過呢,我就知道你不是那種淺嘗輒止的人!這一節我們主要把注意力放在 Retrofit 背后的魔法上面~~

2.1 是誰實際上完成了接口請求的處理?

前面講了這么久,我們始終只看到了我們自己定義的接口,比如:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

而真正我使用的時候肯定不能是接口啊,這個神秘的家伙究竟是誰?其實它是 Retrofit 創建的一個代理對象了,這里涉及點兒 Java 的動態代理的知識,直接來看代碼:

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    //這里返回一個 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);
            }
            //DefaultMethod 是 Java 8 的概念,是定義在 interface 當中的有實現的方法
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            //每一個接口最終實例化成一個 ServiceMethod,并且會緩存
            ServiceMethod serviceMethod = loadServiceMethod(method);

            //由此可見 Retrofit 與 OkHttp 完全耦合,不可分割
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            //下面這一句當中會發起請求,并解析服務端返回的結果
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

簡單的說,在我們調用 GitHubService.listRepos 時,實際上調用的是這里的 InvocationHandler.invoke 方法~~

2.2 來一發完整的請求處理流程

前面我們已經看到 Retrofit 為我們構造了一個 OkHttpCall ,實際上每一個 OkHttpCall 都對應于一個請求,它主要完成最基礎的網絡請求,而我們在接口的返回中看到的 Call 默認情況下就是 OkHttpCall 了,如果我們添加了自定義的 callAdapter ,那么它就會將 OkHttp 適配成我們需要的返回值,并返回給我們。

先來看下 Call 的接口:

我們在使用接口時,大家肯定還記得這一句:

這個 repos 其實就是一個 OkHttpCall 實例, execute 就是要發起網絡請求。

OkHttpCall.execute

我們看到 OkHttpCall 其實也是封裝了 okhttp3.Call ,在這個方法中,我們通過 okhttp3.Call 發起了進攻,額,發起了請求。有關 OkHttp 的內容,我在這里就不再展開了。

parseResponse 主要完成了由 okhttp3.Response 向 retrofit.Response 的轉換,同時也處理了對原始返回的解析:

Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
    ResponseBody rawBody = rawResponse.body();

    //略掉一些代碼
    try {
      //在這里完成了原始 Response 的解析,T 就是我們想要的結果,比如 GitHubService.listRepos 的 List<Repo>
      T body = serviceMethod.toResponse(catchingBody);
      return Response.success(body, rawResponse);
    } catch (RuntimeException e) {
      // If the underlying source threw an exception, propagate that rather than indicating it was
      // a runtime exception.
      catchingBody.throwIfCaught();
      throw e;
    }
  }

至此,我們就拿到了我們想要的數據~~

 

來自:http://dev.qq.com/topic/591aa71ae315487c53deeca9

 

 本文由用戶 mspe9393 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!