一、背景介绍:
问题的出现是因为我的代码中需要进行网络请求,而网络请求又是在一个线程中,间隔几秒就会请求一次,为了方便,就是用的OkHttp进行网络访问,本来,是没问题的,谁知道,某次测试时间长了一点,然后apk就突然闪退了,看了下log,是因为大量创建了线程,栈被撑爆了,OOM,遂卒~,通过网上找资料,最终查明原因是OkHttp的client没有进行单实例封装,每发一次请求,就会创建一个线程池,最终OOM,下面就这个问题,单独写了一个demo进行问题模拟及解决。
二、模拟复现:
1.OkHttp的异步访问代码:
fun sendOkHttpRequest(url: String, callback: okhttp3.Callback){
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(callback)
}
2.MainActivity中直接在onCreate中起一个线程,然后每过2s进行一次网络访问:
class MainActivity : AppCompatActivity() {
private var threadRun = true
val updateUi = 1
val helloWorld = 2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/**
* 每2s执行一次网络访问并更新UI
*/
thread {
var i: Int = 1
while (threadRun){
/* 进行网络访问 */
sendOkHttpRequest("https://www.baidu.com", object: Callback{
override fun onResponse(call: Call, response: Response) {
/* 拿到响应数据 */
val responseData = response.body?.string()
/* 这里为了方便,拿到数据什么也不干,直接去更新UI */
runOnUiThread {
textView.text = if ((i++ % 2) == 1){
"Update UI!"
} else {
"Hello World!"
}
}
}
override fun onFailure(call: Call, e: IOException) {
}
})
Thread.sleep(2000)
}
}
}
}
好了,从代码中我们可以看到,UI上的TextView控件会每隔两秒更新一下text,接着运行程序,打开Profilter,开始监控CPU项,为了快速复现OOM,我们注释掉代码中的两秒延时,很快apk就闪退了,我们先看下log:
然后是CPU的监视情况:
线程在短时间内急速上升,最后在达到1500+之后OOM闪退。
三、原因分析:
在网上查资料说是由于OkHttpClient不是单实例导致的,下面简单看下源码:
在实例化OkHttpClient的时候,其次构造函数会去进行各种资源申请,其中就有ConnectionPool,看下这个的构造:
可以看到,网络连接的线程池生命周期长达5分钟,也就是说,一个OkHttpClient创建成功,需要在5分钟之后才会被回收,如果我们的代码中,5分钟内持续创建多个OkHttpClient,不是特别频繁的话,那么系统在5分钟之后会维持一个线程峰值,之后一直都是这个状态,如果非常频繁,还不到5分钟的话就会OOM,因此,问题的关键在于我们要确保代码中尽量少的创建OkHttpClient。
四、单实例的实现:
我们将OkHttp的代码进行一下单实例封装:
object HttpUtils{
private val client = OkHttpClient()
fun sendOkHttpRequest(url: String, callback: okhttp3.Callback){
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(callback)
}
}
接下来onCreate中做相应修改:
/**
* 每2s执行一次网络访问并更新UI
*/
thread {
var i: Int = 1
while (threadRun){
/* 进行网络访问 */
HttpUtils.sendOkHttpRequest("https://www.baidu.com", object: Callback{
override fun onResponse(call: Call, response: Response) {
/* 拿到响应数据 */
val responseData = response.body?.string()
/* 这里为了方便,拿到数据什么也不干,直接去更新UI */
runOnUiThread {
textView.text = if ((i++ % 2) == 1){
"Update UI!"
} else {
"Hello World!"
}
}
}
override fun onFailure(call: Call, e: IOException) {
}
})
//Thread.sleep(2000)
}
}
这里多说一下,想要实现UI的更新,既可以使用runOnUiThread来实现(这种线程中再创建runOnUiThread会有什么缺点我不知道),也可以使用消息机制(使用bundle附带数据)来实现。
接下来验证一下:
可以看到开启应用之后,线程数一直保持平稳,这样就不会有OOM的情况发生了。