Android 8.0 解决OkHttp问题:HttpURLConnection Leak

在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爷歌里的感觉:
一往无前虎山行,拨开云雾见光明。

你可能感兴趣的:(Android源码学习笔记)