1. 程式人生 > >Retrofit+okhttp網路框架介紹

Retrofit+okhttp網路框架介紹

網路框架選擇過程

目前Github上使用比較多的Android Http庫依次是Retrofit,okhttp,android-async-http,okhttp-utils,async-http-client,等等;
中間經過一系列的分析,根據start,更新速度,作者,庫的實用性,效能,以及拓展性等等,最終選出了3個PK.(Retrofit,okhttp,okhttp-utils)
其中Retrofit,okhttp都是square公司出的okhttp是底層庫,使用起來比較麻煩,肯定需要2次封裝,這裡okhttp-utils是洪洋大神在okhttp上進行的封裝開源庫,剛好彌補了這個缺陷,是目前封裝的比較好的。
而Retrofit同樣也是在okhttp上進行封裝的,使用的是註解,使用起來相當方便,並且有很好的擴充套件性,PK下來。
Retrofit獲勝。

這裡說下自己所瞭解到的

1、okhttp 和 async http是一個基礎的通訊庫,都很強大,但需要自己封裝使用才更方便。另外okhttp已經被谷歌官方用在android原始碼中了。
2、retrofit和 volley是屬於比較高階點的封裝庫了 其中 retrofit是預設使用okhttp volley也支援okhttp作為其底層通訊的部件。retrofit的特點是使用清晰簡單的介面,非常方便,而 volley在使用的時候也還簡單,不過要使用高階一點的功能需要自己自定義很多東西
3、volley是一個簡單的非同步http庫,僅此而已。缺點是不支援同步,這點會限制開發模式;不能post大資料,所以不適合用來上傳檔案。
4、android-async-http。與volley一樣是非同步網路庫,但volley是封裝的httpUrlConnection,它是封裝的httpClient,而android平臺不推薦用HttpClient了,所以這個庫已經不適合android平臺了。
5、okhttp是高效能的http庫,支援同步、非同步,而且實現了spdy、http2、websocket協議,api很簡潔易用,和volley一樣實現了http協議的快取。
6、retrofit與picasso一樣都是在okhttp基礎之上做的封裝,專案中可以直接用了。
7、retrofit主要針對的是url請求 ,okhttp在實際專案中直接用很麻煩,一般需要自己封裝一下(有一個大坑是他onResponse方法預設是在非同步執行緒,不能直接操作UI

既然選擇了Retrofit,那我們就來一起了解一下吧。本次分享要求:理解Retrofit與okhttp的區別,簡單的學會使用Retrofit;

這裡也主要是根據官網給出的案列進行推進講解:

介紹:

Retrofit和Java領域的ORM概念類似, ORM把結構化資料轉換為Java物件,而Retrofit 把REST API返回的資料轉化為Java物件方便操作,並且retrofit非常適合於restful url格式的請求,更多使用註解的方式提供功能。

引用方法:

compile ‘com.squareup.retrofit2:retrofit:2.0.2’

retrofit 用法示例

(1)一般的get請求

retrofit在使用的過程中,需要定義一個介面物件,我們首先演示一個最簡單的get請求,介面如下所示:

public interface IUserBiz
{
    @GET("users")
    Call<List<User>> getUsers();
}

可以看到有一個getUsers()方法,通過@GET註解標識為get請求,@GET中所填寫的value和baseUrl組成完整的路徑,baseUrl在構造retrofit物件時給出。

下面看如何通過retrofit完成上述的請求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://192.168.31.242:8080/springmvc_users/user/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
IUserBiz userBiz = retrofit.create(IUserBiz.class);
Call<List<User>> call = userBiz.getUsers();
        call.enqueue(new Callback<List<User>>()
        {
            @Override
            public void onResponse(Call<List<User>> call, Response<List<User>> response)
            {
                Log.e(TAG, "normalGet:" + response.body() + "");
            }

            @Override
            public void onFailure(Call<List<User>> call, Throwable t)
            {

            }
        });

依然是構造者模式,指定了baseUrl和Converter.Factory,該物件通過名稱可以看出是用於物件轉化的,本例因為伺服器返回的是json格式的陣列,所以這裡設定了GsonConverterFactory完成物件的轉化。

ok,這裡可以看到很神奇,我們通過Retrofit.create就可以拿到我們定義的IUserBiz的例項,呼叫其方法即可拿到一個Call物件,通過call.enqueue即可完成非同步的請求。

具體retrofit怎麼得到我們介面的例項的,以及物件的返回結果是如何轉化的,我們後面具體分析。

版本與細節點分析:

一、URL拼接:

新的URL定義方式
Retrofit 2.0使用了新的URL定義方式。Base URL與@Url 不是簡單的組合在一起而是和<a href="...">的處理方式一致。用下面的幾個例子闡明。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

ps:貌似第二個才符合習慣。

對於 Retrofit 2.0中新的URL定義方式,這裡是我的建議:

  • Base URL: 總是以 /結尾

  • @Url: 不要以 / 開頭

比如 上例的那種

而且在Retrofit 2.0中我們還可以在@Url裡面定義完整的URL:

public interface IUserBiz
{
    @POST("http://192.168.31.242:8080/springmvc_users/user/users")
    Call<List<User>> getUsers();
}

這種情況下Base URL會被忽略,適合一個專案會出現多個BaseUrl的情況。

可以看到Retrofit 1.9在URL的處理方式上發生了很大變化。它和前面的版本完全不同。如果你想把程式碼遷移到Retrofit 2.0,別忘了修正URL部分的程式碼。

二、同步與非同步的區別:

新的Service定義方式,不再有同步和非同步之分
關於在Retrofit 1.9中service 介面的定義,如果你想定義一個同步的函式,你應該這樣定義:

/* Synchronous in Retrofit 1.9 */

public interface APIService {

    @POST("/list")
    Repo loadRepo();

}

而定義一個非同步的則是這樣:

/* Asynchronous in Retrofit 1.9 */

public interface APIService {

    @POST("/list")
    void loadRepo(Callback<Repo> cb);

}

但是在Retrofit 2.0上,只能定義一個模式,因此要簡單得多。

/* Retrofit 2.0 */

public interface APIService {

    @POST("/list")
    Call<Repo> loadRepo();

}

而建立service 的方法也變得和OkHttp的模式一模一樣。如果要呼叫同步請求,只需呼叫execute;而發起一個非同步請求則是呼叫enqueue。

同步請求

// Synchronous Call in Retrofit 2.0

Call<Repo> call = service.loadRepo();
Repo repo = call.execute();

以上的程式碼會阻塞執行緒,因此你不能在安卓的主執行緒中呼叫,不然會面臨NetworkOnMainThreadException。如果你想呼叫execute方法,請在後臺執行緒執行。

非同步請求

// Synchronous Call in Retrofit 2.0

Call<Repo> call = service.loadRepo();
call.enqueue(new Callback<Repo>() {
    @Override
    public void onResponse(Response<Repo> response) {
        // Get result Repo from response.body()
    }

    @Override
    public void onFailure(Throwable t) {

    }
});

以上程式碼發起了一個在後臺執行緒的請求並從response 的response.body()方法中獲取一個結果物件。注意這裡的onResponse和onFailure方法是在主執行緒中呼叫的。

我建議你使用enqueue,它最符合 Android OS的習慣。

取消正在進行中的業務
service 的模式變成Call的形式的原因是為了讓正在進行的事務可以被取消。要做到這點,你只需呼叫call.cancel()。

call.cancel();

事務將會在之後立即被取消。好簡單嘿嘿!

三Converter現在從Retrofit中刪除

在Retrofit 1.9中,GsonConverter 包含在了package 中而且自動在RestAdapter建立的時候被初始化。這樣來自伺服器的son結果會自動解析成定義好了的Data Access Object(DAO)

但是在Retrofit 2.0中,Converter 不再包含在package 中了。你需要自己插入一個Converter 不然的話Retrofit 只能接收字串結果。同樣的,Retrofit 2.0也不再依賴於Gson 。

如果你想接收json 結果並解析成DAO,你必須把Gson Converter 作為一個獨立的依賴新增進來。

compile 'com.squareup.retrofit:converter-gson:2.0.2'

然後使用addConverterFactory把它新增進來。注意RestAdapter的別名仍然為Retrofit。

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://api.nuuneoi.com/base/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();

service = retrofit.create(APIService.class);

這裡是Square提供的官方Converter modules列表。選擇一個最滿足你需求的。

Gson: com.squareup.retrofit:converter-gson

Jackson: com.squareup.retrofit:converter-jackson

Moshi: com.squareup.retrofit:converter-moshi

Protobuf: com.squareup.retrofit:converter-protobuf

Wire: com.squareup.retrofit:converter-wire

Simple XML: com.squareup.retrofit:converter-simplexml

你也可以通過實現Converter.Factory介面來建立一個自定義的converter 。

我比較贊同這種新的模式。它讓Retrofit對自己要做的事情看起來更清晰。
當然也支援自定義,你可以選擇自己寫轉化器完成資料的轉化,這個後面將具體介紹。

那麼,通過這麼一個簡單的例子,應該對retrofit已經有了一個直觀的認識,下面看更多其支援的特性。

以下是引用洪洋的部落格內容:

(2)動態的url訪問@PATH

文章開頭提過,retrofit非常適用於restful url的格式,那麼例如下面這樣的url:

即通過不同的username訪問不同使用者的資訊,返回資料為json字串。

那麼可以通過retrofit提供的@PATH註解非常方便的完成上述需求。

我們再定義一個方法:

public interface IUserBiz
{
    @GET("{username}")
    Call<User> getUser(@Path("username") String username);
}

可以看到我們定義了一個getUser方法,方法接收一個username引數,並且我們的@GET註解中使用{username}聲明瞭訪問路徑,這裡你可以把{username}當做佔位符,而實際執行中會通過@PATH(“username”)所標註的引數進行替換。

那麼訪問的程式碼很類似:

//省略了retrofit的構建程式碼
Call<User> call = userBiz.getUser("zhy");
//Call<User> call = userBiz.getUser("lmj");
call.enqueue(new Callback<User>()
{

    @Override
    public void onResponse(Call<User> call, Response<User> response)
    {
        Log.e(TAG, "getUsePath:" + response.body());
    }

    @Override
    public void onFailure(Call<User> call, Throwable t)
    {

    }
});

(3)查詢引數的設定@Query

看下面的url

public interface IUserBiz
{
    @GET("users")
    Call<List<User>> getUsersBySort(@Query("sortby") String sort);
}

訪問的程式碼,其實沒什麼寫的:

//省略retrofit的構建程式碼
Call<List<User>> call = userBiz.getUsersBySort("username");
//Call<List<User>> call = userBiz.getUsersBySort("id");
//省略call執行相關程式碼

ok,這樣我們就完成了引數的指定,當然相同的方式也適用於POST,只需要把註解修改為@POST即可。

對了,我能剛才學了@PATH,那麼會不會有這樣嘗試的衝動,對於剛才的需求,我們這麼寫:

@GET("users?sortby={sortby}")
 Call<List<User>> getUsersBySort(@Path("sortby") String sort);

乍一看別說好像有點感覺,哈,實際上執行是不支援的~估計是@ Path的定位就是用於url的路徑而不是引數,對於引數還是選擇通過@Query來設定。

(4)POST請求體的方式向伺服器傳入json字串@Body

大家都清楚,我們app很多時候跟伺服器通訊,會選擇直接使用POST方式將json字串作為請求體傳送到伺服器,那麼我們看看這個需求使用retrofit該如何實現。

再次新增一個方法:

public interface IUserBiz
{
 @POST("add")
 Call<List<User>> addUser(@Body User user);
}

提交的程式碼其實基本都是一致的:

//省略retrofit的構建程式碼
 Call<List<User>> call = userBiz.addUser(new User(1001, "jj", "123,", "jj123", "[email protected]"));
//省略call執行相關程式碼

ok,可以看到其實就是使用@Body這個註解標識我們的引數物件即可,那麼這裡需要考慮一個問題,retrofit是如何將user物件轉化為字串呢?下文將詳細解釋~

下面對應okhttp,還有兩種requestBody,一個是FormBody,一個是MultipartBody,前者以表單的方式傳遞簡單的鍵值對,後者以POST表單的方式上傳檔案可以攜帶引數,retrofit也二者也有對應的註解,下面繼續~

(5)表單的方式傳遞鍵值對@FormUrlEncoded

這裡我們模擬一個登入的方法,新增一個方法:

public interface IUserBiz
{
    @POST("login")
    @FormUrlEncoded
    Call<User> login(@Field("username") String username, @Field("password") String password);
}

訪問的程式碼:

//省略retrofit的構建程式碼
Call<User> call = userBiz.login("zhy", "123");
//省略call執行相關程式碼

ok,看起來也很簡單,通過@POST指明url,新增FormUrlEncoded,然後通過@Field新增引數即可。

(6)單檔案上傳@Multipart

下面看一下單檔案上傳,依然是再次添加個方法:

public interface IUserBiz
{
    @Multipart
    @POST("register")
    Call<User> registerUser(@Part MultipartBody.Part photo, @Part("username") RequestBody username, @Part("password") RequestBody password);
}

這裡@MultiPart的意思就是允許多個@Part了,我們這裡使用了3個@Part,第一個我們準備上傳個檔案,使用了MultipartBody.Part型別,其餘兩個均為簡單的鍵值對。

使用:

File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "icon.png", photoRequestBody);

