Andorid效能優化(一) 之 如何給應用進行記憶體優化
1 前言
Android系統為每個應用程序都分配一個有封頂的堆記憶體值,當應用記憶體佔用過高到沒有足夠的記憶體來提供給新物件分配並且垃圾回收機制也已經沒有空間可回收時就會OOM。當一個應用記憶體佔用過高會使一些效能差的手機系統記憶體緊缺,使得整體系統卡頓。而且應用記憶體佔用過高後,一旦退到後臺後,就會容易被系統殺死,這點我們在前面《Android程序回收機制和保活方案》中有介紹過,這時一旦你需要進行一些後臺工作時就會很被動。除此以外,若比起競品中記憶體使用佔用過高就會處於劣勢,也會容易引起使用者反感,從而棄之。今天這篇文章我們就來看看導致應用記憶體佔用過高的情況和解決辦法。
2 相關概念
在Android中程序的記憶體佔用按從大到小可以分為:VSS >= RSS >= PSS >= USS。可以通過adb命令:adb shell procrank來檢視系統中所應用的VSS、RSS、PSS 和USS的值情況,如下圖。
VSS(Virtual Set Size):表示程序總共可訪問的記憶體大小。它包括了分配但尚未使用的虛擬記憶體、所有共享庫所佔用記憶體 和 程序本身佔用記憶體。
RSS(Resident Set Size):表示程序實際使用的實體記憶體大小。它包括了所有共享庫所佔用記憶體(假如有3個程序使用同一個共享庫佔用了30M記憶體,這裡的值是30M) 和 程序本身佔用記憶體
PSS(Proportional Set Size):表示程序實際使用的實體記憶體大小。它包括了按程序比例平分的共享庫所佔用的記憶體(假如有3個程序使用同一個共享庫佔用了30M記憶體,這裡的值是10M) 和 程序本身佔用記憶體。
USS(Unique Set Size):表示程序獨自佔用的實體記憶體大小。它僅包括程序本身佔用記憶體。
一般情況下,反映程序記憶體佔用情況我們會選擇檢視PSS的值。在系統中應用管理或第三方工具中,要檢視一個程序的記憶體使用情況都是用PSS來表示的。也可以通過adb命令:adb shell dumpsys meminfo XXX (XXX表示程序名)來檢視程序的PSS
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,當然如果要巢狀兩個就不如直接使用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>
說明和注意:
- <include>標籤只支援android:id除外的android:layout_開頭的屬性;
- 如果在<include>指定了id,同時被包含的佈局檔案的根元素也指定了id屬性,那麼以<include>的為準;
- <include>標籤如果指定了android:layout_*這種屬性,那麼必須存在layout_width和layout_height屬性,否則不起作用;
- 由於在當前佈局是豎起方向的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。所以在繪製方面要注意下面事項:
- onDraw中儘量不要建立新的區域性物件,這是因為onDrarw方法可能會被頻繁呼叫,這樣就會在一瞬間產生大量的臨時物件,這不僅佔用了過多的記憶體而且還會導致系統更加頻繁gc,降低了程式的執行效率。
- onDraw方法中不要做耗時的任務,也不能執行成千上萬的迴圈操作,儘管是輕易級,大量的迴圈十分搶佔CPU的時間片,這會造成View的繪製過程不流暢。
- onDraw中繪製圖形影象儘量避免過度繪製,換句話說就是晝避免疊加繪製。我們在開發除錯中,可以開啟“開發者模式”->”調示GPU過度繪製”來檢視過度繪製區域。
- 避免過多呼叫invalidate()方法,特別是避免在屬性動畫過程中回撥來呼叫。一般情況下可以在一次onDraw完後間隔16毫秒後再呼叫invalidate()比較適宜。
- 善用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;
好了,記憶體的優化方案暫時就列舉到這裡,後面遇到新情況或方案會繼續補充!!