Android 系统Api YuvImage.compressToJpeg 存在native级别的内存泄漏

写这篇文章前先调侃下Google的Android工程师们吧,作为一个普通的程序员,怎么都不敢去怀疑他们吧。直到现在我找到了标题中的问题,并且找到了解决方案,我依然认为这不是失误,应该是他们别有用意把!
给Google提交了issue和解决方案,但是 一直没有人鸟我。

我认为Android Framework的每一个API都应该是经过压测的,更别说是google这样一家有深度的公司,可惜并没有,标题中的bug,存在于现在所有的Android 版本,至少从4.x,到如今最新的的8.x都是这样的。

不BB那么多了,进入正题:

**背景:**项目中有一个功能,大概就是需要将摄像头预览回调的每一帧Nv21数据通过jpeg压缩,然后传输到后台做处理。
作为一个资深Android工程师(小小装一把逼),我很自然就想到了YuvImage的compressToJpeg方法:

public static @Nullable byte[] convertNv21ToJpeg(byte[] nv21, int w, int h, Rect rect){
        if(nv21 == null) return null;
        ByteArrayOutputStream outputSteam = new ByteArrayOutputStream();
//        outputSteam.reset();
        YuvImage image = new YuvImage(nv21, ImageFormat.NV21, w, h, null);
        image.compressToJpeg(rect, 70, outputSteam);
        return outputSteam.toByteArray();
    }

可是过了一段时间应用就崩溃了,一看crash日志,导到处都是Low Memeory的字样,很多系统服务都被Kill掉了,奇怪了我们的设备有好几个G的内存啊,怎么会全部耗光呢?再说我的APP就算在manifest的application里面加了largeheap,JVM最多也就能用到512M的内存啊。

很直观我就想到肯定不是我JAVA层代码的问题,是C层库JNI里面的问题。于是就拿到了N个Android设备,一个设备跑一个模块的sdk,进行压测。最终项目中用到的其他C库都被排除掉了。懵逼了,啥情况啊?

不到黄河心不死,问题还没有解决,必须继续排除,幸好这个功能的程序流程也就那么长?排除了各个sdk的问题,现在就开始一个函数一个函数排除,反正设备也多,暴力测试法就是while(true){XXX}。
终于定位了这货:YuvImage.compressToJpeg 很奇怪吧!!!

在我印象中,Android压缩就2处直接api,一是YuvImage.compressToJpeg,二就是Bitmap.compress,带着疑惑,我用同样的暴力方法开始测试Bitmap.compress,诡异的事情发生了,跑N久都稳定如初!!!

没有办法,接下来就少不了一波撸源码,毕竟作为一个优秀的IT员不是一哭二闹三上吊,而是一baidu,二google,三撸源码,这里推荐一个非常爽的Android看源码的网址:http://androidxref.com/,无图无真相,就看你hold住不…
这里写图片描述 最好是能把所有源码从官方都git clone到本地的,可惜我操作了好几次都被Abort了,可能是网络不太好吧。

入正题:先看YuvImage.java里面的compressToJpeg方法:

public boolean compressToJpeg(Rect rectangle, int quality, OutputStream stream) {
        Rect wholeImage = new Rect(0, 0, mWidth, mHeight);
        if (!wholeImage.contains(rectangle)) {
            throw new IllegalArgumentException(
                    "rectangle is not inside the image");
        }

        if (quality < 0 || quality > 100) {
            throw new IllegalArgumentException("quality must be 0..100");
        }

        if (stream == null) {
            throw new IllegalArgumentException("stream cannot be null");
        }

        adjustRectangle(rectangle);
        int[] offsets = calculateOffsets(rectangle.left, rectangle.top);

        return nativeCompressToJpeg(mData, mFormat, rectangle.width(),
                rectangle.height(), offsets, mStrides, quality, stream,
                new byte[WORKING_COMPRESS_STORAGE]);
    }

进而追踪到nativeCompressToJpeg

