Skip to content

Instantly share code, notes, and snippets.

@SmartDengg
Last active October 20, 2016 06:33
Show Gist options
  • Select an option

  • Save SmartDengg/dafddcc5e990a62a96a0f077860d4340 to your computer and use it in GitHub Desktop.

Select an option

Save SmartDengg/dafddcc5e990a62a96a0f077860d4340 to your computer and use it in GitHub Desktop.
NetWorkingPractice
**Android生态圈发展步伐之快,网络框架更是层出不穷,本次主讲人将带你回顾网络框架的变迁史,并对比时下流行网络框架,阐述他的经验,以及这些演变背后的原因。**
Android开发生态圈的节奏非常之快。每周都会有新的工具诞生,类库的更新,博客的发表以及技术探讨。
如果你外出度假,当你回来的时候可能已经发布了新版本的
网络框架不同于其他,因为它在一定程度上与业务耦合度极高,甚至从代码的角度来说会遍布于工程的各个角落,很难集中管理,一旦替换甚至还要经过漫长的调试过程。
不同与图像加载,日志打印等框架那样,在短时间内就能够进行重构,而且一直沿用多个迭代版本也是正常的。
这一次我会跟大家一起回顾Android开发中那些常见的网络加载类,还有库。
阐述我的观点,分享我的经验,希望大家能够喜欢。
@SmartDengg
Copy link
Author

SmartDengg commented Oct 17, 2016

Volley

The core Volley library is developed in the open AOSP repository at frameworks/volley
android / platform / frameworks / volley

没有托管在github上,如果你是第一次点开这个项目的地址,你一会感到很困惑,甚至不知所措,因为它看起来就像这样

Volley是Google 推出的 Android 异步网络请求框架和图片加载框架。在Google I/O 2013 大会上发布。除了简单易用之外,Volley在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。

发布演讲时的配图:

a burst or emission of many things or a large amount at once

Maven

dependencies {
    ...
    compile 'com.android.volley:volley:1.0.0'
}

功能特性

  • 自动调度的网络请求
  • 支持多并发的网络请求
  • 使用标准HTTP缓存一致性,透明的磁盘和内存响应缓存
  • 支持分配请求的优先级
  • 提供了取消网络请求的API
  • 基于接口设计,可配置性强,如重试和退避策略等
  • 完全分离网络加载与UI更新
  • 支持图像加载

GET

使用volley时,必须要创建一个请求队列RequestQueue。当然也可以使用官方推荐的做法,创建一个全局单例,见仁见智。

    // Instantiate the RequestQueue. 
    RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
    String url = "http://www.google.com";

    // Request a string response from the provided URL. 
    StringRequest stringRequest = new StringRequest(url, new Response.Listener<String>() {
      @Override public void onResponse(String response) {
        //update ui

      }
    }, new Response.ErrorListener() {
      @Override public void onErrorResponse(VolleyError error) {
        //handle error

      }
    });
    // Add the request to the RequestQueue.
    queue.add(stringRequest);

POST

     // Instantiate the RequestQueue. 
    RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
    String url = "http://www.google.com";

     // Request a string response from the provided URL. 
    StringRequest stringRequest =
        new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
          @Override public void onResponse(String response) {

          }
        }, new Response.ErrorListener() {
          @Override public void onErrorResponse(VolleyError error) {

          }
        }) {
          @Override protected Map<String, String> getParams() throws AuthFailureError {
            Map<String, String> params = new HashMap<String, String>();
            params.put("user", "deng");
            params.put("pass", "123"); 
            return params;
          }
        };

    // Add the request to the RequestQueue. 
    queue.add(stringRequest);

重载Request抽象类中的getParams()返回POST请求所需要的参数集合,同样的,也可以重载getHeaders()来设置本次的请求头信息,或者getPriority()函数来设置请求的优先级等。

