Okhttp3.0 日志拦截器HttpLoggingInterceptor的上传大文件时OutOfMemory以及修复

项目中有使用okhttp+logging-interceptor上传本地保存的log文件,

代码片段:

var logLevel = HttpLoggingInterceptor.Level.BODY //默认BODY级别

val builder = OkHttpClient.Builder()
//...略
.addInterceptor(
    HttpLoggingInterceptor().setLevel(logLevel)

然后由于最开始只有一个log文件,日积月累,日志文件很大,有80M+的(这个已经优化成多个文件),

so第一次上传的时候HttpLoggingInterceptor里面报错了,查log是OutOfMemory,

也就是如下代码片段报错了,HttpLoggingInterceptor中直接读取所有80M+的内容

    @Override 
    public Response intercept(Chain chain) throws IOException {

        //...略
        if (isPlaintext(buffer)) {
          logger.log(buffer.readString(charset)); //打印参数内容时,直接读取全部内容,爆掉了
          logger.log("--> END " + request.method()
              + " (" + requestBody.contentLength() + "-byte body)");
        } else {
          logger.log("--> END " + request.method() + " (binary "
              + requestBody.contentLength() + "-byte body omitted)");
        }  
    }

这里没有加判断条件,也没有大小限制,显示是有问题的。

于是思路就重写日志打印拦截器,主要也就是读取以及打印这块地方:

后面查了官方issue,之前也有人遇到过这个问题,参考他们方案,加了2个全局变量requestBodyLogMax和responseBodyLogMax,设置最大打印的大小,超过了只截取部分内容打印。

设置临界值

val builder = OkHttpClient.Builder()

builder
                    .addInterceptor(
                        CommonHttpLoggingInterceptor().setLevel(logLevel).setRequestBodyLogMax(2000).setResponseBodyLogMax(
                            2000
                        )
                    )
CommonHttpLoggingInterceptor.java文件如下:
class CommonHttpLoggingInterceptor(val logger: Logger = Logger.DEFAULT) : Interceptor {

    @Volatile
    private var level = Level.NONE

    @Volatile
    private var requestBodyLogMax = LOG_LIMITATION_NONE
    @Volatile
    private var responseBodyLogMax = LOG_LIMITATION_NONE

    enum class Level {
        /** No logs.  */
        NONE,
        /**
         * Logs request and response lines.
         *
         *
         * Example:
         * 
`--> POST /greeting http/1.1 (3-byte body)
         *
         * <-- 200 OK (22ms, 6-byte body)
        `
* */ BASIC, /** * Logs request and response lines and their respective headers. * * * Example: *
`--> POST /greeting http/1.1
         * Host: example.com
         * Content-Type: plain/text
         * Content-Length: 3
         * --> END POST
         *
         * <-- 200 OK (22ms)
         * Content-Type: plain/text
         * Content-Length: 6
         * <-- END HTTP
        `
* */ HEADERS, /** * Logs request and response lines and their respective headers and bodies (if present). * * * Example: *
`--> POST /greeting http/1.1
         * Host: example.com
         * Content-Type: plain/text
         * Content-Length: 3
         *
         * Hi?
         * --> END POST
         *
         * <-- 200 OK (22ms)
         * Content-Type: plain/text
         * Content-Length: 6
         *
         * Hello!
         * <-- END HTTP
        `
* */ BODY, /** * Logs request and response lines. * * * Example: *
`--> POST /greeting http/1.1 (3-byte body)
         *  result...(part)
         * <-- 200 OK (22ms, 6-byte body)
         *  result...(part)
         * <-- END GET
        `
* */ SIMPLE } interface Logger { fun log(message: String) companion object { /** A [Logger] defaults output appropriate for the current platform. */ val DEFAULT: Logger = object : Logger { override fun log(message: String) { Platform.get().log(INFO, message, null) } } } } /** Change the level at which this interceptor logs. */ fun setLevel(level: Level?): CommonHttpLoggingInterceptor { if (level == null) throw NullPointerException("level == null. Use Level.NONE instead.") this.level = level return this } fun getLevel(): Level { return level } /** Change the limitation of request body logs size. */ fun setRequestBodyLogMax(requestBodyLogMax: Long): CommonHttpLoggingInterceptor { if (requestBodyLogMax < 0) { this.requestBodyLogMax = LOG_LIMITATION_NONE } else { this.requestBodyLogMax = requestBodyLogMax } return this } /** Change the limitation of response body logs size. */ fun setResponseBodyLogMax(responseBodyLogMax: Long): CommonHttpLoggingInterceptor { if (responseBodyLogMax < 0) { this.responseBodyLogMax = LOG_LIMITATION_NONE } else { this.responseBodyLogMax = responseBodyLogMax } return this } @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val level = this.level val request = chain.request() if (level == Level.NONE) { return chain.proceed(request) } val logBody = level == Level.BODY val logHeaders = logBody || level == Level.HEADERS val logPartBody = level == Level.SIMPLE val requestBody = request.body() val hasRequestBody = requestBody != null val connection = chain.connection() var requestStartMessage = ("--> " + request.method() + ' '.toString() + request.url() + (connection?.protocol()?:"")) if (!logHeaders && hasRequestBody) { requestStartMessage += " (${requestBody!!.contentLength()}-byte body)" } logger.log(requestStartMessage) if (logHeaders) { if (requestBody != null) { // Request body headers are only present when installed as a network interceptor. Force // them to be included (when available) so there values are known. if (requestBody.contentType() != null) { logger.log("Content-Type: ${requestBody.contentType()}") } if (requestBody.contentLength() != -1L) { logger.log("Content-Length: ${requestBody.contentLength()}") } } val headers = request.headers() var i = 0 val count = headers.size() while (i < count) { val name = headers.name(i) // Skip headers from the request body as they are explicitly logged above. if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals( name, ignoreCase = true ) ) { logger.log(name + ": " + headers.value(i)) } i++ } } if (!(logBody || logPartBody) || !hasRequestBody) { logger.log("--> END " + request.method()) } else if (bodyHasUnknownEncoding(request.headers())) { logger.log("--> END " + request.method() + " (encoded body omitted)") } else { val buffer = Buffer() requestBody!!.writeTo(buffer) var charset: Charset? = UTF8 val contentType = requestBody.contentType() if (contentType != null) { charset = contentType.charset(UTF8) } logger.log("") if (isPlaintext(buffer)) { if (buffer.size() <= requestBodyLogMax || requestBodyLogMax < 0) { var reqBodyContent = buffer.readString(charset!!) if (logPartBody && reqBodyContent.length > 200) { reqBodyContent = reqBodyContent.shrink(200, '.') } logger.log(reqBodyContent) } else { logger.log( "Too large to output logs. " + "Current limitation is $requestBodyLogMax" ) } logger.log("--> END ${request.method()} (${requestBody.contentLength()}-byte body)") } else { logger.log( ("--> END ${request.method()} (binary ${requestBody.contentLength()}-byte body omitted)") ) } } val startNs = System.nanoTime() val response: Response try { response = chain.proceed(request) } catch (e: Exception) { logger.log("<-- HTTP FAILED: $e") throw e } val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) val responseBody = response.body() val contentLength = responseBody!!.contentLength() val bodySize = if (contentLength != -1L) contentLength.toString() + "-byte" else "unknown-length" logger.log( ("<-- " + response.code() + (if (response.message().isEmpty()) "" else ' ' + response.message()) + ' '.toString() + response.request().url() + " (" + tookMs + "ms" + (if (!logHeaders && !logPartBody) ", $bodySize body" else "") + ')'.toString()) ) val headers = response.headers() if (logHeaders) { var i = 0 val count = headers.size() while (i < count) { logger.log(headers.name(i) + ": " + headers.value(i)) i++ } } if (!(logBody || logPartBody) || !HttpHeaders.hasBody(response)) { logger.log("<-- END ${request.method()}") } else if (bodyHasUnknownEncoding(response.headers())) { logger.log("<-- END ${request.method()} (encoded body omitted)") } else { val source = responseBody.source() source.request(java.lang.Long.MAX_VALUE) // Buffer the entire body. var buffer = source.buffer() var gzippedLength: Long? = null if ("gzip".equals(headers.get("Content-Encoding") ?: "", ignoreCase = true)) { gzippedLength = buffer.size() var gzippedResponseBody: GzipSource? = null try { gzippedResponseBody = GzipSource(buffer.clone()) buffer = Buffer() buffer.writeAll(gzippedResponseBody) } finally { if (gzippedResponseBody != null) { gzippedResponseBody.close() } } } var charset: Charset? = UTF8 val contentType = responseBody.contentType() if (contentType != null) { charset = contentType.charset(UTF8) } if (!isPlaintext(buffer)) { logger.log("") logger.log("<-- END ${request.method()} (binary ${buffer.size()}-byte body omitted)") return response } if (contentLength != 0L) { if (buffer.size() <= responseBodyLogMax || responseBodyLogMax < 0) { var resBodyContent = buffer.clone().readString(charset!!) if (logPartBody && resBodyContent.length > 200) { resBodyContent = resBodyContent.shrink(200, '.') } logger.log(resBodyContent) } else { logger.log( ("Too large to output logs. " + "Current limitation is $responseBodyLogMax") ) } } if (gzippedLength != null) { logger.log( ("<-- END ${request.method()} (${buffer.size()}-byte, " + gzippedLength + "-gzipped-byte body)") ) } else { logger.log("<-- END ${request.method()} (${buffer.size()}-byte body)") } } return response } private fun bodyHasUnknownEncoding(headers: Headers): Boolean { val contentEncoding = headers.get("Content-Encoding") return (contentEncoding != null && !contentEncoding.equals("identity", ignoreCase = true) && !contentEncoding.equals("gzip", ignoreCase = true)) } companion object { private val UTF8 = Charset.forName("UTF-8") const val LOG_LIMITATION_NONE: Long = -1 /** * Returns true if the body in question probably contains human readable text. Uses a small sample * of code points to detect unicode control characters commonly used in binary file signatures. */ internal fun isPlaintext(buffer: Buffer): Boolean { try { val prefix = Buffer() val byteCount = if (buffer.size() < 64) buffer.size() else 64 buffer.copyTo(prefix, 0, byteCount) for (i in 0..15) { if (prefix.exhausted()) { break } val codePoint = prefix.readUtf8CodePoint() if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { return false } } return true } catch (e: EOFException) { return false // Truncated UTF-8 sequence. } } } }

 

 

你可能感兴趣的:(Android)