Android 8.0解决的OkHttp问题:A connection to xxx was leaked. Did you forget to close a response body?
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?相关推荐
- Android 7.0解决抓取不到https请求的问题
Android 7.0解决抓取不到https请求的问题 参考文章: (1)Android 7.0解决抓取不到https请求的问题 (2)https://www.cnblogs.com/meitian/ ...
- 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 ...
- Android 6.0+ 解决浏览器某些页面无法访问的问题
Android 浏览器因webview 版本过低,可能会导致访问一些页面的时候产生空白或者超时无法显示,卡顿等问题. 解决方法: 更换webview: 使用系统自带的游览器去访问:https://ww ...
- Android 11.0 解决切换横屏时SystemUI导航栏固定在桌面右侧而不是底部的问题
前言 正常情况下横竖屏旋转的时候导航栏也会跟着一起旋转,但是在Android R上面发现导航栏在横屏的时候是固定在右侧的,而不是旋转到底部.这个功能其实是Android 高版本特意修改的,为了是方便横 ...
- Android 8.0学习(28)--- 解决OkHttp问题
Android 8.0 解决OkHttp问题:A connection to xxx was leaked. Did you forget to close a response body? 2535 ...
- OkHttp 内存溢出问题 A connection to xxxxxx was leaked.
OkHttp内存溢出 最近刚接触OkHttp,使用它调用其他的接口,我测试的时候单个调用或者少量多个调用也没问题,因为是钉钉提醒,我没办法做大规模测试,所以使用检查几遍逻辑和代码没问题就上线了,上线第 ...
- Android 8.0学习(31)---Android 8.0 中的 ART 功能改进
Android 8.0 中的 ART 功能改进 在 Android 8.0 版本中,Android Runtime (ART) 有了极大改进.下面的列表总结了设备制造商可以在 ART 中获得的增强功能 ...
- 解决Picasso在Android 5.0以下版本不兼容https导致图片不显示
近期在项目中遇到了一个问题,使用picasso加载图片在Android5.0以下版本图片显示不来. 由于之前在几个项目中都使用过picasso而且未出现类似问题,觉得值得好好研究一下. 简单定位一下问 ...
- as3 android白屏,Android 8.0中一些坑以及对应的解决方法
前言 虽然 Android 9.0 都已经面世了,本篇文章写的有点迟了. 但是迟到好过不到,因此基于此这边还是记录一下项目中遇到的 Android 8.0 的坑及对应解决方法. 每次系统升级,虽然系统 ...
最新文章
- IT职场:程序员如何增加收入?
- (传送门)android studio 一直卡在Gradle:Build Running的解决办法
- C语言-一维数组与指针
- 判断日期是否为当月最后一天_对比Excel,怎么用Python获取指定月最后一天的日期...
- java调用百度api完成人脸识别
- 世界各国各地区名称代码对应表
- php 车牌号限号,机动车限行尾号今天起轮换 周一至周五分别限行4和9、5和0、1和6、2和7、3和8...
- Pajek常用方法保姆级操作指南——社会网络分析
- 【Android安全】Android root原理及方案 | Magisk原理
- 爬虫需谨慎,你不知道的爬虫与反爬虫套路!
- windows无法连接到某个wifi_电脑提示Windows无法连接到这个网络/无线网络的解决方法...
- 简单实用的手机、电脑换IP方法
- 九龙证券|主力出逃大热门互联网股近13亿元!尾盘两股获加仓超亿元
- github 和git_Git和GitHub入门指南
- 数据挖掘 | 判别分析 +朴素贝叶斯分类算法
- 编程(代码、软件)规范(适用嵌入式、单片机、上位机等)
- word饼图如何画引导线_excle怎么画立体饼图/如何在饼形图中添加引导线(Excel)...
- Java设计模式19:观察者模式(Observer)
- 华为服务器检索信息,裸金属服务器使用标签检索资源
- 不坑盒子:强大的word插件,让工作更高效