只需要调用RequestQueueadd(),即可发送一个请求,如下图所示,一旦添加到请求队列中,那么它将通过网络管道,连接服务,对相应进行解析,最终返回给调用者。

通过图表可以了解到,一旦将请求通过add()添加到请求队列中,volley首先通过缓存调度线程从缓存中查询本次请求,如果存在满足条件的缓存,则表示命中缓存,同时读取缓存并进行解析,然后传递给主线程回调。如果缓存未命中,则通过网络调度线程,执行HTTP请求,解析响应,写入缓存(如果需要的话),最后将结果传递给主线程的回调函数。值得一提的是每个请求队列,内部会构造五个线程,其中一个处理缓存线程,四个网络线程。

设置请求的TAG

//为请求标记TAG
stringRequest.setTag(TAG); 
//将设置TAG的请求添加到请求队列中
mRequestQueue.add(stringRequest); 

//通过请求队列取消相同TAG的请求
mRequestQueue.cancelAll(TAG);

请求Josn

  • JsonArrayRequest—A request for retrieving a JSONArray response body at a given URL.
    JsonArrayRequest通过给定的URL地址,检索JSONArray格式的响应体
  • JsonObjectRequest—A request for retrieving a JSONObject response body at a given URL, allowing for an optional JSONObject to be passed in as part of the request body.
    与之相对的可以使用JsonObjectRequest来构造一个JSONObject格式的响应体
// Instantiate the RequestQueue. 
RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
String url = "http://my-json-feed";

JsonObjectRequest jsObjRequest = new JsonObjectRequest
        (Request.Method.GET, url, null, new Response.Listener<JSONObject>() {
    @Override
    public void onResponse(JSONObject response) {

    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {

    }
});

// Add the request to the RequestQueue. 
queue.add(request);

Cache

volley自带的cache管理非常的强大,能够缓存所有的HTTP请求,有效的减少网络请求次数,从而避免流量和电量的浪费。

那么它的缓存策略是这样的,通过读取响应头信息,来判断是否需要缓存当前请求。

如果Header的Cache-Control字段含有no-cacheno-store则表示不缓存。
或者根据Cache-Control和Expires首部,计算出缓存的过期时间和缓存的新鲜度时间
如果两个首部都存在情况下,以Cache-Control为准。

默认的缓存实现,将缓存以文件的形式存储在Disk,保证程序退出后不会丢失。

从cache中加载请求

Cache cache = queue.getCache();
Entry entry = cache.get(url);
if(entry != null){
    try {
        String data = new String(entry.data, "UTF-8");
        // handle data, like converting it to xml, json, bitmap etc.,
    } catch (UnsupportedEncodingException e) {      
        e.printStackTrace();
        }
    }
}else{
    // Cached response doesn't exists. Make network call here
}

禁用缓存

stringRequest.setShouldCache(false);

删除指定URL的缓存

queue.getCache().remove(url);

清空缓存

queue.getCache().clear();

从效率和电量的角度来考虑,有时需要做批量请求,而不是零星的分别执行。这种场景下,我们可能需要获取并展示数据,但是可能还没有重要到必须立即更新这些数据的程度。那么比较好的办法就是等待下一次重要更新后,去覆盖那些已经被标记了”缓存失效“的数据,而在这之前我们可以一直使用这些缓存数据。我们只需要对这些特殊的响应缓存,标记失效即可,而不需要删除它们。这就允许我们一直使用这些已经存在的缓存数据,直到新的响应到来,继而将它们覆盖。

不得不再说一遍:使指定URL的缓存失效,但并不会删除该条缓存,接收到新的响应结果后将自动覆盖这条缓存数据。换句话说就是,虽然本条缓存数据被标记为失效状态,但仍然处于可用状态,直到接收到新的响应结果。

queue.getCache().invalidate(url,true);

Volley框架默认在Android Gingerbread(API 9)及以上都是用HttpURLConnection,9以下用HttpClient。

knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 18, 2016

