Android效能優化(上)

weixin_33912445發表於2017-03-24

效能優化的概念

響應時間

響應時間: 從使用者操作開始到系統給使用者以正確反饋的時間。

一般包括邏輯處理時間 + 網路傳輸時間 + 展現時間。
對於非網路類應用不包括網路傳輸時間。
展現時間即網頁或App介面渲染時間。
響應時間是使用者對效能最直接的感受。

TPS(Transaction Per Second)

TPS為每秒處理的事務數,是系統吞吐量的指標,在搜尋系統中也用QPS(Query Per Second)衡量。TPS一般與響應時間反相關。

通常所說的效能問題就是指響應時間過長、系統吞吐量過低。
對後臺開發來說,也常將高併發下記憶體洩漏歸為效能問題。
對移動開發來說,效能問題還包括電量、記憶體使用這些較特殊情況。

效能優化方式

可以從以下方面進行優化

降低執行時間

  • 利用多執行緒併發或分散式提高TPS
  • 快取(包括物件快取、IO 快取、網路快取等)
  • 資料結構和演算法優化
  • 使用效能更優的底層介面呼叫,如 JNI 實現
  • 邏輯優化
  • 需求優化

同步改非同步:利用多執行緒提高TPS

提前或延遲操作:錯峰提高TPS

Android記憶體洩露

兩個概念:
1.記憶體洩漏(Memory Leak)也稱作“儲存滲漏”,用動態儲存分配函式動態開闢的空間,在使用完畢後未釋放,結果導致一直佔據該記憶體單元。直到程式結束。即所謂記憶體洩漏。 記憶體洩露累計到一定程度就會出現記憶體溢位。

2.記憶體溢位out of memory(OOM),是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢位。

Android中應用記憶體

Android會為每個應用程式分配一個單獨的LINUX使用者。Android會盡量保留一個正在執行程式,只在記憶體資源出現不足時,Android會嘗試停止一些程式從而釋放足夠的資源給其他新的程式使用, 也能保證使用者正在訪問的當前程式有足夠的資源去及時地響應使用者的事件。Android會根據程式中執行的元件類別以及元件的狀態來判斷該程式的重要性,Android會首先停止那些不重要的程式。按照重要性從高到低一共有五個級別就是我們常說的:前臺程式、可見程式、服務程式、後臺程式、空程式。

Android裝置出廠以後,java虛擬機器對單個應用的最大記憶體分配就確定下來了,超出這個值就會OOM。這個屬性值是定義在/system/build.prop檔案中的
dalvik.vm.heapstartsize=8m,它表示堆分配的初始大小,它會影響到整個系統對RAM的使用程度,和第一次使用應用時的流暢程度。它值越小,系統ram消耗越慢,但一些較大應用一開始不夠用,需要呼叫gc和堆調整策略,導致應用反應較慢。它值越大,這個值越大系統ram消耗越快,但是應用更流暢。

dalvik.vm.heapgrowthlimit=64m // 單個應用可用最大記憶體
主要對應的是這個值,它表示單個程式記憶體被限定在64m,即程式執行過程中實際只能使用64m記憶體,超出就會報OOM。(僅僅針對dalvik堆,不包括native堆)

dalvik.vm.heapsize=384m//heapsize參數列示單個程式可用的最大記憶體,但如果存在heapgrowthlimit引數,則以heapgrowthlimit為準.
heapsize表示不受控情況下的極限堆,表示單個虛擬機器或單個程式可用的最大記憶體。而android上的應用是帶有獨立虛擬機器的,也就是每開一個應用就會開啟一個獨立的虛擬機器(這樣設計就會在單個程式崩潰的情況下不會導致整個系統的崩潰)。
注意: 在設定了heapgrowthlimit的情況下,單個程式可用最大記憶體為heapgrowthlimit值。
在android開發中,如果要使用大堆,需要在manifest中指定android:largeHeap為true,這樣dvm heap最大可達heapsize。

不同裝置,這些個值可以不一樣。一般地,廠家針對裝置的配置情況都會適當的修改/system/build.prop檔案來調高這個值。
隨著裝置硬體效能的不斷提升,這個值越來越大。現在手機基本128m,256m了,但都遵循Android框架對每個應用的最小記憶體大小限制,想了解更多可參考官網Compatibility

通過程式碼檢視每個程式可用的最大記憶體,即heapgrowthlimit值:

ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memClass = activityManager.getMemoryClass();//64,以m為單位