Call<User> call = userBiz.registerUser(photo, RequestBody.create(null, "abc"), RequestBody.create(null, "123"));

ok,這裡感覺略為麻煩。不過還是蠻好理解~~多個@Part,每個Part對應一個RequestBody。

這裡插個實驗過程,其實我最初對於檔案,也是嘗試的@Part RequestBody,因為@Part(“key”),然後傳入一個代表檔案的RequestBody,我覺得更加容易理解,後來發現試驗無法成功,而且查了下issue,給出了一個很奇怪的解決方案,這裡可以參考:retrofit#1063。

給出了一個類似如下的方案:

public interface ApiInterface {
        @Multipart
        @POST ("/api/Accounts/editaccount")
        Call<User> editUser (@Header("Authorization") String authorization, @Part("file\"; filename=\"pp.png") RequestBody file , @Part("FirstName") RequestBody fname, @Part("Id") RequestBody id);
    }

可以看到對於檔案的那個@Partvalue竟然寫了這麼多奇怪的東西,而且filename竟然硬編碼了~~這個不好吧,我上傳的檔名竟然不能動態指定。

為了檔名不會被寫死,所以給出了最上面的上傳單檔案的方法,ps:上面這個方案經測試也是可以上傳成功的。

恩,這個奇怪方案,為什麼這麼做可行,下文會給出非常詳細的解釋。