okhttp

An HTTP+HTTP/2 client for Android and Java applications

简介

HTTP是一种现代网络应用层的交互方式。通过它交互数据,媒体等信息。做HTTP优化不仅可以使你的数据加载更快,还能节省你的带宽资源。

OkHttp是一个高效的HTTP客户端:

  • HTTP 2.0支持允许连接到统一主机的所有请求共享一个socket
  • 连接池降低了请求等待(HTTP/2不可用的情况下)
  • GZIP缩小了下载的大小
  • 响应缓存完全避免了重复的网络请求

OKHttp简单易用。它的request/response API被设计成链式调用。它支持同步调用和异步回调调用,两种调用方式。

Install

Maven

在我写这份调研报告的时候,okhttp的最新版本是3.4.1

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.4.1</version>
</dependency>

Gradle

compile 'com.squareup.okhttp3:okhttp:3.4.1'

功能特性

  • 重写请求

在发送请求之前OkHttp会重写你的请求。
比如OkHttp可以为原始的请求添加缺失的消息头信息,包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, and Content-Type。除非Accept-Encoding消息头信息已经存在,否则将会对response进行透明压缩(gzip)。
如果你获取到cookies,OkHttp将添加一个Cookie到其中去。
一些请求会有Response的缓存。当Response的缓存不是最新的,OkHttp会做条件判断,如果缓存不是最新的,则下载最新的的Response。

  • 重写响应

如果透明压缩(gzip)被使用,OkHttp将删除对应的content-Encoding和Content-Length响应头信息,因为它们并不适用于解压缩的response body。

  • 请求重试

如果请求失败:连接池过时或者断开连接,或者服务器本身无法访问。OKHttp将重试有效的请求到不同的路由。

  • 调用
    • 同步调用,你的线程将会阻塞直到响应返回
    • 异步调用, 无论在任何线程中调用,都会被加入请求队列,需要注意的是响应结果并不是在主线程中通过回调返回,而是在其它线程中
  • 任务调度

对于同步调用,需要自己调度线程,并管理并发请求。太多的并发连接浪费资源,尤其会导致请求延迟。

对于异步调用,Dispatcher实现大数量并发请求策略。可以为每个host主机,配置最大并发访问数,默认是5;或者设置应用的最大并发访问数,默认是64

GET

例如,下载一个URL资源,并将内容打印成字符串。

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

POST

例如,利用POST向服务器发送数据。

public static final MediaType JSON
    = MediaType.parse("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(JSON, json);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  Response response = client.newCall(request).execute();
  return response.body().string();
}

Interceptors

在okhttp中Interceptor扮演者相当重要的角色,比如说追踪,重写或者重试,当然也可以做一些日志打印,统计,或者埋点操作。按顺序添加则按顺序调用。

Application Interceptors

  1. 不用担心中间过程的响应,例如重定向和重试。
  2. 始终调用一次,即使HTTP响应来自于缓存。

Network Interceptors

  1. 能操作中间响应,例如重定向和重试。
  2. 发生网络短路的缓存响应时,不被调用。
  3. 观察将通过网络传输的数据。

BridgeInterceptor

public final class BridgeInterceptor implements Interceptor {

  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    ...

    // 1.判断请求头信息"Accept-Encoding"是否存在
    // 2.如果是okhttp添加的"Accept-Encoding: gzip",那么也将负责解压缩
    // 3.如果原始请求头中存在"Accept-Encoding: gzip",那么okhttp不负责对响应进行解压缩,需要手动解压缩
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    ...

    // 4.发生网络传输,并获取响应
    Response networkResponse = chain.proceed(requestBuilder.build());

    Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);

    // 5.判断响应头是否包含"Content-Encoding: gzip"字段,并且由okhttp负责解压缩
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {

      // 6.对响应进行解压缩
      GzipSource responseBody = new GzipSource(networkResponse.body().source());

      Headers strippedHeaders = networkResponse.headers()
          .newBuilder()
          .removeAll("Content-Encoding") // 7.移除"Content-Encoding"响应头字段
          .removeAll("Content-Length")   // 8.移除"Content-Length"响应头字段
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
}

