快速Android開發系列網路篇之Retrofit

AngelDevil發表於2014-05-29

Retrofit是一個不錯的網路請求庫,用官方自己的介紹就是:

A type-safe REST client for Android and Java

看官網的介紹用起來很省事,不過如果不瞭解它是怎麼實現的也不太敢用,不然出問題了就不知道怎麼辦了。這幾天比較閒就下下來看了一下,瞭解一下大概實現方法,細節就不追究了。先來看一個官網的例子,詳細說明去網官

簡單示例

首先定義請求介面,即程式中都需要什麼請求操作

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

 然後通過RestAdapter生成一個剛才定義的介面的實現類,使用的是動態代理。

RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.github.com")
    .build();

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

現在就可以呼叫介面進行請求了

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

使用就是這麼簡單,請求時直接呼叫介面就行了,甚至不用封裝引數,因為引數的資訊已經在定義介面時通過Annotation定義好了。

從上面的例子可以看到介面直接返回了需要的Java型別,而不是byte[]或String,解析資料的地方就是Converter,這個是可以自定義的,預設是用Gson解析,也就是說預設認為伺服器返回的是Json資料,可以通過指定不同的Convert使用不同的解析方法,如用Jackson解析Json,或自定義XmlConvert解析xml資料。

Retrofit的使用就是以下幾步:

  1. 定義介面,引數宣告,Url都通過Annotation指定
  2. 通過RestAdapter生成一個介面的實現類(動態代理)
  3. 呼叫介面請求資料

介面的定義要用用Rtrofit定義的一些Annotation,所以先看一下Annotation的。

Annotation

以上面的示例中的介面來看

@GET("/group/{id}/users")
List<User> groupList(@Path("id") int groupId);

 先看@GET

