Maison > Article > interface Web > Pourquoi réponse.body().string() ne peut-il pas être appelé plusieurs fois ?
Je crois que tout le monde a utilisé ou est entré en contact avec OkHttp lorsque j'utilisais Okhttp récemment, je vais le partager ici afin que tout le monde puisse le contourner lorsqu'il rencontrera des problèmes similaires à l'avenir. >
Juste une solution Les problèmes ne suffisent pas. Cet article se concentrera sur l'analyse de la racine du problème du point de vue du code source, qui regorge d'informations utiles.1. J'ai trouvé le problème
Pendant le développement, j'ai lancé une requête en construisant l'objet OkHttpClient et je l'ai ajouté à la file d'attente. Le serveur a répondu : l'interface de rappel déclenche la méthode onResponse(), puis utilise l'objet Response pour traiter les résultats renvoyés et implémenter la logique métier dans cette méthode. Le code est à peu près le suivant ://注:为聚焦问题,删除了无关代码 getHttpClient().newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) {} @Override public void onResponse(Call call, Response response) throws IOException { if (BuildConfig.DEBUG) { Log.d(TAG, "onResponse: " + response.body().toString()); } //解析请求体 parseResponseStr(response.body().string()); } });Dans onResponse(), pour faciliter le débogage, j'ai imprimé le corps de retour, puis j'ai analysé le corps de retour via la méthode parseResponseStr() (remarque : réponse. body() a été appelé deux fois ici .string() ). Ce code, qui semblait n'avoir aucun problème, s'est en fait trompé après son exécution : il a été vu via la console que les données du corps de retour (json) avaient été imprimées avec succès, mais une exception a ensuite été levée :
java.lang.IllegalStateException: closed
2. Résoudre le problème
Après avoir vérifié le code, j'ai découvert que le problème réside dans l'appel de parseResponseStr() et l'utilisation de réponse.body () encore une fois .string() comme paramètre. Parce que j'étais pressé, j'ai vérifié en ligne et découvert que réponse.body().string() ne peut être appelé qu'une seule fois, j'ai donc résolu le problème en modifiant la logique dans la méthode onResponse() :getHttpClient().newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) {} @Override public void onResponse(Call call, Response response) throws IOException { //此处,先将响应体保存到内存中 String responseStr = response.body().string(); if (BuildConfig.DEBUG) { Log.d(TAG, "onResponse: " + responseStr); } //解析请求体 parseReponseStr(responseStr); } });
3. Analysez le problème combiné avec le code source
Une fois le problème résolu, il doit encore être analysé par la suite. Comme ma compréhension précédente d'OkHttp se limitait à son utilisation et que je n'avais pas soigneusement analysé les détails de son implémentation interne, j'ai pris le temps de l'examiner de haut pendant le week-end et j'ai découvert la cause du problème. Analysons d'abord la question la plus intuitive : pourquoi réponse.body().string() ne peut-il être appelé qu'une seule fois ? En le démontant, récupérez d'abord l'objet ResponseBody (c'est une classe abstraite, nous n'avons pas besoin de nous soucier de la classe d'implémentation spécifique ici) via Response.body(), puis appelez la string() méthode de ResponseBody pour obtenir le contenu du corps de la réponse. Après analyse, il n'y a aucun problème avec la méthode body(). Regardons la méthode string() :public final String string() throws IOException { return new String(bytes(), charset().name()); }C'est très simple. la méthode byte() en spécifiant le jeu de caractères (charset). Le tableau byte[] est converti en un objet String. Il n'y a aucun problème avec la construction. Continuez à regarder la méthode byte() : .
public final byte[] bytes() throws IOException { //... BufferedSource source = source(); byte[] bytes; try { bytes = source.readByteArray(); } finally { Util.closeQuietly(source); } //... return bytes; } //... 表示删减了无关代码,下同。Dans la méthode byte(), lisez le tableau byte[] via l'objet d'interface BufferedSource et renvoyez-le. Combiné avec l'exception mentionnée ci-dessus, j'ai remarqué la méthode Util.closeQuietly() dans le bloc de code final. excusez-moi? Fermer en silence ? ? ? Cette méthode semble bizarre. Est-ce vrai ? Suivez et jetez un œil :
public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (RuntimeException rethrown) { throw rethrown; } catch (Exception ignored) { } } }Il s'avère que l'interface BufferedSource mentionnée ci-dessus peut être comprise comme un tampon de ressources selon le commentaires de la documentation du code. , qui implémente l'interface Closeable et ferme et libère les ressources en remplaçant la méthode close(). Regardez ensuite en bas pour voir ce que fait la méthode close() (dans le scénario actuel, la classe d'implémentation BufferedSource est RealBufferedSource) :
//持有的 Source 对象 public final Source source; @Override public void close() throws IOException { if (closed) return; closed = true; source.close(); buffer.clear(); }Évidemment, la ressource est fermée et libérée via source.close(). En parlant de cela, la fonction de la méthode closeQuietly() va de soi, qui consiste à fermer l'objet d'interface BufferedSource détenu par la sous-classe ResponseBody. Après avoir analysé cela, on se rend compte soudain : lorsque nous appelons Response.body().string() pour la première fois, OkHttp renvoie les ressources tampon du corps de la réponse et appelle la méthode closeQuietly() pour libérer silencieusement les ressources. De cette façon, lorsque nous appelons à nouveau la méthode string(), nous revenons toujours à la méthode byte() ci-dessus. Cette fois, le problème réside dans la ligne de code bytes = source.readByteArray(). Jetons un coup d'œil à la méthode readByteArray() de RealBufferedSource :
@Override public byte[] readByteArray() throws IOException { buffer.writeAll(source); return buffer.readByteArray(); }Continuez à regarder la méthode writeAll() :
@Override public long writeAll(Source source) throws IOException { //... long totalBytesRead = 0; for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) { totalBytesRead += readCount; } return totalBytesRead; }Le problème réside dans la source.read( ) de la boucle for . N'oubliez pas que lors de l'analyse de la méthode close() ci-dessus, elle a appelé source.close() pour fermer et libérer la ressource. Alors, que se passe-t-il lorsque la méthode read() est à nouveau appelée :
@Override public long read(Buffer sink, long byteCount) throws IOException { //... if (closed) throw new IllegalStateException("closed"); //... return buffer.read(sink, toRead); }À ce stade, elle rencontre le crash que j'ai rencontré plus tôt :
java.lang.IllegalStateException: closed
4.Pourquoi OkHttp est-il conçu de cette façon ?
En fouillant le code source, nous avons trouvé la racine du problème, mais j'ai encore une question : pourquoi OkHttp est-il conçu de cette façon ? En fait, la meilleure façon de comprendre ce problème est de consulter la documentation d'annotation de ResponseBody, comme JakeWharton a répondu dans les numéros :reply of JakeWharton in okhttp issuesEn une phrase simple : C'est documenté sur ResponseBody. J'ai donc couru lire le document d'annotation de classe, et je l'ai finalement résumé ainsi :
在实际开发中,响应主体 RessponseBody 持有的资源可能会很大,所以 OkHttp 并不会将其直接保存到内存中,只是持有数据流连接。只有当我们需要时,才会从服务器获取数据并返回。同时,考虑到应用重复读取数据的可能性很小,所以将其设计为 一次性流(one-shot) ,读取后即 '关闭并释放资源'。
5.总结
最后,总结以下几点注意事项,划重点了:
1.响应体只能被使用一次;
2.响应体必须关闭:值得注意的是,在下载文件等场景下,当你以 response.body().byteStream() 形式获取输入流时,务必通过 Response.close() 来手动关闭响应体。
3.获取响应体数据的方法:使用 bytes() 或 string() 将整个响应读入内存;或者使用 source() , byteStream() , charStream() 方法以流的形式传输数据。
4.以下方法会触发关闭响应体:
Response.close() Response.body().close() Response.body().source().close() Response.body().charStream().close() Response.body().byteString().close() Response.body().bytes() Response.body().string()
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!