private static native boolean nativeCompressToJpeg(byte[] oriYuv,
            int format, int width, int height, int[] offsets, int[] strides,
            int quality, OutputStream stream, byte[] tempStorage);

接下来就是:/frameworks/base/core/jni/android/graphics/YuvToJpegEncoder.cpp

bool YuvToJpegEncoder::encode(SkWStream* stream, void* inYuv, int width,
        int height, int* offsets, int jpegQuality) {
    jpeg_compress_struct    cinfo;
    skjpeg_error_mgr        sk_err;
    skjpeg_destination_mgr  sk_wstream(stream);

    cinfo.err = jpeg_std_error(&sk_err);
    sk_err.error_exit = skjpeg_error_exit;
    if (setjmp(sk_err.fJmpBuf)) {
        return false;
    }
    jpeg_create_compress(&cinfo);

    cinfo.dest = &sk_wstream;

    setJpegCompressStruct(&cinfo, width, height, jpegQuality);

    jpeg_start_compress(&cinfo, TRUE);

    compress(&cinfo, (uint8_t*) inYuv, offsets);

    jpeg_finish_compress(&cinfo);
	//jpeg_destroy_compress(&cinfo);  //这句源码中是没有的,也是泄漏的根源
    return true;
}

如果你是一个对libjpeg压缩流程清楚的大神,那到了这里大概都清晰了,上面的接口在调用完jpeg_finish_compress(&cinfo);后,没有调用jpeg_destroy_compress(&cinfo);,这个接口是释放压缩工作过程中所申请的资源,就是代码中的cinfo结构,该结构只占十几个字节的内存, 这样就导致了每压缩一张照片,就泄漏一个cinfo的内存,这就是为何测试过程为何那么辛苦的原因了!

CENTER

如果是不懂的就继续跟我学习笨方法吧,上文提到还有一个压缩的地方就是Bitmap.compress,它为何不会泄漏呢?相信经过对比我们就能发现问题,顺藤摸瓜,让我们继续往下走吧。

public boolean compress(CompressFormat format, int quality, OutputStream stream) {
        checkRecycled("Can't compress a recycled bitmap");
        // do explicit check before calling the native method
        if (stream == null) {
            throw new NullPointerException();
        }
        if (quality < 0 || quality > 100) {
            throw new IllegalArgumentException("quality must be 0..100");
        }
        StrictMode.noteSlowCall("Compression of a bitmap is slow");
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
        boolean result = nativeCompress(mNativePtr, format.nativeInt,
                quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        return result;
    }
    
 //最后也跑进了一个native方法nativeCompress
 private static native boolean nativeCompress(long nativeBitmap, int format,
                                            int quality, OutputStream stream,
                                            byte[] tempStorage);

/frameworks/base/core/jni/android/graphics/Bitmap.cpp

static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle,
                                jint format, jint quality,
                                jobject jstream, jbyteArray jstorage) {

    LocalScopedBitmap bitmap(bitmapHandle);
    SkImageEncoder::Type fm;

    switch (format) {
    case kJPEG_JavaEncodeFormat:
        fm = SkImageEncoder::kJPEG_Type;
        break;
    case kPNG_JavaEncodeFormat:
        fm = SkImageEncoder::kPNG_Type;
        break;
    case kWEBP_JavaEncodeFormat:
        fm = SkImageEncoder::kWEBP_Type;
        break;
    default:
        return JNI_FALSE;
    }

    if (!bitmap.valid()) {
        return JNI_FALSE;
    }

    bool success = false;

    std::unique_ptr<SkWStream> strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));
    if (!strm.get()) {
        return JNI_FALSE;
    }

    std::unique_ptr<SkImageEncoder> encoder(SkImageEncoder::Create(fm));//1.SkImageEncoder
    if (encoder.get()) {
        SkBitmap skbitmap;
        bitmap->getSkBitmap(&skbitmap);
        success = encoder->encodeStream(strm.get(), skbitmap, quality);//2.encodeStream
    }
    return success ? JNI_TRUE : JNI_FALSE;
}

