png 圖片壓縮:
圖片開源庫:
優點:
- 多種圖片格式的緩存,適用於更多的內容表現形式(如Gif、WebP、縮略圖、Video)
- 生命周期集成(根據Activity或者Fragment的生命周期管理圖片加載請求)
- 高效處理Bitmap(bitmap的復用和主動回收,減少系統回收壓力)
- 高效的緩存策略,靈活(Picasso只會緩存原始尺寸的圖片,Glide緩存的是多種規格),加載速度快且內存開銷小(默認Bitmap格式的不同,使得內存開銷是Picasso的一半)
缺點:
- 沒有文件緩存
- java heap比Fresco高
和Square的網絡庫一起能發揮最大作用,因為Picasso可以選擇將網絡請求的緩存部分交給了okhttp實現。使用4.0+系統上的HTTP緩存來代替磁盤緩存.
Picasso 底層是使用OkHttp去下載圖片,所以Picasso底層網絡協議為Http.
不建議使用了;
優點:
- 最大的優勢在於5.0以下(最低2.3)的bitmap加載。在5.0以下系統,Fresco將圖片放到一個特別的內存區域(Ashmem區)
- 大大減少OOM(在更底層的Native層對OOM進行處理,圖片將不再占用App的內存)
- 適用於需要高性能加載大量圖片的場景
缺點:
- 包較大(2~3M)
- 用法復雜
- 底層涉及c++領域,閱讀源碼深入學習難度大
Fresco雖然很強大,但是包很大,依賴很多,使用復雜,而且還要在布局使用SimpleDraweeView控件加載圖片。相對而言Glide會輕好多,上手快,使用簡單,配置方便,而且從加載速度和性能方面不相上下。
1. 圖片分辨率相關
分辨率適配問題。很多情況下圖片所占的內存在整個App內存占用中會占大部分。我們知道可以通過將圖片放到hdpi/xhdpi/xxhdpi等不同文件夾進行適配,通過xml android:background設置背景圖片,或者通過BitmapFactory.decodeResource()方法,圖片實際上默認情況下是會進行縮放的。在Java層實際調用的函數都是或者通過BitmapFactory里的decodeResourceStream函數:
1 public static Bitmap decodeResourceStream(Resources res, TypedValue value, 2 InputStream is, Rect pad, Options opts) { 3 if (opts == null) { 6 opts = new Options(); 7 } 8 if (opts.inDensity == 0 && value != null) { 10 final int density = value.density; 11 if (density == TypedValue.DENSITY_DEFAULT) 12 { 13 opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; 14 } 15 else if (density != TypedValue.DENSITY_NONE) 16 { 17 opts.inDensity = density; 18 } 19 } 20 if (opts.inTargetDensity == 0 && res != null) { 22 opts.inTargetDensity = res.getDisplayMetrics().densityDpi; 23 } 24 return decodeStream(is, pad, opts); 25 }
decodeResource在解析時會對Bitmap根據當前設備屏幕像素密度densityDpi的值進行縮放適配操作,使得解析出來的Bitmap與當前設備的分辨率匹配,達到一個最佳的顯示效果,並且Bitmap的大小將比原始的大,可以參考下騰訊Bugly的詳細分析Android 開發繞不過的坑:你的 Bitmap 究竟占多大內存?
關於Density、分辨率、-hdpi等res目錄之間的關系:
DensityDpi | 分辨率 | 屏幕密度 | Density |
160dpi | 320*533 | mdpi | 1 |
240dpi | 480*800 | hdpi | 1.5 |
320dpi | 720*1280 | xhdpi | 2 |
480dpi | 1080*1920 | xxhdpi | 3 |
560dpi | 1440*2560 | xxxhdpi | 3 |
舉個例子,對於一張1280×720的圖片,如果放在xhdpi,那么xhdpi的設備拿到的大小還是1280×720而xxhpi的設備拿到的可能是1920×1080.
這兩種情況在內存里的大小分別為:3.68M和8.29M,相差4.61M,在移動設備來說這幾M的差距還是很大的。
盡管現在已經有比較先進的圖片加載組件類似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,但是有時就是需要手動拿到一個bitmap或者drawable,特別是在一些可能會頻繁調用的場景(比如ListView的getView),怎樣盡可能對bitmap進行復用呢?這里首先需要明確的是對同樣的圖片,要 盡可能復用,我們可以簡單自己用WeakReference做一個bitmap緩存池,也可以用類似圖片加載庫寫一個通用的bitmap緩存池,可以參考GlideBitmapPool的實現。
我們也來看看系統是怎么做的,對於類似在xml里面直接通過android:background或者android:src設置的背景圖片,以ImageView為例,最終會調用Resource.java里的loadDrawable:
1 Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException 2 { 3 // Next, check preloaded drawables. These may contain unresolved theme 4 // attributes. 5 final ConstantState cs; 6 if (isColorDrawable) 7 { 8 cs = sPreloadedColorDrawables.get(key); 9 }else{ 10 cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); 11 } 12 13 Drawable dr; 14 if (cs != null) { 15 dr = cs.newDrawable(this); 16 } else if (isColorDrawable) { 17 dr = new ColorDrawable(value.data); 18 } else { 19 dr = loadDrawableForCookie(value, id, null); 20 } 21 22 ... 23 24 return dr; 25 }
可以看到實際上系統也是有一份全局的緩存,sPreloadedDrawables, 對於不同的drawable,如果圖片時一樣的,那么最終只會有一份bitmap(享元模式),存放於BitmapState中,獲取drawable時,系統會從緩存中取出這個bitmap然后構造drawable。而通過BitmapFactory.decodeResource()則每次都會重新解碼返回bitmap。所以其實我們可以通過context.getResources().getDrawable再從drawable里獲取bitmap,從而復用bitmap.
然而這里也有一些坑,比如我們獲取到的這份bitmap,假如我們執行了recycle之類的操作,但是假如在其他地方再使用它是那么就會有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”異常。
2. 圖片壓縮
BitmapFactory 在解碼圖片時,可以帶一個Options,有一些比較有用的功能,比如:
-
inTargetDensity 表示要被畫出來時的目標像素密度
-
inSampleSize 這個值是一個int,當它小於1的時候,將會被當做1處理,如果大於1,那么就會按照比例(1 / inSampleSize)縮小bitmap的寬和高、降低分辨率,大於1時這個值將會被處置為2的倍數。例如,width=100,height=100,inSampleSize=2,那么就會將bitmap處理為,width=50,height=50,寬高降為1 / 2,像素數降為1 / 4
-
inJustDecodeBounds 字面意思就可以理解就是只解析圖片的邊界,有時如果只是為了獲取圖片的大小就可以用這個,而不必直接加載整張圖片。
-
inPreferredConfig 默認會使用ARGB_8888,在這個模式下一個像素點將會占用4個byte,而對一些沒有透明度要求或者圖片質量要求不高的圖片,可以使用RGB_565,一個像素只會占用2個byte,一下可以省下50%內存。
-
inPurgeable和inInputShareable 這兩個需要一起使用,BitmapFactory.java的源碼里面有注釋,大致意思是表示在系統內存不足時是否可以回收這個bitmap,有點類似軟引用,但是實際在5.0以后這兩個屬性已經被忽略,因為系統認為回收后再解碼實際會反而可能導致性能問題
-
inBitmap 官方推薦使用的參數,表示重復利用圖片內存,減少內存分配,在4.4以前只有相同大小的圖片內存區域可以復用,4.4以后只要原有的圖片比將要解碼的圖片大既可以復用了。
2.1 質量壓縮
(1)原理:保持像素的前提下改變圖片的位深及透明度,(即:通過算法摳掉(同化)了圖片中的一些某個些點附近相近的像素),達到降低質量壓縮文件大小的目的。
注意:它其實只能實現對file的影響,對加載這個圖片出來的bitmap內存是無法節省的,還是那么大。因為bitmap在內存中的大小是按照像素計算的,也就是width*height,對於質量壓縮,並不會改變圖片的真實的像素(像素大小不會變)。
(2)使用場景:將圖片壓縮后將圖片上傳到服務器,或者保存到本地。根據實際需求來。
(3)源碼示例
1 /** 2 * 質量壓縮: 3 * 設置bitmap options屬性,降低圖片的質量,像素不會減少 4 * 第一個參數為需要壓縮的bitmap圖片對象,第二個參數為壓縮后圖片保存的位置 5 * 設置options 屬性0-100,來實現壓縮 6 * 7 * @param bmp 8 * @param file 9 */ 10 public static void qualityCompress(Bitmap bmp, File file) { 11 // 0-100 100為不壓縮 12 int quality = 20; 13 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 14 // 把壓縮后的數據存放到baos中 15 bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos); 16 try { 17 FileOutputStream fos = new FileOutputStream(file); 18 fos.write(baos.toByteArray()); 19 fos.flush(); 20 fos.close(); 21 } catch (Exception e) { 22 e.printStackTrace(); 23 } 24 }
2.2 尺寸壓縮
(1)原理:通過減少單位尺寸的像素值,正真意義上的降低像素。1020*8880–
(2)使用場景:緩存縮略圖的時候(頭像處理)
(3)源碼示例
1 /** 2 * 尺寸壓縮:(通過縮放圖片像素來減少圖片占用內存大小) 3 * 4 * @param bmp 5 * @param file 6 */ 7 8 public static void sizeCompress(Bitmap bmp, File file) { 9 // 尺寸壓縮倍數,值越大,圖片尺寸越小 10 int ratio = 8; 11 // 壓縮Bitmap到對應尺寸 12 Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888); 13 Canvas canvas = new Canvas(result); 14 Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio); 15 canvas.drawBitmap(bmp, null, rect, null); 16 17 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 18 // 把壓縮后的數據存放到baos中 19 result.compress(Bitmap.CompressFormat.JPEG, 100, baos); 20 try { 21 FileOutputStream fos = new FileOutputStream(file); 22 fos.write(baos.toByteArray()); 23 fos.flush(); 24 fos.close(); 25 } catch (Exception e) { 26 e.printStackTrace(); 27 } 28 }
2.3 采樣率壓縮
(1)原理:設置圖片的采樣率,降低圖片像素
(2) 好處:是不會先將大圖片讀入內存,大大減少了內存的使用,也不必考慮將大圖片讀入內存后的釋放事宜。
(3)問題:因為采樣率是整數,所以不能很好的保證圖片的質量。如我們需要的是在2和3采樣率之間,用2的話圖片就大了一點,但是用3的話圖片質量就會有很明顯的下降,這樣也無法完全滿足我的需要。
(4)源碼示例
1 /** 2 * 采樣率壓縮(設置圖片的采樣率,降低圖片像素) 3 * 4 * @param filePath 5 * @param file 6 */ 7 public static void samplingRateCompress(String filePath, File file) { 8 // 數值越高,圖片像素越低 9 int inSampleSize = 8; 10 BitmapFactory.Options options = new BitmapFactory.Options(); 11 options.inJustDecodeBounds = false; 12 // options.inJustDecodeBounds = true;//為true的時候不會真正加載圖片,而是得到圖片的寬高信息。 13 //采樣率 14 options.inSampleSize = inSampleSize; 15 Bitmap bitmap = BitmapFactory.decodeFile(filePath, options); 16 17 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 18 // 把壓縮后的數據存放到baos中 19 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); 20 try { 21 if (file.exists()) { 22 file.delete(); 23 } else { 24 file.createNewFile(); 25 } 26 FileOutputStream fos = new FileOutputStream(file); 27 fos.write(baos.toByteArray()); 28 fos.flush(); 29 fos.close(); 30 } catch (Exception e) { 31 e.printStackTrace(); 32 } 33 }
3. 緩存池大小
現在很多圖片加載組件都不僅僅是使用軟引用或者弱引用了,實際上類似Glide 默認使用的事LruCache,因為軟引用 弱引用都比較難以控制,使用LruCache可以實現比較精細的控制,而默認緩存池設置太大了會導致浪費內存,設置小了又會導致圖片經常被回收,所以需要根據每個App的情況,以及設備的分辨率,內存計算出一個比較合理的初始值,可以參考Glide的做法。
4 想辦法減少 Bitmap 內存占用:
4.1 Jpg 和 Png
jpg 是一種有損壓縮的圖片存儲格式,而 png 則是 無損壓縮的圖片存儲格式,顯而易見,jpg 會比 png 小.
Bitmap 在內存當中占用的大小其實取決於:
-
色彩格式,前面我們已經提到,如果是 ARGB8888 那么就是一個像素4個字節,如果是 RGB565 那就是2個字節
-
原始文件存放的資源目錄(是 hdpi 還是 xxhdpi 可不能傻傻分不清楚哈)
-
目標屏幕的密度(所以同等條件下,紅米在資源方面消耗的內存肯定是要小於三星S6的)
如果僅僅是為了 Bitmap 讀到內存中的大小而考慮的話,jpg 也好 png 也好,沒有什么實質的差別;二者的差別主要體現在:
-
alpha 你是否真的需要?如果需要 alpha 通道,那么沒有別的選擇,用 png。
-
你的圖色值豐富還是單調?就像剛才提到的,如果色值豐富,那么用jpg,如果作為按鈕的背景,請用 png。
-
對安裝包大小的要求是否非常嚴格?如果你的 app 資源很少,安裝包大小問題不是很凸顯,看情況選擇 jpg 或者 png(不過,我想現在對資源文件沒有苛求的應用會很少吧。。)
-
目標用戶的 cpu 是否強勁?jpg 的圖像壓縮算法比 png 耗時。這方面還是要酌情選擇,前幾年做了一段時間 Cocos2dx,由於資源非常多,項目組要求統一使用 png,可能就是出於這方面的考慮。
4.2 使用 inSampleSize (采樣率壓縮)
這個方法主要用在圖片資源本身較大,或者適當地采樣並不會影響視覺效果的條件下,這時候我們輸出地目標可能相對較小,對圖片分辨率、大小要求不是非常的嚴格。
既然圖片最終是要被模糊的,也看不太情況,還不如直接用一張采樣后的圖片,如果采樣率為 2,那么讀出來的圖片只有原始圖片的 1/4 大小:
1 BitmapFactory.Options options = new Options(); 2 options.inSampleSize = 2; 3 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
4.3 使用矩陣
大圖小用用采樣,小圖大用用矩陣。
還是用前面模糊圖片的例子,我們不是采樣了么?內存是小了,可是圖的尺寸也小了啊,我要用 Canvas 繪制這張圖可怎么辦?當然是用矩陣了:
方式一:
1 Matrix matrix = new Matrix(); 2 matrix.preScale(2, 2, 0f, 0f); 3 //如果使用直接替換矩陣的話,在Nexus6 5.1.1上必須關閉硬件加速 4 canvas.concat(matrix); 5 canvas.drawBitmap(bitmap, 0,0, paint);
需要注意的是,在使用搭載 5.1.1 原生系統的 Nexus6 進行測試時發現,如果使用 Canvas 的 setMatrix 方法,可能會導致與矩陣相關的元素的繪制存在問題,本例當中如果使用 setMatrix 方法,bitmap 將不會出現在屏幕上。因此請盡量使用 canvas 的 scale、rotate 這樣的方法,或者使用 concat 方法。
方式二:
1 Matrix matrix = new Matrix(); 2 matrix.preScale(2, 2, 0, 0); 3 canvas.drawBitmap(bitmap, matrix, paint);
這樣,繪制出來的圖就是放大以后的效果了,不過占用的內存卻仍然是我們采樣出來的大小。
如果我要把圖片放到 ImageView 當中呢?一樣可以,請看:
1 Matrix matrix = new Matrix(); 2 matrix.postScale(2, 2, 0, 0); 3 imageView.setImageMatrix(matrix); 4 imageView.setScaleType(ScaleType.MATRIX); 5 imageView.setImageBitmap(bitmap);
4.4 合理選擇Bitmap的像素格式
其實前面我們已經多次提到這個問題。
ARGB8888格式的圖片,每像素占用 4 Byte,而 RGB565則是 2 Byte。我們先看下有多少種格式可選:
格式 | 描述 |
ALPHA_8 | 只有一個alpha通道 |
ARGB_4444 | 這個從API 13開始不建議使用,因為質量太差 |
ARGB_8888 | ARGB四個通道,每個通道8bit |
RGB_565 | 每個像素占2Byte,其中紅色占5bit,綠色占6bit,藍色占5bit |
這幾個當中,
ALPHA8 沒必要用,因為我們隨便用個顏色就可以搞定的。
ARGB4444 雖然占用內存只有 ARGB8888 的一半,不過已經被官方嫌棄,失寵了。。『又要占省內存,又要看着爽,臣妾做不到啊T T』。
ARGB8888 是最常用的,大家應該最熟悉了。
RGB565 看到這個,我就看到了資源優化配置無處不在,這個綠色。。(不行了,突然好邪惡XD),其實如果不需要 alpha 通道,特別是資源本身為 jpg 格式的情況下,用這個格式比較理想。