Andorid性能優化(一) 之 如何給App進行內存優化

1 前言

Android系統爲每個應用進程都分配一個有封頂的堆內存值,當應用內存佔用過高到沒有足夠的內存來提供給新對象分配並且垃圾回收機制也已經沒有空間可回收時就會OOM。當一個應用內存佔用過高會使一些性能差的手機系統內存緊缺,使得整體系統卡頓。而且應用內存佔用過高後,一旦退到後臺後,就會容易被系統殺死,這點我們在前面《Android進程回收機制和保活方案》中有介紹過,這時一旦你需要進行一些後臺工作時就會很被動。除此以外,若比起競品中內存使用佔用過高就會處於劣勢,也會容易引起用戶反感,從而棄之。今天這篇文章我們就來看看導致應用內存佔用過高的情況和解決辦法。

2 相關概念

在Android中進程的內存佔用按從大到小可以分爲:VSS >= RSS >= PSS >= USS。可以通過adb命令:adb shell procrank來查看系統中所應用的VSS、RSS、PSS 和USS的值情況,如下圖。

VSS(Virtual Set Size)表示進程總共可訪問的內存大小。它包括了分配但尚未使用的虛擬內存所有共享庫所佔用內存進程本身佔用內存

RSSResident Set Size表示進程實際使用的物理內存大小。它包括了所有共享庫所佔用內存假如有3個進程使用同一個共享庫佔用了30M內存,這裏的值是30M) 和 進程本身佔用內存

PSSProportional Set Size表示進程實際使用的物理內存大小。它包括了按進程比例平分的共享庫所佔用的內存假如有3個進程使用同一個共享庫佔用了30M內存,這裏的值是10M) 和 進程本身佔用內存

USSUnique Set Size表示進程獨自佔用的物理內存大小。它僅包括進程本身佔用內存

一般情況下,反映進程內存佔用情況我們會選擇查看PSS的值。在系統中應用管理或第三方工具中,要查看一個進程的內存使用情況都是用PSS來表示的。也可以通過adb命令:adb shell dumpsys meminfo XXX (XXX表示進程名)來查看進程的PSS值和組成部分,如下圖。

要想知道一臺手機給一個進程實際上能分配到多大的堆內存,可以使用代碼來查看:

ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int memory = am.getMemoryClass();
int largeMemory = am.getLargeMemoryClass();

其中,getMemoryClass()方法返回的是標準的內存封頂值;而getLargeMemoryClass()方法返回的是最大的內存封頂值,要想你的App內存封頂值能使用到最大,可以在AndroidManifest中的Application中添加 android:largeHeap=“true”。例如筆者手上的一臺小米6,手機本身是6GB的內存,而使用上面的代碼後輸出的結果是:256M和512M。

 

3 內存優化方案

3.1 解決內存泄漏

一個應用中若存在大量的內存泄漏是使應用內存佔用升高的主要原因之一。處理好內存泄漏就是對內存優化最爲立竿見影的方法。關於內存泄漏場景和定位內存泄漏方法可看後面兩篇文章《Andorid性能優化(二) 之 內存泄漏場景介紹》《Andorid性能優化(三) 之 如何定位內存泄漏》。

3.2 避免代碼量過多

代碼量多的應用,除了影響到安裝包和安裝後的佔用空間外,其實還會對內存佔用影響。我們從上面通過adb命令獲得進程PSS值和組成部分中可以看到,有.so mmap和.dex mmap,它們就是分別對應Native層和Java層代碼量所佔用的內存。因爲使用代碼本身也佔用內存,Android會把進程所使用的代碼也算入進程所使用的內存,也就是說,你的應用功能所需要的Java代碼量很多的話,.dex mmap就會越大。所以我們在日常開發版本迭代中,如若明確不需要的功能不要因爲捨不得而不忍心移除,一個應用中代碼能簡便簡。還有避免使用過多的第三方庫從而導致安裝包大小變大和因代碼量過多導致內存佔用高。

