Android 网络框架 Retrofit 源码解析

在之前的文章 《Andriod 网络框架 OkHttp 源码解析》 中我们分析了 OkHttp 的源代码。现在我们就来分析一下 OkHttp 的兄弟框架 Retrofit。关于 Retrofit 的注解的使用,可以参考其官方文档:https://square.github.io/retrofit/

Retrofit 也是 Square 发布的一个开源的库,它是一个类型安全的 Http 客户端,适用于 Android 和 Java。本质上,Retrofit 使用了 Java 的动态代理,内部使用 OkHttp 来进行网络访问,并且可以通过指定 “请求适配器” 和 “类型转换器” 来完成:请求的适配,方法参数到 OkHttp 请求的转换,以及响应到 Java 类型的转换。

1、基本使用

Retrofit 设计的一个好的地方就是它把我们上面提到的 “请求适配器” 和 “类型转换器” 使用策略模式解耦出来。用户可以根据自己的需求通过实现指定的接口来自定义自己的类型转换器。所以,当我们使用 Gson 进行转换和 RxJava2 进行适配的时候,就需要指定下面三个依赖:

api 'com.squareup.retrofit2:retrofit:2.4.0'
api 'com.squareup.retrofit2:converter-gson:2.4.0'
api 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

然后,我们需要根据自己的 API 接口的信息,在代码里用一个接口来对该 API 进行声明:

public interface WXInfoService {
    @GET("/sns/userinfo")
    Observable<WXUserInfo> getWXUserInfo(
        @Query("access_token") String accessToken, @Query("openid") String openId);
}

这里的 WXUserInfo 是由该 API 接口返回的 Json 生成的 Java 对象。然后,我们可以像下面这样获取一个该接口的代理对象:

WXInfoService wXInfoService = new Retrofit.Builder()
    .baseUrl("https://api.weixin.qq.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .client(okHttpClient)
    .build().create(WXInfoService.class);

然后,我们就可以使用该对象并调用其方法来获取接口返回的信息了:

Disposable disposable = wxInfoService.getWXUserInfo(accessToken, openId)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(wxUserInfo -> { /*...拿到结果之后进行处理...*/ });

上面我们只使用了 Retrofit 最基础的 GET 接口。当然,Retrofit 本身的功能远比这要丰富得多,关于其更多的使用,可以参考其官方的文档。

2、动态代理:魔力发生的地方

上面我们使用 Retrofit 进行网络请求,实际其内部使用 OkHttp 来完成网络请求的。仅定义了一个接口并调用了该接口的方法,就拿到了请求的结果,这看上去非常简洁,而这其中的功不可没的就是动态代理

当我们使用 Retrofit.Buildercreate() 方法获取一个 WXInfoService 实例的时候,实际返回的是经过代理之后的对象。该方法内部会调用 Proxy 的静态方法 newProxyInstance() 来得到一个代理之后的实例。为了说明这个方法的作用,我们写了一个例子:

public static void main(String...args) {
    Service service = getProxy(Service.class);
    String aJson = service.getAInfo();
    System.out.println(aJson);
    String bJson = service.getBInfo();
    System.out.println(bJson);
}

private static <T> T getProxy(final Class<T> service) {
    InvocationHandler h = (proxy, method, args) -> {
        String json = "{}";
        if (method.getName().equals("getAInfo")) {
            json = "{A请求的结果}";
        } else if (method.getName().equals("getBInfo")) {
            json = "{B请求的结果}";
        }
        return json;
    };
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, h);
}

该程序的输出结果:

 {A请求的结果}
 {B请求的结果}

在上面的这个例子中,我们先使用 getProxy() 获取一个代理之后的实例,然后依次调用它的 getAInfo()getBInfo() 方法,来模拟调用 A 接口和 B 接口的情形,并依次得到了 A 请求的结果和 B 请求的结果。

上面的效果近似于我们使用 Retrofit 访问接口的过程。为了说明这个过程中发生了什么,我们需要先了解一下这里的 newProxyInstance() 方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
    // ...
}