記憶體洩露分析

Android應用記憶體洩漏的的原因可能有有以下幾個:

  • 查詢資料庫後沒有關閉遊標cursor
  • 構造Adapter時,沒有使用convertView重用
  • Bitmap物件不在使用時呼叫recycle()釋放記憶體
  • 物件被生命週期長的物件引用,如activity被靜態集合引用導致activity不能釋放

頻繁的使用static關鍵字修飾
static宣告變數的生命週期其實是和APP的生命週期一樣的(程式級別)。大量的使用的話,就會佔據記憶體空間不釋放,積少成多也會造成記憶體的不斷開銷,直至掛掉。static的合理使用一般用來修飾基本資料型別或者輕量級物件,儘量避免修飾集合或者大物件,常用作修飾全域性配置項、工具類方法、內部類。


Bitmap使用不當
Bitmap的不當處理極可能造成OOM,絕大多數情況應用程式OOM都是因這個原因出現的。Bitamp點陣圖是Android中當之無愧的胖子,所以在操作的時候必須小心。
解決方案:

  • 及時釋放recycle
    雖然,系統能夠確認Bitmap分配的記憶體最終會被銷燬,但是由於它佔用的記憶體過多,所以很可能會超過Java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。
  • 設定二次取樣
    需求允許的話,應該去對BItmap進行一定的縮放,通過BitmapFactory.Options的inSampleSize屬性進行控制。如果僅僅只想獲得Bitmap的屬性,其實並不需要根據BItmap的畫素去分配記憶體,只需在解析讀取Bitmap的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。
  • 軟引用或者弱引用並進行本地快取
SoftReference<Bitmap>  bitmap_ref  = new SoftReference<Bitmap>(BitmapFactory.decodeStream(inputstream)); 
if (bitmap_ref .get() != null)
          bitmap_ref.get().recycle();
  • 使用圖片快取
    █Bitmap快取分為兩種:
    一種是記憶體快取,一種是硬碟快取。
    █記憶體快取(LruCache):以犧牲寶貴的應用記憶體為代價,記憶體快取提供了快速的Bitmap訪問方式。系統提供的LruCache類是非常適合用作快取Bitmap任務的,它將最近被引用到的物件儲存在一個強引用的LinkedHashMap中,並且在快取超過了指定大小之後將最近不常使用的物件釋放掉。
    █注意: 以前有一個非常流行的記憶體快取實現是SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap快取方案,然而現在已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更著重於對軟/弱引用的回收,這使得上述的方案相當無效。
    █硬碟快取(DiskLruCache):一個記憶體快取對加速訪問最近瀏覽過的Bitmap非常有幫助,但是你不能侷限於記憶體中的可用圖片。GridView這樣有著更大的資料集的元件可以很輕易消耗掉記憶體快取。你的應用有可能在執行其他任務(如打電話)的時候被打斷,並且在後臺的任務有可能被殺死或者快取被釋放。一旦使用者重新聚焦(resume)到你的應用,你得再次處理每一張圖片。
    在這種情況下,硬碟快取可以用來儲存Bitmap並在圖片被記憶體快取釋放後減小圖片載入的時間(次數)。當然,從硬碟載入圖片比記憶體要慢,並且應該在後臺執行緒進行,因為硬碟讀取的時間是不可預知的。

廣播接收器、註冊觀察者未取消註冊
註冊廣播接收器、註冊觀察者等等,比如:
假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen中定義一個PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。
  但是如果在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen無法被GC回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得system_process程式掛掉。
雖然有些系統程式,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在我們的程式中明確的取消註冊,程式結束時應該把所有的註冊都取消掉。


集合中物件沒清理造成的記憶體洩露
我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。


資源物件沒關閉造成的記憶體洩露
資源性物件比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於Java虛擬機器內,還存在於Java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該立即呼叫它的close()函式,將其關閉掉,然後再置為null.在我們的程式退出時一定要確保我們的資源性物件已經關閉。

程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在長時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。


構造Adapter時,沒有使用快取的 convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提共了方法:

public View getView(intposition, View convertView, ViewGroup parent)

來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的螢幕佈局例項化一定數量的view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。

由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費時間,也造成記憶體垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機器將不得不給該應用程式分配更多的記憶體,造成不必要的記憶體開支。

最後:篇幅有點長,希望大家耐心閱讀,小夥伴們bug--,效能++,happy++。

相關文章