3.3 高效使用Bitmap

3.3.1 使用採樣率高效加載圖片

通常情況下,內存佔用高的是使用Bitmap造成的,特別現在手機分辨率越來越大,在Android中加載一張圖片它所佔用的內存可能會在幾M到幾十M不等。很多時候界面中需要顯示的圖片大小並沒有源圖片尺寸那麼大,這時若加載源圖片就會產生浪費內存,所以合理加載Bitmap是非常重要的。

在Android中通過使用BitmapFactory類提供了四類方法:decodeFile、decodeResurce、decodeStream和decodeByteArray,分別用於支持從文件系統、資源、輸入流以及字節數組中加載出一個Bitmap對象。而採用BitmapFactory.Options對象的inSampleSize參數設置採樣率可以將圖片的加載進行縮放,從而可高效地加載所需尺寸的Bitmap。

情況1,當inSampleSize == 1時:採樣後的圖片大小爲圖片原始大小。

情況2,當inSampleSize < 1時:其作用相當於1,即無縮放效果。

情況3,當inSampleSize > 1時:比如值是2,那麼採樣後的圖片其寬/高均爲原圖大小的1/2,而像素爲原圖的1/4,(縮放比率爲1 / ( inSamplesize 2 )),假設採用的是ARGB8888格式存儲的話,佔用內存大小爲原圖的1/4。例如,一張1024 *1024像素的圖片來說,採用ARGB8888格式存儲(8個bit等於1byte,所以這裏是4byte),它佔有的內存爲1024*1024*4,即4MB,如果inSampleSize爲2,那麼採樣後的圖片其內存佔用人有512*512*4,即1MB。

建議:最新的官方文檔中指出,inSampleSize的取值應該總是2的指數,比如1、2、4、8、16等等。如果傳入不爲2指數,系統會向下取整並選擇一個最接近2的指數來代替,比如3,會用2來代替,但是經過驗證發現這個結論並非在所有的Android版本中都成立,因此把它當成一個開發建議即可。

封裝方法代碼如下:

public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();

    // inJustDecodeBounds爲true時,是輕量級的加載,BitmapFactory只會解析圖片的原始寬/高信息,並不會去真正地加載圖片
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 計算 inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    if (reqWidth == 0 || reqHeight == 0) {
        return 1;
    }
    // outWidth 和 outHeight 表示原圖大小
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

調用代碼:

Bitmap bitmapObj = decodeSampledBitmapFromResource(getResources(), R.mipmap.myImage, 100, 100);
mImageView.setImageBitmap(bitmapObj);

當Bitmap對象使用完後,一定要進行資源回收,如代碼:

if (!bitmapObj.isRecycled()) {
    bitmapObj.recycle();
    System.gc();
}

3.3.2 Bitmap格式存儲類型選擇

在不考慮透明度的情況下,一個像素點的顏色在計算機中的表示方法有以下3種:

浮點數編碼就是RGB(1.0, 1.0, 1.0),每個顏色值各佔1個float,其取值範圍都是0.0~1.0。

24位整數編碼就是RGB(255, 255, 255),每個顏色值各佔8bit,其取值範圍都是0~255。

16位整數編碼就是RGB(31, 255, 31),第1和第3個顏色值各佔5bit,其取值範圍都是0~31;第2個顏色值佔6bit,其取值範圍是0~63。

在Java中,float和int類型的變量都是佔32 bit,short和char類型的變量都是佔16 bit,可得結論:

浮點數編碼中一個像素的顏色內存佔用量是96 bit,即12個byte;

24位整數編碼中一個像素的顏色內存佔用量只要一個int類型,即4個byte(高8bit空着,低24bit用於表示顏色);

16位整數編碼中一個像素的顏色內存佔用量只要一個short類型,即2個byte。

所以採用整數法編碼的顏色值是可以大大節省內存的,在Android中獲取Bitmap的時候一般也是採用整數編碼。其中RGB編碼格式有:

RGB565(short)R、G、B分別佔5bit、6bit、5bit。