该方法接收三个参数:

  1. 第一个是类加载器;
  2. 第二个是接口的 Class 类型;
  3. 第三个是一个处理器,你可以将其看作一个用于回调的接口。当我们的代理实例触发了某个方法的时候,就会触发该回调接口的方法。

InvocationHandler 是一个接口,它内部定义了一个方法如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

该方法也接收三个参数,第一个是触发该方法的代理实例;第二个是触发的方法;第三个是触发的方法的参数。invoke() 方法的返回结果会作为代理类的方法执行的结果。

所以,当了解了 newProxyInstance() 方法的定义之后,我们可以做如下总结:当我们使用 newProxyInstance() 方法获取了一个代理实例 service 并调用其 getAInfo() 方法之后,该方法的信息和参数信息会分别通过 methodargs 传入到 hinvoke() 中。所以,最终的效果就是,当我们调用 servicegetAInfo() 时候会触发 hinvoke()。在 invoke() 方法中我们根据 method 得知触发的方法是 getAInfo。于是,我们把它对应的请求从 invoke() 方法中返回,并作为 service.getAInfo() 的返回结果。

所以,我们可以总结 Retrofit 的大致工作流程:当我们获取了接口的代理实例,并调用它的 getWXUserInfo() 方法之后,该方法的请求参数会传递到代理类的 InvocationHandler.invoke() 方法中。然后在该方法中,我们将这些信息转换成 OkHttp 的 Request 并使用 OkHttp 进行访问。从网络中拿到结果之后,我们使用 “转换器” 将响应转换成接口指定的 Java 类型。

上面是 Retrofit 请求处理的基本流程,下面我们看一下 Retrofit 的代理方法内部究竟发生了什么。

3、Retrofit 的源码解析

3.1 创建 Retrofit

根据上面的例子,当使用 Retrofit 的时候,首先我们需要使用 Retrofit 的构建者来创建 Retrofit 的实例。这里的构建者有几个重要的方法需要提及一下:

3.1.1 addConverterFactory 方法

该方法用来向 Retrofit 中添加一个 Converter.FactoryConverter.Factory,顾名思义是一种工厂模式。它是一个接口需要,实现两个重要的方法。每个方法需要返回一个转换器:某种数据类型到请求体的转换器,响应体到我们需要的数据类型的转换器。当我们使用 Gson 来完成这个转换,那么我们就需要使用 GsonConverterFactory.create() 来得到一个适用于 Gson 的 Converter.Factory

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

3.1.2 addCallAdapterFactory 方法

CallAdapter.Factory 用于获取 CallAdapter 对象, CallAdapter 对象用于把原生的 OkHttp 的 Call 转换成我们指定的请求类型。比如,上面的例子中,我们用来将其转换成 Observable<WXUserInfo>。下面是该方法的定义:

public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
  callAdapterFactories.add(checkNotNull(factory, "factory == null"));
  return this;
}

3.1.3 build 方法

当根据用户的自定义设置完了参数之后,就可以调用 build() 方法,来获取一个 Retrofit 的实例。在该方法中会将上述方法传入的 “适配器” 和 “转换器” 添加到各自的列表中,然后 new 出一个 Retrofit 的实例并返回。

3.1.4 小结

为了说明适配器 CallAdapter 和转换器 Converter 的作用,我们绘制了下图:

Retrofit类图

从上面我们看出,CallAdapter 主要用来将某个请求转换成我们指定的类型。比如,在我们最开始的例子中,要将请求转换成 Observable<WXUserInfo>。如果转换之后的请求是 Observable 类型的,那么当我们对转换后的请求进行订阅的时候,就启动了 OkHttp 的网络请求过程。

在进行网络请求之前会先使用 Converter 将请求的参数转换成一个 RequestBody。这里将其作为一个接口的好处是便于解耦。比如,上面我们用 Gson 来完成转换过程,你也可以通过自定义转换器来使用其他的框架,比如 Moshi 等。当拿到了响应之后,我们又会再次使用 Converter 来将响应体 ResponseBody 转换成我们要求的类型。比如 WXUserInfo

