JNI方法實(shí)現(xiàn)圖片壓縮(壓縮率極高)
前言
直接使用項(xiàng)目或直接復(fù)制libs中的so庫(kù)到項(xiàng)目中即可(當(dāng)前只構(gòu)建了armeabi),需要其他ABI可檢下項(xiàng)目另外使用CMake構(gòu)建即可。
結(jié)果預(yù)覽:

效果圖.png
jni_278KB.png

quality_484KB.png

sample_199KB.png

size_238KB.png
原圖大小5.99M~~ 我們把所有經(jīng)過壓縮的圖片放到同等大小的情況后,很明顯,采樣壓縮跟尺寸壓縮都不是我們想要的結(jié)果,而質(zhì)量壓縮跟JNI壓縮我設(shè)置的質(zhì)量壓縮值都是30,JNI壓縮出來只有278KB,直接質(zhì)量壓縮出來的有484KB,綜合之后,JNI才是綜合最優(yōu)的方式,當(dāng)然,如果只是頭像,我們?cè)O(shè)置可以把配置值設(shè)置得更小,圖片就更小。
為什么iPhone手機(jī)圖片的質(zhì)量比Android的好?
首先了解兩個(gè)圖像處理庫(kù):libjpeg、Skia。
Skia:圖像處理引擎,Google在Android系統(tǒng)上就是采用Skia,它是基于libjpeg的二次封裝,Google在很多其它產(chǎn)品也使用了這個(gè)庫(kù),比如Chorme,F(xiàn)irefox等等。
libjpeg:早期的圖像處理引擎,用于PC端。
官方文檔可以看到libjpeg.doc這樣一段話:
boolean optimize_coding
TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.
boolean optimize_coding:
- 參數(shù)為TRUE時(shí),圖片壓縮算法使用最優(yōu)的哈夫曼編碼表,它需要額外傳遞數(shù)據(jù),因此會(huì)耗費(fèi)CPU運(yùn)算時(shí)間,以及開辟很多臨時(shí)內(nèi)存空間。
- 參數(shù)為FALSE時(shí),使用默認(rèn)的哈夫曼編碼表。在大多數(shù)情況,使用最優(yōu)哈夫曼編碼表相比默認(rèn)哈夫曼編碼表,能節(jié)省圖像文件很大比例的大小。
為什么使用最優(yōu)哈夫曼編碼表可以節(jié)省圖像文件很大的比例大小呢?
哈夫曼樹和哈夫曼編碼
當(dāng)樹中的節(jié)點(diǎn)被賦予一個(gè)表示某種意義的數(shù)值,我們稱之為該節(jié)點(diǎn)的權(quán)。從樹的根節(jié)點(diǎn)到任意節(jié)點(diǎn)的路徑長(zhǎng)度(經(jīng)過的邊數(shù))與該節(jié)點(diǎn)上權(quán)值的乘積稱為該節(jié)點(diǎn)的帶權(quán)路徑長(zhǎng)度。樹中所有葉節(jié)點(diǎn)的帶權(quán)路徑長(zhǎng)度之和稱為該樹的帶權(quán)路徑長(zhǎng)度(WPL)。當(dāng)帶權(quán)路徑長(zhǎng)度最小的二叉樹被稱為哈夫曼樹,也成為最優(yōu)二叉樹。
如下圖所示,有三課二叉樹,每個(gè)樹都有四個(gè)葉子節(jié)點(diǎn)a,b,c,d,分別取帶權(quán)7,5,2,4。他們的帶權(quán)路徑長(zhǎng)度分別為
(a) WPL = 7x2+5x2+2x2+4x2=36
(b) WPL = 2X1+4X2+7X3+5X3 = 46
(c) WPL = 7x1+5x2+2x3+4x3 = 35

節(jié)點(diǎn)如果像c中的方式分布的話,WPL能取最小值(可證明),我們稱為哈夫曼樹。
哈夫曼樹構(gòu)造
哈夫曼樹在構(gòu)造時(shí)每次從備選節(jié)點(diǎn)中挑出兩個(gè)權(quán)值最小的節(jié)點(diǎn)進(jìn)行構(gòu)造,每次構(gòu)造完成后會(huì)生成新的節(jié)點(diǎn),將構(gòu)造的節(jié)點(diǎn)從備選節(jié)點(diǎn)中刪除并將新產(chǎn)生的節(jié)點(diǎn)加入到備選節(jié)點(diǎn)中。新產(chǎn)生的節(jié)點(diǎn)權(quán)值為參與構(gòu)造的兩個(gè)節(jié)點(diǎn)權(quán)值之和。舉例如下:

- 備選節(jié)點(diǎn)為a,b,c,d,權(quán)值分別為7,5,2,4
- 選出c和d進(jìn)行構(gòu)造(權(quán)值最小),生成新節(jié)點(diǎn)為e(權(quán)值為6),備選節(jié)點(diǎn)變?yōu)?,5,6
- 選出b和e進(jìn)行構(gòu)造,生成新節(jié)點(diǎn)f(權(quán)值為11),備選節(jié)點(diǎn)為7,11
- 將最后的7和11節(jié)點(diǎn)進(jìn)行構(gòu)造,最后生成如圖所示的哈夫曼樹
哈夫曼樹應(yīng)用
在處理字符串序列時(shí),如果對(duì)每個(gè)字符串采用相同的二進(jìn)制位來表示,則稱這種編碼方式為定長(zhǎng)編碼。若允許對(duì)不同的字符采用不等長(zhǎng)的二進(jìn)制位進(jìn)行表示,那么這種方式稱為可變長(zhǎng)編碼。可變長(zhǎng)編碼其特點(diǎn)是對(duì)使用頻率高的字符采用短編碼,而對(duì)使用頻率低的字符則采用長(zhǎng)編碼的方式。這樣我們就可以減少數(shù)據(jù)的存儲(chǔ)空間,從而起到壓縮數(shù)據(jù)的效果。而通過哈夫曼樹形成的哈夫曼編碼是一種的有效的數(shù)據(jù)壓縮編碼。
如果沒有一個(gè)編碼是另一個(gè)編碼的前綴,則稱這樣的編碼為前綴編碼。如0,101和100是前綴編碼。由前綴碼形成的序列可以被唯一的組成一個(gè)字符串序列。如00101100可以被唯一的分析為0,0,101和100。
示例:
我們對(duì)一個(gè)字符串進(jìn)行統(tǒng)計(jì)發(fā)現(xiàn)a-f出現(xiàn)的頻率分別為a:45,b:13,c:12,d:16,e:9,f:5,我們對(duì)該字符串進(jìn)行采用哈夫曼編碼進(jìn)行存儲(chǔ)。

