前言
最近在解決一個(gè)遺留已久的BUG時(shí),發(fā)現(xiàn)調(diào)用 Bitmap 的 compress 方法將 bitmap 導(dǎo)出到文件流時(shí),如果導(dǎo)出的 bitmap 特別大且導(dǎo)出編碼為 Bitmap.CompressFormat.JPEG 的話該方法會(huì)直接返回 false 而沒(méi)有拋出任何錯(cuò)誤。
而對(duì)于同一個(gè) bitmap ,改用 Bitmap.CompressFormat.PNG 就不會(huì)返回 false 而是能正常導(dǎo)出。
原因與解決方法
懶得看分析過(guò)程的可以直接看這里:
經(jīng)過(guò)我的分析,導(dǎo)致 compress 方法返回 false 的原因是 jpg 編碼格式對(duì)于分辨率有最大限制。
谷歌得到的這個(gè)最大限制為:
655,35 X 655,35
但是我使用模擬器和真機(jī)實(shí)際測(cè)試最大尺寸為:
655,00 X 163,93
需要注意的是:
1.上述數(shù)值不區(qū)分寬和高,也就是說(shuō)兩個(gè)值可以互換。
2.上述分辨率尺寸是我使用模擬器(Android 11.0 arm64-v8a)和真機(jī)(小米10u,MIUI12.5.3 ,Android 11)測(cè)試得到的,可能不同系統(tǒng)版本,不同手機(jī)的限制不同,因?yàn)槭诸^設(shè)備有限,無(wú)法一一測(cè)試,網(wǎng)上也沒(méi)有足夠的資料,所以使用時(shí)最好自己實(shí)際測(cè)試一下。
注意
以上只是導(dǎo)致返回 false 的原因之一,實(shí)際原因還有很多,請(qǐng)結(jié)合實(shí)際情況自行判斷。
分析過(guò)程
目前已知的情況是:
- 該方法除了返回了 false 外,沒(méi)有其他任何錯(cuò)誤拋出,也沒(méi)有其他任何日志可以供參考。
- 已知會(huì)返回 false 的情況是:第一個(gè)參數(shù)也就是圖片編碼為 Bitmap.CompressFormat.JPEG 且 bitmap 特別大。
- 如果將圖片編碼改為 Bitmap.CompressFormat.PNG 則不會(huì)返回 false。
當(dāng)我遇到這個(gè)BUG的時(shí)候,結(jié)合上述已知情況,我首先想到的是要追蹤 compress 方法的實(shí)現(xiàn)方式,試圖從源碼中找到造成這個(gè)錯(cuò)誤的原因。
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;
}
可以看到,改方法只是做了一些簡(jiǎn)單的判斷,其核心調(diào)用了 JNI 代碼。
所以追蹤到C++源碼如下:
(源碼來(lái)自:Android圖片編碼機(jī)制深度解析(Bitmap,Skia,libJpeg))
static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
int format, int quality,
jobject jstream, jbyteArray jstorage) {
SkImageEncoder::Type fm; //創(chuàng)建類型變量
//將java層類型變量轉(zhuǎn)換成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;
}
//判斷當(dāng)前bitmap指針是否為空
bool success = false;
if (NULL != bitmap) {
SkAutoLockPixels alp(*bitmap);
if (NULL == bitmap->getPixels()) {
return false;
}
//創(chuàng)建SkWStream變量用于將壓縮后的圖片數(shù)據(jù)輸出
SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
if (NULL == strm) {
return false;
}
//根據(jù)編碼類型,創(chuàng)建SkImageEncoder變量,并調(diào)用encodeStream對(duì)bitmap
//指針指向的圖片數(shù)據(jù)進(jìn)行編碼,完成后釋放資源。
SkImageEncoder* encoder = SkImageEncoder::Create(fm);
if (NULL != encoder) {
success = encoder->encodeStream(strm, *bitmap, quality);
delete encoder;
}
delete strm;
}
return success;
}
從上述源碼可以看出,可能返回 false 的地方有:
- 編碼格式不存在
- bitmap 為空
- SkWStream 創(chuàng)建失敗
- 最后是調(diào)用的 encodeStream 返回 false
經(jīng)過(guò)我的一一確認(rèn),1-3點(diǎn)是沒(méi)有問(wèn)題的,所以最后只剩下了第4點(diǎn),但是第4點(diǎn)又是調(diào)用了另外一個(gè)很復(fù)雜的庫(kù),實(shí)在是無(wú)心去查看。
于是我轉(zhuǎn)變思路,既然會(huì)導(dǎo)致這個(gè)問(wèn)題出現(xiàn)的原因有兩個(gè),就是編碼為JPG時(shí)且bitmap特別大時(shí),那會(huì)不會(huì)是內(nèi)存溢出呢?
雖然正常來(lái)說(shuō),內(nèi)存溢出會(huì)拋出OOM錯(cuò)誤(事實(shí)上,如果我手動(dòng)把bitmap設(shè)置的特別大,也會(huì)拋出OOM),但是我們不妨試一下,看看兩者之間有何聯(lián)系。
測(cè)試代碼如下:
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
}
}
通過(guò)使用上述代碼,我不停的測(cè)試到底分辨率達(dá)到多少時(shí),會(huì)返回 false ,終于,測(cè)出來(lái)達(dá)到 655,00 X 163,93 能夠剛好不返回 false。
至此,可以確定,之所以會(huì)返回 false 確實(shí)和分辨率有關(guān)。
至于為什么會(huì)有限制以及為什么是這個(gè)尺寸,剛興趣的可以去了解一下 jpg 編碼的實(shí)現(xiàn),以及研究一下 libjpeg 的源碼,我水平有限,就不深究了。