从上面我们看出,Retrofit 设计的非常妙的地方就在于上面的两个过程的解耦(策略模式+工厂模式+适配器模式)。一次是将请求转换成 Observable 的过程,一次是将请求体和响应体转换成 OkHttp 要求的 RequestBodyResponseBody 的过程。对于前者,不论我们使用的是 RxJava 1 还是 RxJava 2,只要传入一个 CallAdapter 即可。对于后者,不论我们使用哪种 Json 转换框架,只要实现了 Converter 接口皆可。

3.2 获取代理实例

3.2.1 划分平台:Platform

创建了 Retrofit 的实例之后,我们就可以使用它的 create() 方法来获取代理之后的服务实例。下面是这个方法的定义。在这里,我们会先根据 validateEagerly 变量来判断是否立即对传入的服务接口的方法进行解析。然后,我们使用 Proxy 的静态方法获取一个代理实例。

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    // 这里的 validateEagerly 在 Retrofit 构建的时候设置
    if (validateEagerly) {
        // 是否立即对 Service 方法的内容进行解析
        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, @Nullable Object[] args)
                        throws Throwable {
                    // 该方法是 Object 的方法,直接触发该方法
                    if (method.getDeclaringClass() == Object.class) {
                        return method.invoke(this, args);
                    }
                    // 如果是 default 方法,那么使用该 Java8 平台的方法执行
                    if (platform.isDefaultMethod(method)) {
                        return platform.invokeDefaultMethod(method, service, proxy, args);
                    }
                    // 获取服务方法的信息,并将其包装成 ServiceMethod
                    ServiceMethod<Object, Object> serviceMethod =
                            (ServiceMethod<Object, Object>) loadServiceMethod(method);
                    OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
                    return serviceMethod.adapt(okHttpCall);
                }
            });
}

这里的 eagerlyValidateMethods() 方法定义如下:

private void eagerlyValidateMethods(Class<?> service) {
    // 获取程序当前运行的平台
    Platform platform = Platform.get();
    for (Method method : service.getDeclaredMethods()) {
        // 判断该方法是否是 default 方法
        if (!platform.isDefaultMethod(method)) {
            loadServiceMethod(method);
        }
    }
}

它的作用是立即对服务接口的方法进行解析,并将解析之后的结果放进一个缓存中。这样,当这个服务方法被触发的时候,直接从缓存当中获取解析之后的 ServiceMethod 来使用即可。该方法会先会根据当前程序运行的平台来决定是否应该加载服务的方法。因为,Java 8 之后,我们可以为接口增加 default 类型的方法,所以,如果是 default 类型的话,我们不会调用 loadServiceMethod() 进行解析,而是调用 Java8 平台的 invokeDefaultMethod() 来处理。在 invokeDefaultMethod() 中,会根据传入的信息创建一个实例并使用反射触发它的方法。此时,就间接地触发了该 default 方法。

判断平台的时候,使用了如下这段代码:

platform.isDefaultMethod(Method)

这里的 platform 是调用 Platform.get() 的时候得到的。它会在 get() 方法中尝试使用反射去获取一个只有 Java8 平台才具有的类,以此来判断是否是 Java8 的环境。在 Retrofit 中,提供了 Java8Android 两个类来区分所在的平台,并会根据运行环境来决定返回哪个实例。

所以,Platform 应用了策略模式,以对不同的平台做不同的处理。在当前的版本中,它的主要作用是对 default 类型的方法进行处理。

3.2.2 解析服务方法:ServiceMethod

上面我们提到过 loadServiceMethod() 方法,它的主要作用:首先会尝试从缓存当中获取该方法对应的 ServiceMethod 实例,如果取到的话,就将其返回;否则,就使用构建者模式创建一个并放进缓存中,然后将其返回。

ServiceMethod<?, ?> loadServiceMethod(Method method) {
    // 从缓存中进行获取
    ServiceMethod<?, ?> result = serviceMethodCache.get(method);
    if (result != null) return result;
    synchronized (serviceMethodCache) {
        result = serviceMethodCache.get(method);
        if (result == null) {
            // 创建ServiceMethod实例
            result = new ServiceMethod.Builder<>(this, method).build();
            serviceMethodCache.put(method, result);
        }
    }
    return result;
}