ARGB4444(short):A、R、G、B各點48bit;(使用它的話圖片質量太差,已經不推薦使用了)

ARGB8888(int):A、R、G、B各點8bit;

由此可得出計算一個長和寬都是1024的Btimap所佔用的內存應該是:

RGB565:        1024 * 1024 * 2 = 2M

ARGB4444:  1024 * 1024 * 2 = 2M

ARGB8888:  1024 * 1024 * 4 = 4M

所以我們在平時使用Bitmap時,如果需要展示的界面效果要求不是特別地高和不需要考慮透明時,就可以考慮使用RGB565格式存儲類型,這樣會比使用ARGB8888省下一半的內存佔用。

3.3.3 能不使用Bitmap就儘量不使用

有些時候,我們需求中可能只需要比較簡單漸變色或圖形,那麼不使用Bitmap,改用Android提供的Drawable也是可以的,Drawable常被用來作爲View的背景使用。它一般都是通過XML來定義。又或者可以通過自義View的onDraw來繪製實現。經驗證,無論使用Drawable還是自定義View的onDraw,它們所佔用的內存都是非常小的,這樣就可以大大地省下了使用Bitmap的內存了。

有時候在不得不用圖時,先要考慮一下能否使用尺寸儘量小的圖或使用.9圖,因爲前面也提到過,圖片的尺寸大小和內存佔用是成正比的。

3.4 使用臨時進程

在文章的開始時,我們有提到過,Android系統爲每個應用進程都分配一個有封頂的堆內存值,我們喚此值爲Heap Size。Heap Size等於未被使用的堆內存Free Size + 當前實際已分配的堆內存Allocated Size。一般情況下,系統不會一開始就分配給一個進程一個封頂值,現假設封頂值爲120M。目前應用中只使用到10M,即Allocated Size是10M,系統可能就只分配給你的應用的Heap Size是15M,當你的應用在使用過程中使用到的內存分配變成了50M,那麼系統可能就會將Heap Size漲到了60M。當然實際多少是根據系統計算,我在這只是假設一個值。不管怎樣,能看出來系統會隨着你的應用的實際需求Allocated Size來增加Heap Size值。但是值得注意的是,當你應用中觸發了GC後,Allocated Size得到了下降後,Heap Size並不會隨着Allocated Size而下降

得出結論

如果應用中邏輯執行過程中使用了大量的內存,即使運行完之後已經釋放了中間過程的內存,內存在短時間內仍然會高企不下,這是由Android的內存調度機制決定的,而且這段短時間可能是幾十秒到幾百分鐘不等。

解決方案

一般在大多數應用中,都會存在一個前臺進程後臺進程,前臺進程專門負責UI的展示和用戶交互,後臺進程一般用於後臺計算、輪詢、保活等操作。前臺進程在用戶退出界面後,不用擔心Heap Size是否高低放心死去。而後臺進程的進程優先級並不高,如若還存在Heap Size很高的話,就會很容易被系統回收殺死。所以我們在架構層面可以考慮引進臨時進程,臨時進程只負責做一些一次性性質並且高耗內存的邏輯處理,這樣就能有效控制前臺進程和後臺進程的Heap Size。臨時進程做完事情後如果有返回結果可將結果通過AIDL傳遞到前臺進程或後臺進程,然後自殺掉自己就完事了,只要不超出封頂的Heap Size值,是完全不是擔心內存問題。

實施注意

1.分析是否一次性性質邏輯

何爲一次性性質邏輯?一般地,不需要直接輸出結果的邏輯就是一次性性質邏輯,例如下載文件並保存起來、做一些複雜的邏輯處理後將其結果保存到SharedPreferences中,等。那麼哪些爲非一次性性質邏輯?一般地,需要直接輸出結果的邏輯,例如通過一系列計算後,將其結果返回到外部進行使用,又例如內存緩存性質的邏輯,像從數據庫讀取一些數據緩存到內存中,供外部進行快速讀取,等。