/** Make a GET request to a REST path relative to base URL. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod("GET")
public @interface GET {
  String value();
}

@GET本身也被幾個Anotation註解,@Target表示@GET註解是用於方法的,value方法就返回這個註解的value值,在上例中就是/group/{id}/users,然後就是@RestMethod

@Documented
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
public @interface RestMethod {
  String value();
  boolean hasBody() default false;
}

RestMethod是一個用於Annotation的Annotation,比如上面的例子中用來註解的@GET,value方法就返回GET,hasBody表示是否有Body,對於POST這個方法就返回true

@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod(value = "POST", hasBody = true)
public @interface POST {
  String value();
}

Retrofit的Annotation包含請求方法相關的@GET、@POST、@HEAD、@PUT、@DELETA、@PATCH,和引數相關的@Path、@Field、@Multipart等。

定義了Annotation要就有解析它的方法,在Retrofit中解析的位置就是RestMethodInfo,但在這之前需要先看哪裡使用了RestMethodInfo,前面說了Retrofit使用了動態代理生成了我們定義的介面的實現類,而這個實現類是通過RestAdapter.create返回的,所以使用動態代理的位置就是RestAdapter,接下來就看一下RestAdapter

RestAdapter

RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.github.com")
    .build();

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

public RestAdapter build() {
  if (endpoint == null) {
    throw new IllegalArgumentException("Endpoint may not be null.");
  }
  
  ensureSaneDefaults();
  
  return new RestAdapter(endpoint, clientProvider, httpExecutor, callbackExecutor,
      requestInterceptor, converter, profiler, errorHandler, log, logLevel);
}

setEndPoint就不說了,介面中定義的都是相對Url,EndPoint就是域名,build方法呼叫ensureSaneDefaults()方法,然後就構造了一個RestAdapter物件,建構函式的引數中傳入了EndPoint外的幾個物件,這幾個物件就是在ensureSaneDefaults()中初始化的。

private void ensureSaneDefaults() {
  if (converter == null) { converter = Platform.get().defaultConverter(); }
  if (clientProvider == null) { clientProvider = Platform.get().defaultClient(); }
  if (httpExecutor == null) { httpExecutor = Platform.get().defaultHttpExecutor(); }
  if (callbackExecutor == null) { callbackExecutor = Platform.get().defaultCallbackExecutor(); }
  if (errorHandler == null) { errorHandler = ErrorHandler.DEFAULT; }
  if (log == null) { log = Platform.get().defaultLog(); }
  if (requestInterceptor == null) { requestInterceptor = RequestInterceptor.NONE; }
}

ensureSaneDefaults()中初始化了很多成員,errorHandler、log就不看了,其他的除了requestInterceptor都是通過Platform物件獲得的,所以要先看下Platform

Platform

private static final Platform PLATFORM = findPlatform();
  static final boolean HAS_RX_JAVA = hasRxJavaOnClasspath();

  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) {
    }

    if (System.getProperty("com.google.appengine.runtime.version") != null) {
      return new AppEngine();
    }

    return new Base();
  }

使用了單例的PLATFORM,通過findPlatform()初始化例項,如果是Android平臺就使用Platform.Android,如果是Google AppEngine就使用Platform.AppEngine,否則使用Platform.Base,這些都是Platform的子類,其中AppEngine又是Base的子類。

Platform是一個抽象類,定義了以下幾個抽象方法,這幾個方法的作用就是返回一些RestAdapter中需要要用到成員的預設實現

abstract Converter defaultConverter(); // 預設的Converter,用於將請求結果轉化成需要的資料,如GsonConverter將JSON請求結果用Gson解析成Java物件
  abstract Client.Provider defaultClient(); // Http請求類,如果是AppEngine就使用`UrlFetchClient`,否則如果有OKHttp就使用OKHttp,如果是Android,2.3以後使用HttpURLConnection,2.3以前使用HttpClient
  abstract Executor defaultHttpExecutor(); // 用於執行Http請求的Executor
  abstract Executor defaultCallbackExecutor(); // Callback呼叫中用於執行Callback的Executor(可能是同步的)
  abstract RestAdapter.Log defaultLog(); // Log介面,用於輸出Log
看完Platform的介面再看ensureSaneDefaults就清楚了,初始化轉化資料的Converter、執行請求的Client、執行請求的Executor、執行Callback的Executor、Log輸出類、錯誤處理類和用於在請求前新增額外處理的攔截請求的Interceptor。

Converter預設都是用的GsonConverter,就不看了,defaultClient返回執行網路請求的Client

Platform.Android

@Override Client.Provider defaultClient() {
  final Client client;
  if (hasOkHttpOnClasspath()) {
    client = OkClientInstantiator.instantiate();
  } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
    client = new AndroidApacheClient();
  } else {
    client = new UrlConnectionClient();
  }
  return new Client.Provider() {
    @Override public Client get() {
      return client;
    }
  };
}
Platform.Base
@Override Client.Provider defaultClient() {
  final Client client;
  if (hasOkHttpOnClasspath()) {
    client = OkClientInstantiator.instantiate();
  } else {
    client = new UrlConnectionClient();
  }
  return new Client.Provider() {
    @Override public Client get() {
      return client;
    }
  };
}

 Platform.AppEngine

@Override Client.Provider defaultClient() {
  final UrlFetchClient client = new UrlFetchClient();
  return new Client.Provider() {
    @Override public Client get() {
      return client;
    }
  };
}

對於Android,優先使用OKHttp,否則2.3以後使用HttpUrlConnection,2.3以前使用HttpClient

defaultHttpExecutor就是返回一個Executor,執行請求的執行緒在這個Executor中執行,就做了一件事,把執行緒設定為後臺執行緒

defaultCallbackExecutor用於執行Callback型別的請求時,提供一個Executor執行Callback的Runnable

Platform.Base

@Override Executor defaultCallbackExecutor() {
    return new Utils.SynchronousExecutor();
}

Platform.Android

@Override Executor defaultCallbackExecutor() {
    return new MainThreadExecutor();
}

SynchronousExecutor

static class SynchronousExecutor implements Executor {
    @Override public void execute(Runnable runnable) {
      runnable.run();
    }
}
MainThreadExecutor
public final class MainThreadExecutor implements Executor {
  private final Handler handler = new Handler(Looper.getMainLooper());

  @Override public void execute(Runnable r) {
    handler.post(r);
  }
}
如果是Android,通過Handler將回撥傳送到主執行緒執行,如果非Android,直接同步執行。

Platform看完了,RestAdapter的成員初始化完成,就要看怎麼通過RestAdapter.create生成我們定義的介面的實現類了

RestAdapter.create

  public <T> T create(Class<T> service) {
    Utils.validateServiceClass(service);
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new RestHandler(getMethodInfoCache(service)));
  }
  
  Map<Method, RestMethodInfo> getMethodInfoCache(Class<?> service) {
    synchronized (serviceMethodInfoCache) {
      Map<Method, RestMethodInfo> methodInfoCache = serviceMethodInfoCache.get(service);
      if (methodInfoCache == null) {
        methodInfoCache = new LinkedHashMap<Method, RestMethodInfo>();
        serviceMethodInfoCache.put(service, methodInfoCache);
      }
      return methodInfoCache;
    }
  }

使用了動態代理,InvocationHandlerRestHandlerRestHandler有一個引數,是Method->RestMethodInfo的對映,初始化時這個對映是空的。重點就是這兩個了:RestHandlerRestMethodInfo,

@Override public Object invoke(Object proxy, Method method, final Object[] args)
    throws Throwable {
  // If the method is a method from Object then defer to normal invocation.
  if (method.getDeclaringClass() == Object.class) { // 1
    return method.invoke(this, args);
  }
  
  // Load or create the details cache for the current method.
  final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method); // 2
  
  if (methodInfo.isSynchronous) { // 3
    try {
      return invokeRequest(requestInterceptor, methodInfo, args);
    } catch (RetrofitError error) {
      Throwable newError = errorHandler.handleError(error);
      if (newError == null) {
        throw new IllegalStateException("Error handler returned null for wrapped exception.",
            error);
      }
      throw newError;
    }
  }
  
  if (httpExecutor == null || callbackExecutor == null) {
    throw new IllegalStateException("Asynchronous invocation requires calling setExecutors.");
  }
  
  // Apply the interceptor synchronously, recording the interception so we can replay it later.
  // This way we still defer argument serialization to the background thread.
  final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
  requestInterceptor.intercept(interceptorTape); // 4
  
  if (methodInfo.isObservable) { // 5
    if (rxSupport == null) {
      if (Platform.HAS_RX_JAVA) {
        rxSupport = new RxSupport(httpExecutor, errorHandler);
      } else {
        throw new IllegalStateException("Observable method found but no RxJava on classpath");
      }
    }
    
    return rxSupport.createRequestObservable(new Callable<ResponseWrapper>() {
      @Override public ResponseWrapper call() throws Exception {
        return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
      }
    });
  }
  
  Callback<?> callback = (Callback<?>) args[args.length - 1]; // 6
  httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
    @Override public ResponseWrapper obtainResponse() {
      return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
    }
  });
  
  return null; // Asynchronous methods should have return type of void.
}

執行請求時會呼叫RestHandlerinvoke方法,如上所示,主要是上面程式碼中標註有6點

  1. 如果呼叫的是Object的方法,不做處理直接呼叫。
  2. 通過getMethodInfo獲取呼叫的Method對應的RestMethodInfo,前面說了,構造RestHandler物件時傳進來了一個Method->RestMethodInfo的對映,初始時是空的。
  static RestMethodInfo getMethodInfo(Map<Method, RestMethodInfo> cache, Method method) {
    synchronized (cache) {
      RestMethodInfo methodInfo = cache.get(method);
      if (methodInfo == null) {
        methodInfo = new RestMethodInfo(method);
        cache.put(method, methodInfo);
      }
      return methodInfo;
    }

getMethodInfo中判斷如果相應的對映不存在,就建立這個對映,並如名字所示快取起來 
3. 如果是同步呼叫(介面中直接返回資料,不通過Callback或Observe),直接呼叫invokeRequest 
4. 如果是非同步呼叫,先通過RequestInterceptorTape記錄攔截請求,記錄後在後臺執行緒做實際攔截,後面會提到。 
5. 如果是Observe請求(RxJava),執行第5步,對RxJava不瞭解,略過 
6. 如果是Callback形式,交由執行緒池執行

介面中的每一個Method有一個對應的RestMethodInfo,關於介面中Annotation資訊的處理就都在這裡了

RestMethodInfo

private enum ResponseType {
    VOID,
    OBSERVABLE,
    OBJECT
}
RestMethodInfo(Method method) {
    this.method = method;
    responseType = parseResponseType();
    isSynchronous = (responseType == ResponseType.OBJECT);
    isObservable = (responseType == ResponseType.OBSERVABLE);
}

在建構函式中呼叫了parseResponseTypeparseResponseType解析了方法簽名,根據方法的返回值型別及最後一個引數的型別判斷方法的型別是哪種ResponseType

無論是哪種ResponseType,最終都是呼叫invokeRequest執行實際的請求,接下來依次看下invokeRequest的執行步驟

RestAdapter.invokeRequest

第一步是呼叫methodInfo.init()解析呼叫的方法,方法裡有做判斷,只在第一次呼叫時解析,因為處一次解析後這個物件就被快取起來了,下次調同一個方法時可以直接使用

  synchronized void init() {
    if (loaded) return;

    parseMethodAnnotations();
    parseParameters();

    loaded = true;
  }

RestMethodInfo.init中分別呼叫

  • parseMethodAnnotations():解析所有方法的Annotation
  • parseParameters():解析所有引數的Annotation
for (Annotation methodAnnotation : method.getAnnotations()) {
  Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
  RestMethod methodInfo = null;
  // Look for a @RestMethod annotation on the parameter annotation indicating request method.
  for (Annotation innerAnnotation : annotationType.getAnnotations()) {
    if (RestMethod.class == innerAnnotation.annotationType()) {
      methodInfo = (RestMethod) innerAnnotation;
      break;
    }
  }
  ...
}
parseMethodAnnotations中,會獲取方法所有的Annotation並遍歷:
  • 對於每一個Annotation,也會獲取它的Annotation,看它是否是被RestMethod註解的Annotation,如果是,說明是@GET,@POST型別的註解,就呼叫parsePath解析請求的Url,requestParam(URL中問號後的內容)及Url中需要替換的引數名(Url中大括號括起來的部分)
  • 尋找Headers Annotation解析Header引數
  • 解析RequestType:SIMPLE,MULTIPART,FORM_URL_ENCODED

parseParameters解析請求引數,即引數的Annotation,@PATH@HEADER@FIELD

第二步是RequestBuilder和Interceptor,這兩個是有關聯的,所以一起看。

RequestBuilder requestBuilder = new RequestBuilder(serverUrl, methodInfo, converter);
requestBuilder.setArguments(args);
requestInterceptor.intercept(requestBuilder);
Request request = requestBuilder.build();

先說RequestInterceptor,作用很明顯,當執行請求時攔截請求以做一些特殊處理,比如新增一些額外的請求引數。

/** Intercept every request before it is executed in order to add additional data. */
public interface RequestInterceptor {
  /** Called for every request. Add data using methods on the supplied {@link RequestFacade}. */
  void intercept(RequestFacade request);

