您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)android使用okhttp可能引發(fā)OOM的示例分析的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。
遇到一個(gè)問題: 需要給所有的請(qǐng)求加簽名校驗(yàn)以防刷接口;傳入請(qǐng)求url及body生成一個(gè)文本串作為一個(gè)header傳給服務(wù)端;已經(jīng)有現(xiàn)成的簽名檢驗(yàn)方法String doSignature(String url, byte[] body);當(dāng)前網(wǎng)絡(luò)庫基于com.squareup.okhttp3:okhttp:3.14.2.這很簡單了,當(dāng)然是寫一個(gè)interceptor然后將request對(duì)象的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是一個(gè)抽象類,內(nèi)容輸出只有writeTo方法,將內(nèi)容寫入到一個(gè)BufferedSink接口實(shí)現(xiàn)體里,然后再將數(shù)據(jù)轉(zhuǎn)成byte[]也就是內(nèi)存數(shù)組.能達(dá)到目的的類只有Buffer,它實(shí)現(xiàn)了BufferedSink接口并能提供轉(zhuǎn)成內(nèi)存數(shù)組的方法readByteArray. 這貌似沒啥問題呀,能造成OOM?
是的,要看請(qǐng)求類型,如果是一個(gè)上傳文件的接口呢?如果這個(gè)文件比較大呢?上傳接口有可能會(huì)用到public static RequestBody create(final @Nullable MediaType contentType, final File file)方法,如果是針對(duì)文件的實(shí)現(xiàn)體它的writeTo方法是sink.writeAll(source);而我們傳給簽名方法時(shí)用到的Buffer.readByteArray是將緩沖中的所有內(nèi)容轉(zhuǎn)成了內(nèi)存數(shù)組, 這意味著文件中的所有內(nèi)容被轉(zhuǎn)成了內(nèi)存數(shù)組, 就是在這個(gè)時(shí)機(jī)容易造成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); } } }; }
可以看到實(shí)現(xiàn)體持有了文件,Content-Length返回了文件的大小, 內(nèi)容全部轉(zhuǎn)給了Source對(duì)象。
這確實(shí)是以前非常容易忽略的一個(gè)點(diǎn),很少有對(duì)請(qǐng)求體作額外處理的操作,而一旦這個(gè)操作變成一次性的大內(nèi)存分配, 非常容易造成OOM. 所以要如何解決呢? 簽名方法又是如何處理的呢? 原來這個(gè)簽名方法在這里偷了個(gè)懶——它只讀取傳入body的前4K內(nèi)容,然后只針對(duì)這部分內(nèi)容進(jìn)行了加密,至于傳入的這個(gè)內(nèi)存數(shù)組本身多大并不考慮,完全把風(fēng)險(xiǎn)和麻煩丟給了外部(優(yōu)秀的SDK!).
快速的方法當(dāng)然是羅列白名單,針對(duì)上傳接口服務(wù)端不進(jìn)行加簽驗(yàn)證, 但這容易掛一漏萬,而且增加維護(hù)成本, 要簽名方法sdk的人另寫合適的接口等于要他們的命, 所以還是得從根本解決. 既然簽名方法只讀取前4K內(nèi)容,我們便只將內(nèi)容的前4K部分讀取再轉(zhuǎn)成方法所需的內(nèi)存數(shù)組不就可了? 所以我們的目的是: 期望RequestBody能夠讀取一部分而不是全部的內(nèi)容. 能否繼承RequestBody重寫它的writeTo? 可以,但不現(xiàn)實(shí),不可能全部替代現(xiàn)有的RequestBody實(shí)現(xiàn)類, 同時(shí)ok框架也有可能創(chuàng)建私有的實(shí)現(xiàn)類. 所以只能針對(duì)writeTo的參數(shù)BufferedSink作文章, 先得了解BufferedSink又是如何被okhttp框架調(diào)用的.
BufferedSink相關(guān)的類包括Buffer, Source,都屬于okio框架,okhttp只是基于okio的一坨, okio沒有直接用java的io操作,而是另行寫了一套io操作,具體是數(shù)據(jù)緩沖的操作.接上面的描述, Source是怎么創(chuàng)建, 同時(shí)又是如何操作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進(jìn)行了各種讀操作, 但是它的read方法參數(shù)卻是個(gè)Buffer實(shí)例,它又是從哪來的,又怎么和BufferedSink關(guān)聯(lián)的? 只好再繼續(xù)看BufferedSink.writeAll的實(shí)現(xiàn)體。
BufferedSink的實(shí)現(xiàn)類就是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; }
原來是顯式的調(diào)用了Source.read(Buffer,long)方法,這樣就串起來了,那個(gè)Buffer參數(shù)原來就是自身。
基本可以確定只要實(shí)現(xiàn)BufferedSink接口類, 然后判斷讀入的內(nèi)容超過指定大小就停止寫入就返回就可滿足目的, 可以名之FixedSizeSink.
然而麻煩的是BufferedSink的接口非常多, 將近30個(gè)方法, 不知道框架會(huì)在什么時(shí)機(jī)調(diào)用哪個(gè)方法,只能全部都實(shí)現(xiàn)! 其次是接口方法的參數(shù)有很多okio的類, 這些類的用法需要了解, 否則一旦用錯(cuò)了效果適得其反. 于是對(duì)一個(gè)類的了解變成對(duì)多個(gè)類的了解, 沒辦法只能硬著頭皮寫.
第一個(gè)接口就有點(diǎn)蛋疼: Buffer buffer(); BufferedSink返回一個(gè)Buffer實(shí)例供外部調(diào)用, BufferedSink的實(shí)現(xiàn)體即是Buffer, 然后再返回一個(gè)Buffer?! 看了半天猜測BufferedSink是為了提供一個(gè)可寫入的緩沖對(duì)象, 但框架作者也懶的再搞接口解耦的那一套了(唉,大家都是怎么簡單怎么來). 于是FixedSizeSink至少需要持有一個(gè)Buffer對(duì)象, 它作實(shí)際的數(shù)據(jù)緩存,同時(shí)可以在需要Source.read(Buffer ,long)的地方作為參數(shù)傳過去.
同時(shí)可以看到RequestBody的一個(gè)實(shí)現(xiàn)類FormBody, 用這個(gè)Buffer對(duì)象直接寫入一些數(shù)據(jù):
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; }
有這樣的操作就有可能限制不了緩沖區(qū)大小變化!不過數(shù)據(jù)量應(yīng)該相對(duì)小一些而且這種用法場景相對(duì)少,我們指定的大小應(yīng)該能覆蓋的了這種情況。
接著還有一個(gè)接口BufferedSink write(ByteString byteString), 又得了解ByteString怎么使用, 真是心力交瘁啊...
@Override public Buffer write(ByteString byteString) { byteString.write(this); return this; }
Buffer實(shí)現(xiàn)體里可以直接調(diào)用ByteString.write(Buffer)因?yàn)槭前L問,自己實(shí)現(xiàn)的FixedSizeSink聲明在和同一包名package okio;也可以這樣使用,如果是其它包名只能先轉(zhuǎn)成byte[]了, ByteString應(yīng)該不大不然也不能這么搞(沒有找到ByteString讀取一段數(shù)據(jù)的方法):
@Override public BufferedSink write(@NotNull ByteString byteString) throws IOException { byte[] bytes = byteString.toByteArray(); this.write(bytes); return this; }
總之就是把這些對(duì)象轉(zhuǎn)成內(nèi)存數(shù)組或者Buffer能夠接受的參數(shù)持有起來!
重點(diǎn)關(guān)心的writeAll反而相對(duì)好實(shí)現(xiàn)一點(diǎn), 我們連續(xù)讀取指定長度的內(nèi)容直到內(nèi)容長度達(dá)到我們的閾值就行.
還有一個(gè)蛋疼的點(diǎn)是各種對(duì)象的read/write數(shù)據(jù)流方向:
Caller.read(Callee)/Caller.write(Callee),
有的是從Caller到Callee, 有的是相反,被一個(gè)小類整的有點(diǎn)頭疼……
最后上完整代碼, 如果發(fā)現(xiàn)什么潛在的問題也可以交流下~:
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(); }}
感謝各位的閱讀!關(guān)于“android使用okhttp可能引發(fā)OOM的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。