我們在分析是否一次性性質邏輯時,要完整地分析出所實現的功能,不能放過任何一個細節,並確保不存在與原有進程的互相依賴關係。

2.改造非一次性性質邏輯

像通過計算後返回結果的情況,我們完全可以通過改造它將其返回結果通過AIDL的callbakc的接口方式來實現像一次性性質邏輯一樣放在臨時進程中去處理。但要注意分析出哪些數據作爲跨進程傳輸的關鍵數據,一般來說,傳遞的數據越少越好。

3.注意原來功能完整性

我們將邏輯移到臨時進程後,需要進行完整的邏輯覆蓋自測,畢竟分析階段只是屬於理論,得看真實效果。

3.5 界面佈局優化

在Android界面佈局的XML中,控件越少和層級嵌套越少,繪製的工作量也就越少,從而應用佔用內存也就越少,性能也就越高。界面佈局優化的手段一般有下面這些情況:

3.5.1控制佈局層級

儘可能地控制佈局層級,刪除佈局中無用的控制和層級

3.5.2 選擇性能較好的ViewGroup

有選擇地使用性能較好的ViewGroup,比如能使用LinearLayout或FrameLayout不使用RelativeLayout,因爲像LinearLayout如果不使用weight屬性的話,只measure一次,而RelativeLayout是和它的子View存在彼此依賴關係,所以是需要measure兩次的。當然如果要嵌套兩個就不如直接使用RelativeLayout。

3.5.3使用<include>和<merge>標籤

<include>和<merge>兩個標籤一般地都是配合使用,它們能使佈局降低減少佈局的層級,從而也可以使XML代碼更加簡潔。<include>標籤可以將一個指定的佈局文件加載到當前的佈局文件中,而<merge>標籤一般是要跟<include>標籤配合使用,從而去掉多餘的一層嵌套,它們的示例如下:

<LinearLayout
    android:orientation=”vertical”
    ……>
    <include android:id=“@+id/test_include”
        android:layout_width=”match_parent”
        android:layout_height=”match_parent”
        layout=”@layout/test_include” />
    ……
</LinearLayout>

Test_include.xml:

<merge xmlns:android=”http://schemas.android.com/apk/res/android”>
    <Button
        ……>
    <Button
        ……>
</merge>

說明和注意:

  1. <include>標籤只支持android:id除外的android:layout_開頭的屬性;
  2. 如果在<include>指定了id,同時被包含的佈局文件的根元素也指定了id屬性,那麼以<include>的爲準;
  3. <include>標籤如果指定了android:layout_*這種屬性,那麼必須存在layout_width和layout_height屬性,否則不起作用;
  4. 由於在當前佈局是豎起方向的LinearLayout,這時如果被包含的佈局文件也是採用豎直的LinearLayout,那麼就多餘了,所以通過<merge>標籤可去掉多餘的一層LinearLayout。

3.5.4使用ViewStub按需加載

ViewStub繼承了View,它非常輕量級且寬/高都是0,因此它本身不參與任何的佈局和繪製過程。它的意義在於按需加載所需的佈局文件。比如網絡異常時的界面,這時就沒必要在整個界面初始化時將其加載進來,通過ViewStub就可以做到使用的時候再加載,提高了程序初始化時的性能。使用示例如下

<ViewStub
    android:id=”@+id/stub_import”
        android:inflatedId=”@+id/panel_import”
        android:layout=”@layout/layout_network_error”
        android:layout_width=”match_parent”
        android:layout_height=”wrap_content”
        android:layout_gravity=”bottom” />

說明:

1.stub_import是ViewStub的id,而屬性inflatedId中panel_import是layout_network_error這個佈局的根元素的id;