挑重点,1.SkImageEncoder 2.encodeStream (/external/skia/src/images/SkImageEncoder.cpp

bool SkImageEncoder::encodeStream(SkWStream* stream, const SkBitmap& bm,
                                  int quality) {
    quality = SkMin32(100, SkMax32(0, quality));
    return this->onEncode(stream, bm, quality);
}

继续挑重点onEncode ( /external/skia/src/images/SkImageDecoder_libjpeg.cpp

class SkJPEGImageEncoder : public SkImageEncoder {
protected:
    virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) {
#ifdef TIME_ENCODE
        SkAutoTime atm("JPEG Encode");
#endif

        SkAutoLockPixels alp(bm);
        if (nullptr == bm.getPixels()) {
            return false;
        }

        jpeg_compress_struct    cinfo;
        skjpeg_error_mgr        sk_err;
        skjpeg_destination_mgr  sk_wstream(stream);

        // allocate these before set call setjmp
        SkAutoTMalloc<uint8_t>  oneRow;

        cinfo.err = jpeg_std_error(&sk_err);
        sk_err.error_exit = skjpeg_error_exit;
        if (setjmp(sk_err.fJmpBuf)) {
            return false;
        }

        // Keep after setjmp or mark volatile.
        const WriteScanline writer = ChooseWriter(bm);
        if (nullptr == writer) {
            return false;
        }

        jpeg_create_compress(&cinfo);
        cinfo.dest = &sk_wstream;
        cinfo.image_width = bm.width();
        cinfo.image_height = bm.height();
        cinfo.input_components = 3;
        // FIXME: Can we take advantage of other in_color_spaces in libjpeg-turbo?
        cinfo.in_color_space = JCS_RGB;

        // The gamma value is ignored by libjpeg-turbo.
        cinfo.input_gamma = 1;

        jpeg_set_defaults(&cinfo);

        // Tells libjpeg-turbo to compute optimal Huffman coding tables
        // for the image.  This improves compression at the cost of
        // slower encode performance.
        cinfo.optimize_coding = TRUE;
        jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);

        jpeg_start_compress(&cinfo, TRUE);

        const int       width = bm.width();
        uint8_t*        oneRowP = oneRow.reset(width * 3);

        const SkPMColor* colors = bm.getColorTable() ? bm.getColorTable()->readColors() : nullptr;
        const void*      srcRow = bm.getPixels();

        while (cinfo.next_scanline < cinfo.image_height) {
            JSAMPROW row_pointer[1];    /* pointer to JSAMPLE row[s] */

            writer(oneRowP, srcRow, width, colors);
            row_pointer[0] = oneRowP;
            (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
            srcRow = (const void*)((const char*)srcRow + bm.rowBytes());
        }

        jpeg_finish_compress(&cinfo);
        jpeg_destroy_compress(&cinfo);

        return true;
    }
};

看到木有,解决方案就在class SkJPEGImageEncoder的onEncode中结尾的时候比前面多了jpeg_destroy_compress(&cinfo);

相信到了这里大家都清晰了,虽然问题是找到了,但是能不能彻底解决还是得靠各个大厂了。毕竟Android是开源的,各个大厂都有各自不同程度的定制化。如果你是在手机上开发像我一样类似的应用,估计要哭了,因为你是没有办法改别人手机中装的ROM的。

不过总有解决方案:你可以自行编译libjpeg或者turbo-jpeg的源码,然后自己写jni接口去调用。附上几个github地址吧:https://github.com/ChineseBoyLY/CBLYTurboJPEG

问题解决了心里还是很别扭,这个问题肯定有人发现过,于是又开始翻google issues皇天不负有心人,
终于让我发现GOOGLE大牛在google issues 70016687认同了这个bug
这里写图片描述

原来google的码神也会犯错,哈哈哈。。。END

更新

在这里插入图片描述
哥哥大牛们已经在android-9开始,修复了问题。

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 29
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值