最後看下多檔案上傳~

(7)多檔案上傳@PartMap

再新增一個方法~~~

public interface IUserBiz
 {
     @Multipart
     @POST("register")
      Call<User> registerUser(@PartMap Map<String, RequestBody> params,  @Part("password") RequestBody password);
}

這裡使用了一個新的註解@PartMap,這個註解用於標識一個Map,Map的key為String型別,代表上傳的鍵值對的key(與伺服器接受的key對應),value即為RequestBody,有點類似@Part的封裝版本。

執行的程式碼:

File file = new File(Environment.getExternalStorageDirectory(), "messenger_01.png");
        RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
Map<String,RequestBody> photos = new HashMap<>();
photos.put("photos\"; filename=\"icon.png", photo);
photos.put("username",  RequestBody.create(null, "abc"));

Call<User> call = userBiz.registerUser(photos, RequestBody.create(null, "123"));

可以看到,可以在Map中put進一個或多個檔案,鍵值對等,當然你也可以分開,單獨的鍵值對也可以使用@Part,這裡又看到設定檔案的時候,相對應的key很奇怪,例如上例”photos\”; filename=\”icon.png”,前面的photos就是與伺服器對應的key,後面filename是伺服器得到的檔名,ok,引數雖然奇怪,但是也可以動態的設定檔名,不太影響使用~~

