Android 8.0 OkHttp問題の解決:HttpURLConnection Leak
17199 ワード
Androidでは、ネットワークにアクセスする際の最も簡単な方法は、次のようなものです.
最近8.0の携帯電話で上記のようなコードを走っていると、次のようなロゴが確率的に印刷されていることがわかりました.
コードをよくチェックしてみると、接続が切れた後、すでにdisconnectされているのに、どうしてこのような不快なコードを印刷するのですか?
この問題を解決するために、国内外のウェブサイトで長い間探していましたが、本当に実行可能な解決策は見つかりませんでした.
仕方なく、強引にソースコードを引っ張って、やっと問題の原因と解決策を見つけた.そこで、このブログで重要なことをメモしておきます.
Androidのソースコードでは、URLのopenConnection関数の下位実装がOkHttpライブラリに依存していることを知っています.この部分の流れについては、後でドキュメントを書いて記録します.
OkHttpライブラリで作成されたHttpリンクがRealConnectionオブジェクトであることを知る必要があります.多重化の効果を達成するために、OkHttpはすべてのRealConnectionを管理するためにConnectionPoolオブジェクトを作成しました.これは、スレッドプールがすべてのスレッドを管理するのと同じです.
新しいRealConnectionを作成すると、ConnectionPoolのput関数が呼び出されます.
では、cleanupRunnableが何をするかを見てみましょう.
cleanup関数の正体は次のとおりです.
cleanup関数により,connectionPoolに空きがあるRealConnectionを逐次クリーンアップすることが主な目的であることが分かる.
唯一の疑問点は、上記のprune AndGetAllocationCount関数です.
上記のコードから分かるように、prune AndGetAllocationCountが参照されていないRealConnectionを発見すると、上記のleaked logが印刷されます.
個人的には,冒頭のコードの実行が完了した場合,GCはまずHttpURLConnection(直接所有ではない)などRealConnectionを持つオブジェクトを回収し,その後RealConnectionを回収すると推測する.また、HttpURLConnectionを回収した後、RealConnectionを回収する前にpruneAndGetAllocationCountを実行すると、このようなlogが印刷される可能性があります.これも注記で述べたように、pruneAndGetAllocationCountはGCに依存している.
しかし、コードから見ると、これは問題なく、Androidシステムはこれらのリソースを回収します.
冒頭のコードで、最後に呼び出されたHttpURLConnectionのdisconnect関数.StreamAllocationのcancel関数のみが呼び出され、RealConnectionのcancel関数に最終的に呼び出されます.
この方法はsocketのみを閉じ,参照を除去せず,我々が直面する問題を解決しないことがわかる.
ソースコードの試行と読み取りを続けた結果、次の方法でこの問題を解決できることがわかりました.
HttpURLConnectionのinputStreamをアクティブに閉じると、StreamAllocationのnoNewStreamsとstreamFinished関数が相次いで呼び出されます.
そこで,この問題の原因と対応策をやっと知ることができた.
上記のコードはHttpURLConnectionと下位OkHttpの多くの流れを省略して、重要な部分だけを提供して、後で私は専門的にブログを書いてこのコードを補充して分析します.
この問題は正直、個人的な感覚は重要ではありませんが、本当に原理を理解するには、ソースコードを細かく読む必要があります.本当に分かったら、確かにGAI爺の歌の中の感じがします:前に虎の山に行って、雲霧をかき分けて光を見ます.
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の携帯電話で上記のようなコードを走っていると、次のようなロゴが確率的に印刷されていることがわかりました.
A connection to xxxxxx was leaked. Did you forget to close a response body?
コードをよくチェックしてみると、接続が切れた後、すでにdisconnectされているのに、どうしてこのような不快なコードを印刷するのですか?
この問題を解決するために、国内外のウェブサイトで長い間探していましたが、本当に実行可能な解決策は見つかりませんでした.
仕方なく、強引にソースコードを引っ張って、やっと問題の原因と解決策を見つけた.そこで、このブログで重要なことをメモしておきます.
Androidのソースコードでは、URLのopenConnection関数の下位実装がOkHttpライブラリに依存していることを知っています.この部分の流れについては、後でドキュメントを書いて記録します.
OkHttpライブラリで作成されたHttpリンクがRealConnectionオブジェクトであることを知る必要があります.多重化の効果を達成するために、OkHttpはすべてのRealConnectionを管理するためにConnectionPoolオブジェクトを作成しました.これは、スレッドプールがすべてのスレッドを管理するのと同じです.
新しい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を逐次クリーンアップすることが主な目的であることが分かる.
唯一の疑問点は、上記のprune AndGetAllocationCount関数です.
/**
* 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();
}
上記のコードから分かるように、prune AndGetAllocationCountが参照されていない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爺の歌の中の感じがします:前に虎の山に行って、雲霧をかき分けて光を見ます.