遇到一个问题: 须要给全部的请求加签名校验以防刷接口;传入请求url及body生成一个文本串做为一个header传给服务端;已经有现成的签名检验方法String doSignature(String url, byte[] body);
当前网络库基于com.squareup.okhttp3:okhttp:3.14.2
.java
这很简单了,固然是写一个interceptor
而后将request对象的url及body传入就好.因而有:android
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
对象。bash
这确实是之前很是容易忽略的一个点,不多有对请求体做额外处理的操做,而一旦这个操做变成一次性的大内存分配, 很是容易形成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
中:ide
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
的实现体。ui
BufferedSink
的实现类就是Buffer
, 而后它的writeAll
方法:this
@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, 0, 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, offset, 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",
byteCount, mLimitSize, 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 beginIndex, int endIndex,
@NotNull Charset charset) throws IOException {
mBuffer.writeString(string, beginIndex, 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();
}
}
复制代码
果真仍是出问题了!
在一个发送大文件的请求中界面变的十分卡顿,log显示一直在调用BufferedSink.write(Buffer,Long)
接口,而后OOM了!猜想卡顿的缘由应该是内存被急剧消耗致使。明明已经限制了FixedSizeSink
的大小啊,为何还会OOM呢?!
痛苦的调试后。。。(这个请求由于要显示发送进度用了Okio的ForwardingSink
, 框架调用又用的是RealBufferedSink
, 光名字就看的人眼晕而后还有各类调用关系,总之就是痛苦!)
原来,文件数据被框架先用内存缓存了起来,而后将一段一段的内存缓存经过方法写入到自定义的缓存,也就是咱们的FixedSizeSink
,传过来的Buffer
实例是RealBufferedSink
持有的实例。这里的关键点是要把传过来的Buffer
实例中的数据须要所有消费掉,不然残留数据会堆积在原有的Buffer
实例中愈来愈大直至OOM,真是防不胜防啊……
只要定位到缘由,改起来就很容易了,直接上diff,顺便把另外两个接口改了下:
@Override
public BufferedSink write(@NotNull ByteString byteString) throws IOException {
- byte[] bytes = byteString.toByteArray();
- this.write(bytes);
+ this.write(byteString.asByteBuffer());
return this;
}
@@ -206,19 +205,30 @@ public class SignInterceptor implements Interceptor {
@Override
public int write(ByteBuffer src) throws IOException {
final int available = mLimitSize - (int) mBuffer.size();
- if (available < src.remaining()) {
+ if (available >= src.remaining()) {
+ return mBuffer.write(src);
+ } else if (available > 0) {
byte[] bytes = new byte[available];
src.get(bytes);
this.write(bytes);
return bytes.length;
} else {
- return mBuffer.write(src);
+ return 0;
}
}
@Override
public void write(@NotNull Buffer source, long byteCount) throws IOException {
- mBuffer.write(source, Math.min(byteCount, mLimitSize - mBuffer.size()));
+ long available = mLimitSize - mBuffer.size();
+ long delta = byteCount - available;
+ if (delta > 0) {
+ if (available > 0) {
+ mBuffer.write(source, available);
+ }
+ source.skip(delta);
+ } else {
+ mBuffer.write(source, byteCount);
+ }
}
复制代码