  interface RequestFacade {
    void addHeader(String name, String value);
    void addPathParam(String name, String value);
    void addEncodedPathParam(String name, String value);
    void addQueryParam(String name, String value);
    void addEncodedQueryParam(String name, String value);
  }

  /** A {@link RequestInterceptor} which does no modification of requests. */
  RequestInterceptor NONE = new RequestInterceptor() {
    @Override public void intercept(RequestFacade request) {
      // Do nothing.
    }
  };
}

RequestInterceptor只有一個方法intercept,接收一個RequestFacade引數,RequestFacadeRequestInterceptor內部的一個介面,這個介面的方法就是新增請求引數,Query、Header什麼的。大概可以看出RequestInterceptor的作用了,如果RequestFacade表示一個請求相關的資料,RequestInteceptor.intercept的作用就是向這個RequestFacade中新增額外Header,Param等引數。

RequestFacade的一個子類叫RequestBuilder,用來處理Request請求引數,在invokeRequest中會對RequestBuilder呼叫intercept方法向RequestBuilder新增額外的引數。

有一個叫RequestInterceptorTape的類,同時實現了RequestFacadeRequestInterceptor,它的作用是:

  • 當作為RequestFacade使用時作為引數傳給一個RequestInteceptor,這個RequestInterceptor呼叫它的addHeader等方法時,它把這些呼叫及引數記錄下來
  • 然後作為RequestInterceptor使用時,將之前記錄的方法呼叫及引數重新應用到它的intercept引數RequestFacade

