Android性能優化-圖片優化

很久之前寫的了,發了吧,原用來總結學習的,再不發估計轉行了,文章也參考了一些資料,摳用了一些圖,主要是爲了說明問題,總結學習

前言

app開發中,圖片是少不了的。各種圖標圖片資源,如果不能很好的處理圖片的利用。會導致app性能嚴重下降,影響用戶體驗,最直觀的感受就是卡頓,手機發熱,有時候還OOM

android系統給每個app分配有一定的內存,android系統的進程(app級別)有最大內存限制,超過這個限制系統就會拋出OOM錯誤。雖然在4.0後可以通過在application節點中設置屬性android:largeHeap=”true”來突破這個上限,但是由於圖片處理不當帶來的影響總是影響app性能的

這裏引申一下關於android系統給app分配的內存:

ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int memorySize = activityManager.getMemoryClass();

Android根據設備屏幕尺寸和dpi的不同,給系統分配的單應用程序內存大小也不同,可以實踐一下通過上面的方法獲取應用的內存,跟下表對比下:

屏幕尺寸 DPI 應用內存
small / normal / large ldpi / mdpi 16MB
small / normal / large tvdpi / hdpi 32MB
small / normal / large xhdpi 64MB
small / normal / large 400dpi 96MB
small / normal / large xxhdpi 128MB
xlarge mdpi 32MB
xlarge tvdpi / hdpi 64MB
xlarge xhdpi 128MB
xlarge 400dpi 192MB
xlarge xxhdpi 256MB

拋磚

備案學習文章

oom產生原因

  • 一個頁面一次加載過多圖片

  • 加載大圖片沒有進行壓縮(尺寸,質量)。。直接使用了imageView.setImageResource()

  • android列表加載大量bitmap沒有使用緩存。。。

android 支持的圖片

  • png:

無損壓縮,比較大,需要進行壓縮,網站tinypng,一般都是讓美工處理。但解碼相對簡單

  • jpeg:

有損壓縮,不支持透明通道,比如在ps裏背景透明的圖片,保持成jpg就不透明瞭,這裏不深入瞭解。但是解碼相對複雜

  • webp:

google2010發佈,支持有損無損壓縮,支持透明通道,所以對圖片質量和大小有限制的情況下,webp是首選

  • gif:

系統本身不支持,三方圖片庫支持:glide,fresco

關於android中圖片格式的使用,谷歌官方建議:儘量少使用png文件,建議使用webp格式的圖片,相比png小45%。所以,項目中圖片格式該如何平衡,這個還需要美工結合技術需求拿捏,結合每種格式圖片的優缺點,合理規劃開發。既然谷歌建議了,那大概是考慮到png佔內存大導致的,app開闢的運行內存是一定的,當然內存開銷越小,app越流暢嘛

圖片存儲優化

圖片佔用內存計算

備案學習文章

這裏的圖片佔用內存是指在Navtive中佔用的內存,當然BitMap使用的絕大多數內存就是該內存。
因此我們可以簡單的認爲它就是BitMap所佔用的內存

Android中一張圖片(BitMap)佔用的內存主要和以下幾個因數有關:圖片長度,圖片寬度,單位像素佔用的字節數,圖片長度和圖片寬度的單位是像素。所以有如下計算公式:

內存 = 圖片長度 * 圖片寬度 * 單位像素佔用的字節數

這裏注意一下,圖片(BitMap)佔用的內存應該和屏幕密度(Density)無關,創建一個BitMap時,其單位像素佔用的字節數由其參數BitmapFactory.Options的inPreferredConfig變量決定。inPreferredConfig爲Bitmap.Config類型。Bitmap.Config類是個枚舉類型,如下:

