从源码分析OkHttp3异步请求网络更新UI报错问题

前言

前一段时间,在一个使用OkHttp进行异步请求网络,然后拿到返回结果进行UI界面的更新的时候,时不时的会报错闪退!

出错代码

在使用OkHttp进行异步请求网络的时候,然后更新UI的时候,是这样写的:

OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
    .get()
    .url(URL)
    .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 {
        textview.setText(response.body().string());
    }
});

然后运行的时候,会出现下面这样的错误:

E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
Process: com.xxxx.xxx, PID: xxx
android.view.ViewRootImpl$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.

然后报错代码的位置在:

textview.setText(response.body().string());

这一句。
上边报错信息Only the original thread that created a view hierarchy can touch its views.,字面意思是只有创建视图层次结构的原始线程才能操作它的View,明显是线程安全相关的。
我们知道Android中相关的view和控件操作都不是线程安全的,所以Android才会禁止在非UI线程更新UI,那么在使用OkHttp进行异步请求,然后得到回调方法中的response运行在非UI线程吗?下面我们带着这个问题,在源码中了解一下整个过程是如何进行的!

分析

首先创建OkHttpClientRequest实例就不再介绍了,我们从异步请求开始分析。
源码基于最新的版本:3.10.0

那么我们先来看OkHttpClient#newCall(Request request)

@Override 
public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
}

然后:

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;
  }

这一步用于将Request转换为Call对象,Call为一个接口,实现类为RealCall,这里我们就得到了一个RealCall的实例。

接着调用RealCall#enqueue(Callback responseCallback)开始异步请求:

@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));
}

到这里,我们注意最后一句:
client.dispatcher().enqueue(new AsyncCall(responseCallback));
首先调用OkHttpClient#dispatcher()得到一个Dispatcherd对象,该对象的初始化就在OkHttpClient 初始化的时候。然后调用Dispatcherd#enqueue(AsyncCall call)方法:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
}

这里边有okhttp任务调度缓存队列的内容,我们不关注,只看executorService().execute(call);这一句,首先Dispatcherd#executorService():

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));
    }
    return executorService;
}

我们可以看到这里初始化了一个线程池,得到这一线程池实例之后,调用其实现接口Executor#execute(Runnable command)方法。
到这里我们回过头看,
client.dispatcher().enqueue(new AsyncCall(responseCallback))
这句中传入的参数new AsyncCall(responseCallback)

AsyncCall(Callback responseCallback) {
      super("OkHttp %s", redactedUrl());
      this.responseCallback = responseCallback;
}

构造方法中传入了我们的回调接口对象Callback
AsyncCall继承了NamedRunnable,而NamedRunnable实现了Runnable接口,其实现的run方法如下:

@Override 
public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();

run()方法中执行一个抽象方法execute(),所以上边在线程池对象创建之后,执行其excute(),会调用参数Runnable对象run()方法,从而调用到NamedRunnablerun()方法中的execute(),而上边我们说传入的参数AsyncCall继承了NamedRunnable,所以最终要执行AsyncCall实现的execute()抽象方法。

哎呀,终于到头了,下面我们看下AsyncCall#execute()方法:

@Override 
protected void execute() {
      boolean signalledCallback = false;
      try {
        //执行具体的耗时任务
        Response response = getResponseWithInterceptorChain();
        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);
      }
  }

上面我们只关注,本篇文章发生问题的地方,那就是回调方法。
由上边的分析,我们知道responseCallback.onFailure()responseCallback.onResponse()两个回调方法的执行是在线程池中,也就是一个Runnable对象的run()方法中,而非UI线程,所以我们在回调方法中去更新UI界面,就出错了!

解决方案

那么我们已经知道问题的所在了,那就是在非UI线程里边去更新了UI界面,所以我们要异步去更新UI。
到这,我想就不需要再多说什么了吧!
你可以使用runOnUiThread(),或者View#post(),或者使用Handler等都可以,而不要再在回调中直接去更新UI

参考文章
OkHttp3源码分析[任务队列]
Android进阶——Handler的应用之解决Only the original thread that created a view hierarchy can touch its views
详解Android中OkHttp3的例子和在子线程更新UI线程的方法

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值