功能決定現在,性能決定未來!
記憶體洩漏是指由于代碼編寫不當導緻不再使用的對象無法得到及時釋放。記憶體洩漏産生的記憶體垃圾不僅浪費資源,拖慢運作效率,甚至還可能造成記憶體溢出,直接導緻應用崩潰。
對于android應用,比較容易發生洩漏的是activity、fragment對象,此類對象的共性是其都有一定的生命周期。以activity為例,一個activity執行個體的生命起始于oncreate(),終結于ondestroy()。當一個activity不再使用時,系統會調用回調方法activity.ondestroy()方法做一些清理操作。但是對于activity對象本身所占記憶體,則完全由虛拟機的垃圾回收器來完成回收。垃圾回收器會檢查該執行個體是否被持有強引用,如果存在指向該對象的強引用,則不會回收其所占記憶體空間,這塊記憶體空間也就成了記憶體垃圾。由此可見記憶體洩漏是由不當的強引用導緻的。
從gc root到洩漏對象的引用鍊能精準地定位導緻記憶體洩漏的原因。對象無法被垃圾回收器回收,一定是由于gc root直接或間接持有了它的強引用。
常見的gcroot有:聲明為static的變量,未停止的線程,application對象,甚至是棧記憶體中的局部變量。
程式設計過程中,我們常常會把一些對象加入到集合中。在我們不再需要該對象時,如果沒有及時把它從集合中清理掉,就會導緻這個集合占用的記憶體越來越大。同時如果這個集合是靜态的話,那情況就更嚴重了。如下的代碼段中在每次啟動activity的時候都往靜态集合中添加了一個對象,如果activity被頻繁啟動,set将不斷變大,影響app的正常運作。
是以,集合中不再使用的對象應及時釋放掉。上述代碼應該在activity的ondestroy()方法中,及時清理set裡的元素,避免無用對象繼續存在強引用,例如:
這樣可以保證set持有的強引用都被釋放。
單例的靜态特性使得其生命周期可能跟應用的生命周期一樣長,如果使用不恰當的話,很容易造成記憶體洩漏。
如下代碼是一個簡單的單例模式實作:
在建立單例的時候,如果我們傳入目前activity的context,例如:
單例testcontexthelper裡面一直儲存着該activity的引用,當這個context 對應的 activity 退出時,由于該 context 的引用一直被單例對象持有,是以該activity占用的記憶體并不會被回收,造成洩漏。在使用單例模式時,一定要避免持有短生命周期對象的引用,比如上述代碼在引用context時可以使用application的context代替activity的context,即:
因為application在應用的運作過程中一直存在,不會退出。
在啟動頻繁的activity中,為了避免反複建立某些資源,提高加載速度,我們可能會在activity内部建立一個靜态執行個體,每次啟動activity時都會使用該執行個體,如下代碼:
此時activity内部有一個靜态單例,且為非靜态内部類的執行個體。由于非靜态内部類預設會持有外部類的引用,并且該類建立了一個靜态執行個體,該執行個體的生命周期和應用的一樣長,這就導緻了該靜态執行個體一直會持有該activity的引用,導緻activity的記憶體資源不能正常回收。為了避免這一問題,在使用過程中,正确的做法是将内部類設為靜态類或者變成單獨的類。
在android應用中,handler通過發送message與其他線程互動,發出的message被存儲在目标線程的messagequeue中的,并且message不一定馬上就被處理,駐留時間可能比較久。比如我們用handler發送一個延時比較久的message:
而message中持有handler執行個體的強引用,如果message在queue中一直存在,就會導緻handler執行個體無法被回收,而handler持有activity的強引用,activity對象也不會被回收,這就造成了執行個體洩露。是以,在建立handler時,最好使用弱引用來引用目标activity對象,比如:
這樣可以避免由于handler持有強引用導緻activity無法回收。
如果成員變量被聲明為 static,其生命周期将與整個應用程序的生命周期一樣。如果靜态變量直接或間接強引用了某一短生命周期對象(比如activity),這會導緻即使app切到背景,這部分記憶體也不會被釋放。下面的錯誤示範代碼中,在activity啟動的時候,直接将其引用賦給了靜态變量obj,會導緻該activity一直不能被回收,導緻記憶體洩露。
是以,在使用靜态變量時,應該避免其持有短生命周期對象的強引用,可以使用弱引用來代替強引用。
對于使用了broadcastreceiver,contentobserver,file,遊标cursor,stream,bitmap等資源的使用,應該在activity銷毀時及時關閉或者登出,否則這些資源将可能不會被回收,造成記憶體洩漏。雖然有些系統程式,它本身可以自動取消注冊的(非即時),但是我們還是應該在我們的程式中明确的取消注冊,程式結束時應該把所有的注冊都取消掉。
記憶體溢出(oom, out ofmemory)是指當已存在的對象的占用了絕大部分或者全部配置設定給該程序的記憶體空間時,如果程序再申請新的記憶體空間,由于沒有空餘記憶體可用于配置設定,或可配置設定的記憶體不夠滿足申請者的需求,此時系統就會抛出記憶體溢出異常。
很大一部分記憶體溢出都是由于記憶體洩露導緻,由于已配置設定的記憶體被洩露對象占用并且無法釋放,随着洩露的對象執行個體越來越多,導緻可用記憶體越來越少,最終當記憶體耗盡時,系統就會抛出記憶體溢出異常。此時隻要解決了記憶體洩露,也就解決了記憶體溢出。
另一個記憶體溢出的重要原因就是應用加載了多個占用記憶體較多的對象。比如應用在運作過程中加載并儲存了多個較大的bitmap,導緻可用記憶體急劇減少。是以,在代碼編寫過程中,對于可能占據大量記憶體空間的對象,我們應該使用軟引用或虛引用持有該對象,使得系統gc能在記憶體吃緊時回收該對象釋放空間。并且在不使用bitmap時,應及時recycle,主動釋放記憶體空間。
在應用抛出記憶體溢出時,深度性能測試會主動捕獲這一異常,給出抛出該異常的堆棧資訊。并分析目前應用程序占用的總記憶體大小,已配置設定的記憶體大小和可用記憶體大小,友善開發者定位問題。
如下圖,測試報告中首先給出發生記憶體溢出的機型,同時指出檢測到記憶體溢出時應用自身和裝置記憶體的使用情況,可以看到native heap和vm heap的空餘記憶體都已不多。打開stacktrace後,可以看到出現oom錯誤的代碼行,由此我們發現可能是在加載bitmap的時候導緻的記憶體溢出。圖中紅色箭頭所指的地方是應用自身的代碼,我們根據這些提示就能夠快速找到源檔案中出錯的代碼,立即修複。
記憶體抖動指的是短時間内大量對象被建立和回收。由于短時間内産生了大量的對象,需要配置設定大量記憶體,此時需要垃圾回收器(gc)頻繁工作,回收不再使用的對象來騰出記憶體空間。gc的頻繁啟動占用了一定的系統資源,最終影響應用表現。
常見的記憶體抖動主要是由于在循環或其他場合中不停地建立新對象,并且短時間内這些對象又被釋放。瞬間産生大量的對象會嚴重占用記憶體區域,當達到閥值,剩餘空間不夠的時候觸發gc。即使每次配置設定的對象占用了很少的記憶體,但是他們疊加在一起會增加heap的壓力。gc啟動時會占用cpu等資源,直接導緻應用運作受到影響,可能出現界面操作不流暢等現象。
圖中給出了3種gc發生的時刻和記憶體變化的曲線圖。3種gc分别為:
gc_explicit:應用主動調用system.gc()産生的gc事件
gc_for_alloc:記憶體配置設定時,發現可用記憶體不夠時觸發的gc事件
gc_concurrent:已配置設定的記憶體大小達到某一門檻值時會觸發的gc事件
其中後兩種是系統自己決定啟動的gc,應用無法控制。但是我們可以優化代碼,避免頻繁生成和回收對象,比如不要在循環中頻繁new新的對象。
界面卡頓指的是短時間内界面對使用者操作沒有響應。應用在出現卡頓的時候,就算知道是哪個頁面出了問題,但是很難定位到具體的代碼。應用卡頓檢測就是幫助您快速定位卡頓的具體位置,友善您進行針對性的修複。
android應用的ui繪制和使用者操作消息分發都發生在應用主線程,如果主線程來不及處理ui更新和響應使用者操作,使用者就會感覺應用發生了卡頓。是以卡頓發生時嘗嘗伴随着主線程阻塞。如果在主線程中進行磁盤讀寫、網絡操作或者大量計算時,嘗嘗會導緻主線程被阻塞,發生界面卡頓。
過度繪制一般指的是螢幕上的某些區域在一幀中被多次繪制,一般是在界面的同一個地方疊加了多個控件。這樣會加重gpu的工作負擔,可能導緻應用運作過程中頻繁掉幀,影響使用者體驗。
當手機開啟過度繪制時,螢幕上會标記發生過度繪制的區域,并根據不同的繪制次數使用不同的顔色,顔色辨別從好到差依次是:藍色-綠色-淡紅色-紅色,分别代表該區域被繪制1次、2次、3次和4次。一般情況下,最好把繪制控制在2次以下,3次繪制有時候是不能避免的,盡量避免,4次的繪制基本上是不允許的。
為了減少過度繪制,開發者應減少複雜的、層級較多的布局,去掉多餘的背景色。簡單的界面盡量使用線性布局;較為複雜的界面可以使用相對布局,避免嵌套過多的線性布局。可以使用viewstub來動态加載界面。
啟動分析通過分析應用啟動過程産生的trace檔案來得到應用的啟動時間等資訊。通常來說,android應用的啟動方式分為兩種:冷啟動和熱啟動。
冷啟動:當啟動應用時,背景沒有該應用的程序,此時系統會建立一個新的程序配置設定給該應用。冷啟動因為系統會建立一個新的程序配置設定給它,是以會先建立和初始化application類,随後建立和初始化mainactivity類(包括一系列的測量、布局、繪制),最後顯示在界面上。
熱啟動:當啟動應用時,背景已有該應用的程序(例:按back鍵、home鍵,應用雖然會退出,但是該應用的程序是依然會保留在背景,可進入任務清單檢視),這種啟動會從已有的程序中來啟動應用。熱啟動因為會從已有的程序中來啟動,是以熱啟動就不會建立新的application,而是直接建立和初始化mainactivity,而不必建立和初始化application,因為一個應用從新程序的建立到程序的銷毀,application隻會初始化一次。一般來講,熱啟動時間都會在一定程度上小于冷啟動時間。
嚴苛模式(strictmode)是一個開發輔助工具,可以幫助開發者發現那些由于編碼過程中不注意而造成的問題。
strictmode經常用于捕獲那些在應用主線程中進行的磁盤讀寫操作和網絡請求。由于應用主線程是接收ui操作消息和執行界面渲染的地方,為了使應用運作更加流暢和更快響應,請盡量不要在主線程執行磁盤操作和網絡請求。當然,這也是避免系統彈出anr對話框和提高應用穩定性的好方法。一旦檢測到違反政策(policyviolation),系統将會輸出一條相關的日志,其一般包含一個調用棧,來顯示應用在何處發生違例。
注意:盡管android裝置的磁盤一般都是閃存盤,然而實際中很多裝置隻能以很有限的并發數來操作檔案系統。雖然磁盤讀寫很快,但是具體過程中可能由于其他程序占用了i/o接口,等待的過程會導緻整個磁盤操作流程比較慢。如果可以,請盡量假設磁盤讀寫是一個比較耗時的操作。
stricmode除了可以檢測主線程的磁盤操作和網絡請求以外,還可以發現主線程中執行時間較長的方法。當應用中有繼承了closeable接口的對象沒有關閉的時候,例如檔案流等,或者沒有使用https進行網絡請求,或者同一個activity的執行個體太多,strictmode都會給出提示。其能發現的錯誤主要包括:
a.應用在主線程中進行磁盤讀寫;
b.應用在主線程中進行網絡請求;
c.應用在主線程中的某些自定義方法的執行時間比較長;
d.sqlcursor對象在使用之後沒有關閉;
e.繼承了closeable接口的對象在使用之後沒有關閉;
f. 某一activity有較多的執行個體;
g.檔案讀取接口暴露給外部應用;
h.注冊某些對象(廣播接收器、觀察者、listener等)後沒有取消注冊;
i. 沒有使用加密網絡(https)進行網絡資料傳輸。