在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 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> references = connection.allocations;
for (int i = 0; i < references.size(); ) {
Reference 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 reference = connection.allocations.get(i);
//移除该StreamAllocation对应的引用
//解决我们遇到的问题
if (reference.get() == this) {
connection.allocations.remove(i);
return;
}
}
throw new IllegalStateException();
}
到此,我们终于知道出现该问题的原因及对应的解决方案了。
上述代码省略了HttpURLConnection及底层OkHttp的许多流程,
仅给出了重要的部分,后续我会专门写一篇博客来补充分析这部分代码。
这个问题说实话,个人感觉并不是很重要,
但想真正明白原理,还是需要细致阅读源码的。
一旦真正搞懂,确实有点GAI爷歌里的感觉:
一往无前虎山行,拨开云雾见光明。