https://www.jianshu.com/p/8ebe0ddd21f7
https://www.jianshu.com/p/f305fb008ab6
一 . 圖片的基本知識
圖像是由像素組成的,而像素實際上是帶着座標的位置和顏色信息的。它是有若干行和若干列的點組成的,他們相交就會有無數個點。假如我們隨便取出一個點A 那個這個點可以表示爲
A[m,n] = [blue,green,red]
m和n 就是圖像中的第m行和n列
blue 表示藍色,是三原色(RGB)的第一個值
green 表示綠色,是三原色(RGB)的第二個值
red 表示紅色,是三原色(RGB)的第三個值
- 分辨率
我們通常說圖片分辨率其實就是指像素數,通俗的說是橫向多少個像素 x 縱向多個像素 。那什麼是像素: 每張圖片是有色點組成的,每個色點就稱之爲像素。比如一張圖片有10萬個色點構成,那麼這個圖片像素就是 10W。
我們舉個例子:一張圖片分辨率爲 500x400 ,那麼圖片是有橫向 500個像素、縱向 400個像素,(合計20000像素點)構成。
- 圖片格式
我們在實際項目開開發中遇到比較多的圖片格式 一般有 .PNG、.JPG、.JPEG、 .WebP 、.GIF、.SVG 等等
PNG:它是一種無損數據壓縮位圖圖形文件格式。這也就是說PNG 只支持無損壓縮。對於PNG 格式是有8 位、24位、32位的三種形式的。區別就是對透明度的支持。
JPG:其實就是 JPEG的另一種叫法
JPEG:它是一種有損壓縮的圖片格式
WebP:Google 開發出的一種支持alpha 通道的有損壓縮。同等質量情況下比 JPEG和PNG小 25%~45%.
GIF:它是動態圖片的一種格式,和PNG 一樣是一種無損壓縮。
**SVG **: 是一種無損、矢量圖(放大不失真)
就目前來說,Android 原生支持的格式只有 JPEG、PNG、GIF、WEBP(android 4.0 加入)、BMP。而在android層代碼中我們只能調用的編碼方式只有PNG、JPEG、和WEBP 三種。不過目前來說android 還不支持對GIF 這種的動態編碼。
注意 :我們日常所有的 .png、.jpg、.jpeg 等等指的是圖像數據經過壓縮編碼後在媒體上的封存形式,是不能和PNG 、JPG、JEPG 混爲一談的。
我們不是說圖片怎麼壓縮的嗎?爲什麼要說圖片格式。因爲他們之間是存在聯繫的,比如其中 PNG是無損壓縮格式的圖片,JPEG是有損壓縮格式的圖片,所以對應的也有各自的壓縮算法,比如在android中PNG壓縮使用的就是 libpng 進行壓縮的,而JPEG的壓縮是用libjepg(7.0之前) 壓縮的,7.0 之後改爲libjpeg-turbo是基於libjepg修改的,而比libjepg更快。最大的變化就是相同質量的下7.0之後的機器比7.0之前的機器壓縮的圖片要小。
二 . Bitmap
對於開發android的 小夥伴,對Bitmap肯定是不陌生的,甚至有的小夥伴被它虐的“體無完膚”。在Android中任何圖片資源的顯示對象都是通過bitmap 來顯示的(XML資源通Canvas繪製除外)。
Bitmap 它是圖像處理中的一個非常重要對象。
1.何爲bitmap?
我們可以稱之爲位圖,是一種存儲像素的數據結構,通過這個對象我們可以獲取到一系列和圖片相關的屬性, 並且可以對圖像進行處理,比如切割,放大等等,相關操作。
2.bitmap 存儲空間
隨着android系統的不斷升級,bitmap的存儲空間也在發生變成,而bitmap 的存儲空間主要有三個地方:
1)Native Memory
在android 2.3 以下的版本,bitmap像素數據是存儲在 Native 空間中,如果需要釋放是要主動調用 recycle()方法
2)Dalvik Heap
在Android 3.0 以上版本,bitmap的像素數據存儲在虛擬機堆中,不需要我們再去主動調用recycle()方法,gc會幫我們去回收。
3)Ashmem
很多小夥伴可能不知道這個是什麼,它是匿名共享存儲空間。我們在實際開發中使用圖片加載庫中,有一個庫就是利用這個一個空間來進行bitmap 對象的存儲的,他就是大名鼎鼎的Fresco 圖片加載庫。
不過這裏需要注意一點: 在Android 4.4 以前的版本中 Ashmem 是和App 進程空間是隔離的互相不影響,而在Android 4.4以後的版本中,Ashmemk空間是包含在App所佔用的內存空間。
說了這麼多我們還是不知道bitmap 在內存空間中到底是佔用多大的,其實bitmap 在內存空間中所佔用的內存計算是這樣的:
pixelWidth x pixelHeight x bytesPerPixel(bitmap 的寬 x 高 x 每個像素所佔的字節),所有如果相同的Bitmap對象, 每個像素所佔用的字節大小,決定了這個bitmap 在內存中所佔用的內存大小。
上面既然說了,所佔用的字節大小決定bitmap內存大小,那麼怎麼樣能讓每個像素所佔的字節變小。在我們Bitmap對象中有一個比較重要的枚舉類 Config ,這個Config 是用來設置顏色配置信息的。對於Config配置有四個變量。
Config 配置:
-
alpha_8 : 佔用8位,1個字節,顏色信息只由透明組成。
-
argb_4444: 佔用16位,2個字節,顏色有透明度和R (red)G(green)B(blue)組成。
3.argb_8888: 佔用34位,4個字節,顏色有透明度和R (red)G(green)B(blue)組成。
這裏可能有人會問爲什麼 argb_4444和 argb_8888都是 有ARGB組成爲什麼所佔的字節不同,因爲每個部分所佔用的字節是不同的,argb_4444每個部分佔用4個字節,而argb_8888每個部分佔用8位。
4.rgb_565: 佔用16位,2個字節,顏色有R (red)G(green)B(blue)組成。
提示:我們通常在操作bitmap 的時候,是必須要和這個配置打交道的,搞明白對使用bitmap的時候,提供幫助。特別是防止OOM,有很大作用。如果我們平時對圖片處理,如多對圖片的透明度沒特別要求,比較建議使用 rgb_565 這個配置,因爲他和其它幾個比較,性價比最高的。比argb_4444 顯示圖片清晰,argb_8888佔用內存少一半,而alpha_8只有透明度,對圖片沒什麼意思。既然我們知道這寫參數的意義了,我們就可以通過設置該配置,來讓我們bitmap 佔用內存空間變小。
說了這麼多,那麼Bitmap在內存究竟佔用多少內存?使用Android Api的 getByteCount 方法即可。
通過這個方法,我們就可以獲取當前運行的 bitmap 佔用的內存。
三. 壓縮
既然我們知道了,bitmap在內存中佔用空間 = bitmap 的寬 x 高 x 每個像素所佔的字節數。那麼Bitmap 壓縮都是圍繞這個來做文章的。這裏的三個參數我們,減少任意一個的值,就可能會達到了我們壓縮的效果了。
- 圖片存在的形式
圖片存在大致可以分爲三種形式:
file形式 ,我們存在硬盤上的 都是以file 文件的形式存在的。
bitmap或 stream 形式,圖片在內存中要麼以bitmap形式要麼已 stream形式存在的。
stream 形式,我們圖片在網絡傳輸的過程中,都是已stream 形式存在。
那麼在android中 圖片文件主要是有png,jpg,webP,gif 等幾種類型格式進行存儲的。其實我們圖片壓縮也是在幾個類型中做處理。
我們爲什麼要說圖片存在的形式,這會對我們以後開發過程中對圖片處理需要有一定的幫助的。
- api
在介紹壓縮之前我們先看下相關api吧,讓我們在使用的時候更加方便和選擇合適的pai來做相關操作。
我們再對圖片進行相關操作的時候,主要涉及的類有 Bitmap,BitmapFactory,Matrix。等
Bitmap
//將位圖壓縮到指定的outputstream中
boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
// 創建一個Bitmap 對象 ,該方法有個重載函數
Bitmap createBitmap (Bitmap src)
//根據新配置 拷貝一份新的bitmap ,第二次參數的意思 他的像素是否可以修改
//返回的位圖具有與原圖相同的密度和顏色空間
Bitmap copy (Bitmap.Config config, boolean isMutable)
//創建一個新的bitmap ,根據傳入的寬和高進行縮放。
Bitmap createScaledBitmap (Bitmap src, int dstWidth, int dstHeight, boolean filter)
// 表示圖片以什麼格式的算法進行壓縮,壓縮後爲何格式
Bitmap.CompressFormat . JPEG / PNG/ WEBP
BitmapFactory
// 根據文件路徑解碼成位圖
Bitmap decodeFile (String pathName, BitmapFactory.Options opts)
//根據 留解碼成位圖
Bitmap decodeStream (InputStream is)
//根據 資源解碼成爲位圖
Bitmap decodeResource (Resources res, int id, BitmapFactory.Options opts)
//根據 數組解碼成位圖
Bitmap decodeByteArray (byte[] data, int offset, int length, BitmapFactory.Options opts)
// opts.inJustDecodeBounds表示是否將圖片加載到內存中 true 不加載但可以獲取圖片的寬高等相關信息 ,false 加載
這裏另外需要注意的是, BitmapFactory 獲取圖片的寬/高和圖片位置相關信息是和程序運行的設備有關的。
什麼意思:就是將一張圖片放到不同的 drawable 目錄下,在不同屏幕密度的設置上運行,獲取到的結果是不同的,這個是和android資源加載機制有關係的,感興趣的小夥伴可以去研究研究。
BitmapFactory.Options.inSampleSize
圖片縮放的倍數,主要這個只必須大於1,這個值很重要,後面說到的尺寸壓縮,就是對這個值的計算,也就是對原圖進行採樣,最後放回一個較小的圖片放到內存中的。例如 inSampleSize == 2 的時候返回一個圖像,那它的寬和高 就是原圖的 1/2 , 像素就是原來的 1/4。 這裏inSampleSize 最終值必須是2的冪,任何其他值都將四捨五入到接近2的冪的值。
BitmapFactory.Options.inPreferredConfig
表示一個像素需要多大的空間存儲,設置解碼器色彩模式 ,默認模式爲ARGB_8888 前面我們說了每個模式下佔的字節數,通過改字段控制bitmap 最後在內存中佔用多大內存。不過有點需要注意的是,就算我們設置了別的模式,也有可能還是默認模式的。爲什麼了,官方是這麼解釋的 : 解碼器將嘗試根據系統的屏幕深度選擇最佳匹配配置,以及原始圖像的特徵,比如它是否具有每個像素的 alpha值。
Matrix
//對圖片進行旋轉
setRotate(float degrees, float px, float py)
//對圖片進行縮放
setScale(float sx, float sy)
//對圖片進行平移
setTranslate(float dx, float dy)
3.圖片壓縮
說了這麼多總算說到正題了,對於圖片壓縮按照分類的話,大概可以分爲兩類吧,一種爲質量壓縮,一種爲尺寸壓縮。我們這裏不管說的是那種壓縮,其實用的都是 google 在android 中封裝好了的壓縮算法,我們只是使用封裝好api進行講解,和一些我們做壓縮的時候的一些經驗吧。
1)質量壓縮
何爲質量壓縮,在文章開頭摘要中就簡單的介紹了,這裏我們再詳細的說下質量壓縮,前面提到過,質量壓縮其實是不改變原圖片的尺寸的前提下改變圖片的透明度和位深,原圖尺寸不改變,像素自然也是沒有改變的。隨着他壓縮圖片文件大小變小,可是在bitmap內存中佔用的內存是不會改變的和原圖相比。它雖然沒有改變圖片的像素,可是它壓縮了圖片中每個像素的質量,這樣就會出現,如果質量比較低,那麼這個圖片就會變得非常模糊,色彩失真很嚴重的。我在開發中對圖片壓縮的時候,一個壓縮好的bitmap對象,在寫入文件的時候,因爲我設置圖片CompressFormat 屬性爲 Bitmap.CompressFormat.PNG,結果寫入到文件中的圖片體積比原圖還大。這裏一定要注意的就是,png格式的圖片是不適合質量壓縮的,不管你壓縮質量多低,內部壓縮算法根本是不會對其進行壓縮的,爲什麼? 我們前面在說圖片格式的時候說過,png 圖片是一種無損的圖片壓縮格式,這也是爲什麼在說圖片壓縮之前,對圖片的一些基本知識做個簡單介紹。在需求上,這種壓縮非常不適合做縮略圖,也不適合想通過壓縮圖片來減小對內存的使用。個人認爲這個只適合,既想保證圖片的尺寸或像素,而同時又希望減小文件大小的需求而已。說了這麼多,那麼在android中質量壓縮通過什麼api來實現的了,其實google 一下這種代碼滿屏都是的,爲了減少看到這個文章的小夥伴去查閱代碼的時間,就貼上一小段代碼吧:
2)尺寸壓縮
尺寸壓縮 其實就是針對圖片的尺寸進行修改,這個過程就是一個圖片重新採樣。在android 中圖片重採樣是提供了兩種方法的,一種是臨近採樣,也是我們比較熟悉,通過改變 inSampSize 值,也叫這採樣率壓縮。第二種叫做雙線性採樣。前面我們在介紹Api是時候對這個字段進行詳細介紹了。這個方法也是android 開發小夥伴人人皆知的辦法了,還有很多比較出名的壓縮庫都說是通過該方法來做的。其實我個人認爲,採樣壓縮其實是比較粗暴的。
- 臨近採樣,這個方式是比較粗暴的,它是直接選中其中的一個像素作爲生成的像素,它採用的算法叫做臨近點插值算法,它是圖像縮放算法。可能還有小夥伴不明白,舉個例子:
假設我們有張圖片,他的像素是這樣的
綠 黃 綠 黃
綠 黃 綠 黃
綠 黃 綠 黃
綠 黃 綠 黃
這樣的圖片是一個綠色的像素隔着一個黃色的像素,官方給我的解釋是 x (x爲2的倍數)個像素最後對應一個像素,由於採樣率設置爲1/2,所以是兩個像素生成一個像素,另一個自己就被拋棄了,如果只選擇綠色,黃色被拋棄,造成圖片只有一種顏色綠色了。
那到底怎麼樣獲取採樣率了?
將BitmapFactory.Options的inJustDecodeBounds參數設置爲true並加載圖片
從BitmapFactory.Options取出圖片的原始寬高信息, 他們對應於outWidth和outHeight參數
根據採樣率的規則並結合目標View的所需大小計算出採樣率inSampleSize
將BitmapFactory.Options的inJustDecodeBounds參數設爲false, 然後重新加載.
通過以上的四個步驟,我們最終獲取到一個最接近我們想要的圖片。在上面步驟中最重要的就是計算 inSampleSize的值,我們下官方推薦我們的計算做法是怎麼寫的,看代碼:
這個就不做過多解釋了,應該都能看得懂的,很簡單,就是根據需要的寬和高,和圖片的原始寬和高,一直循環算出一個合適的 inSampleSize 的值。
- 雙線性採樣
雙線性採樣使用的是雙線性內插算法,這個算法不像臨近採樣那樣粗暴,它是參考了源像素對應的位置周圍 2*2個點的值根據相對位置取對應的權重,經過計算之後得到目標圖像。
雙線性採樣在android使用方式 一般有兩種,我們看實現代碼:
其實第一種,在最終也是使用了第二種方式的 matrix 進行縮放的。我們發現 createScaledBitmap 函數的源碼中
還是使用matrix進行縮放的
上面兩種也是android中圖片尺寸壓縮中最常見的兩種方法了。對臨近採樣它的方式是最快的,因爲我們說過它是直接選擇其中一個像素作爲生成像素,生成的圖片可能會相對比較失真,壓縮的太厲害會產生明顯的鋸齒。而雙線性採樣相對來說失真就沒有這樣嚴重。這裏可能有小夥伴就要問了,既然雙線性採樣比臨近採樣要好。爲什麼很多壓縮框架都是採用臨近採樣來做的?問的好,我也查閱過相關資料和官方文檔,可惜沒有找到比較有權威的說法來證明這點。我說說我對這個觀點的看法吧,如果我們要是使用雙線性採樣對圖片尺寸壓縮的話,不管是採用第一種還是第二種,我們都必須要有個bitmap對象,而再拿到這個bitmap對象的時候,我們是要寫入到內存中的,而如果圖片太大的話,在decode的時候,程序就已經OOM了,特別是處理大圖的時候,這個方法肯定是不合適的。而臨近採樣是可以在圖片不decode到內存的情況下,對圖片進行壓縮處理,最後獲取到的bitmap 是很小的,基本不會導致OOM的。
3.LibJpeg壓縮
通過Ndk調用LibJpeg庫進行壓縮,保留原有的像素,清晰度。這個庫廣泛的使用在開源的圖片壓縮上的。
4.壓縮策略
其實對於壓縮策略,我認爲無法肯定的說一定是1,或者2。它其實是看需求的,根據需求來定一個合理的壓縮策略。我個人認爲網上的 luban 壓縮庫,他的策略在某種程度上還算比較具有通用性的,適合大部分需求吧。對應 luban的壓縮 算法,我這裏簡單的介紹下,他是將圖片的比例值(短邊除以長邊)分爲了三個區間值:
[1, 0.5625) 即圖片處於 [1:1 ~ 9:16) 比例範圍內
[0.5625, 0.5) 即圖片處於 [9:16 ~ 1:2) 比例範圍內
[0.5, 0) 即圖片處於 [1:2 ~ 1:∞) 比例範圍內
然後再去判斷圖片的最長的邊是否超過這個區間的邊界值,然後再去計算這個臨近採樣的 inSampleSize的值。如果想要具體的瞭解這個策略,去github 上下載源碼去研究研究,代碼還是很簡單的。在實際的開發的過程中我們可以根據我們需求,結合上面幾種壓縮機制,自己制定一個比較適合自己的壓縮策略。不過一般情況都不需要我們自己制定圖片的壓縮策略,採樣壓縮 ,基本已經滿足我們的需求的
爲什麼Android上的圖片就不如IOS上的
libjpeg是廣泛使用的開源JPEG圖像庫,安卓也依賴libjpeg來壓縮圖片。但是安卓並不是直接封裝的libjpeg,而是基於了另一個叫Skia的開源項目來作爲的圖像處理引擎。Skia是谷歌自己維 護着的一個大而全的引擎,各種圖像處理功能均在其中予以實現,並且廣泛的應用於谷歌自己和其它公司的產品中(如:Chrome、Firefox、 Android等)。Skia對libjpeg進行了良好的封裝,基於這個引擎可以很方便爲操作系統、瀏覽器等開發圖像處理功能。
libjpeg在壓縮圖像時,有一個參數叫optimize_coding,關於這個參數,libjpeg.doc有如下解釋:
就是上面那個解釋optimize_coding這段
這段話大概的意思就是如果設置optimize_coding爲TRUE,將會使得壓縮圖像過程中基於圖像數據計算哈弗曼表(關於圖片壓縮中的哈弗曼表,請自行查閱相關資料),由於這個計算會顯著消耗空間和時間,默認值被設置爲FALSE。
谷歌的Skia項目工程師們最終沒有設置這個參數,optimize_coding在Skia中默認的等於了FALSE,這就意味着更差的圖片質量和更大的圖片文件,而壓縮圖片過程中所耗費的時間和空間其實反而是可以忽略不計的。那麼,這個參數的影響究竟會有多大呢?
經我們實測,使用相同的原始圖片,分別設置optimize_coding=TRUE和FALSE進行壓縮,想達到接近的圖片質量(用Photoshop 放大到像素級逐塊對比),FALSE時的圖片大小大約是TRUE時的5-10倍。換句話說,如果我們想在FALSE和TRUE時壓縮成相同大小的JPEG 圖片,FALSE的品質將大大遜色於TRUE的(雖然品質很難量化,但我們不妨說成是差5-10倍)。
什麼意思呢?意思就是現在設備發達啦,是時候將optimize_coding設置成true了,但是問題來了,Android系統代碼對於APP來說修改不了,我們有沒有什麼辦法將這個參數進行設置呢?答案肯定是有的,那就是自己使用自己的so庫,不用系統的不就完了。
分析源碼
既然外國基友都說了是Android系統集成了這個庫,但是參數沒設置好,咱也不明白爲啥Android就是不改…但是我們也得驗證一下外國基友說的對不對是吧。
那我們就從Bitmap.compress這個方法說起
public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
這個方法進行質量壓縮,而且可能失去alpha精度
我們看到quality只能是0-100的值
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;
}
利用流和byte數組生成SkJavaOutputStream對象
相關更多內容查看
https://www.jianshu.com/p/072b6defd938
至此壓縮就完成了,我們也就看出Android系統是通過libjpeg進行壓縮的。
但是Android集成的libjpeg和我們使用的也有一些不一樣,所以我建議使用自己編譯開元so進行操作,這樣可以根據我們需求來定製參數達到更好的符合我們項目的目的。
小結:
我們已經知道Android系統中是使用skia庫進行壓縮的,skia庫中又是使用其他開元庫進行壓縮對於jpg的壓縮就是使用libjpeg這個庫。
- Android中有圖片所佔內存因素分析
我們經常因爲圖片太大導致oom,但是很多小夥伴,只是借鑑網上的建議和方法,並不知道原因,那麼我們接下來就大致分析一下圖片在Android中加載由那些因素決定呢?
getByteCount()
表示存儲bitmap像素所佔內存
public final int getByteCount() {
return getRowBytes() * getHeight();
}
getAllocationByteCount()
Returns the size of the allocated memory used to store this bitmap’s pixels.
返回bitmap所佔像素已經分配的大小
This can be larger than the result of getByteCount() if a bitmap is reused to decode other bitmaps of smaller size, or by manual reconfiguration. See reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap. If a bitmap is not modified in this way, this value will be the same as that returned by getByteCount().
This value will not change over the lifetime of a Bitmap.
如果一個bitmap被複用更小尺寸的bitmap編碼,或者手工重新配置。那麼實際尺寸可能偏小。具體看reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap.如果不牽扯複用否是新產生的,那麼就和getByteContent()相同。
這個值在bitmap生命週期內不會改變
所以從代碼看mBuffer.length就是緩衝區真是長度
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer 代表存儲 Bitmap 像素數據的字節數組。
return getByteCount();
}
return mBuffer.length;
}
然後我們看看佔用內存如何計算的
Bitamp 佔用內存大小 = 寬度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一個像素所佔的內存
那麼一個像素佔用的內存多大呢?這個就和配置的規格有關係
SkBitmap.cpp
static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0, // Unknown
1, // Alpha_8
2, // RGB_565
2, // ARGB_4444
4, // RGBA_8888
4, // BGRA_8888
1, // kIndex_8
};
常用的就是RGBA_8888也就是一個像素佔用四個字節大小
- ARGB_8888:每個像素佔四個字節,A、R、G、B 分量各佔8位,是 Android 的默認設置;
- RGB_565:每個像素佔兩個字節,R分量佔5位,G分量佔6位,B分量佔5位;
- ARGB_4444:每個像素佔兩個字節,A、R、G、B分量各佔4位,成像效果比較差;
- Alpha_8: 只保存透明度,共8位,1字節;
於此同時呢,在BitmapFactory 的內部類 Options 有兩個成員變量 inDensity 和 inTargetDensity其中
- inDensity 就 Bitmap 的像素密度,也就是 Bitmap 的成員變量 mDensity默認是設備屏幕的像素密度,可以通過 Bitmap#setDensity(int) 設置
- inTargetDensity 是圖片的目標像素密度,在加載圖片時就是 drawable 目錄的像素密度
當資源加載的時候會進行這兩個值的初始化
調用的是 BitmapFactory#decodeResource 方法,內部調用的是 decodeResourceStream 方法
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//實際上,我們這裏的opts是null的,所以在這裏初始化。
/**
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
*/
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;//這裏density的值如果對應資源目錄爲hdpi的話,就是240
}
}
//請注意,inTargetDensity就是當前的顯示密度,比如三星s6時就是640
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
會根據設備屏幕像素密度到對應 drawable 目錄去尋找圖片,這個時候 inTargetDensity/inDensity = 1,圖片不會做縮放,寬度和高度就是圖片原始的像素規格,如果沒有找到,會到其他 drawable 目錄去找,這個時候 drawable 的屏幕像素密度就是 inTargetDensity,會根據 inTargetDensity/inDensity 的比例對圖片的寬度和高度進行縮放。
所以歸結上面影響圖片內存的原因有:
- . 色彩格式,前面我們已經提到,如果是 ARGB8888 那麼就是一個像素4個字節,如果是 RGB565 那就是2個字節
- 原始文件存放的資源目錄
- 目標屏幕的密度
- 圖片本身的大小
3.圖片的幾種壓縮辦法
-
質量壓縮
public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
注意這種方式,是通過改變alpha通道,改變色彩度等方式達到壓縮圖片的目的,壓縮使得存儲大小變小,但是並不改變加載到內存的大小,也就是說,如果你從1M壓縮到了1K,解壓縮出來在內存中大小還是1M。而且有個很坑的問題,就是如果設置quality=100,這個圖片存儲大小會增大,而且會小幅度失真。具體原因,我在上面分析源碼的時候還沒仔細研究,初步判斷可能是利用傅里葉變換導致。 -
尺寸壓縮
尺寸壓縮在使用的時候BitmapFactory.Options 類型的參數當置 BitmapFactory.Options.inJustDecodeBounds=true只讀取圖片首行寬高等信息,並不會將圖片加載到內存中。設置 BitmapFactory.Options 的 inSampleSize 屬性可以真實的壓縮 Bitmap 佔用的內存,加載更小內存的 Bitmap。
設置 inSampleSize 之後,Bitmap 的寬、高都會縮小 inSampleSize 倍。
inSampleSize 比1小的話會被當做1,任何 inSampleSize 的值會被取接近2的冪值 -
色彩模式壓縮
也就是我們在色彩模式上進行變換,通過設置通過 BitmapFactory.Options.inPreferredConfig改變不同的色彩模式,使得每個像素大小改變,從而圖片大小改變 -
Matrix 矩陣變換
使用:
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
Matrix matrix = new Matrix();
float rate = computeScaleRate(bitmapWidth, bitmapHeight);
matrix.postScale(rate, rate);
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
其實這個操作並不是節省內存,他只是結合我們對尺寸壓縮進行補充,我們進行尺寸壓縮之後難免不會滿足我們對尺寸的要求,所以我們就藉助Matrix進行矩陣變換,改變圖片的大小。
- Bitmap#createScaledBitmap
這個也是和Matrix一個道理,都是進行縮放。不改變內存。
3.圖片壓縮的最終解決方案
我們通過上面的總結我們歸納出,圖片的壓縮目的有兩種:
- 壓縮內存,防止產生OOM
- 壓縮存儲空間,目的節約空間,但是解壓到內存中大小不變。還是原來沒有壓縮圖片時候的大小。
那麼我們應該怎麼壓縮才合理呢,其實這個需要根據需求來定,可能有人就會說我說的是廢話,但是事實如此。我提供一些建議: - 使用libjpeg開源項目,不使用Android集成的libjpeg,因爲我們可以根據需要修改參數,更符合我們項目的效果。
- 合理通過尺寸變換和矩陣變換在內存上優化。
- 對不同屏幕分辨率的機型壓縮進行壓縮的程度不一樣。
那麼我們就開始我們比較難的一個環節就是集成開源庫。
4.編譯libjpeg生成so庫
首先確保我們安裝了ndk環境,不管是Linux還是windows還是macOs都可以編譯,只要我們有ndk
我們必須知道我們NDK能夠使用,並且可以調用到我們ndk裏面的工具,這就要求我們要配置環境變量,當然Linux和windows不一樣,macOS由於我這種窮逼肯定買不起所以我也布吉島怎麼弄。但是思想就是要能用到ndk工具
- windows是在我們環境變量中進行配置
- Linux
echo "export ANDROID_HOME='Your android ndk path'" >> ~/.bash_profile
source ~/.bash_profile
當然Linux還可以寫.sh來個腳本豈不更好
NDK=/opt/ndk/android-ndk-r12b/
PLATFORM=$NDK/platforms/android-15/arch-arm/
PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/
CC=$PREBUILT/bin/arm-linux-androideabi-gcc
./configure --prefix=/home/linc/jpeg-9b/jni/dist --host=arm CC="$CC --sysroot=$PLATFORM"
最執行寫的.sh
這個腳本是根據config文件寫的,那裏面有我們需要的參數還有註釋,所以我們要能看懂那個纔可以。一般情況出了問題我們在研究那個吧
引用https://blog.csdn.net/lincyang/article/details/51085737
- 構建libjpeg-turbo.so
cd ../libjpeg-turbo-android/libjpeg-turbo/jni
ndk-build APP_ABI=armeabi-v7a,armeabi
- 這個時候就可以得到libjpegpi.so在…/libjpeg-turbo-android/libjpeg-turbo/libs/armeabi和armeabi-v7a目錄下複製我們的libjpegpi.so到 …/bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni
cd ../bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni
ndk-build
得到 libjpegpi.so and libpijni.so
jni使用的時候一定java的類名要和jni裏面方法前面的單詞要對上
static {
System.loadLibrary("jpegpi");
System.loadLibrary("pijni");
}
所以如果不改項目的話類名必須爲com.pi.common.util.NativeUtil
5.庫函數的介紹
package net.bither.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.util.Log;
public class NativeUtil {
private static String Tag = NativeUtil.class.getSimpleName();
private static int DEFAULT_QUALITY = 95;
/**
* @Description: JNI基本壓縮
* @param bit
* bitmap對象
* @param fileName
* 指定保存目錄名
* @param optimize
* 是否採用哈弗曼表數據計算 品質相差5-10倍
* @author XiaoSai
* @date 2016年3月23日 下午6:32:49
* @version V1.0.0
*/
public static void compressBitmap(Bitmap bit, String fileName, boolean optimize) {
saveBitmap(bit, DEFAULT_QUALITY, fileName, optimize);
}
/**
* @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄
* @param image
* bitmap對象
* @param filePath
* 要保存的指定目錄
* @author XiaoSai
* @date 2016年3月23日 下午6:28:15
* @version V1.0.0
*/
public static void compressBitmap(Bitmap image, String filePath) {
// 最大圖片大小 150KB
int maxSize = 150;
// 獲取尺寸壓縮倍數
int ratio = NativeUtil.getRatioSize(image.getWidth(),image.getHeight());
// 壓縮Bitmap到對應尺寸
Bitmap result = Bitmap.createBitmap(image.getWidth() / ratio,image.getHeight() / ratio,Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, image.getWidth() / ratio, image.getHeight() / ratio);
canvas.drawBitmap(image,null,rect,null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質量壓縮方法,這裏100表示不壓縮,把壓縮後的數據存放到baos中
int options = 100;
result.compress(Bitmap.CompressFormat.JPEG, options, baos);
// 循環判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
while (baos.toByteArray().length / 1024 > maxSize) {
// 重置baos即清空baos
baos.reset();
// 每次都減少10
options -= 10;
// 這裏壓縮options%,把壓縮後的數據存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, options, baos);
}
// JNI保存圖片到SD卡 這個關鍵
NativeUtil.saveBitmap(result, options, filePath, true);
// 釋放Bitmap
if (!result.isRecycled()) {
result.recycle();
}
}
/**
* @Description: 通過JNI圖片壓縮把Bitmap保存到指定目錄
* @param curFilePath
* 當前圖片文件地址
* @param targetFilePath
* 要保存的圖片文件地址
* @author XiaoSai
* @date 2016年9月28日 下午17:43:15
* @version V1.0.0
*/
public static void compressBitmap(String curFilePath, String targetFilePath,int maxSize) {
//根據地址獲取bitmap
Bitmap result = getBitmapFromFile(curFilePath);
if(result==null){
Log.i(Tag,"result is null");
return;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質量壓縮方法,這裏100表示不壓縮,把壓縮後的數據存放到baos中
int quality = 100;
result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循環判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
while (baos.toByteArray().length / 1024 > maxSize) {
// 重置baos即清空baos
baos.reset();
// 每次都減少10
quality -= 10;
// 這裏壓縮quality,把壓縮後的數據存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
}
// JNI保存圖片到SD卡 這個關鍵
NativeUtil.saveBitmap(result, quality, targetFilePath, true);
// 釋放Bitmap
if (!result.isRecycled()) {
result.recycle();
}
}
/**
* 計算縮放比
* @param bitWidth 當前圖片寬度
* @param bitHeight 當前圖片高度
* @return int 縮放比
* @author XiaoSai
* @date 2016年3月21日 下午3:03:38
* @version V1.0.0
*/
public static int getRatioSize(int bitWidth, int bitHeight) {
// 圖片最大分辨率
int imageHeight = 1280;
int imageWidth = 960;
// 縮放比
int ratio = 1;
// 縮放比,由於是固定比例縮放,只用高或者寬其中一個數據進行計算即可
if (bitWidth > bitHeight && bitWidth > imageWidth) {
// 如果圖片寬度比高度大,以寬度爲基準
ratio = bitWidth / imageWidth;
} else if (bitWidth < bitHeight && bitHeight > imageHeight) {
// 如果圖片高度比寬度大,以高度爲基準
ratio = bitHeight / imageHeight;
}
// 最小比率爲1
if (ratio <= 0)
ratio = 1;
return ratio;
}
/**
* 通過文件路徑讀獲取Bitmap防止OOM以及解決圖片旋轉問題
* @param filePath
* @return
*/
public static Bitmap getBitmapFromFile(String filePath){
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;//只讀邊,不讀內容
BitmapFactory.decodeFile(filePath, newOpts);
int w = newOpts.outWidth;
int h = newOpts.outHeight;
// 獲取尺寸壓縮倍數
newOpts.inSampleSize = NativeUtil.getRatioSize(w,h);
newOpts.inJustDecodeBounds = false;//讀取所有內容
newOpts.inDither = false;
newOpts.inPurgeable=true;//不採用抖動解碼
newOpts.inInputShareable=true;//表示空間不夠可以被釋放,在5.0後被釋放
// newOpts.inTempStorage = new byte[32 * 1024];
Bitmap bitmap = null;
FileInputStream fs = null;
try {
fs = new FileInputStream(new File(filePath));
} catch (FileNotFoundException e) {
Log.i(Tag,"bitmap :"+e.getStackTrace());
e.printStackTrace();
}
try {
if(fs!=null){
bitmap = BitmapFactory.decodeFileDescriptor(fs.getFD(),null,newOpts);
//旋轉圖片
int photoDegree = readPictureDegree(filePath);
if(photoDegree != 0){
Matrix matrix = new Matrix();
matrix.postRotate(photoDegree);
// 創建新的圖片
bitmap = Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}else{
Log.i(Tag,"fs :null");
}
} catch (IOException e) {
e.printStackTrace();
} finally{
if(fs!=null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}
/**
*
* 讀取圖片屬性:旋轉的角度
* @param path 圖片絕對路徑
* @return degree旋轉的角度
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
/**
* 調用native方法
* @Description:函數描述
* @param bit
* @param quality
* @param fileName
* @param optimize
* @author XiaoSai
* @date 2016年3月23日 下午6:36:46
* @version V1.0.0
*/
private static void saveBitmap(Bitmap bit, int quality, String fileName, boolean optimize) {
compressBitmap(bit, bit.getWidth(), bit.getHeight(), quality, fileName.getBytes(), optimize);
}
/**
* 調用底層 bitherlibjni.c中的方法
* @Description:函數描述
* @param bit
* @param w
* @param h
* @param quality
* @param fileNameBytes
* @param optimize
* @return
* @author XiaoSai
* @date 2016年3月23日 下午6:35:53
* @version V1.0.0
*/
private static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
boolean optimize);
/**
* 加載lib下兩個so文件
*/
static {
System.loadLibrary("jpegbither");
System.loadLibrary("bitherjni");
}
}
所以我們最後的核心就是使用saveBitmap就會將圖片壓縮並且保存在sd卡上。而且我們獲取圖片的時候也對內存做了判斷,防止產生oom
https://www.jianshu.com/p/072b6defd938
https://github.com/AndroidHensen/BitmapCompress
https://blog.csdn.net/qq_25412055/article/details/53878655
https://blog.csdn.net/talkxin/article/details/50696511
https://www.cnblogs.com/mc-ksuu/p/6443254.html
https://www.jianshu.com/p/8f21d88d4439
在Android項目中如何使用libjpeg-trubo
首先你要安裝ndk,如果不知道ndk是什麼,建議你先從一些簡單的Android知識開始補這文章可能不太適合你;
第二你要安裝git(如果不會請google)
git clone git://git.linaro.org/people/tomgall/libjpeg-turbo/libjpeg-turbo.git -b linaro-android
把最新的版本克隆下來
2、編譯
克隆下來的文件夾名爲libjpeg-turbo,所以我們在使用NDK編譯前需要將文件夾命名爲JNI:
mv libjpeg-turbo jni
使用NDK編譯時,這裏需要注意的是APP_ABI這個參數,若需要在不同的平臺下運行,則需要設置平臺參數,如例所示,將編譯出兩個cpu平臺的so庫,不同的平臺用逗號分隔
ndk-build APP_ABI=armeabi-v7a,armeabi
這時就可以看到在jni同目錄下會生成libs與objs兩個文件夾,生成的.so類庫就在libs文件夾內。
====以上內容來自http://blog.csdn.net/talkxin/article/details/50696511 ========
你還需要把頭文件找齊,都在我們剛剛克隆下來的代碼目錄裏
cderror.h
cdjpeg.h
config.h
jconfig.h
jerror.h
jinclude.h
jmorecfg.h
jpeglib.h
jversion.h
好了,接下來是怎麼編寫c代碼
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "jpeg/jpeglib.h"
#import <omp.h>
#ifdef ANDROID
#include <jni.h>
#include <android/log.h>
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, " (>_<)", format, ##__VA_ARGS__)
#define LOGI(format, ...) __android_log_print(ANDROID_LOG_INFO, "(^_^)", format, ##__VA_ARGS__)
#else
#define LOGE(format, ...) printf("(>_<) " format "\n", ##__VA_ARGS__)
#define LOGI(format, ...) printf("(^_^) " format "\n", ##__VA_ARGS__)
#endif
int write_JPEG_file(const char *filename, unsigned char *yData, unsigned char *uData,
unsigned char *vData, int quality, int image_width, int image_height);
void Java_${這裏替換成類的全路徑}_writeJpegFile(JNIEnv *env, jobject jobj,
jstring fileName,
jobject yBuffer,
jint yLen,
jobject cbBuffer,
jint cbLen,
jobject crBuffer,
jint uvStride,
jint quality,
jint width, jint height) {
char *filename[500] = {0};
sprintf(filename, "%s", (*env)->GetStringUTFChars(env, fileName, NULL));
jbyte *y = (*env)->GetDirectBufferAddress(env, yBuffer);
jbyte *cb = (*env)->GetDirectBufferAddress(env, cbBuffer);
jbyte *cr = (*env)->GetDirectBufferAddress(env, crBuffer);
uint8_t *uData = malloc(cbLen);
uint8_t *vData = malloc(cbLen);
int j, k;
int uLimit = 0;
int vLimit = 0;
if (uvStride == 2) { // yuv420 sp uv交錯
#pragma omp parallel for num_threads(4)
for (j = 0; j < cbLen; j++) {
if (j % 2 == 0) {
uData[uLimit++] = cb[j];
} else {
vData[vLimit++] = cb[j];
}
}
#pragma omp parallel for num_threads(4)
for (k = 0; k < cbLen; k++) {
if (k % 2 == 0) {
uData[uLimit++] = cr[k];
} else {
vData[vLimit++] = cr[k];
}
}
write_JPEG_file(filename, y, uData, vData, quality, width, height);
} else { // yuv420p
write_JPEG_file(filename, y, cb, cr, quality, width, height);
}
free(uData);
free(vData);
}
int write_JPEG_file(const char *filename, unsigned char *yData, unsigned char *uData,
unsigned char *vData, int quality, int image_width, int image_height) {
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *outfile;
JSAMPIMAGE buffer;
unsigned char *pSrc, *pDst;
int band, i, buf_width[3], buf_height[3];
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
if ((outfile = fopen(filename, "wb")) == NULL) {
return -1;
}
jpeg_stdio_dest(&cinfo, outfile);
cinfo.image_width = image_width; // image width and height, in pixels
cinfo.image_height = image_height;
cinfo.input_components = 3; // # of color components per pixel
cinfo.in_color_space = JCS_RGB; //colorspace of input image
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
cinfo.raw_data_in = TRUE;
cinfo.jpeg_color_space = JCS_YCbCr;
cinfo.comp_info[0].h_samp_factor = 2;
cinfo.comp_info[0].v_samp_factor = 2;
jpeg_start_compress(&cinfo, TRUE);
buffer = (JSAMPIMAGE) (*cinfo.mem->alloc_small)((j_common_ptr) &cinfo,
JPOOL_IMAGE, 3 * sizeof(JSAMPARRAY));
#pragma omp parallel for num_threads(4)
for (band = 0; band < 3; band++) {
buf_width[band] = cinfo.comp_info[band].width_in_blocks * DCTSIZE;
buf_height[band] = cinfo.comp_info[band].v_samp_factor * DCTSIZE;
buffer[band] = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo,
JPOOL_IMAGE, buf_width[band], buf_height[band]);
}
unsigned char *rawData[3];
rawData[0] = yData;
rawData[1] = uData;
rawData[2] = vData;
int src_width[3], src_height[3];
#pragma omp parallel for num_threads(4)
for (i = 0; i < 3; i++) {
src_width[i] = (i == 0) ? image_width : image_width / 2;
src_height[i] = (i == 0) ? image_height : image_height / 2;
}
int max_line = cinfo.max_v_samp_factor * DCTSIZE;
int counter;
#pragma omp parallel for num_threads(4)
for (counter = 0; cinfo.next_scanline < cinfo.image_height; counter++) {
//buffer image copy.
#pragma omp parallel for num_threads(4)
for (band = 0; band < 3; band++) { //每個分量分別處理
int mem_size = src_width[band];//buf_width[band];
pDst = (unsigned char *) buffer[band][0];
pSrc = (unsigned char *) rawData[band] + counter * buf_height[band] *
src_width[band];//buf_width[band]; //yuv.data[band]分別表示YUV起始地址
#pragma omp parallel for num_threads(4)
for (i = 0; i < buf_height[band]; i++) { //處理每行數據
memcpy(pDst, pSrc, mem_size);
pSrc += src_width[band];//buf_width[band];
pDst += buf_width[band];
}
}
jpeg_write_raw_data(&cinfo, buffer, max_line);
}
jpeg_finish_compress(&cinfo);
fclose(outfile);
jpeg_destroy_compress(&cinfo);
return 0;
}
這裏面用到了openMP對for循環進行併線處理,感興趣的同學可以去google一下,這裏只簡單介紹一下怎麼用
首先 建立一個項目 吧那個C++啥的勾上 然後當前Moudlegradle配置
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.a15735.test"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
//這裏選你複製在jni 文件下的自帶的CMakeLists.txt
path "src/main/jni/CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
複製所有下載號的源碼
https://github.com/libjpeg-turbo/libjpeg-turbo 下載地址
然後就重新編譯就行了 SO庫在