2.需要時加載,可以在代碼中這樣實現:

        ((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);

        或

        View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

3.ViewStub不支持<merge>標籤。

3.6 繪製優化

繪製優化是指自定義View中進行繪製的優化,有些時候一個自定義View中加入動畫效果後就會佔用了大量的內存。如果不做好性能處理的話,若果繪製的界面放置時間一長就很簡單造成OOM。所以在繪製方面要注意下面事項:

  1. onDraw中儘量不要創建新的局部對象,這是因爲onDrarw方法可能會被頻繁調用,這樣就會在一瞬間產生大量的臨時對象,這不僅佔用了過多的內存而且還會導致系統更加頻繁gc,降低了程序的執行效率。
  2. onDraw方法中不要做耗時的任務,也不能執行成千上萬的循環操作,儘管是輕易級,大量的循環十分搶佔CPU的時間片,這會造成View的繪製過程不流暢。
  3. onDraw中繪製圖形圖像儘量避免過度繪製,換句話說就是晝避免疊加繪製。我們在開發調試中,可以打開“開發者模式”->”調示GPU過度繪製”來查看過度繪製區域。
  4. 避免過多調用invalidate()方法,特別是避免在屬性動畫過程中回調來調用。一般情況下可以在一次onDraw完後間隔16毫秒後再調用invalidate()比較適宜。
  5. 善用onWindowVisibilityChanged、onAttachedToWindow 和onDetachedFromWindow回調方法監測窗口情況來處理動畫的播放和停止。

更多關於自定義View繪製事項,可以參考之前的《Android中的自繪View的那些事兒》系列文章。

3.7 線程優化

大量的線程的創建和銷燬也會帶來內存的開銷。線程池可以重用內部的線程,所以可以避免這種創建和銷燬線程的開銷,而且還能有效地控制線程池的最大併發數,避免大量的線程互相搶佔系統資源從而導致阻塞現象發生。更多線程池的使用和介紹可以參考之前的文章《Android中的線程池》

3.8 使用註解代替枚舉

我們在日常開發中會很經常需要使用到枚舉,但是其實枚舉佔用的內存空間要比整型大。所以可以考慮使用Typedef註解來代替枚舉的使用,關於註解的介紹可以參考之前的文章《Android中註解(Support Annotations)的使用》

3.9 善用Android特有的數據結構

使用SparseArray或ArrayMap代替HashMap

SparseArray和ArrayMap比HashMap更省內存,它們對數據採取了壓縮的方式來表示稀疏數組的數據,從而節約內存空間,但是同時也犧牲了效率,因爲它們使用了二分查找法,並且當刪除或者添加數據時,會對空間重新調整。如果key的類型是int、long或者boolean類型,那麼使用SparseArray,因爲它避免了自動裝箱的過程;如果key類型爲其它的類型,則使用ArrayMap。兩個數據結構都適合數據量不是特別大的情況。使用示例:

SparseArray<String> sparseArray = new SparseArray<String>();
sparseArray.put(1, "zyx");
sparseArray.put(2, "子云心");
//通過int類型的key獲取value
sparseArray.get(1);
//獲取索引處的key與value
sparseArray.keyAt(1);
sparseArray.valueAt(1);

ArrayMap<String, String> arrayMap = new ArrayMap<>();
arrayMap.put("username", "zyx");
arrayMap.get("username");

善用Pair

Pair是一組元素,是成對存在,使用上跟Map很像。正常Map是有一個關鍵的key來完成比較和取value等一系列操作,但是Pair不一樣,它就幾乎只有3個用法:equals()、first、second。在某些情況下,既需要以鍵值的方式存儲數據列表,還需要在輸出的時候保持順序。HashMap滿足前者,ArrayList則滿足後者,再不打算去多做修改且數據類型相對簡單時,可以選擇Pair和搭配ArrayList使用。Pair使用示例:

Pair p1 = new Pair(1, "子");
Pair p2 = Pair.create(2, "雲");
Pair p3 = Pair.create(3, "心");
boolean result = p1.equals(p2);
int index = (int)p1.first;
String name = (String)p1.second;

 

好了,內存的優化方案暫時就列舉到這裏,後面遇到新情況或方案會繼續補充!!

 

 

 

 

 

 

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