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?

  • 2535

In Android, when we access the network, the simplest way is similar to:

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();
    }
}
  • When I recently ran a code similar to the above in an 8.0 mobile phone, I suddenly found that a log similar to the following would be printed probabilistically:
A connection to xxxxxx was leaked. Did you forget to close a response body?
  • I checked the code carefully and found that after the connection is used up, it has been disconnected. 

How can it still print this kind of code that makes people feel uncomfortable?

In order to solve this problem, I have searched for a long time on websites at home and abroad, 
but have not been able to find a real feasible solution.

In desperation, I had to swipe the source code, and finally found the cause of the problem and a solution. 
Therefore, record the more important places in this film blog.


In the source code of Android, we know that the underlying implementation of the openConnection function of the URL depends on the OkHttp library. 
For this part of the process, I will write a document to record it later.

Now what we need to know is: 
The Http link created in the OkHttp library is a RealConnection object. 
In order to achieve the effect of reuse, OkHttp specially created a ConnectionPool object to manage all RealConnections. 
It's a bit like the thread pool manages all the threads.

When we create a new RealConnection, the put function of ConnectionPool is called:

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connections.isEmpty()) {
        //执行一个cleanupRunnable
        executor.execute(cleanupRunnable);
    }
    //将新的connection加入池子中
    connections.add(connection);
}
  • Now, let's see what cleanupRunnable does:
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) {
                    }
                }
            }
        }
    }
};
  • The true face of the cleanup function is as follows:
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;
}
  • Through the cleanup function, it is not difficult to see that the main purpose of the function is: 

Gradually clean up the RealConnection that has been idle in the connectionPool.

The only doubt now is the pruneAndGetAllocationCount function above:

/**
 * 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的许多流程, 
仅给出了重要的部分,后续我会专门写一篇博客来补充分析这部分代码。



Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325538708&siteId=291194637