写这篇文章前先调侃下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 strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));
if (!strm.get()) {
return JNI_FALSE;
}
std::unique_ptr 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 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