Bitmap.Config description
ALPHA_8 Each pixel is stored as a single translucency (alpha) channel. This is very useful to efficiently store masks for instance. No color information is stored. With this configuration, each pixel requires 1 byte of memory.</br>此時圖片只有alpha值,沒有RGB值,一個像素佔用一個字節
ARGB_4444 This field is deprecated. Because of the poor quality of this configuration, it is advised to use ARGB_8888instead. </br>這種格式的圖片,看起來質量太差,已經不推薦使用。</br>Each pixel is stored on 2 bytes. The three RGB color channels and the alpha channel (translucency) are stored with a 4 bits precision (16 possible values.) This configuration is mostly useful if the application needs to store translucency information but also needs to save memory. It is recommended to use ARGB_8888 instead of this configuration.</br>一個像素佔用2個字節,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各佔4個bites,共16bites,即2個字節
ARGB_8888 Each pixel is stored on 4 bytes. Each channel (RGB and alpha for translucency) is stored with 8 bits of precision (256 possible values.) This configuration is very flexible and offers the best quality. It should be used whenever possible </br>一個像素佔用4個字節,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各佔8個bites,共32bites,即4個字節 </br>這是一種高質量的圖片格式,電腦上普通採用的格式。它也是Android手機上一個BitMap的默認格式。
RGB_565 Each pixel is stored on 2 bytes and only the RGB channels are encoded: red is stored with 5 bits of precision (32 possible values), green is stored with 6 bits of precision (64 possible values) and blue is stored with 5 bits of precision. This configuration can produce slight visual artifacts depending on the configuration of the source. For instance, without dithering, the result might show a greenish tint. To get better results dithering should be applied. This configuration may be useful when using opaque bitmaps that do not require high color fidelity.</br> 一個像素佔用2個字節,沒有alpha(A)值,即不支持透明和半透明,Red(R)值佔5個bites ,Green(G)值佔6個bites ,Blue(B)值佔5個bites,共16bites,即2個字節.對於沒有透明和半透明顏色的圖片來說,該格式的圖片能夠達到比較的呈現效果,相對於ARGB_8888來說也能減少一半的內存開銷。因此它是一個不錯的選擇。另外我們通過android.content.res.Resources來取得一個張圖片時,它也是以該格式來構建BitMap的.

舉個例子,圖片大小的計算:

圖片格式 公式 一張100 * 100的圖片佔用內存大小
ALPHA_8 圖片長度 * 圖片寬度 100 * 100=10000字節
ARGB_4444 圖片長度 * 圖片寬度 * 2 100 * 100 * 2 = 20000字節
ARGB_8888 圖片長度 * 圖片寬度 * 4 100 * 100 * 4 = 40000字節
RGB_565 圖片長度 * 圖片寬度 * 2 100 * 100 * 2 = 20000字節

注意: ARGB _ 4444 從Android4.0開始,該選項無效。即使設置爲該值,系統任然會採用 ARGB _ 8888 來構造圖片,系統在把res的圖片解析成bitmap時默認是採用ARGB_8888的配置,如下源碼

        if (config != null) {
            switch (config) {
                case RGB_565:
                    newConfig = Config.RGB_565;
                    break;
                case ALPHA_8:
                    newConfig = Config.ALPHA_8;
                    break;
                case RGBA_F16:
                    newConfig = Config.RGBA_F16;
                    break;
                //noinspection deprecation
                case ARGB_4444:
                case ARGB_8888:
                default:
                    newConfig = Config.ARGB_8888;
                    break;
            }
        }

圖片解碼格式枚舉下面在講解圖片存儲優化-質量壓縮時候會給出例子

圖片存儲優化

備案學習文章

備案學習文章

上面瞭解了儘量減少PNG圖片的大小是Android裏面很重要的一條規範,下面我們需要了解一下爲什麼要做內存的優化,如何做:

Android的Heap空間是不會自動做兼容壓縮的,意思就是如果Heap空間中的圖片被收回之後,這塊區域並不會和其他已經回收過的區域做重新排序合併處理,那麼當一個更大的圖片需要放到heap之前,很可能找不到那麼大的連續空閒區域,那麼就會觸發GC,使得heap騰出一塊足以放下這張圖片的空閒區域,如果無法騰出,就會發生OOM

所以,把圖片做小,圖片內存重用這是眼前的解決方案,我們可以從三個方面來降低圖片內存開銷:

尺寸壓縮

關於圖片縮放,有幾種方法:

1:Pre-scaling Bitmaps

2:inSampleSize

第一種,android中經常會做圖片的縮放,所以,預縮放意義很明顯,能縮小圖片(這裏不單單是縮放圖片尺寸,而是操作的bitmap),降低內存分配,提升顯示性能,api爲createScaledBitmap()。如下:

    /**
     * bitmap指定寬高
     * @param bitmap
     * @param width
     * @param height
     * @return
     */
    public static Bitmap resizeBitmap(Bitmap bitmap, int width, int height) {
        return Bitmap.createScaledBitmap(bitmap, width, height, true);
    }