RestHandler.invoke中,如果判斷方法的呼叫不是同步呼叫,就通過下面的兩行程式碼將使用者設定的interceptor需要新增的引數記錄到RequestInterceptorTape,然後在invokeRequest中再實際執行引數的新增。

// Apply the interceptor synchronously, recording the interception so we can replay it later.
// This way we still defer argument serialization to the background thread.
final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
requestInterceptor.intercept(interceptorTape);

 RequestBuilder.setArguments()解析呼叫介面時的實際引數。然後通過build()方法生成一個Request物件

第三步執行請求,Response response = clientProvider.get().execute(request);

第四步就是解析並分發請求結果了,成功請求時返回結果,解析失敗呼叫ErrorHandler給使用者一個自定義異常的機會,但最終都是通過異常丟擲到invoke()中的,如果是同步呼叫,直接拋異常,如果是Callback呼叫,會回撥Callback.failure

CallbackRunnable

請求型別有同步請求,Callback請求,Observable請求,來看下Callback請求:

Callback<?> callback = (Callback<?>) args[args.length - 1];
httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
    @Override public ResponseWrapper obtainResponse() {
      return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
    }
});

 Callback請求中函式最後一個引數是一個Callback的例項,httpExecutor是一個Executor,用於執行Runnable請求,我們看到,這裡new了一個CallbackRunnable執行,並實現了它的obtainResponse方法,看實現:

abstract class CallbackRunnable<T> implements Runnable {
  private final Callback<T> callback;
  private final Executor callbackExecutor;
  private final ErrorHandler errorHandler;