WPL = 1x45+3x(13+12+16)+4x(5+9)=224
這樣算下來使用224二進(jìn)制位就可以將該字符串存儲(chǔ)起來,因?yàn)楣蚵a是前綴碼,所以可以唯一的還原出原來的字符序列。如果我們每個(gè)字符使用3位進(jìn)行存儲(chǔ)(至少3位),那么需要300bit才能將該字符串存儲(chǔ)下。
其次了解下libjpeg使用哈夫曼編碼是對(duì)圖片上的每個(gè)像素(ARGB)進(jìn)行編碼,比如
ARGB(每個(gè)顏色通道取值范圍0-255)的編碼分別是:
A:001
R:010
G:011
B:100
如果采用定長(zhǎng),那一個(gè)圖片下來一個(gè)人像素就是001010011100...
如果我們使用可變長(zhǎng)編碼方式,遍歷、再嵌套遍歷,再嵌套遍歷每一個(gè)像素來獲取前綴編碼(沒錯(cuò),這個(gè)運(yùn)算過程很大,而且臨時(shí)變量?jī)?nèi)存也需要很大,但相對(duì)于今天的手機(jī)CPU來說,so easy),我們大致可以得到這樣的編碼:
A:01
R:10
G:11
B:100
那一個(gè)圖片下來一個(gè)人像素就是011011100...
編碼長(zhǎng)度的優(yōu)化后,接下來干的事就是計(jì)算權(quán)重以及每個(gè)顏色通道對(duì)應(yīng)的編碼的出現(xiàn)頻次構(gòu)建哈夫曼樹了,這里就參考上面的圖片了。
通過上面的介紹,可以知道最優(yōu)哈夫曼編碼其實(shí)是使用了可變長(zhǎng)編碼方式,而默認(rèn)的哈夫曼編碼使用了定長(zhǎng)編碼方式,因此需要更多的存儲(chǔ)空間,呈現(xiàn)出來的手機(jī)圖片自然會(huì)大很大。
libjpeg把optimize_coding參數(shù)默認(rèn)設(shè)置為FALSE是因?yàn)?0多年前的Android手機(jī)CPU跟內(nèi)存都非常吃緊,所以當(dāng)年沒有設(shè)置為TRUE。如今的手機(jī)CPU跟內(nèi)存都“起飛了”,Goolge的Skia圖像處理引擎卻還是使用optimize_coding的默認(rèn)值FALSE。我們無(wú)法修改系統(tǒng)Skia的這個(gè)參數(shù)值,所以只能默默忍受size很大的圖像文件。
經(jīng)過大量圖像壓縮測(cè)試結(jié)果,得到兩個(gè)結(jié)論:
1.圖片壓縮到相同的質(zhì)量,F(xiàn)ALSE所產(chǎn)出的圖像文件大小是TRUE的5-10倍。
2.圖片壓縮到相同的質(zhì)量,Android所產(chǎn)出的圖像文件大小比iOS也是大5-10倍。
所以,通過使用libjpeg編譯自己的native library修改optimize_coding參數(shù)的值,達(dá)圖像質(zhì)量相同,所產(chǎn)出的圖像卻能節(jié)省5-10倍空間大小的效果。
實(shí)現(xiàn)的步驟:
1.構(gòu)建libjpeg的so庫(kù)
到官方下載對(duì)應(yīng)自己電腦系統(tǒng)類型的壓縮包,創(chuàng)建Android項(xiàng)目導(dǎo)入壓縮包里頭的xx.h、xx.c文件構(gòu)建so庫(kù)。bither/bither-android-lib已經(jīng)做了這個(gè)工作,因此我們只需直接拿他的libjpegbither.so即可。
2.導(dǎo)入libjpeg的聲明頭文件,因?yàn)椴襟E1的libjpegbither.so是對(duì)這些頭文件的實(shí)現(xiàn),因此需要導(dǎo)入這些頭文件。
3.創(chuàng)建CMake腳本
cmake_minimum_required(VERSION 3.4.1)
add_library( effective-bitmap
SHARED
src/main/cpp/effective-bitmap.c )
include_directories( src/main/cpp/jpeg/
)
add_library(jpegbither SHARED IMPORTED)
set_target_properties(jpegbither
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libjpegbither.so)
find_library( log-lib
log )
find_library( jnigraphics-lib jnigraphics )
target_link_libraries( effective-bitmap
jpegbither
${log-lib}
${jnigraphics-lib})
4.配置gradle關(guān)聯(lián)CMakeLists.txt構(gòu)建腳本,以及指定產(chǎn)出的ABI的類型
android {
// ...
defaultConfig {
// ...
externalNativeBuild {
cmake {
cppFlags ""
}
}
ndk {
abiFilters 'armeabi'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
5.編寫C/C++代碼
int generateJPEG(BYTE* data, int w, int h, int quality,
const char* outfilename, jboolean optimize) {
int nComponent = 3;
// jpeg的結(jié)構(gòu)體,保存的比如寬、高、位深、圖片格式等信息
struct jpeg_compress_struct jcs;
struct my_error_mgr jem;
jcs.err = jpeg_std_error(&jem.pub);
jem.pub.error_exit = my_error_exit;
if (setjmp(jem.setjmp_buffer)) {
return 0;
}
jpeg_create_compress(&jcs);
// 打開輸出文件 wb:可寫byte
FILE* f = fopen(outfilename, "wb");
if (f == NULL) {
return 0;
}
// 設(shè)置結(jié)構(gòu)體的文件路徑
jpeg_stdio_dest(&jcs, f);
jcs.image_width = w;
jcs.image_height = h;
// 設(shè)置哈夫曼編碼
jcs.arith_code = false;
jcs.input_components = nComponent;
if (nComponent == 1)
jcs.in_color_space = JCS_GRAYSCALE;
else
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
jcs.optimize_coding = optimize;
jpeg_set_quality(&jcs, quality, true);
// 開始?jí)嚎s,寫入全部像素
jpeg_start_compress(&jcs, TRUE);
JSAMPROW row_pointer[1];
int row_stride;
row_stride = jcs.image_width * nComponent;
while (jcs.next_scanline < jcs.image_height) {
row_pointer[0] = &data[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs, row_pointer, 1);
}
jpeg_finish_compress(&jcs);
jpeg_destroy_compress(&jcs);
fclose(f);
return 1;
}
6.構(gòu)建so庫(kù)
源碼:https://github.com/zengfw/EffectiveBitmap (本地下載)
參考鏈接:
Why the image quality of iPhone is much better than Android?
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Android實(shí)現(xiàn)的簡(jiǎn)單藍(lán)牙程序示例
這篇文章主要介紹了Android實(shí)現(xiàn)的簡(jiǎn)單藍(lán)牙程序,結(jié)合實(shí)例形式分析了Android藍(lán)牙程序的原理與客戶端、服務(wù)器端具體實(shí)現(xiàn)步驟,需要的朋友可以參考下2016-10-10
java,Android:在eclipse中的快捷鍵(經(jīng)典收藏)
下面的快捷鍵是常用的,本人就本身喜好且常用的收拾一下,現(xiàn)在曬出來與大家分享,感興趣的朋友可以了解小哦2013-01-01
Android自定義View實(shí)現(xiàn)點(diǎn)贊控件
這篇文章主要介紹了Android自定義View實(shí)現(xiàn)點(diǎn)贊控件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
詳解android項(xiàng)目由Gradle 2.2 切換到 3.0的坑
本篇文章主要介紹了詳解android項(xiàng)目由Gradle 2.2 切換到 3.0的坑,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02
WebView的介紹與簡(jiǎn)單實(shí)現(xiàn)Android和H5互調(diào)的方法
這篇文章主要給大家介紹了關(guān)于WebView與簡(jiǎn)單實(shí)現(xiàn)Android和H5互調(diào)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05

