Android OkHttp3 :最简单&粗暴(使用与原理)讲解

1.前言

  • Android开发过程中,使用第三方的框架库已成家常便饭,使用第三方好处避免重复造轮子、降低成本、提升效率、降低风险等等,当遇到框架库不能满足现有业务、框架库设计缺陷或者漏洞、API使用深度不够时,如果停留只会使用层面,就会增加修改过程的难度,所以对于使用的框架库最好还是有个系统的认识;
  • 本文带大家深入讲解 OkHttp
  • 文章中实例 linhaojian的Github

2.目录

目录.png

3.定义

  • 一款处理网络请求的开源项目,由Square公司贡献。

4.作用


5.特点

  • 1.同时支持HTTP1.1与支持HTTP2.0
  • 2.同时支持同步与异步请求;
  • 3.同时具备HTTP与WebSocket功能;
  • 4.拥有自动维护的socket连接池,减少握手次数;
  • 5.拥有队列线程池,轻松写并发;
  • 6.拥有Interceptors(拦截器),轻松处理请求与响应额外需求(例:请求失败重试、响应内容重定向等等);

6.OkHttp系统图

okhttp系统图.png

7.OkHttpClient(封装请求参数)

  • OkHttpClient 通过建造者模式(Builder Pattern)方式,完成请求参数配置。常用如下:
    • connectTimeout :连接超时
    • readTimeout:读取超时
    • writeTimeout:写入超时
    • pingInterval:websocket情况下连接心跳间隔
    • interceptors:自定义拦截器
    • networkInterceptors:自定义网络连接成功的拦截器
  • OkHttpClient 除了完成请求参数的配置之外,还提供获取WebSocket、Call(Call实现类为RealCall,下文会介绍)相关类;

7.1 WebSocket

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议,支持服务器想客户端的发送请求,由OkHttpClient创建,源码如下:
  /**
   * Uses {@code request} to connect a new web socket.
   */
  @Override public WebSocket newWebSocket(Request request, WebSocketListener listener) {
    RealWebSocket webSocket = new RealWebSocket(request, listener, new Random(), pingInterval);// 1
    webSocket.connect(this); 
    return webSocket;
  }

注释1:WebSocket是一个接口,它的实现类RealWebSocket,该类完成WebSocket的连接、数据请求与接收功能。

7.2 Call初始化

  //OkHttpClient 初始化Call的函数
  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }
  // RealCall 初始化函数
  static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
  }

8.RealCall同步异步

  • RealCall 是真正触发网络请求的类(实现Call接口,一次请求 = 一个RealCall实例),它提供了同步请求、异步请求

8.1 同步请求

  @Override public Response execute() throws IOException {
    // 处理不能重复请求,因为一个RealCall对应一个请求。
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);//1
      Response result = getResponseWithInterceptorChain();//2
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this);
    }
  }
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());//3
    return chain.proceed(originalRequest);//4
  }

注释1:将RealCall实例添加至Dispatcher中(下文会介绍Dispatcher)。
注释2:通过getResponseWithInterceptorChain()获取响应。
注释3:通过封装好的拦截器集合,获取第一个拦截器的任务。
注释4:触发第一个拦截器的任务,该任务就触发一下拦截器的任务,以此类推,原理(Android事件传递机制)如下图:


任务链.png

8.2 异步请求

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));//1
  }

注释1:把AsyncCall请求对象传递进Dispatcher线程池管理;

  • AsyncCall 将请求业务放入到Runnable中。
  final class AsyncCall extends NamedRunnable {
      // ......
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();//2
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

注释2:通过getResponseWithInterceptorChain()获取响应;


9.Dispatcher线程池

  • Dispatcher 管理网络请求的线程池,其实就是把同步(RealCall)与异步(AsyncCall)的请求放进集合中统一管理,然后通过线程池执行AsyncCall的请求。

9.1 Dispatcher中同步(RealCall)

  • RealCall在Dispatcher中,其实主要就是一个存储功能(即用一个集合把RealCall的请求进行存储)。

9.2 Dispatcher中异步(AsyncCall)

  • AsyncCall在Dispatcher中,除了使用集合存储AsyncCall的请求,Dispatcher还初始化了一个线程池(ThreadPoolExecutor)处理AsyncCall的网络请求。
public final class Dispatcher {
  //最大请求数量
  private int maxRequests = 64;
  //相同host的最大请求数据
  private int maxRequestsPerHost = 5;
  // ......
  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));//1
    }
    return executorService;
  }
  // ......
  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {//2
      runningAsyncCalls.add(call);//3
      executorService().execute(call);//4
    } else {
      readyAsyncCalls.add(call);//5
    }
  }
  /** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }
  /** Used by {@code Call#execute} to signal completion. */
  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);//6
  }
  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();//7
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }
  private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();
      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }
      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }
  // ......
}