(8)下載檔案

這個其實我覺得直接使用okhttp就好了,使用retrofit去做這個事情真的有點瞎用的感覺~~

增加一個方法:

@GET("download")
Call<ResponseBody> downloadTest();

呼叫:

Call<ResponseBody> call = userBiz.downloadTest();
call.enqueue(new Callback<ResponseBody>()
{
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
    {
        InputStream is = response.body().byteStream();
        //save file
    }

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

    }
});

可以看到可以返回ResponseBody,那麼很多事都能幹了~~

but,也看出這種方式下載感覺非常雞肋,並且onReponse回撥雖然在UI執行緒,但是你還是要處理io操作,也就是說你在這裡還要另外開執行緒操作,或者你可以考慮同步的方式下載。

最後還是建議使用okhttp去下載,例如使用okhttputils.

有人可能會問,使用okhttp,和使用retrofit會不會造成新建多個OkHttpClient物件呢,其實是可設定的,參考下文。

ok,上面就是一些常用的方法,當然還涉及到一些沒有介紹的註解,但是通過上面這麼多方法的介紹,再多一二個註解的使用方式,相信大家能夠解決。

總結:

首先構造retrofit,幾個核心的引數呢,主要就是baseurl,callFactory(預設okhttpclient),converterFactories,adapterFactories,excallbackExecutor。
然後通過create方法拿到介面的實現類,這裡利用Java的Proxy類完成動態代理的相關代理
在invoke方法內部,拿到我們所宣告的註解以及實參等,構造ServiceMethod,ServiceMethod中解析了大量的資訊,最痛可以通過toRequest構造出okhttp3.Request物件。有了okhttp3.Request物件就可以很自然的構建出okhttp3.call,最後calladapter對Call進行裝飾返回。
拿到Call就可以執行enqueue或者execute方法了