Android 圖片優化


png 圖片壓縮:

Android 美工工具,以及圖片壓縮

Android icon 分辨率

圖片開源庫:

Glide

優點:

  • 多種圖片格式的緩存,適用於更多的內容表現形式(如Gif、WebP、縮略圖、Video)
  • 生命周期集成(根據Activity或者Fragment的生命周期管理圖片加載請求)
  • 高效處理Bitmap(bitmap的復用和主動回收,減少系統回收壓力)
  • 高效的緩存策略,靈活(Picasso只會緩存原始尺寸的圖片,Glide緩存的是多種規格),加載速度快且內存開銷小(默認Bitmap格式的不同,使得內存開銷是Picasso的一半)

缺點:

  • 沒有文件緩存 
  • java heap比Fresco高

Picasso

和Square的網絡庫一起能發揮最大作用,因為Picasso可以選擇將網絡請求的緩存部分交給了okhttp實現。使用4.0+系統上的HTTP緩存來代替磁盤緩存.
Picasso 底層是使用OkHttp去下載圖片,所以Picasso底層網絡協議為Http.

不建議使用了;

Fresco

優點:

  • 最大的優勢在於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. 圖片壓縮

Android 大位圖加載

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%內存。

  • inPurgeableinInputShareable 這兩個需要一起使用,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. 緩存池大小

Android LruCache(Picasso內存緩存)

 現在很多圖片加載組件都不僅僅是使用軟引用或者弱引用了,實際上類似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 格式的情況下,用這個格式比較理想。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM