android 使用okhttp可能引发OOM的一个点

android 使用okhttp可能引发OOM的一个点,第1张

概述android 使用okhttp可能引发OOM的一个点 遇到一个问题: 需要给所有的请求加签名校验以防刷接口;传入请求url及body生成一个文本串作为一个header传给服务端;已经有现成的签名检验方法String doSignature(String url, byte[] body);当前网络库基于com.squareup.okhttp3:okhttp:3.14.2. 这很简单了,当然是写一个interceptor然后将request对象的url及body传入就好.于是有: public class SignInterceptor implem

遇到一个问题: 需要给所有的请求加签名校验以防刷接口;传入请求url及body生成一个文本串作为一个header传给服务端;已经有现成的签名检验方法String doSignature(String url,byte[] body);当前网络库基于com.squareup.okhttp3:okhttp:3.14.2.
这很简单了,当然是写一个interceptor然后将request对象的url及body传入就好.于是有:

public class SignInterceptor implements Interceptor {  @NonNull  @OverrIDe  public Response intercept(@NonNull Chain chain) throws IOException {    Request request = chain.request();    Requestbody body = request.body();    byte[] bodyBytes = null;    if (body != null) {      final Buffer buffer = new Buffer();      body.writeto(buffer);      bodyBytes = buffer.readByteArray();    }    Request.Builder builder = request.newBuilder();    httpUrl oldUrl = request.url();    final String url = oldUrl.toString();    final String signed = doSignature(url,bodyBytes));    if (!TextUtils.isEmpty(signed)) {      builder.addheader(SIGN_KEY_name,signed);    }    return chain.proceed(builder.build());  }}

okhttp的Reqeustbody是一个抽象类,内容输出只有writeto方法,将内容写入到一个BufferedSink接口实现体里,然后再将数据转成byte[]也就是内存数组.能达到目的的类只有Buffer,它实现了BufferedSink接口并能提供转成内存数组的方法readByteArray. 这貌似没啥问题呀,能造成OOM?

是的,要看请求类型,如果是一个上传文件的接口呢?如果这个文件比较大呢?上传接口有可能会用到public static Requestbody create(final @Nullable MediaType ContentType,final file file)方法,如果是针对文件的实现体它的writeto方法是sink.writeall(source);而我们传给签名方法时用到的Buffer.readByteArray是将缓冲中的所有内容转成了内存数组,这意味着文件中的所有内容被转成了内存数组,就是在这个时机容易造成OOM! Requestbody.create源码如下:

 public static Requestbody create(final @Nullable MediaType ContentType,final file file) {  if (file == null) throw new NullPointerException("file == null");  return new Requestbody() {   @OverrIDe public @Nullable MediaType ContentType() {    return ContentType;   }   @OverrIDe public long contentLength() {    return file.length();   }   @OverrIDe public voID writeto(BufferedSink sink) throws IOException {    try (Source source = Okio.source(file)) {     sink.writeall(source);    }   }  }; }

可以看到实现体持有了文件,Content-Length返回了文件的大小,内容全部转给了Source对象。

这确实是以前非常容易忽略的一个点,很少有对请求体作额外处理的 *** 作,而一旦这个 *** 作变成一次性的大内存分配,非常容易造成OOM. 所以要如何解决呢? 签名方法又是如何处理的呢? 原来这个签名方法在这里偷了个懒——它只读取传入body的前4K内容,然后只针对这部分内容进行了加密,至于传入的这个内存数组本身多大并不考虑,完全把风险和麻烦丢给了外部(优秀的SDK!).

快速的方法当然是罗列白名单,针对上传接口服务端不进行加签验证,但这容易挂一漏万,而且增加维护成本,要签名方法sdk的人另写合适的接口等于要他们的命,所以还是得从根本解决. 既然签名方法只读取前4K内容,我们便只将内容的前4K部分读取再转成方法所需的内存数组不就可了? 所以我们的目的是: 期望Requestbody能够读取一部分而不是全部的内容. 能否继承Requestbody重写它的writeto? 可以,但不现实,不可能全部替代现有的Requestbody实现类,同时ok框架也有可能创建私有的实现类. 所以只能针对writeto的参数BufferedSink作文章,先得了解BufferedSink又是如何被okhttp框架调用的.

BufferedSink相关的类包括Buffer,Source,都属于okio框架,okhttp只是基于okio的一坨,okio没有直接用java的io *** 作,而是另行写了一套io *** 作,具体是数据缓冲的 *** 作.接上面的描述,Source是怎么创建,同时又是如何 *** 作BufferedSink的? 在Okio.java中:

 public static Source source(file file) throws fileNotFoundException {  if (file == null) throw new IllegalArgumentException("file == null");  return source(new fileinputStream(file)); } public static Source source(inputStream in) {  return source(in,new Timeout()); } private static Source source(final inputStream in,final Timeout timeout) {  return new Source() {   @OverrIDe public long read(Buffer sink,long byteCount) throws IOException {    try {     timeout.throwIfReached();     Segment tail = sink.writableSegment(1);     int maxTocopy = (int) Math.min(byteCount,Segment.SIZE - tail.limit);     int bytesRead = in.read(tail.data,tail.limit,maxTocopy);     if (bytesRead == -1) return -1;     tail.limit += bytesRead;     sink.size += bytesRead;     return bytesRead;    } catch (AssertionError e) {     if (isAndroIDGetsocknameError(e)) throw new IOException(e);     throw e;    }   }   @OverrIDe public voID close() throws IOException {    in.close();   }   @OverrIDe public Timeout timeout() {    return timeout;   }  }; }