更多的调用案例,请参考Recipes所展示的示例。

同步GET - SynchronousGet

异步GET - AsynchronousGet

比如上传文件 - PostFile

缓存响应 - CacheResponse

取消请求 - CancelCall

不在此一一举例,只给出固定链接,因为官方示例太多了,而且很完整。

Knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 18, 2016

retrofit

Type-safe HTTP client for Android and Java by Square

为Android和java而生的类型安全的HTTP客户端。

简介

基于动态代理,运行时反射,在okhttp的基础上构建的,高效,便捷,类型安全的HTTP客户端。

Retrofit requires at minimum Java 7 or Android 2.3

install

截至我在写这份调研报告的时候,retrofit最新版本是2.1.0

Maven

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>2.1.0</version>
</dependency>

Gradle

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

基本使用

  1. 将要请求的HTTP API定义到java接口中
public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}
  1. 使用Retrofit实例,生成GitHubService接口的实现类,值得注意的是这个生成由动态代理完成
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);
  1. 通过GitHubService所创建的Call,执行同步或异步的HTTP请求。
Call<List<Repo>> repos = service.listRepos("octocat");

synchronous

List<Repo> repositories = repos.execute().body();

asynchronous

repos.enqueue(new Callback<List<Repo>>() {
      @Override public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {

      }

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

      }
    });

一句话来说就是,使用注解来描述HTTP请求。

不仅支持URL参数的替换,还支持query参数的添加

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

.list(1) ===> /list?page=1

@GET("/list")
Call<ResponseBody> list(@Query("category") String category);

.list(null) ===> /list

@GET("/list")
Call<ResponseBody> list(@Query("category") String... categories);

.list("bar", "baz") ===> /list?category=bar&category=baz

另外值得一提是,这些参数,无论是name还是value,在网络传输过程中都会默认进行URL编码,也就是说一些特殊字符比如“+”也会被编码,这可能不是我们想要的结果,可以通过encoded=true来改变这个行为:

 @GET("/search")
 Call<ResponseBody> list(@Query(value="foo", encoded=true) String foo);

.list() ===> .list("foo+bar")) yields /search?foo=foo+bar

当然还有更多,更丰富,更灵活的使用方式,在次不一一介绍,具体使用方法,请参考官方示例。

适当总结

美中不足,不支持请求优先级。

但几乎所有东西都可以自定义配置,替换原有的回调接口,而不是在他们的基础上进行二次封装,提供了对象转换的工厂类Converter.Factory,无论是json,protocol buffer还是xml结构的响应,添加想要的任何的序列化/反序列化解析器,同时也支持自定的的CallAdapter轻松支持RxJava,Java8, Guava等。

Knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 18, 2016

volley vs okhttp vs retrofit

repository First release Last release Now Release Contributors Pull request Issues
okhttp on 6 May 2013 on 10 Jul 2016 3.4.1 43 121 1448 1302(closed) 108(open)
retrofit on 14 Jun 2012 on 15 Jun 2016 2.1.0 38 103 687 1323(closed) 41(open)

最显而易见的区别,

volley需要指定完整路径的访问地址,并且是在调用的时候以参数的的形式动态添加,可返回JSONObject或者JSONArray,不过这都依赖于所实现的接口类型,默认没有处理反序列化操作。

retrofit可以指定一个base URL,每一个请求只需要提供相对访问路径即可,请求参数均通过java注解来描述,支持动态添加和修改,让我们不必关注请求项的拼接,支持泛型的返回结果,让开发者不必再手动反序列化响应。

我们终于能够站在云端思考业务,而不必蹲在泥里写实现了。