第二種是inSampleSize,作用是對原圖降採樣,通過設置inJustDecodeBounds = true 在圖片不加載進內存的情況下能獲取圖片寬高,計算合適的壓縮比,設置inSampleSize。
inSampleSize具體原理是直接從點陣中隔行抽取最有效率,所以爲了兼顧效率, inSampleSize只能是2的整數次冪,如果不是的話,向下取得最大的2的整數次冪.
比如你將 inSampleSize 賦值爲3,系統實際使用的縮放比率爲2,那就是每隔2行採1行,每隔2列採一列,那你解析出的圖片就是原圖大小的1/4.
這個值也可以填寫非2的倍數,非2的倍數會被四捨五入.
綜上,用這個參數解析bitmap就是爲了減少內存佔用

Q: inSampleSize取值多少最佳?

A: inSampleSize優化 這裏提供了具體的計算算法

總體來講:就是以圖片寬高較大的一邊爲參考邊進行壓縮,目的還是爲了避免壓縮過大導致圖片失真嚴重,效果不好。。

注意:inSampleSize官方解釋爲必須是2的整數次冪,如果不是,也會向減小方向尋找最近的2的整數次冪的數。但是,經過測試,並不是這樣,貌似有些是2的整數倍就行,比如6,24等。官方計算解釋是這樣的:

        // Calculate the largest inSampleSize value that is a power of 2 and
        // keeps both
        // height and width larger than the requested height and width.
        
計算最大的inSampleSize值,它是2和的冪,並且仍保持高度和寬度大於要求的高度和寬度

但是inSampleSize的一些規則,可能會有這樣的場景,700的圖片壓縮到600.。結果是不會壓縮的,因爲計算的inSampleSize還是1,其實解碼圖片時候,影響大小的不止inSampleSize,還有其他一些參數:inDensity、inTargetDensity和inScreenDensity(備案學習文章

inScaled:將它設置爲true,那麼代表這張圖片可以縮放

inDensity:圖片的原來密度,默認一般爲160.

inTargetDensity:圖片的目標密度,圖片操作之後的密度

inScreenDensity:這個參數默認一直是0,源碼中對這個參數沒有賦值的地方,只有一處使用的地方

因爲項目用的圖片一般都放在drawable文件夾中,所以,Options中的inDensity屬性會根據drawable文件夾的分辨率來賦值,inTartgetDensity會根據屏幕的像素密度來賦值

對應關係如下:

設備dpi 密度類型
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi
drawable類型 分辨率
ldpi 120
mdpi 160
hdpi 240
xhdpi 320
xxhdpi 480
xxxhdpi 640

輸出圖片寬高的公式如下:

輸出圖片的寬高= 原圖片的寬高 / inSampleSize * (inTargetDensity / inDensity)

注意:

1:上面計算公式僅針對於drawable文件夾的圖片來說,而對於一個file或者stream那麼inDensity和inTargetDensity是不考慮的!他們默認就是0

2:對於drawable中的圖片,inDensity是有默認值的,上面對應關係能看出來

3:inTargetDensity是跟屏幕密度有關的,這是屏幕參數,是常量,所以,可以通過以下方式獲得。

        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        Log.i("MainActivity", "onCreate: " + metrics.densityDpi);

        DisplayMetrics windowm = getApplicationContext().getResources().getDisplayMetrics();
        Log.i("MainActivity", "onCreate: " + windowm.densityDpi);

繼續看,下面有具體的例子演示如何使用:

質量壓縮

質量壓縮就是解碼率壓縮,常見格式的圖片在設置到ui上之前需要經過解碼過程

Q:如何從解碼方面降低圖片內存佔用

A:使用RGB_565代替ARGB_8888可以降低圖片內存佔用

1:因爲它可以降低一個像素佔用的內存,RGB_565一個像素佔2個字節,ARGB_8888一個像素佔4個字節。通過設置options.inPreferredConfig = Bitmap.Config.RGB_565來處理

    private void testPicOptimize(ImageView imageView, int size) {
        String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
        String filePath = sdcard + "/xxx.jpg";

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filePath, options);

        int width = options.outWidth;
        options.inSampleSize = width / 200;
        options.inScaled = true;
        int calsize=options.outHeight>options.outWidth?options.outWidth:options.outHeight;
        options.inTargetDensity =(size*options.inDensity)/(calsize/options.inSampleSize);
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
        imageView.setImageBitmap(bitmap);
    }