ServiceMethod 的构建过程比较简单,只需要把当前的 Retrofit 实例和服务方法 method 传入进去,然后调用它的 build() 方法就完成了整个创建过程。在 build() 方法中,会完成对 method 的解析,比如根据注解判断是什么类型的请求,根据方法的参数来解析请求的请求体等等。

所以,ServiceMethod 的作用是缓存服务方法对应的请求信息,这样下次我们就不需要再次解析了。同时,它提供了以下几个方法。它们的主要作用是用来从 ServiceMethod 中获取请求相关的信息:

toCall() 用来获取用于 OkHttp 请求的 Call 对象:

okhttp3.Call toCall(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
            contentType, hasBody, isFormEncoded, isMultipart);
    // ...
    return callFactory.newCall(requestBuilder.build());
}

toResponse(ResponseBody) 用来把 OkHttp 得到的响应体转换成 Java 对象等(在示例中是WXUserInfo):

R toResponse(ResponseBody body) throws IOException {
    return responseConverter.convert(body);
}

adapt(Call<R>) 用来将 OkHttp 的请求转换成我们的服务方法的返回类型(在示例中是Observable<WXUserInfo>):

T adapt(Call<R> call) {
    return callAdapter.adapt(call);
}

3.2.3 请求封装:OkHttpCall

解析完毕服务方法之后,我们得到了 ServiceMethod 实例。然后,我们使用它来创建 OkHttpCall 实例。这里的 OkHttpCall 实现了 Retrofit 中定义的 Call 接口,会在方法内调用 ServiceMethodtoCall() 方法来获取 OkHttp 中的 Call 对象,然后使用它进行网络访问。当拿到了请求的结果之后又使用 ServiceMethodtoResponse() 把响应转换成我们指定的类型。下面是该类中的几个比较重要的方法:

  1. execute() 方法,用来同步执行网络请求:
    @Override
    public Response<T> execute() throws IOException {
        okhttp3.Call call;
        synchronized (this) {
            // ...
            call = rawCall;
            if (call == null) {
                try {
                    // 创建 OkHttp 的 Call 实例
                    call = rawCall = createRawCall();
                } catch (IOException | RuntimeException | Error e) {
                    throwIfFatal(e);
                    creationFailure = e;
                    throw e;
                }
            }
        }
        if (canceled) {
            call.cancel();
        }
        // 同步执行请求,并解析结果
        return parseResponse(call.execute());
    }
  1. createRawCall() 用来创建 OkHttp 的 Call 实例:
    // 使用 serviceMethod 的 toCall 方法获取 OkHttp 的 Call 实例
    private okhttp3.Call createRawCall() throws IOException {
        okhttp3.Call call = serviceMethod.toCall(args);
        if (call == null) {
            throw new NullPointerException("Call.Factory returned null.");
        }
        return call;
    }
  1. parseResponse() 用来将 OkHttp 的响应转换成我们接口中定义的类型。比如,在我们的例子中,返回的是 Observable<WXUserInfo>:
    // 使用 serviceMethod 的 toResponse 方法获取 OkHttp 的 Response 实例
    Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
        ResponseBody rawBody = rawResponse.body();
        rawResponse = rawResponse.newBuilder()
                .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
                .build();
        // ...
        ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
        try {
            // 使用 serviceMethod 的 toResponse 方法获取 OkHttp 的 Response 实例
            T body = serviceMethod.toResponse(catchingBody);
            return Response.success(body, rawResponse);
        } catch (RuntimeException e) {
            catchingBody.throwIfCaught();
            throw e;
        }
    }

3.3 Retrofit 的工作过程

上面是 Retrofit 框架设计中几个关键的部分的功能的解析。下面,我们再来具体看一下,从触发代理类的方法到拿到响应的结果,这一整个过程中,都有哪些类的哪些方法参与,以及它们在什么时候,扮演什么样的角色。这里我们仍然使用最初的示例:

Retrofit的执行过程