注释1:获取自定义线程池;
注释2:判断正在执行的异步请求数量与请求集合中相同host的数量是否满足,如果满足就添加到执行中的集合中,并添加至线程池中执行请求;如果不满足就添加至待执行请求的集合中,等待执行中的请求完成之后,再执行相同host数量判断满足才添加至线程池中执行请求;
注释3:将请求对象AsyncCall添加进请求执行的集合中;
注释4:将请求对象AsyncCall添加进线程池中执行;
注释5:当不满足执行条件时(注释2),把请求对象添加至待执行的集合中;
注释6:每当一个请求执行完毕时,就会调用finished()去掉对应集合中的存储对象,并在次判断待执行的集合中是否有满足条件的请求,若满足就添加至执行的集合与线程池中执行,若不满足继续等待下一个请求完成再次判断。
注释7:判断待执行的集合中是否满足可执行的对象。


10.Interceptor拦截器及调用链

  • Interceptor 拦截器,供使用者可在请求过程或者响应过程中自定义额外的业务处理(例如:最常见的请求失败重试、响应数据的重定向等等)。
  • OkHttp3中,除了可自定义额外的拦截器之外,它内部也存储一些固定的拦截器处理其内部业务逻辑,下面就会介绍它们(RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor、CallServerInterceptor);

10.1 RetryAndFollowUpInterceptor

  • 定义:重定向拦截器;
  • 作用:在无法请求服务器或者请求失败时,服务器会告诉客户端可以处理请求的url,然后重定向拦截器承当重新请求新url的作用(服务器返回3XX错误码为重定向,可以通过响应头的Location获取新请求的url);

10.2 BridgeInterceptor

  • 定义:桥拦截器;
  • 作用:封装请求头(Content-Type、Connection、Cookie...)与响应头("Content-Encoding...)的信息。

10.3 CacheInterceptor

  • 定义:缓存拦截器;
  • 作用:为网络请求提供缓存功能,加快相同请求的访问速度,减少资源损耗。

10.4 ConnectInterceptor

  • 定义:连接拦截器;
  • 作用:与服务器建立通讯连接。

10.5 CallServerInterceptor

  • 定义:请求服务器拦截器;
  • 作用:与服务器进行数据通讯(包含请求头、请求内容)。

10.6 调用链

  • 上文也提到任务链结构图(责任链模式):
    任务链.png
  • 其实它的原理类似于(Android 事件传递机制),向下传递请求,向上反馈响应,在调用RealInterceptorChain的proceed()时,创建下一个拦截器的任务,并通过拦截器中intercept()把任务传递至当前拦截器进行关联,然后以此类推,相关代码如下:
public final class RealInterceptorChain implements Interceptor.Chain {
 public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    // ......
    // 创建下一个拦截器的任务
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    // 获取当前任务中的拦截器
    Interceptor interceptor = interceptors.get(index);
    // 将下一个任务传递至当前拦截器中进行关联,并在拦截器中传入的任务触发执行下一个拦截器
    Response response = interceptor.intercept(next);
    // ......
    return response;
  }
}

11.缓存机制CacheInterceptor

  • 看看缓存机制CacheInterceptor的实现原理:
