我这使用okhttp短时间进行大量请求的时候会出现java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory的报错,毫无以为这就是溢出,我们熟悉的OOM。接着去看详细的信息。
java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:753)
java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:970)
java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1388)
okhttp3.Dispatcher.enqueue(Dispatcher.java:132)
okhttp3.RealCall.enqueue(RealCall.java:100)
最终的错误指向到okhttp3
一. 那么为什么okhttp会造成OOM
看到pthread_create就大概能猜到是线程的问题,应该是一个不断的创建线程所导致的。但是到这里我就觉得很奇怪,这样的网络请求框架应该是有线程池的啊,查看了源码,一看名字我就找到OkHttpClient里面有一个叫ConnectionPool的,根据名字应该是这个吧,打开里面一看

这个线程池的创建是写在静态域里面的,那就更不会有问题啊。看来还得从请求的源码开始追踪找线索。
我们从报错的日志从下往上看,第一行RealCall是在调newCall方法的时候创建的
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
然后你自然就能知道Call.enqueue就是这个RealCall的enqueue方法,找到它
public void enqueue(Callback responseCallback) {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.captureCallStackTrace();
this.eventListener.callStart(this);
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
看到this.client.dispatcher().enqueue就知道是调用OkHttpClient的Dispatcher的enqueue方法,找打Dispatcher
synchronized void enqueue(AsyncCall call) {
if (this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
this.runningAsyncCalls.add(call);
this.executorService().execute(call);
} else {
this.readyAsyncCalls.add(call);
}
}
看到了有引用线程池executorService,我们在这个类中看这个线程池相关的代码,AS能做搜索什么的操作,看源码还是挺方便的。
public synchronized ExecutorService executorService() {
if (this.executorService == null) {
this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
}
return this.executorService;
}
从这里可以看出每个okHttpClient对象在请求的时候都会创建一个线程池,而且线程池的keepAliveTime是1分钟
那么问题就找到了。我之前以为client表示连接,每个连接都应该是单独的对象,而且它使用的是Builder模式,所以我是在每次请求都去创建一个新的okHttpClient对象,所以会造成会new出一个新的线程池,那在1分钟之内大量进行请求(创建okHttpClient)的话当然会炸
解决的办法当然就是所有请求只使用同一个okHttpClient对象,使用单例模式之类的方法都可以解决。
二. okHttpClient设置属性的问题
那么问题又来了,我们使用单例,但是我上面说过,okHttpClient的创建是使用的Builder模式,那它的所有参数都是在Builder对象中传进去的,没有办法再创建完okHttpClient对象之后再去用setXXX方法去改参数。
举个栗子,我这个版本的设置请求超时时间是在okHttpClient中设置的
OkHttpClient.Builder okBuilder = new OkHttpClient.Builder();
okBuilder.connectTimeout(3000, TimeUnit.SECONDS);
OkHttpClient okHttpClient = okBuilder.build();
简单的写是这样,但是我每个请求都要求设置不同的请求时间怎么办,okHttpClient 只有一个对象,又没有setXXX方法。
去查找之后发现okHttpClient 有一个叫newBuilder的方法,这个方法就有意思的
public OkHttpClient.Builder newBuilder() {
return new OkHttpClient.Builder(this);
}
第一眼看这个方法,觉得就是重新创建一个OkHttpClient对象,实则另藏玄机

看到了没有,一个是new新的Dispatcher,一个是复用之前的Dispatcher,我们这里走的newBuilder就是调下面的那个方法,复用Dispatcher,那就不会创建新的线程池,就不会产生OOM。我也是第一次才知道,Builder模式还有这样的玩法
所以想要为某次请求改属性的时候可以这样写
okHttpClient().newBuilder().readTimeout(3000, TimeUnit.SECONDS).build().newCall(request);
三. 总结
- 使用OkHttp时,所有请求应使用同一个OkHttpClient,就是你不想使用同一个,也不能在短时间内大量创建。
- OkHttpClient可以使用newBuilder的方法去更改OkHttpClient的属性。
网友评论