前言
前一段时间,在一个使用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线程吗?下面我们带着这个问题,在源码中了解一下整个过程是如何进行的!
分析
首先创建OkHttpClient
和Request
实例就不再介绍了,我们从异步请求开始分析。
源码基于最新的版本: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()
方法,从而调用到NamedRunnable
的run()
方法中的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线程的方法