設置inJustDecodeBounds=true,解析圖片,在不加載進內存的情況下獲取圖片寬高,然後進行設置尺寸壓縮、解碼格式,然後在inJustDecodeBounds=false,重新加載圖片到內存中,再講圖片設置到ui上

  • 內存重用

android 3.0以後,BitmapFactory.Options提供了一個參數options.inBitmap。如果你使用了這個屬性,那麼使用這個屬性的decode過程中 會直接參考 inBitmap 所引用的那塊內存,,大家都知道 很多時候ui卡頓是因爲gc操作過多而造成的。使用這個屬性 能避免大內存塊的申請和釋放。帶來的好處就是gc 操作的數量減少。這樣cpu會有更多的時間 做ui線程,界面會流暢很多,同時還能節省大量內存!

    private void testInBitmap(ImageView imageView) {
        String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
        String filePath1 = sdcard + "/xxx.jpg";

        BitmapFactory.Options options = new BitmapFactory.Options();
        //size必須爲1 否則是使用inBitmap屬性會報異常
        options.inSampleSize = 1;
        //這個屬性一定要在用在src Bitmap decode的時候 不然你再使用哪個inBitmap屬性去decode時候會在c++層面報異常
        //BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
        //一定要設置爲true 這樣返回的bitmap 纔是mutable 也就是可重用的,否則是不能重用的
        options.inMutable = true;
        Bitmap bitmap1 = BitmapFactory.decodeFile(filePath1, options);
        
        //設置複用內存,加載bitmap1已經開闢過內存,所以後續設置了options.inBitmap的圖片加載會首先嚐試利用bitmap1所指向的內存
        options.inBitmap = bitmap1;
        String filePath2 = sdcard + "/xxx2.jpg";
        //這時候bitmap2的內存是bitmap1的內存
        Bitmap bitmap2 = BitmapFactory.decodeFile(filePath2, options);
        imageView.setImageBitmap(bitmap2);
    }

如上例子,實現了第二張圖複用了第一張圖的內存。。有個條件,複用內存的圖片要小於被複用的內存的大小,不然複用不了

使用options.inBitmap需要注意幾點:

1:在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小爲100-100,那麼新申請的bitmap必須也爲100-100才能夠被重用

2:從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小

3:新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那麼就不能支持4444與565格式的bitmap了,不同的編碼格式佔用的內存是不同的,有時候也可以根據需求指定編碼格式

上面的注意點很大程度上限制了我們使用內存重用的靈活性,難道app中的圖片都要一樣?解碼格式也要一樣?需求不同可能圖片處理就不同,所以,怎們充分的利用options.inBitmap纔是真正提升app內存性能的關鍵:

有一種思路:就是inBitmap池,也就是說管理一個包含多種典型可重用的bitmap集合。這樣,就很大程度的提升了bitmap內存重用概率

這種方案,現流行框架glide就是這麼處理的,當然,細節更深

Bitmap內存管理

Bitmap 對象在不使用時,我們應該先調用recycle()釋放內存,然後才置空,因爲加載bitmap對象的內存空間,一部分是java的,一部分是c的(因爲Bitmap分配的底層是通過jni調用的,BitMap底層是skia圖形庫,skia圖形庫是c實現的,通過jni的方法在java層進行封裝)。這個recycle()函數就是針對c部分的內存釋放

Q:bitmap的存儲在3.0前後有什麼改變?api的調用有什麼變化?

A:在android 3.0之前,像素數據支持保存在本地內存中的。而位圖bitmap本身是存儲在Dalvik堆中的,bitmap數據操作完之後,需要調用bitmap.recycle去釋放這些像素數據。3.0之後,像素數據和位圖都是存儲在Dalvik堆中的,所以bitmap對象是會自動回收的

通過dumpsys meminfo命令可以查看一個進程的內存使用情況,
當然也可以通過它來觀察我們創建或銷燬一張BitMap圖片內存的變化,從而推斷出圖片佔用內存的大小

adb shell "dumpsys meminfo com.lenovo.robin"

圖片加載優化

mipmap