Source把文件作为输入流inputstream进行了各种读 *** 作,但是它的read方法参数却是个Buffer实例,它又是从哪来的,又怎么和BufferedSink关联的? 只好再继续看BufferedSink.writeall的实现体。

BufferedSink的实现类就是Buffer, 然后它的writeall方法:

 @OverrIDe public long writeall(Source source) throws IOException {  if (source == null) throw new IllegalArgumentException("source == null");  long totalBytesRead = 0;  for (long readCount; (readCount = source.read(this,Segment.SIZE)) != -1; ) {   totalBytesRead += readCount;  }  return totalBytesRead; }

原来是显式的调用了Source.read(Buffer,long)方法,这样就串起来了,那个Buffer参数原来就是自身。

基本可以确定只要实现BufferedSink接口类,然后判断读入的内容超过指定大小就停止写入就返回就可满足目的,可以名之FixedSizeSink.

然而麻烦的是BufferedSink的接口非常多,将近30个方法, 不知道框架会在什么时机调用哪个方法,只能全部都实现! 其次是接口方法的参数有很多okio的类,这些类的用法需要了解,否则一旦用错了效果适得其反. 于是对一个类的了解变成对多个类的了解,没办法只能硬着头皮写.

第一个接口就有点蛋疼: Buffer buffer(); BufferedSink返回一个Buffer实例供外部调用,BufferedSink的实现体即是Buffer,然后再返回一个Buffer?! 看了半天猜测BufferedSink是为了提供一个可写入的缓冲对象,但框架作者也懒的再搞接口解耦的那一套了(唉,大家都是怎么简单怎么来). 于是FixedSizeSink至少需要持有一个Buffer对象,它作实际的数据缓存,同时可以在需要Source.read(Buffer,long)的地方作为参数传过去.

同时可以看到Requestbody的一个实现类FormBody,用这个Buffer对象直接写入一些数据:

 private long writeOrCountBytes(@Nullable BufferedSink sink,boolean countBytes) {  long byteCount = 0L;  Buffer buffer;  if (countBytes) {   buffer = new Buffer();  } else {   buffer = sink.buffer();  }  for (int i = 0,size = encodednames.size(); i < size; i++) {   if (i > 0) buffer.writeByte('&');   buffer.writeUtf8(encodednames.get(i));   buffer.writeByte('=');   buffer.writeUtf8(encodedValues.get(i));  }  if (countBytes) {   byteCount = buffer.size();   buffer.clear();  }  return byteCount; }

有这样的 *** 作就有可能限制不了缓冲区大小变化!不过数据量应该相对小一些而且这种用法场景相对少,我们指定的大小应该能覆盖的了这种情况。

接着还有一个接口BufferedSink write(ByteString byteString),又得了解ByteString怎么使用,真是心力交瘁啊...

 @OverrIDe public Buffer write(ByteString byteString) {  byteString.write(this);  return this; }

Buffer实现体里可以直接调用ByteString.write(Buffer)因为是包名访问,自己实现的FixedSizeSink声明在和同一包名package okio;也可以这样使用,如果是其它包名只能先转成byte[]了,ByteString应该不大不然也不能这么搞(没有找到ByteString读取一段数据的方法):

  @OverrIDe  public BufferedSink write(@NotNull ByteString byteString) throws IOException {    byte[] bytes = byteString.toByteArray();    this.write(bytes);    return this;  }

总之就是把这些对象转成内存数组或者Buffer能够接受的参数持有起来!

重点关心的writeall反而相对好实现一点,我们连续读取指定长度的内容直到内容长度达到我们的阈值就行.

还有一个蛋疼的点是各种对象的read/write数据流方向:

Caller.read(Callee)/Caller.write(Callee),有的是从Caller到Callee,有的是相反,被一个小类整的有点头疼……

最后上完整代码,如果发现什么潜在的问题也可以交流下~:

public class FixedSizeSink implements BufferedSink {  private static final int SEGMENT_SIZE = 4096;  private final Buffer mBuffer = new Buffer();  private final int mlimitSize;  private FixedSizeSink(int size) {    this.mlimitSize = size;  }  @OverrIDe  public Buffer buffer() {    return mBuffer;  }  @OverrIDe  public BufferedSink write(@NotNull ByteString byteString) throws IOException {    byte[] bytes = byteString.toByteArray();    this.write(bytes);    return this;  }  @OverrIDe  public BufferedSink write(@NotNull byte[] source) throws IOException {    this.write(source,source.length);    return this;  }  @OverrIDe  public BufferedSink write(@NotNull byte[] source,int offset,int byteCount) throws IOException {    long available = mlimitSize - mBuffer.size();    int count = Math.min(byteCount,(int) available);    androID.util.Log.d(TAG,String.format("FixedSizeSink.offset=%d,"             "count=%d,limit=%d,size=%d",offset,byteCount,mlimitSize,mBuffer.size()));    if (count > 0) {      mBuffer.write(source,count);    }    return this;  }  @OverrIDe  public long writeall(@NotNull Source source) throws IOException {    this.write(source,mlimitSize);    return mBuffer.size();  }  @OverrIDe  public BufferedSink write(@NotNull Source source,long byteCount) throws IOException {    final long count = Math.min(byteCount,mlimitSize - mBuffer.size());    final long BUFFER_SIZE = Math.min(count,SEGMENT_SIZE);    androID.util.Log.d(TAG,String.format("FixedSizeSink.count=%d,limit=%d"             ",size=%d,segment=%d",mBuffer.size(),BUFFER_SIZE));    long totalBytesRead = 0;    long readCount;    while (totalBytesRead < count && (readCount = source.read(mBuffer,BUFFER_SIZE)) != -1) {      totalBytesRead = readCount;    }    return this;  }  @OverrIDe  public int write(ByteBuffer src) throws IOException {    final int available = mlimitSize - (int) mBuffer.size();    if (available < src.remaining()) {      byte[] bytes = new byte[available];      src.get(bytes);      this.write(bytes);      return bytes.length;    } else {      return mBuffer.write(src);    }  }  @OverrIDe  public voID write(@NotNull Buffer source,long byteCount) throws IOException {    mBuffer.write(source,Math.min(byteCount,mlimitSize - mBuffer.size()));  }  @OverrIDe  public BufferedSink writeUtf8(@NotNull String string) throws IOException {    mBuffer.writeUtf8(string);    return this;  }  @OverrIDe  public BufferedSink writeUtf8(@NotNull String string,int beginIndex,int endindex)      throws IOException {    mBuffer.writeUtf8(string,beginIndex,endindex);    return this;  }  @OverrIDe  public BufferedSink writeUtf8CodePoint(int codePoint) throws IOException {    mBuffer.writeUtf8CodePoint(codePoint);    return this;  }  @OverrIDe  public BufferedSink writeString(@NotNull String string,@NotNull Charset charset) throws IOException {    mBuffer.writeString(string,charset);    return this;  }  @OverrIDe  public BufferedSink writeString(@NotNull String string,int endindex,endindex,charset);    return this;  }  @OverrIDe  public BufferedSink writeByte(int b) throws IOException {    mBuffer.writeByte(b);    return this;  }  @OverrIDe  public BufferedSink writeShort(int s) throws IOException {    mBuffer.writeShort(s);    return this;  }  @OverrIDe  public BufferedSink writeShortLe(int s) throws IOException {    mBuffer.writeShortLe(s);    return this;  }  @OverrIDe  public BufferedSink writeInt(int i) throws IOException {    mBuffer.writeInt(i);    return this;  }  @OverrIDe  public BufferedSink writeIntLe(int i) throws IOException {    mBuffer.writeIntLe(i);    return this;  }  @OverrIDe  public BufferedSink writeLong(long v) throws IOException {    mBuffer.writeLong(v);    return this;  }  @OverrIDe  public BufferedSink writeLongLe(long v) throws IOException {    mBuffer.writeLongLe(v);    return this;  }  @OverrIDe  public BufferedSink writeDecimalLong(long v) throws IOException {    mBuffer.writeDecimalLong(v);    return this;  }  @OverrIDe  public BufferedSink writeHexadecimalUnsignedLong(long v) throws IOException {    mBuffer.writeHexadecimalUnsignedLong(v);    return this;  }  @OverrIDe  public voID flush() throws IOException {    mBuffer.flush();  }  @OverrIDe  public BufferedSink emit() throws IOException {    mBuffer.emit();    return this;  }  @OverrIDe  public BufferedSink emitCompleteSegments() throws IOException {    mBuffer.emitCompleteSegments();    return this;  }  @OverrIDe  public OutputStream outputStream() {    return mBuffer.outputStream();  }  @OverrIDe  public boolean isopen() {    return mBuffer.isopen();  }  @OverrIDe  public Timeout timeout() {    return mBuffer.timeout();  }  @OverrIDe  public voID close() throws IOException {    mBuffer.close();  }}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

总结

以上是内存溢出为你收集整理的android 使用okhttp可能引发OOM的一个点全部内容,希望文章能够帮你解决android 使用okhttp可能引发OOM的一个点所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1145804.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存