  CallbackRunnable(Callback<T> callback, Executor callbackExecutor, ErrorHandler errorHandler) {
    this.callback = callback;
    this.callbackExecutor = callbackExecutor;
    this.errorHandler = errorHandler;
  }

  @SuppressWarnings("unchecked")
  @Override public final void run() {
    try {
      final ResponseWrapper wrapper = obtainResponse();
      callbackExecutor.execute(new Runnable() {
        @Override public void run() {
          callback.success((T) wrapper.responseBody, wrapper.response);
        }
      });
    } catch (RetrofitError e) {
      Throwable cause = errorHandler.handleError(e);
      final RetrofitError handled = cause == e ? e : unexpectedError(e.getUrl(), cause);
      callbackExecutor.execute(new Runnable() {
        @Override public void run() {
          callback.failure(handled);
        }
      });
    }
  }

  public abstract ResponseWrapper obtainResponse();
} 
就是一個普通的Runnable,在run方法中首先執行obtailResponse,從名字可以看到是執行請求返回Response,這個從前面可以看到執行了invokeRequest,和同步呼叫中一樣執行請求。

緊接著就提交了一個Runnable至callbackExecutor,在看Platform時看到了callbackExecotor是通過Platform.get().defaultCallbackExecutor()返回的,Android中是向主執行緒的一個Handler發訊息

值得注意的事,對於同步呼叫,如果遇到錯誤是直接拋異常,而對於非同步呼叫,是呼叫Callback.failure()

Mime

執行網路請求,需要向服務端傳送請求引數,如表單資料,上傳的檔案等,同樣需要解析服務端返回的資料,在Retrofit中對這些做了封裝,位於Mime包中,也只有封裝了,才好統一由指定的Converter執行資料的轉換

TypedInputTypedOutput表示輸入輸出的資料,都包含mimeType,並分別支援讀入一個InputStream或寫到一個OutputStrem

/**
 * Binary data with an associated mime type.
 *
 * @author Jake Wharton (jw@squareup.com)
 */
public interface TypedInput {

  /** Returns the mime type. */
  String mimeType();

  /** Length in bytes. Returns {@code -1} if length is unknown. */
  long length();

  /**
   * Read bytes as stream. Unless otherwise specified, this method may only be called once. It is
   * the responsibility of the caller to close the stream.
   */
  InputStream in() throws IOException;
}

/**
 * Binary data with an associated mime type.
 *
 * @author Bob Lee (bob@squareup.com)
 */
public interface TypedOutput {
  /** Original filename.
   *
   * Used only for multipart requests, may be null. */
  String fileName();

  /** Returns the mime type. */
  String mimeType();

  /** Length in bytes or -1 if unknown. */
  long length();

  /** Writes these bytes to the given output stream. */
  void writeTo(OutputStream out) throws IOException;
}

TypedByteArray,內部資料是一個Byte陣列

  private final byte[] bytes;

  @Override public long length() {
    return bytes.length;
  }

  @Override public void writeTo(OutputStream out) throws IOException {
    out.write(bytes);
  }

  @Override public InputStream in() throws IOException {
    return new ByteArrayInputStream(bytes);
  }

TypedString,繼承自TypedByteArray,內部表示是一樣的

public TypedString(String string) {
    super("text/plain; charset=UTF-8", convertToBytes(string));
  }

  private static byte[] convertToBytes(String string) {
    try {
      return string.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

其他的也一樣,從名字很好理解:TypedFileMultipartTypedOutputFormEncodedTypedOutput

其他

Retrofit對輸入和輸出做了封裝,通過TypedOutput向伺服器傳送資料,通過TypedInput讀取伺服器返回的資料。

通過MultipartTypedOutput支援檔案上傳,讀取伺服器資料時,如果要求直接返回未解析的Response,Restonse會被轉換為TypedByteArray,所以不能是大檔案類的

Retrofit支援不同的Log等級,當為LogLevel.Full時會把Request及Response的Body列印出來,所以如果包含檔案就不行了。

Retrofit預設使用GsonConverter,所以要想獲取原始資料不要Retrofit解析,要麼自定義Conveter,要麼直接返回Response了,返回Response也比較麻煩

總體來說Retrofit看起來很好用,不過要求服務端返回資料最好要規範,不然如果請求成功返回一種資料結構,請求失敗返回另一種資料結構,不好用Converter解析,介面的定義也不好定義,除非都返回Response,或自定義Converter所有介面都返回String

在Twitter上JakeWharton這麼說:

Gearing up towards a Retrofit 1.6.0 release and then branching 1.x so we can push master towards a 2.0 and fix long-standing design issues.

要出2.0了,內部API會改,介面應該不怎麼變

相關文章