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

29 篇文章 0 订阅

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()) {

//执行一个cleanupRunnable

executor.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) {

//记录在使用的connection

int inUseConnectionCount = 0;


//记录空闲的connection

int idleConnectionCount = 0;


//记录空闲时间最长的connection

RealConnection 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.

// 轮询每一个RealConnection

if (pruneAndGetAllocationCount(connection, now) > 0) {

inUseConnectionCount++;

continue;

}


idleConnectionCount++;


//找到空闲时间最长的RealConnection

long idleDurationNs = now - connection.idleAtNanos;

if (idleDurationNs > longestIdleDurationNs) {

longestIdleDurationNs = idleDurationNs;

longestIdleConnection = connection;

}

}


//空闲时间超过限制或空闲connection数量超过限制,则移除空闲时间最长的connection

if (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,等待被cleanup

if (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);

}

}

//调用deallocate

deallocate(false, false, true);

}


//连续调用两次,第1、3个参数分别为true

private void deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {

RealConnection connectionToClose = null;

synchronized (connectionPool) {

if (streamFinished) {

//第二次,stream置为null

this.stream = null;

}


if (released) {

this.released = true;

}


if (connection != null) {

if (noNewStreams) {

//第一次,noNewStreams置为true

connection.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博客

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
E/AndroidRuntime: FATAL EXCEPTION: Thread-18 Process: com.example.read, PID: 22568 java.lang.RuntimeException: java.net.UnknownServiceException: CLEARTEXT communication to 192.168.210.113 not permitted by network security policy at com.example.read.upload_serverActivity$1.run(upload_serverActivity.java:111) at java.lang.Thread.run(Thread.java:920) Caused by: java.net.UnknownServiceException: CLEARTEXT communication to 192.168.210.113 not permitted by network security policy at okhttp3.internal.connection.RealRoutePlanner.planConnectToRoute$okhttp(RealRoutePlanner.kt:195) at okhttp3.internal.connection.RealRoutePlanner.planConnect(RealRoutePlanner.kt:152) at okhttp3.internal.connection.RealRoutePlanner.plan(RealRoutePlanner.kt:67) at okhttp3.internal.connection.FastFallbackExchangeFinder.launchTcpConnect(FastFallbackExchangeFinder.kt:118) at okhttp3.internal.connection.FastFallbackExchangeFinder.find(FastFallbackExchangeFinder.kt:62) at okhttp3.internal.connection.RealCall.initExchange$okhttp(RealCall.kt:267) at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:32) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:84) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:65) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:205) at okhttp3.internal.connection.RealCall.execute(RealCall.kt:158) at com.example.read.upload_serverActivity$1.run(upload_serverActivity.java:106) at java.lang.Thread.run(Thread.java:920) 怎么解决
05-29

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值