前言
最近在解决一个遗留已久的BUG时,发现调用 Bitmap 的 compress 方法将 bitmap 导出到文件流时,如果导出的 bitmap 特别大且导出编码为 Bitmap.CompressFormat.JPEG 的话该方法会直接返回 false 而没有抛出任何错误。
而对于同一个 bitmap ,改用 Bitmap.CompressFormat.PNG 就不会返回 false 而是能正常导出。
原因与解决方法
懒得看分析过程的可以直接看这里:
经过我的分析,导致 compress 方法返回 false 的原因是 jpg 编码格式对于分辨率有最大限制。
谷歌得到的这个最大限制为:
655,35 X 655,35
但是我使用模拟器和真机实际测试最大尺寸为:
655,00 X 163,93
需要注意的是:
1.上述数值不区分宽和高,也就是说两个值可以互换。
2.上述分辨率尺寸是我使用模拟器(Android 11.0 arm64-v8a)和真机(小米10u,MIUI12.5.3 ,Android 11)测试得到的,可能不同系统版本,不同手机的限制不同,因为手头设备有限,无法一一测试,网上也没有足够的资料,所以使用时最好自己实际测试一下。
注意
以上只是导致返回 false 的原因之一,实际原因还有很多,请结合实际情况自行判断。
分析过程
目前已知的情况是:
- 该方法除了返回了 false 外,没有其他任何错误抛出,也没有其他任何日志可以供参考。
- 已知会返回 false 的情况是:第一个参数也就是图片编码为 Bitmap.CompressFormat.JPEG 且 bitmap 特别大。
- 如果将图片编码改为 Bitmap.CompressFormat.PNG 则不会返回 false。
当我遇到这个BUG的时候,结合上述已知情况,我首先想到的是要追踪 compress 方法的实现方式,试图从源码中找到造成这个错误的原因。
compress 方法的源码如下:
@WorkerThread
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;
}
可以看到,改方法只是做了一些简单的判断,其核心调用了 JNI 代码。
所以追踪到C++源码如下:
(源码来自:Android图片编码机制深度解析(Bitmap,Skia,libJpeg))
static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
int format, int quality,
jobject jstream, jbyteArray jstorage) {
SkImageEncoder::Type fm; //创建类型变量
//将java层类型变量转换成Skia的类型变量
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 false;
}
//判断当前bitmap指针是否为空
bool success = false;
if (NULL != bitmap) {
SkAutoLockPixels alp(*bitmap);
if (NULL == bitmap->getPixels()) {
return false;
}
//创建SkWStream变量用于将压缩后的图片数据输出
SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
if (NULL == strm) {
return false;
}
//根据编码类型,创建SkImageEncoder变量,并调用encodeStream对bitmap
//指针指向的图片数据进行编码,完成后释放资源。
SkImageEncoder* encoder = SkImageEncoder::Create(fm);
if (NULL != encoder) {
success = encoder->encodeStream(strm, *bitmap, quality);
delete encoder;
}
delete strm;
}
return success;
}
从上述源码可以看出,可能返回 false 的地方有:
- 编码格式不存在
- bitmap 为空
- SkWStream 创建失败
- 最后是调用的 encodeStream 返回 false
经过我的一一确认,1-3点是没有问题的,所以最后只剩下了第4点,但是第4点又是调用了另外一个很复杂的库,实在是无心去查看。
于是我转变思路,既然会导致这个问题出现的原因有两个,就是编码为JPG时且bitmap特别大时,那会不会是内存溢出呢?
虽然正常来说,内存溢出会抛出OOM错误(事实上,如果我手动把bitmap设置的特别大,也会抛出OOM),但是我们不妨试一下,看看两者之间有何联系。
测试代码如下:
package com.example.myapplication
import android.graphics.Bitmap
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.widget.Button
import java.io.File
import java.io.FileOutputStream
private const val TAG = "el, in Main"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startCompress()
}
private fun startCompress() {
val mainBtn = findViewById<Button>(R.id.main_btn)
mainBtn.setOnClickListener {
//val width = 65500
//val height = 16393
val height = 65500
val width = 16393
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val savePath = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString())
saveBitmap2File(bitmap, "test", savePath, 50)
}
}
private fun saveBitmap2File(
bitmap: Bitmap,
fileName: String,
savePath: File?,
quality: Int): File {
val f = File(savePath, "$fileName.jpg")
val imgFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG
if (!f.createNewFile()) {
Log.w(TAG, "file " + f + "has already exist")
}
val outputStream = FileOutputStream(f)
//Log.i(TAG, "saveBitmap2File: bitmap's width="+bitmap.getWidth()+" height="+bitmap.getHeight());
if (!bitmap.compress(imgFormat, quality, outputStream)) {
Log.e(TAG, "saveBitmap2File: write bitmap to file fail!")
throw Exception("saveBitmap2File: write bitmap to file fail!")
}
try {
outputStream.flush()
outputStream.close()
} catch (e: Exception) {
Log.e(TAG, "saveBitmap2File: ", e)
}
return f
}
}
通过使用上述代码,我不停的测试到底分辨率达到多少时,会返回 false ,终于,测出来达到 655,00 X 163,93 能够刚好不返回 false。
至此,可以确定,之所以会返回 false 确实和分辨率有关。
至于为什么会有限制以及为什么是这个尺寸,刚兴趣的可以去了解一下 jpg 编码的实现,以及研究一下 libjpeg 的源码,我水平有限,就不深究了。