response.body().string()을 여러 번 호출할 수 없는 이유는 무엇입니까?

亚连
풀어 주다: 2018-06-13 10:31:02
원래의
2583명이 탐색했습니다.

모두가 OkHttp를 사용했거나 접해본 적이 있을 것입니다. 최근 Okhttp를 사용하면서 함정에 빠졌습니다. 나중에 비슷한 문제가 발생하면 우회할 수 있도록 여기에 공유하겠습니다. 문제는 충분하지 않습니다. 이 기사는 소스 코드 관점에서 문제의 근원을 분석하는 데 중점을 두고 유용한 정보로 가득 차 있습니다.

1. 문제 발견개발 중에 OkHttpClient 개체를 구성하여 요청을 시작하고 이를 대기열에 추가했는데, 콜백 인터페이스가 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 {
    if (BuildConfig.DEBUG) {
      Log.d(TAG, "onResponse: " + response.body().toString());
    }
    //解析请求体
    parseResponseStr(response.body().string());
  }
});
로그인 후 복사

onResponse()에서는 디버깅의 편의를 위해 반환 본문을 인쇄한 다음, 반환 본문을 parsResponseStr() 메서드를 통해 구문 분석했습니다(참고: response.body().string( ) 여기서는 두 번 호출됩니다) .

문제가 없을 것 같았던 이 코드는 실제로 실행 후 오류가 발생했습니다. 콘솔을 통해 반환 본문 데이터(json)가 성공적으로 인쇄된 것을 확인했지만 예외가 발생했습니다:

java.lang.IllegalStateException: closed
로그인 후 복사
로그인 후 복사

2. 문제를 해결해보세요코드를 확인해보니,parseResponseStr() 호출시 response.body().string()이 다시 파라미터로 사용되는 것이 문제인 것을 발견했습니다. 급해서 온라인으로 확인해 보니 response.body().string()은 한 번만 호출할 수 있다는 것을 알고 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 . 소스코드로 문제를 분석하세요 문제는 해결됐지만, 이후에도 분석이 필요합니다. 이전에 OkHttp에 대한 이해는 사용에 국한되어 있었고 내부 구현의 세부 사항을 주의 깊게 분석하지 않았기 때문에 주말 동안 시간을 ​​내어 살펴보고 문제의 원인을 파악했습니다.

가장 직관적인 질문을 먼저 분석해 보겠습니다. 왜 response.body().string()은 한 번만 호출할 수 있나요?

디스어셈블리를 보면 먼저 response.body()를 통해 ResponseBody 객체(추상 클래스이므로 여기서는 특정 구현 클래스에 대해 신경 쓸 필요가 없습니다)를 가져온 다음 ResponseBody의 string() 메서드를 호출합니다. 응답 본문의 내용을 가져옵니다.

분석 후 body() 메서드에는 문제가 없습니다. string() 메서드를 살펴보겠습니다.

public final String string() throws IOException {
 return new String(bytes(), charset().name());
}
로그인 후 복사

매우 간단합니다. byte() 메서드에서 반환된 byte[] 배열을 다음과 같이 변환합니다. 문자 집합(charset) 지정 및 구성 문제 없습니다. 계속해서 byte() 메서드를 살펴보세요.

public final byte[] bytes() throws IOException {
 //...
 BufferedSource source = source();
 byte[] bytes;
 try {
  bytes = source.readByteArray();
 } finally {
  Util.closeQuietly(source);
 }
 //...
 return bytes;
}
//... 表示删减了无关代码,下同。
로그인 후 복사

byte() 메서드에서 BufferedSource 인터페이스 개체를 통해 byte[] 배열을 읽고 반환합니다. 위에서 언급한 예외와 결합하여 finally 코드 블록에서 Util.closeQuietly() 메서드를 발견했습니다. 실례합니다? 조용히 닫으시겠습니까? ? ?

이 메서드가 이상해 보이는데 맞나요? 후속 조치를 취하고 살펴보세요:

public static void closeQuietly(Closeable closeable) {
 if (closeable != null) {
  try {
   closeable.close();
  } catch (RuntimeException rethrown) {
   throw rethrown;
  } catch (Exception ignored) {
  }
 }
}
로그인 후 복사

코드 문서 주석에 따르면 위에서 언급한 BufferedSource 인터페이스는 Closeable을 구현하는 리소스 버퍼로 이해될 수 있습니다. 리소스를 닫고 해제하기 위해 close() 메소드를 복사하여 인터페이스. 그런 다음 close() 메서드가 수행하는 작업을 살펴보세요(현재 시나리오에서 BufferedSource 구현 클래스는 RealBufferedSource입니다).

//持有的 Source 对象
public final Source source;
@Override
public void close() throws IOException {
 if (closed) return;
 closed = true;
 source.close();
 buffer.clear();
}
로그인 후 복사

분명히 리소스는 source.close()를 통해 닫히고 해제됩니다. 말하자면, closeQuietly() 메서드의 기능은 자명합니다. 이는 ResponseBody 하위 클래스가 보유하는 BufferedSource 인터페이스 객체를 닫는 것입니다.

분석의 이 시점에서 우리는 갑자기 response.body().string()을 호출할 때 OkHttp가 응답 본문의 버퍼 리소스를 반환하고 closeQuietly() 메서드를 호출하여 자동으로 해제한다는 사실을 깨달았습니다. 자원.

이런 식으로 string() 메서드를 다시 호출하면 여전히 위의 byte() 메서드로 돌아갑니다. 이번에는 bytes = source.readByteArray() 코드 줄에 문제가 있습니다. RealBufferedSource의 readByteArray() 메서드를 살펴보겠습니다.

@Override
public byte[] readByteArray() throws IOException {
 buffer.writeAll(source);
 return buffer.readByteArray();
}
로그인 후 복사

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;
}
로그인 후 복사

문제는 for 루프의 source.read()에 있습니다. 위의 close() 메서드를 분석할 때 리소스를 닫고 해제하기 위해 source.close()를 호출했다는 사실을 기억하세요. 따라서 read() 메서드가 다시 호출되면 어떻게 될까요?

@Override
public long read(Buffer sink, long byteCount) throws IOException {
  //...
  if (closed) throw new IllegalStateException("closed");
  //...
  return buffer.read(sink, toRead);
}
로그인 후 복사

이 시점에서는 이전에 발생한 충돌과 일치합니다.

java.lang.IllegalStateException: closed
로그인 후 복사
로그인 후 복사

4 OkHttp는 왜 이렇게 설계되었나요? 소스 코드를 망쳐 문제의 근본 원인을 찾았지만 여전히 질문이 있습니다. OkHttp는 왜 이렇게 설계되었나요?

사실 이 문제를 이해하는 가장 좋은 방법은 JakeWharton이 문제에 대해 응답한 것처럼 ResponseBody의 주석 문서를 보는 것입니다.

reply of JakeWharton in okhttp issues
로그인 후 복사

간단한 문장으로: ResponseBody에 문서화되어 있습니다. 그래서 저는 클래스 주석 문서를 읽기 위해 달려갔습니다. , 최종 요약은 다음과 같습니다.

在实际开发中,响应主体 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()
로그인 후 복사

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

在Javascript中如何实现网页抢红包

详细解读ES6语法中可迭代协议

详细解读在React组件“外”如何使用父组件

微信小程序如何实现涂鸦

위 내용은 response.body().string()을 여러 번 호출할 수 없는 이유는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