android 使用okhttp可能引发OOM的一个点
遇到一个问题:需要给所有的请求加签名校验以防刷接口;传入请求url及body生成一个文本串作为一个header传给服务端;已经有现成的签名检验方法StringdoSignature(Stringurl,byte[]body);当前网络库基于com.squareup.okhttp3:okhttp:3.14.2.
这很简单了,当然是写一个interceptor然后将request对象的url及body传入就好.于是有:
publicclassSignInterceptorimplementsInterceptor{
@NonNull
@Override
publicResponseintercept(@NonNullChainchain)throwsIOException{
Requestrequest=chain.request();
RequestBodybody=request.body();
byte[]bodyBytes=null;
if(body!=null){
finalBufferbuffer=newBuffer();
body.writeTo(buffer);
bodyBytes=buffer.readByteArray();
}
Request.Builderbuilder=request.newBuilder();
HttpUrloldUrl=request.url();
finalStringurl=oldUrl.toString();
finalStringsigned=doSignature(url,bodyBytes));
if(!TextUtils.isEmpty(signed)){
builder.addHeader(SIGN_KEY_NAME,signed);
}
returnchain.proceed(builder.build());
}
}
okhttp的ReqeustBody是一个抽象类,内容输出只有writeTo方法,将内容写入到一个BufferedSink接口实现体里,然后再将数据转成byte[]也就是内存数组.能达到目的的类只有Buffer,它实现了BufferedSink接口并能提供转成内存数组的方法readByteArray.这貌似没啥问题呀,能造成OOM?
是的,要看请求类型,如果是一个上传文件的接口呢?如果这个文件比较大呢?上传接口有可能会用到publicstaticRequestBodycreate(final@NullableMediaTypecontentType,finalFilefile)方法,如果是针对文件的实现体它的writeTo方法是sink.writeAll(source);而我们传给签名方法时用到的Buffer.readByteArray是将缓冲中的所有内容转成了内存数组,这意味着文件中的所有内容被转成了内存数组,就是在这个时机容易造成OOM!RequestBody.create源码如下:
publicstaticRequestBodycreate(final@NullableMediaTypecontentType,finalFilefile){
if(file==null)thrownewNullPointerException("file==null");
returnnewRequestBody(){
@Overridepublic@NullableMediaTypecontentType(){
returncontentType;
}
@OverridepubliclongcontentLength(){
returnfile.length();
}
@OverridepublicvoidwriteTo(BufferedSinksink)throwsIOException{
try(Sourcesource=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中:
publicstaticSourcesource(Filefile)throwsFileNotFoundException{
if(file==null)thrownewIllegalArgumentException("file==null");
returnsource(newFileInputStream(file));
}
publicstaticSourcesource(InputStreamin){
returnsource(in,newTimeout());
}
privatestaticSourcesource(finalInputStreamin,finalTimeouttimeout){
returnnewSource(){
@Overridepubliclongread(Buffersink,longbyteCount)throwsIOException{
try{
timeout.throwIfReached();
Segmenttail=sink.writableSegment(1);
intmaxToCopy=(int)Math.min(byteCount,Segment.SIZE-tail.limit);
intbytesRead=in.read(tail.data,tail.limit,maxToCopy);
if(bytesRead==-1)return-1;
tail.limit+=bytesRead;
sink.size+=bytesRead;
returnbytesRead;
}catch(AssertionErrore){
if(isAndroidGetsocknameError(e))thrownewIOException(e);
throwe;
}
}
@Overridepublicvoidclose()throwsIOException{
in.close();
}
@OverridepublicTimeouttimeout(){
returntimeout;
}
};
}
Source把文件作为输入流inputstream进行了各种读操作,但是它的read方法参数却是个Buffer实例,它又是从哪来的,又怎么和BufferedSink关联的?只好再继续看BufferedSink.writeAll的实现体。
BufferedSink的实现类就是Buffer,然后它的writeAll方法:
@OverridepubliclongwriteAll(Sourcesource)throwsIOException{
if(source==null)thrownewIllegalArgumentException("source==null");
longtotalBytesRead=0;
for(longreadCount;(readCount=source.read(this,Segment.SIZE))!=-1;){
totalBytesRead+=readCount;
}
returntotalBytesRead;
}
原来是显式的调用了Source.read(Buffer,long)方法,这样就串起来了,那个Buffer参数原来就是自身。
基本可以确定只要实现BufferedSink接口类,然后判断读入的内容超过指定大小就停止写入就返回就可满足目的,可以名之FixedSizeSink.
然而麻烦的是BufferedSink的接口非常多,将近30个方法,不知道框架会在什么时机调用哪个方法,只能全部都实现!其次是接口方法的参数有很多okio的类,这些类的用法需要了解,否则一旦用错了效果适得其反.于是对一个类的了解变成对多个类的了解,没办法只能硬着头皮写.
第一个接口就有点蛋疼:Bufferbuffer();BufferedSink返回一个Buffer实例供外部调用,BufferedSink的实现体即是Buffer,然后再返回一个Buffer?!看了半天猜测BufferedSink是为了提供一个可写入的缓冲对象,但框架作者也懒的再搞接口解耦的那一套了(唉,大家都是怎么简单怎么来).于是FixedSizeSink至少需要持有一个Buffer对象,它作实际的数据缓存,同时可以在需要Source.read(Buffer,long)的地方作为参数传过去.
同时可以看到RequestBody的一个实现类FormBody,用这个Buffer对象直接写入一些数据:
privatelongwriteOrCountBytes(@NullableBufferedSinksink,booleancountBytes){
longbyteCount=0L;
Bufferbuffer;
if(countBytes){
buffer=newBuffer();
}else{
buffer=sink.buffer();
}
for(inti=0,size=encodedNames.size();i0)buffer.writeByte('&');
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');
buffer.writeUtf8(encodedValues.get(i));
}
if(countBytes){
byteCount=buffer.size();
buffer.clear();
}
returnbyteCount;
}
有这样的操作就有可能限制不了缓冲区大小变化!不过数据量应该相对小一些而且这种用法场景相对少,我们指定的大小应该能覆盖的了这种情况。
接着还有一个接口BufferedSinkwrite(ByteStringbyteString),又得了解ByteString怎么使用,真是心力交瘁啊...
@OverridepublicBufferwrite(ByteStringbyteString){
byteString.write(this);
returnthis;
}
Buffer实现体里可以直接调用ByteString.write(Buffer)因为是包名访问,自己实现的FixedSizeSink声明在和同一包名packageokio;也可以这样使用,如果是其它包名只能先转成byte[]了,ByteString应该不大不然也不能这么搞(没有找到ByteString读取一段数据的方法):
@Override
publicBufferedSinkwrite(@NotNullByteStringbyteString)throwsIOException{
byte[]bytes=byteString.toByteArray();
this.write(bytes);
returnthis;
}
总之就是把这些对象转成内存数组或者Buffer能够接受的参数持有起来!
重点关心的writeAll反而相对好实现一点,我们连续读取指定长度的内容直到内容长度达到我们的阈值就行.
还有一个蛋疼的点是各种对象的read/write数据流方向:
Caller.read(Callee)/Caller.write(Callee),有的是从Caller到Callee,有的是相反,被一个小类整的有点头疼……
最后上完整代码,如果发现什么潜在的问题也可以交流下~:
publicclassFixedSizeSinkimplementsBufferedSink{
privatestaticfinalintSEGMENT_SIZE=4096;
privatefinalBuffermBuffer=newBuffer();
privatefinalintmLimitSize;
privateFixedSizeSink(intsize){
this.mLimitSize=size;
}
@Override
publicBufferbuffer(){
returnmBuffer;
}
@Override
publicBufferedSinkwrite(@NotNullByteStringbyteString)throwsIOException{
byte[]bytes=byteString.toByteArray();
this.write(bytes);
returnthis;
}
@Override
publicBufferedSinkwrite(@NotNullbyte[]source)throwsIOException{
this.write(source,0,source.length);
returnthis;
}
@Override
publicBufferedSinkwrite(@NotNullbyte[]source,intoffset,
intbyteCount)throwsIOException{
longavailable=mLimitSize-mBuffer.size();
intcount=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,offset,count);
}
returnthis;
}
@Override
publiclongwriteAll(@NotNullSourcesource)throwsIOException{
this.write(source,mLimitSize);
returnmBuffer.size();
}
@Override
publicBufferedSinkwrite(@NotNullSourcesource,longbyteCount)throwsIOException{
finallongcount=Math.min(byteCount,mLimitSize-mBuffer.size());
finallongBUFFER_SIZE=Math.min(count,SEGMENT_SIZE);
android.util.Log.d(TAG,String.format("FixedSizeSink.count=%d,limit=%d"
",size=%d,segment=%d",
byteCount,mLimitSize,mBuffer.size(),BUFFER_SIZE));
longtotalBytesRead=0;
longreadCount;
while(totalBytesRead
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。