Volley

  1. Volley的优势在于处理小文件的http请求;
  2. 在Volley中也是可以使用Okhttp作为传输层
  3. Volley在处理高分辨率的图像压缩上有很好的支持;
  4. NetworkImageView在GC的使用模式上更加保守,在请求清理上也更加积极,networkimageview仅仅依赖于强大的内存引用,并当一个新请求是来自ImageView或ImageView离开屏幕时 会清理掉所有的请求数据。

OKHttp

  1. OKHttp是Android版Http客户端。非常高效,支持SPDY、连接池、GZIP和 HTTP 缓存。
  2. 默认情况下,OKHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题。
  3. 如果你的应用程序中集成了OKHttp,Retrofit默认会使用OKHttp处理其他网络层请求。

Retrofit

  1. 性能最好,处理最快
  2. 使用REST API时简单方便
  3. 传输层默认就使用OkHttp
  4. 支持NIO,对IO流的操作更高效
  5. 拥有出色的API文档和社区支持
  6. 速度上比volley更快
  7. 默认使用Gson

@SmartDengg
Copy link
Author

SmartDengg commented Oct 19, 2016

接下来的这一段,我想跟大家分享一下,我在日常开发中,尤其是处理网络操作的时候,遇到的那些棘手问题,并且经常让我感到困惑的事情。

这一段应该插在httpurlconnection和httpclient的对比之后。文字有限,不再赘述,大部分都会来自于口述。

那么现在假设,我们已经有了封装性极好的http客户端,我们不关心它的内部实现,也就是说它可以由HttpUrlconnection实现,也可以由
httpclient实现,只不过这些细节,我们不再关注。假设它的健壮性非常好,合理的线程池调度,超时时间,甚至网络缓存等。

OK,我们有了一个配置如此强大的客户端后,那就开始使用吧,但是需要注意的是,我们还没有处理响应的回调接口,没关系,我们还有一个老朋友AsyncTask,他可以帮助我们来处理这些异步任务,而且我们自己封装的HTTP客户端在绝大多数工程中都是跟AsyncTask搭配使用的。不过AsyncTask并不属于本次演讲的内容,因此不做过多赘述,你只需要记住,它是一个在future-task的基础上,封装了Handler消息机制,通过线程池调度处理异步任务的类

比如,我要加载一些URL,以字符串的形式展示这些地址的返回结果。

public class DownloadWebPageTask extends AsyncTask<String, Voice, String> {

   ...

  @Override protected String doInBackground(String... urls) {

    String response = "";

    for (String url : urls) {

      try {
        InputStream inputStream = EffectiveHttpClient.retrieveService(url);
        BufferedReader buffered = new BufferedReader(new InputStreamReader(inputStream));

        String s = "";
        while ((s = buffered.readLine()) != null) {
          response += s;
        }
      } catch (IOException e) {
        //handle IOException

        //handle JSONException ?

        //handle inner Exception ?

        //handle Exception when nest call ?
      }
    }

    return response;
  }

  @Override protected void onPostExecute(String result) {
    mTextView.setText(result);
  }

  @Override protected void onCancelled(String result) {
    // handle cancel with result may be null
  }
}

假设EffectiveHttpClient就是我在上面提到的,自定义的高效HTTP客户端,再所一遍,我们不考虑它的内部细节实现。

那么回到这个例子中,看一看,哪些地方使我们不能接受的,或者说,这样会带来什么隐患。