備案學習文章

  • 在App中,無論你將圖片放在drawable還是mipmap目錄,系統只會加載對應density中的圖片。
    而在Launcher中,如果使用mipmap,那麼Launcher會自動加載更加合適的密度的資源。
  • 應用內使用到的圖片資源,並不會因爲你放在mipmap或者drawable目錄而產生差異。單純只是資源路徑的差異R.drawable.xxx或者R.mipmap.xxx。(也可能在低版本系統中有差異)
  • 一句話來說就是,自動跨設備密度展示的能力是launcher的,而不是mipmap的。

總的來說,app圖標(launcher icon) 必須放在mipmap目錄中,並且最好準備不同密度的圖片,否則縮放後可能導致失真。
而應用內使用到的圖片資源,放在drawable目錄亦或是mipmap目錄中是沒有區別的,該準備多個密度的還是要準備多個密度,如果只想使用一份切圖,那儘量將切圖放在高密度的文件夾中

內存佔用與drawable文件夾關係

如標題換個說法,同一張圖片,放置在不同的drawable文件夾,在同一設備上運行,對圖片大小及內存佔用有什麼影響,下面一步步瞭解

手機屏幕密度對應屏幕密度類型

  • 獲取手機屏幕密度:百度吧

工具..這個算出來的,爲啥跟代碼算出來的不一樣?

  • 設備屏幕密度
設備dpi 密度類型
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi

從上面的屏幕密度匹配類型能看出,假如你的手機dpi是260,那麼你的手機屏幕密度類型就是xhdpi,加載圖片,系統首先會去drawable-xhdpi目錄下查找,其他查找規則請繼續往下看

圖片大小以及dp和px關係一覽表

假設,在mdpi屏幕密度的手機上,你將一張60px乘60px的圖片放到mdpi中,它的大小是60乘60;若把它拿到hdpi中,那麼它的大小應該是45 乘 45,圖片縮小,因爲系統認爲這些圖片都是給高分辨率設備使用的.(由上表可以算出60*3/4)

加載順序

APP在查找圖片資源的時候遵循先高後低的原則,假設設備的分辨率是xxhdpi,那麼查找順序如下

  • 先去drawable-xxhdpi文件夾查找,如果有這張圖片就使用,這個時候圖片不會縮放
  • 如果沒有找到,則去更高密度的文件夾下找,例如drawable-xxxhdpi,密度依次遞增,如果找到了,圖片將會縮小,因爲系統認爲這些圖片都是給高分辨率設備使用的
  • 所有高密度文件夾都沒有的話,就會去drawable-nodpi文件夾去找,如果找到,不縮放,使用原圖
  • 還是沒有的話,就會去更低密度的文件夾下面找,xhdpi,hdpi等,密度依次遞減,如果找到了,圖片將會放大,因爲系統認爲這個圖片是給低分辨率設備使用的

總的來說,系統的規則也是優先向減小app運行內存的方向查找處理資源的,因爲找更高密度drawable下的圖片,加載爲bitmap是要縮小的

圖片加載

圖片從res中加載到內存都是以圖片的原始寬高比進行加載的,比如上文中博主採用的圖片是7201280,錘子T1的分辨率是 10801960,
把圖片放在drawable-xhdpi文件夾下,圖片的大小爲10801920,而不是充滿屏幕高度的1960。因爲圖片加載時首先滿足的是寬度,比如把720
放大到1080,此時保持圖片的寬高比不變,高度應該是等比例放大,h = 1280
1080/720。

圖片加載優化

常用方案有4個方向:

  • 異步請求:圖片在線程或後臺請求
  • 圖片緩存:列表中的圖片進行緩存
  • 網絡請求:使用OKHttp
  • 懶加載:當圖片呈現到可視區域再進行加載

android 大圖片加載方案

BitmapRegionDecoder。這個是api 10時候google提供打開大小超過屏幕的圖片的方案。這裏不多說了。查了一些資料,用法簡單,文章幾乎都一毛一樣,大家自行學習瞭解

說一點,由於是打開的超大圖片,所以,解決方案中就用到了前面所說的inJustDecodeBounds。通過設置inJustDecodeBounds = true 在圖片不加載進內存的情況下能獲取圖片寬高

框架優化圖片加載

常見框架:Universal image loader,picasso,glide,fresco等,待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章