public final class CacheInterceptor implements Interceptor {
  // ....
  @Override public Response intercept(Chain chain) throws IOException {
    // 根据请求内容通过Cache类判断是否已经存在响应的缓存信息
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    long now = System.currentTimeMillis();
    // 通过请求对象与缓存对象获取缓存策略,根据请求头的内容(Date、Expires、Last-Modified....)制定缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    // 记录请求过程的相关数据(请求次数、缓存次数....)
    if (cache != null) {
      cache.trackResponse(strategy);
    }
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
    // 当请求头中包含only-if-cached时,networkRequest 与 cacheResponse 都为空,表示不进行网络请求
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }
    // 不进行网络请求,并请求头包含缓存标识时,构建缓存内容.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    Response networkResponse = null;
    try {
      // 执行下一个拦截器任务
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
    // 如果本地有缓存,并且响应结果为没有修改时,直接从本地缓存获取相关信息数据
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
        // 更新响应对象至缓存中
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
   // 没有响应缓存时,封装请求返回的响应并添加至本地缓存中
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // 添加本地缓存中
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      // 去掉(POST\PUT\DELETE\MOVE\PATCH)请求方法的本地缓存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
    return response;
  }
}

12.连接与请求(StreamAllocation,RealConnection,HttpCodec)

  • OkHttp3把网络连接、请求数据通讯过程封装StreamAllocation,RealConnection,HttpCodec中;
  • StreamAllocation:负责初始化RealConnection、HttpCodec,并将前2者与RealCall进行关联;
    1.StreamAllocation初始化(在RetryAndFollowUpInterceptor进行实例化)
public final class RetryAndFollowUpInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
    //......
    // 实例化
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    //......
    }
}

2.RealConnection与HttpCodec初始化(RealConnection在ConnectInterceptor中通过StreamAllocation的newStream()初始化,而HttpCodec在RealConnection中被初始化)

public final class ConnectInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    // 获取HttpCodec 对象
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}
public final class StreamAllocation {
    //......
  public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();
    try {
      // 初始化RealConnection,最后会调用RealConnection的connect函数建立网络连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
     // 通过RealConnection 实例化HttpCpdec
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }
}
public final class RealConnection extends Http2Connection.Listener implements Connection {
  public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
      StreamAllocation streamAllocation) throws SocketException {
    //判断HTTP是否为Http2,如果是实例化Http2Codec,否则实例化Http1Codec
    if (http2Connection != null) {
      return new Http2Codec(client, chain, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }
}
  • RealConnection:真正的负责完成网络连接;
public final class RealConnection extends Http2Connection.Listener implements Connection {
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");
    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
          // 最后会调用socket.connect()进行网络连接
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;
        eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }
    if (route.requiresTunnel() && rawSocket == null) {
      ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
          + MAX_TUNNEL_ATTEMPTS);
      throw new RouteException(exception);
    }
    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }
}
  • HttpCodec:负责完成发送请求头与数据内容(使用okio完成数据的写入与读出);
    • Http1Codec:处理HTTP1.1协议的数据传递。
    • Http2Codec:处理HTTP2.0协议的数据传递。

13.实例

  • 编写一个简单GET请求:
public void get(){
        OkHttpClient okHttpClient = new OkHttpClient()
                .newBuilder()
//                .addInterceptor()
                .readTimeout(30, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
//                .header()
                .url("http://www.baidu.com")
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {             
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });
}

14.总结

  • 到此,Android OkHttp3就讲解完毕。
  • 如果喜欢我的分享,可以点击 关注 或者 ,你们支持是我分享的最大动力 。
  • linhaojian的Github

欢迎关注linhaojian_CSDN博客或者linhaojian_简书

不定期分享关于安卓开发的干货。


写技术文章初心

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

推荐阅读更多精彩内容

  • Okhttp 基础知识导图 Okhttp 使用1,创建一个客户端。2,创建一个请求。3,发起请求(入参回调)。 一...
    gczxbb阅读 2,051评论 0 2
  • 网络基础知识:okhttp是一个网络框架,帮我们封装了网络请求中需要重复做的事。 发起网络请求一般都是使用http...
    angeliur阅读 1,052评论 0 0
  • OkHttp3的使用 1、创建OkHttpClient;2、创建Request请求对象;3、OkHttpClien...
    Samuel_Tom阅读 296评论 0 0
  • 前言 用OkHttp很久了,也看了很多人写的源码分析,在这里结合自己的感悟,记录一下对OkHttp源码理解的几点心...
    Java小铺阅读 1,494评论 0 13
  • 一.网络通信概念理解 1.http协议概述 当我们在自己电脑的web浏览器地址栏敲入网址url,点击enter,...
    铜雀春深锁不住阅读 4,893评论 0 3