上图中,我们将 Retrofit 的请求的过程分成三个过程来进行说明:

  1. 创建代理实例的过程:在这个过程中主要是调用 Proxy.newProxyInstance() 来获取一个代理实例。相关的主要参数是 validateEagerly,我们会使用它来决定是否立即对传入的接口的方法进行解析。不论我们什么时候进行解析,都会把解析的结果缓存起来。
  2. 触发代理方法的过程:触发代理方法是整个请求的第二过程。这个时候,我们调用了 WXInfoService 代理实例的 getWXUserInfo() 方法。此时,会触发 InvocationHandler.invoke() 方法。在该方法内部会调用 ServiceMethod 的构建者模式来创建 serviceMethod 实例。当调用构建者模式的 build() 方法的时候,会对方法 getWXUserInfo() 的信息进行解析。然后,使用 serviceMethod 创建 okHttpCall。最后,调用 serviceMethod.adapt() 方法将 okHttpCall 实例转换成 Observable<WXUserInfo>。在转换的过程中会使用 CallAdapteradapt() 方法来完成适配。
  3. 执行网络请求的过程:拿到了 Observable<WXUserInfo> 之后,需要对其进行订阅才能触发网络请求。相关的逻辑在 CallAdapter 中完成。首先,它会根据你使用同步还是异步的来决定使用哪个执行器。这里存在两个执行器,它们的区别是一个会在内部调用 OkHttpCallenqueue(),另一个会在执行器中调用 OkHttpCallexecute() 方法。不论调用 enqueue() 还是 execute(),都会先使用 OkHttpCalltoCall() 方法获取一个 Call 请求。获取请求的过程中会使用 Converter 来将某个实例转换成请求体。拿到了请求之后,使用该请求来进行网络访问。当从网络中拿到了响应之后,会使用 Converter 来将响应体转换成对象。这样,拿到了实际的结果之后,就会调用 ObserveronNext() 方法把结果通知给观察者。

4、总结

在这篇文章中,我们先简单介绍了 Retrofit 的使用,然后,因为 Retrofit 内部使用动态代理来实现的,所以,我们对动态代理相关内容进行了介绍。最后,我们对 Retrofit 的源码进行了分析,先从设计思路,后从各个环节的执行过程进行了说明。最后的最后,我们将两者结合起来用一个时序图做了说明。

从上文中可以看出来,Retrofit 设计的几个值得我们借鉴的地方:

  1. 使用运行时注解和反射简化请求描述,但是考虑到反射的效率比较低,所以将一次反射之后的结果缓存起来,以便于下次使用。
  2. 动态代理:使用接口描述请求的好处是它简洁,而且 “描述” 本来就是它的责任。但是,一般我们需要去实现接口才能使用。而这里告诉我们,使用动态代理一样可以使用接。
  3. 解耦:从我们上面的图中也可以看出来,Retrofit 的设计的思路是比较清晰的。它将一个请求的几个过程解耦出来。首先是我们 Observable 到请求的转换,这里使用适配器来完成;然后是请求体和响应体的转换,基本就是 Json 的转换,使用转换器来完成。这样,不论你使用 RxJava 1 还是 RxJava 2,不论是 Gson 还是 FastXml 都可以和 Retrifut 配合使用。

以上就是我们对 Retrofit 的源码的分析。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 目录介绍 1.首先回顾Retrofit简单使用方法 2.Retrofit的创建流程源码分析2.1 Retrofit...
    杨充211阅读 1,015评论 0 16
  • Retrofit 2 源码解析 关于Retrofit 2的使用请看上一篇https://www.jianshu.c...
    gogoingmonkey阅读 456评论 0 1
  • Retrofit是一个Android网络框架。是一个对OKHttp框架的简单封装。所以其内部实现原理实际上也是...
    龙城_阅读 12,677评论 0 16
  • 《彳亍行.叹归》 文/亍頔 不喜江南雨,犹恋北国寒; 一夕身感,十年未还。 三十无禄尘归土,千里玄冰夜伴寒。 .....
    亍頔阅读 468评论 1 8
  • 爱和喜欢是不一样的: 淡淡的爱是喜欢,深深的喜欢是爱! 喜欢的后来是爱,爱的后来是亲情! 世间有种情感叫“喜欢”,...
    和果阅读 254评论 0 4