Android 8.0 解决OkHttp问题:A connection to xxx was leaked. Did you forget to close a response body?

Android中,我们访问网络时,最简单的方式类似与:

HttpURLConnection connection = null;try {//xxxxx为具体的网络地址URL url = new URL("xxxxx");connection = (HttpURLConnection) url.openConnection();connection.connect();//进行一些操作...............} catch (IOException e) {e.printStackTrace();} finally {if (connection != null) {connection.disconnect();}}

最近在8.0的手机里跑类似上述代码时,突然发现会概率性地打印类似如下的log:

A connection to xxxxxx was leaked. Did you forget to close a response body?

仔细check了一下代码,发现connection用完后,已经disconnect了,

怎么还会打印这种让人觉得不太舒服的代码?

为了解决这个问题,在国内外的网站上找了很久,但都没能找到真正可行的解决方案。

无奈之下,只好硬撸了一边源码,总算是找到了问题的原因和一个解决方案。 
因此,在本片博客中记录一下比较重要的地方。


Android的源码中,我们知道URL的openConnection函数的底层实现依赖于OkHttp库, 
对于这部分的流程,我之后专门写一篇文档记录一下。

现在我们需要知道的是: 
OkHttp库中的创建的Http链接为RealConnection对象。 
为了达到复用的效果,OkHttp专门创建了ConnectionPool对象来管理所有的RealConnection。 
这有点像线程池会管理所有的线程一样。

当我们创建一个新的RealConnection时,会调用ConnectionPool的put函数:

void put(RealConnection connection) {assert (Thread.holdsLock(this));if (connections.isEmpty()) {//执行一个cleanupRunnableexecutor.execute(cleanupRunnable);}//将新的connection加入池子中connections.add(connection);}

现在,我们来看看cleanupRunnable会干些啥:

private Runnable cleanupRunnable = new Runnable() {@Override public void run() {while (true) {//容易看出,其实就是周期性地执行cleanup函数long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);synchronized (ConnectionPool.this) {try {ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {}}}}}};

​​​​cleanup函数的真面目如下:

long cleanup(long now) {//记录在使用的connectionint inUseConnectionCount = 0;//记录空闲的connectionint idleConnectionCount = 0;//记录空闲时间最长的connectionRealConnection longestIdleConnection = null;//记录最长的空闲时间long longestIdleDurationNs = Long.MIN_VALUE;synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();// If the connection is in use, keep searching.// 轮询每一个RealConnectionif (pruneAndGetAllocationCount(connection, now) > 0) {inUseConnectionCount++;continue;}idleConnectionCount++;//找到空闲时间最长的RealConnectionlong idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}//空闲时间超过限制或空闲connection数量超过限制,则移除空闲时间最长的connectionif (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {// We've found a connection to evict. Remove it from the list, then close it below (outside// of the synchronized block).connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {// A connection will be ready to evict soon.//返回下一次执行cleanup需等待的时间return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {// All connections are in use. It'll be at least the keep alive duration 'til we run again.// 返回最大可等待时间return keepAliveDurationNs;} else {// No connections, idle or in use.return -1;}}//特意放到同步锁的外面释放,减少持锁时间Util.closeQuietly(longestIdleConnection.getSocket());return 0;}

通过cleanup函数,不难看出该函数主要的目的就是:

逐步清理connectionPool中已经空闲的RealConnection。

现在唯一的疑点就是上文中的pruneAndGetAllocationCount函数了:

/*** Prunes any leaked allocations and then returns the number of remaining live allocations on* {@code connection}. Allocations are leaked if the connection is tracking them but the* application code has abandoned them. Leak detection is imprecise and relies on garbage* collection.*/private int pruneAndGetAllocationCount(RealConnection connection, long now) {//获取使用该RealConnection的对象的引用List<Reference<StreamAllocation>> references = connection.allocations;for (int i = 0; i < references.size(); ) {Reference<StreamAllocation> reference = references.get(i);//引用不为null,说明仍有java对象持有它if (reference.get() != null) {i++;continue;}//没有持有它的对象,说明上层持有RealConnection已经被回收了// We've discovered a leaked allocation. This is an application bug.Internal.logger.warning("A connection to " + connection.getRoute().getAddress().url()+ " was leaked. Did you forget to close a response body?");//移除引用references.remove(i);connection.noNewStreams = true;// If this was the last allocation, the connection is eligible for immediate eviction.//没有任何引用时, 标记为idle,等待被cleanupif (references.isEmpty()) {connection.idleAtNanos = now - keepAliveDurationNs;return 0;}}return references.size();}

从上面的代码可以看出,pruneAndGetAllocationCount发现没有被引用的RealConnection时, 就会打印上文提到的leaked log。

个人猜测,如果开头的代码执行完毕后,GC先回收HttpURLConnection(非直接持有)等持有RealConnection的对象,后回收RealConnection。 
且在回收HttpURLConnection后,回收RealConnection前,刚好执行了pruneAndGetAllocationCount,就可能会打印这种log。 
这也是注释中提到的,pruneAndGetAllocationCount依赖于GC。

不过从代码来看,这并没有什么问题,Android系统仍会回收这些资源。

在文章开头的代码中,最后调用的HttpURLConnection的disconnect函数。 
该函数仅会调用StreamAllocation的cancel函数,且最终调用到RealConnection的cancel函数:

public void cancel() {// Close the raw socket so we don't end up doing synchronous I/O.Util.closeQuietly(rawSocket);}

可以看出,该方法仅关闭了socket,并没有移除引用,不会解决我们遇到的问题。


经过不断地尝试和阅读源码,我发现利用下述方式可以解决这个问题:

HttpURLConnection connection = null;try {//xxxxx为具体的网络地址URL url = new URL("xxxxx");connection = (HttpURLConnection) url.openConnection();connection.connect();//进行一些操作...............} catch (IOException e) {e.printStackTrace();} finally {if (connection != null) {try {//主动关闭inputStream//这里不需要进行判空操作connection.getInputStream().close();} catch (IOException e) {e.printStackTrace();}connection.disconnect();}}

当我们主动关闭HttpURLConnection的inputStream时,将会先后调用到StreamAllocation的noNewStreams和streamFinished函数:

public void noNewStreams() {deallocate(true, false, false);}public void streamFinished(HttpStream stream) {synchronized (connectionPool) {if (stream == null || stream != this.stream) {throw new IllegalStateException("expected " + this.stream + " but was " + stream);}}//调用deallocatedeallocate(false, false, true);}//连续调用两次,第1、3个参数分别为trueprivate void deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {RealConnection connectionToClose = null;synchronized (connectionPool) {if (streamFinished) {//第二次,stream置为nullthis.stream = null;}if (released) {this.released = true;}if (connection != null) {if (noNewStreams) {//第一次,noNewStreams置为trueconnection.noNewStreams = true;}//stream此时为null, 其它两个条件满足一个if (this.stream == null && (this.released || connection.noNewStreams)) {//就可以执行release函数release(connection);if (connection.streamCount > 0) {routeSelector = null;}//idle的RealConnection可以在下文被关闭if (connection.allocations.isEmpty()) {connection.idleAtNanos = System.nanoTime();if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {connectionToClose = connection;}}connection = null;}}}if (connectionToClose != null) {Util.closeQuietly(connectionToClose.getSocket());}}//最后看看release函数private void release(RealConnection connection) {for (int i = 0, size = connection.allocations.size(); i < size; i++) {Reference<StreamAllocation> reference = connection.allocations.get(i);//移除该StreamAllocation对应的引用//解决我们遇到的问题if (reference.get() == this) {connection.allocations.remove(i);return;}}throw new IllegalStateException();}

到此,我们终于知道出现该问题的原因及对应的解决方案了。

上述代码省略了HttpURLConnection及底层OkHttp的许多流程, 
仅给出了重要的部分,后续我会专门写一篇博客来补充分析这部分代码。

转载于:Android 8.0学习(28)--- 解决OkHttp问题_zhangbijun1230的博客-CSDN博客

Android 8.0解决的OkHttp问题:A connection to xxx was leaked. Did you forget to close a response body?相关推荐

  1. Android 7.0解决抓取不到https请求的问题

    Android 7.0解决抓取不到https请求的问题 参考文章: (1)Android 7.0解决抓取不到https请求的问题 (2)https://www.cnblogs.com/meitian/ ...

  2. AndroidStudio_A connection was leaked. Did you forget to close a response body?---Android原生开发工作笔记241

    下面是报错内容,使用okhttp,的时候报错的.这个不关闭,好像时间久了会报内存溢出错误. W/OkHttp: A connection to http://172.19.128.64:8061/ w ...

  3. Android 6.0+ 解决浏览器某些页面无法访问的问题

    Android 浏览器因webview 版本过低,可能会导致访问一些页面的时候产生空白或者超时无法显示,卡顿等问题. 解决方法: 更换webview: 使用系统自带的游览器去访问:https://ww ...

  4. Android 11.0 解决切换横屏时SystemUI导航栏固定在桌面右侧而不是底部的问题

    前言 正常情况下横竖屏旋转的时候导航栏也会跟着一起旋转,但是在Android R上面发现导航栏在横屏的时候是固定在右侧的,而不是旋转到底部.这个功能其实是Android 高版本特意修改的,为了是方便横 ...

  5. Android 8.0学习(28)--- 解决OkHttp问题

    Android 8.0 解决OkHttp问题:A connection to xxx was leaked. Did you forget to close a response body? 2535 ...

  6. OkHttp 内存溢出问题 A connection to xxxxxx was leaked.

    OkHttp内存溢出 最近刚接触OkHttp,使用它调用其他的接口,我测试的时候单个调用或者少量多个调用也没问题,因为是钉钉提醒,我没办法做大规模测试,所以使用检查几遍逻辑和代码没问题就上线了,上线第 ...

  7. Android 8.0学习(31)---Android 8.0 中的 ART 功能改进

    Android 8.0 中的 ART 功能改进 在 Android 8.0 版本中,Android Runtime (ART) 有了极大改进.下面的列表总结了设备制造商可以在 ART 中获得的增强功能 ...

  8. 解决Picasso在Android 5.0以下版本不兼容https导致图片不显示

    近期在项目中遇到了一个问题,使用picasso加载图片在Android5.0以下版本图片显示不来. 由于之前在几个项目中都使用过picasso而且未出现类似问题,觉得值得好好研究一下. 简单定位一下问 ...

  9. as3 android白屏,Android 8.0中一些坑以及对应的解决方法

    前言 虽然 Android 9.0 都已经面世了,本篇文章写的有点迟了. 但是迟到好过不到,因此基于此这边还是记录一下项目中遇到的 Android 8.0 的坑及对应解决方法. 每次系统升级,虽然系统 ...

最新文章

  1. IT职场:程序员如何增加收入?
  2. (传送门)android studio 一直卡在Gradle:Build Running的解决办法
  3. C语言-一维数组与指针
  4. 判断日期是否为当月最后一天_对比Excel,怎么用Python获取指定月最后一天的日期...
  5. java调用百度api完成人脸识别
  6. 世界各国各地区名称代码对应表
  7. php 车牌号限号,机动车限行尾号今天起轮换 周一至周五分别限行4和9、5和0、1和6、2和7、3和8...
  8. Pajek常用方法保姆级操作指南——社会网络分析
  9. 【Android安全】Android root原理及方案 | Magisk原理
  10. 爬虫需谨慎,你不知道的爬虫与反爬虫套路!
  11. windows无法连接到某个wifi_电脑提示Windows无法连接到这个网络/无线网络的解决方法...
  12. 简单实用的手机、电脑换IP方法
  13. 九龙证券|主力出逃大热门互联网股近13亿元!尾盘两股获加仓超亿元
  14. github 和git_Git和GitHub入门指南
  15. 数据挖掘 | 判别分析 +朴素贝叶斯分类算法
  16. 编程(代码、软件)规范(适用嵌入式、单片机、上位机等)
  17. word饼图如何画引导线_excle怎么画立体饼图/如何在饼形图中添加引导线(Excel)...
  18. Java设计模式19:观察者模式(Observer)
  19. 华为服务器检索信息,裸金属服务器使用标签检索资源
  20. 不坑盒子:强大的word插件,让工作更高效

热门文章

  1. 交换机堆叠和热备的区别
  2. Route [register] not defined. 的解决办法
  3. 【贪心思想】兄弟总爱贪小便宜,原来是把贪心算法掌握得如此熟练【经典例题讲解】
  4. 软考中级 真题 2014年下半年 系统集成项目管理工程师 应用技术 下午试卷
  5. swiper异形切换(可视化)
  6. 抓包_Appium安装和配置
  7. 获取验证码按钮 点击后开始倒计时
  8. 输入一个整数,判断是不是质数?
  9. WebAssembly – Where is it going?
  10. HaaS助力蜂农脱贫致富之路