我们先从代码的角度来考虑问题:

  1. 这种写法,迫使我们不得不添加丑陋的try-catch代码块,捕获异常并处理,这虽然看起来还不错。但是,当我们需要调用多个请求,后一个请求依赖于前一个请求的结果的时候,我们可能要分别处理这些异常,甚至还要校验返回的数据,判断它们的合法性,很难抽离归纳出一个健全的error handler。而且嵌套调用通常很难调试,ugly code!
  2. 虽然我们能够提供“key-value”的请求参数的拼接,但是我们很难搞定参数替换,为特殊的请求设定特殊的请求头信息,也是个麻烦事,很难动态添加和修改。没有人愿意写重复,枯燥的代码。
  3. 解析数据POJO?过滤?缓存?序列化数据至preference或者SQLite?这都是难题
  4. 重试机制和退避策略?
  5. 很难在Android开发中控制线程,进行优雅的线程切换。
  6. AsyncTask自身的设计缺陷,如果开启AsyncTask的ACT/Fragment已经被销毁了,也就是说执行到了其生命周期的onDestroy()。而AsyncTask运行,由于非静态内部类会持有外部类的引用,这就造成了ACT/Fragment的泄漏,如果继续在onPostExecute()中更新UI,可能引起NPE,或者其他崩溃隐患,而且我们很难真正取消一个正在运行的线程。
  7. CALL BACK HELL!

@SmartDengg
Copy link
Author

SmartDengg commented Oct 19, 2016

ALL RXJAVA WITH RETROFIT

优雅线程切换,统一的错误处理,回调天堂,统统给你。

比如说现在有一个需求,通过webservice获取城市列表 -> 根据返回的结果,通过城市ID获取电影列表 -> 根绝返回的结果,通过电影ID获取电影详情 -> 进行展示。

首先定义java接口

interface Service {

    //获取城市列表集合
    @GET("movie/citys") Observable<List<CityListResponse>> getCityList();

    //根据城市ID获取电影列表集合
    @GET("movie/movies.today") Observable<List<MovieListResponse>> getMovies(
        @Query("cityid") String cityId);

    //根据电影ID获取电影详情
    @GET("movie/query") Observable<MovieDetailResponse> getMovieDetail(
        @Query("movieid") String movieId);
  }

处理网络操作的逻辑如下:

private void retrieveMovies() {

    final Service service = ServiceGenerator.createService(Service.class);

    //1. 获取城市列表(网络操作)
    service.getCityList()
        .concatMap(new Func1<List<CityListResponse>, Observable<CityListResponse>>() {
          @Override
          public Observable<CityListResponse> call(List<CityListResponse> cityListResponses) {
            //2.依次取出"城市列表"集合中的元素
            return Observable.from(cityListResponses);
          }
        })
        .concatMap(new Func1<CityListResponse, Observable<List<MovieListResponse>>>() {
          @Override
          public Observable<List<MovieListResponse>> call(CityListResponse cityListResponse) {
            //3.根据每一个城市的id,获取该城市的电影列表(网络操作)
            return service.getMovies(cityListResponse.cityId);
          }
        })
        .concatMap(new Func1<List<MovieListResponse>, Observable<MovieListResponse>>() {
          @Override
          public Observable<MovieListResponse> call(List<MovieListResponse> movieListResponses) {
            //4.依次取出"电影列表"集合中的元素
            return Observable.from(movieListResponses);
          }
        })
        .concatMap(new Func1<MovieListResponse, Observable<MovieDetailResponse>>() {
          @Override
          public Observable<MovieDetailResponse> call(MovieListResponse movieListResponse) {
            //5.根据每一个电影的ID,获取该电影详情(网络操作)
            return service.getMovieDetail(movieListResponse.movieId);
          }
        })
        .subscribeOn(Schedulers.newThread())// 6.线程切换,上游的所有操作执行在工作线程
        .observeOn(AndroidSchedulers.mainThread())//7.线程切换,下游的所有将操作执行在UI线程
        .subscribe(new Subscriber<MovieDetailResponse>() {
          @Override public void onCompleted() {
          // finishCompletion
          }

          @Override public void onError(Throwable e) {
          //error handler
          }

          @Override public void onNext(MovieDetailResponse movieDetailResponse) {
            //8.根据电影ID所获取到的电影详情,该函数会被调用多次。
            textView.append(movieDetailResponse.movieName);
          }
        });
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment