本文已同步發表到我的技術微信公眾號,掃一掃文章底部的二維碼或在微信搜尋 “程式設計師驛站”即可關注,不定期更新優質技術文章。同時,也歡迎加入QQ技術群(群號:650306310)一起交流學習!
Android的記憶體優化,一直是個讓開發者頭痛的問題,這篇文章是”Android電量優化全解析“後關於Android效能的又一篇原創文章,希望對大家有所幫助。今天我講述的內容按照下面的結構進行。
Android電量優化全解析
Android渲染優化解析(已發公眾號)
Android計算優化解析(已發公眾號)
記憶體與垃圾回收器
不是所有指令都執行得又快又好,下面介紹記憶體及它如何影響系統執行。
普遍認為,多數程式語言接近硬體或高效能,如C、C++和Fortran,通常程式設計師會自己管理記憶體,高手工程師對記憶體的分配,會慎重處理,並在未來結束使用時再次分配,一旦確認何時及怎樣分配記憶體,記憶體管理的品質就依賴於工程師的技能跟效率。實際情況是工程師們,不都會去追蹤那零碎的記憶體碎片。程式開發是個混亂又瘋狂的過程,記憶體通常都沒辦法完全被釋放,這些被囚禁的記憶體叫記憶體洩露。
記憶體洩露佔用了大量資源,這些資源其實可以更好地使用,為減少洩露引起的混亂、負擔、甚至資金損失,便有了記憶體管理語言。
這些語言在執行時跟蹤記憶體分配,以便當程式不再需要時釋放系統記憶體,完全不用工程師親自操作,這些記憶體回收藝術或科學,在記憶體管理環節下叫垃圾清理。這個設計概念在1959年,當初為了解決lisp語言問題,由John McCarthy發明的。
垃圾清理的基本概念有:
- 第一,找到未來無法存取的資料,例如所有不受指令操控的記憶體。
- 第二,回收被利用過的資源。
原理簡單,但是兩百萬行編碼,跟4gigs的分配,在實際操作時卻非常困難。如果在程式中有20000個物件分配,垃圾清理會讓人困惑,哪一個是沒用的?或者,何時啟動垃圾清理釋放記憶體?這些問題其實很複雜。好在50年來,我們找到了解決問題的方法,就是Android Runtime中的垃圾清理。比McCarthy最初的方法更高階,速度快且是非侵入性的。經由分配型別,及系統如何有效地組織分配以利GC的執行,並作為新的配置。所有影響android runtime的記憶體堆都被分割到空間中,根據這些特點,哪些資料適合放到什麼空間,取決於哪個Android版本。
最重要的一點是,每個空間都有預設的大小,在分配目標時要跟蹤綜合大小,且空間不斷地擴大,系統需要執行垃圾清理,以確保記憶體分配的正常執行,值得一提的是使用不同的Android runtime,GC的執行方式就會不同。例如在Dalvik中很多GC是停止事件,意思是很多指令的執行直到操作完成才會停止。
當這些GCs所用時間超過一般值,或者一大堆一起執行會耗費龐大的幀象時間,這是很麻煩的事情。
Android工程師花費大量時間降低干擾,確保這些程式以最快的速度執行,話雖如此,在指令中影響程式執行的問題仍然存在,首先程式在任意幀內執行GCs所用的時間越多,消除少於16毫秒的呈像障礙,所必需的時間就會變少,如果有許多GCs或一大串指令一個接一個地操作,幀象時間很可能會超過16毫秒的呈像障礙,這會導致隱形的碰撞或閃躲。其次,指令流程可能造成GCs強制執行的次數增多,或者,執行時間超過正常值。例如,在一個長期執行的迴圈最內側分配囤積物件,很多資料就會汙染記憶體堆,馬上就會有許多GCs啟動,由於這一額外的記憶體壓力,雖然記憶體環境管理良好,計算比其他語言複雜,記憶體洩露仍會產生,這些漏洞在GCs啟動時,通過無法被釋放的資料汙染記憶體堆,嚴重降低可用空間的總量,並以常規方式強制GC的執行。就是這樣,如果要減少任意幀內啟動GC的次數,需要著重優化程式的記憶體使用量,從指令的角度看,或許很難追蹤這些問題的起因,但是,多虧Android SDK擁有一組不錯的工具。
Memory Monitor工具
我們來介紹一個叫作Memory Monitor的工具,Memory Monitor用於測試程式在一段時間後佔用了多少記憶體,下面來操作一下。點選開啟,然後會在Android Studio右下邊的視窗裡,開啟一個製表鍵,一旦發現在執行的程式,就會馬上開始記錄記憶體使用量,正如這裡所示,在Memory Monitor視窗的左上端,可以切換當前連線的裝置,右邊這裡可以選擇要監測的程式。幾乎佔用全部視窗的疊層圖,表示還有多少記憶體可用。深藍色的區域,表示當前正在使用中的記憶體總量,淺藍色或者淺灰色區域,表示空閒記憶體或者叫作未分配記憶體。圖表會在記憶體使用量變化時不斷更新,隨著時間推移,它也會不斷顯示可用記憶體量。隨著時間推移,它也會不斷顯示可用記憶體量,總之,如果程式都沒有在執行,圖表就完全是平坦的。
光從效能角度看,這是相當理想的狀態,但隨著程式分配跟記憶體釋放,圖表的分配總量也在跟著變化。如果要裝的程式急需大量記憶體,記憶體分配也急劇增加,顯示在空格里,不然的話,裝置記憶體不足會導致當機。所以對於記憶體分配,不管什麼時候都要特別小心,當垃圾清理開啟時就要特別留意記憶體量,在這個範例中垃圾清理運作良好。另外,如圖所示這裡也可能有問題,這裡有個程式佔用了大量記憶體,然後又一下子釋放了剛被佔用的記憶體。生成這些又細又窄的鋒線,不斷重複,這就是程式在花大量時間執行垃圾清理,執行垃圾清理所用的時間越多,其他可用時間就越少,像播放和傳送錄音。我們來看下實際情況。 momory monitor已經在監測Sunshine情況了,點選一個日期,看下具體內容,點選返回鍵,重複這個動作,記憶體就會持續被佔用,如這裡所顯示的。如果想要新的資料,只要改變幾次座標就行了,看下所得的天氣預報,不錯,星期三天氣明朗。記憶體被慢慢的佔用,最終,記憶體會被全部佔用,這種情況如果持續下去,垃圾清理就會啟動,釋放大塊的記憶體,這裡可以看到變化。要記得,因為Android記憶體管理系統是固有的,所以垃圾清理不會釋放所有的記憶體。我們的利器,可以強制執行單項的垃圾清理,在Memory Monitor的左上方有個garbage truck工具,單擊一下,就會開啟單項的垃圾清理,注意圖表右邊的變化。現在可以多點選幾次,再繼續點選,所有可被釋放的記憶體都會被釋放,裝置會恢復到初始狀態。接下來我們將瞭解記憶體洩露和heap viewer工具。
記憶體洩露
Android的Java語言有個最大的優點,是託管記憶體環境,物件在建立或消除時不用特別小心。這點儘管不錯,但也有些潛在的問題不易被發現。劃分到Android執行時的記憶體堆,是根據宣告型別和利於垃圾清理操作的角度來分配的,每一區域都有其預設的記憶體空間。
當一個程式所需的總儲存空間接近上限,垃圾清理就會啟動,刪除掉沒用的資料,一般情況下不用特別注意垃圾清理的執行。
但是大量的清理動作不斷地重複,很快地消耗掉幀像週期,花費在垃圾清理上的時間越多,播放或傳送錄音等事情的時間就越少。
工程師們製造的記憶體洩露,是垃圾清理執行的常見因素,記憶體洩露是不能被繼續使用的空間,但是垃圾收集器卻無法辨別出來,結果他們就一直存在於堆中,佔用有效空間,永遠無法被刪除,隨著記憶體不斷洩露,堆中的可用空間就不斷變小,這意味著為了執行常用的程式,垃圾清理需要啟動的次數越來越多
搜尋跟修復洩露是個很棘手的問題,有些洩露很容易就會產生,例如對沒有使用的物件的迴圈引用。不過有些也很複雜,例如,在類別載入器安裝未完成就強制執行,不管怎樣,一個程式想要執行得又快又好,就需留意可能存在的記憶體洩露。你的程式碼將允許在各種各樣的裝置上,又互相結合,不是所有的資料都佔用同樣的記憶體,不過,還在有一個簡單的工具,可以檢視Android SDK中潛在的漏洞。
Heap Viewer工具
Heap Viewer是個很簡單的工具,利用它可以檢視記憶體狀態,以及空間佔用率的情況。通過Heap Viewer可知程式在特定時間內的記憶體使用量,跟原來一樣,先在裝置上開啟Android Studio裡的sunshine,在執行start Heap Viewer前,先開啟Android Device Monitor。 我們看到,每次垃圾清理後,Heap都會更新,點選Cause GC,發現所有的資料都更新了,更新後的表格顯示,在Heap上哪些資料是可用的,選中其中任一行資料,就可以看到詳細資料,點選class object,螢幕上馬上出現大量更新的資料,矩形圖列出這一資料記憶體分配的數量,跟確切的容量。我們這裡討論的是class object,heap viewer可以有效地分析程式在堆中所分配的資料型別,以及數量和大小。這裡列出在堆中各別型別程式的總容量,例如,這兩個在堆裡超過1400的資料組,用掉約1200個千位元組,而這個只有27的資料組,卻佔用了約2個兆位元組。heap viewer能夠準確地,辨別出程式分配的型別和數量,以及各自在堆中的容量。比方說,這個27的資料組佔用了近2兆的位元組,可這4個2000的資料組,目前佔用了228個千位元組。在搜尋記憶體漏洞時,這是個相當不錯的工具。
使用Memory Monitor觀察記憶體洩露
討論下記憶體洩露的問題,記憶體洩露的行蹤,常常神出鬼沒,常慢慢不動聲色的出現,有時要幾天或幾個星期後,才會被發現。實際上,可能到程式莫名其妙地操作緩慢時,才會發現記憶體不足的問題。只要用對工具,耐心分析,解決記憶體洩露不是難事。首先用Memory Monitor,觀察漏洞是怎樣生成的,在下一個影片中,再利用Heap Viewer做初步確認。舉例說明漏洞的生成,以及SDK工具,如何偵測這樣微小的漏洞,先把手機旋轉幾下,然後開啟Memory Monitor,這樣做的目的是要說明,一個簡單的動作就會產生漏洞。像這樣不斷改變手機方向,就會有漏洞產生,聽起來很奇怪,但是藉由這一動作,可知漏洞是怎麼緩慢且隱祕地產生的。首先,漏洞慢慢吞噬程式內的可用記憶體,直到GC的啟動,再來,值得注意的是由於程式上有漏洞,導致GC無法回收全部垃圾。結果大約30秒後,就會啟動第二次GC,當漏洞吞噬所有的可用記憶體時,Android調整並分配給程式更高的記憶體上限。這樣做的同時,如果漏洞沒有修復,記憶體會不斷地被吞噬,結果導致系統無法再配置,手機也就沒辦法再用了,最後當機。稍等下,第三次的GC就會啟動,第四次跟前兩次類似,現在這組指令在持續執行,系統分配更多的記憶體量,可以用同樣的方法操作Heap Viewer。
使用Heap Viewer觀察記憶體洩露
通過Heap Viewer,可知第一次GC僅釋放了1.39兆記憶體,這種結果顯示,因為漏洞的存在,垃圾清理無法回收全部垃圾。Heap viewer顯示第二次GC後,系統必須經由配置更多的記憶體,來調整記憶體量。堆從第一次GC的20兆,增加到32兆,此次Java堆釋放了12.9兆,這是,系統不斷地為程式配置更多的記憶體。以上動作如果一再重複,系統終會無法配置記憶體,程式也就掛了。切記,記憶體漏洞非常緩慢又不易被發現,需要時間,跟適當的環境來確認,有時,這樣的資料,也表示記憶體的正當存取。比如,處理圖片跟照片的程式,表面看似記憶體在洩露,實際上它針對核心功能的儲存器,不停地進行資料評估。因此,要明白記憶體洩露如何顯示在SD上,也要清楚,記憶體洩露如何顯示在擁有SDK的工具上,如Memory Monitor和Heap Viewer。但是,各位可能不知道他們源於何地,以下這些方法可以防止漏洞的出現。利用編碼檢視程式的壽命,清理不用的檔案,接下來,辨別漏洞產生的原因。
追蹤記憶體洩露的程式碼
ListenerCollector.java
import android.view.View;
import java.util.WeakHashMap;
public class ListenerCollector {
static private MyView.MyListener sListener;
public void setsListener(View view, MyView.MyListener listener){
sListener = listener;
}
}
複製程式碼
MyView.java
import android.content.Context;
import android.view.View;
public class MyView extends View{
public MyView(Context context){
super(context);
init();
}
public interface MyListener{
public void myListenerCallback();
}
private void init(){
ListenerCollector collector = new ListenerCollector();
collector.setsListener(this,myListener);
}
public MyListener myListener = new MyListener() {
@Override
public void myListenerCallback() {
System.out.print("有被呼叫");
}
};
}
複製程式碼
注意到自定義控制元件init方法中如下程式碼:
private void init() {
ListenerCollector collector = new ListenerCollector();
collector.setListener(this, mListener);
}
複製程式碼
儲存一個Activity中所有檢視監聽器,這個想法看似無害,但如果你忘了清理它們,你可能會不經意地造成一個緩慢的洩漏。相關程式碼:
collector.setListener(this, mListener);
複製程式碼
當Activity被銷燬和建立時,這一問題被複雜化。在示例中,由於裝置的方向變化使一個新的Activity建立,相關聯的監聽被建立,但是當Activity被銷燬時,該監聽永遠不會被釋放。這意味著,監聽無法被GC回收,這裡導致了記憶體洩露。當裝置旋轉並呼叫當前Activity的onStop方法時,一定要清理所有檢視的監聽。
ListenerCollector可以做如下優化:
import android.view.View;
import java.util.WeakHashMap;
public class ListenerCollector {
static private WeakHashMap<View,MyView.MyListener> sListener = new WeakHashMap<>();
public void setsListener(View view, MyView.MyListener listener){
sListener.put(view,listener);
}
public static void clearListeners(){
//移除所有監聽。
sListener.clear();
};
}
複製程式碼
使用Allocation Tracker觀察記憶體洩露
另外,分配追蹤器,可以辨別額外的記憶體膨脹,這是由於記憶體的歷史瀏覽記錄不斷擴充產生的。選擇一組仍在堆中的資料或者程式,這組資料堆中,在這個操作裡,堆中資料叫作onCreate。這樣一來,手機每旋轉一次就有新的動作,類似的資料組,基本上就會在堆中膨脹。所以,如果在漏洞存在時旋轉手機,垃圾清理無法清除這些資料,就會在堆中產生大量的垃圾。藉由分配追蹤器,可以弄清這一問題。
什麼是記憶體抖動?
我們解決了哪些討厭的洩露,現在遇到了更大的問題,記憶體抖動。要知道,堆記憶體都有一定的大小,能容納的資料是有限制的,當Java堆的大小太大時,垃圾收集會啟動停止堆中不再應用的物件,來釋放記憶體。現在,記憶體抖動這個術語可用於描述在極短時間內分配給物件的過程。例如,當你在迴圈語句中配置一系列臨時物件,或者在繪圖功能中配置大量物件時,這相當於內迴圈,當螢幕需要重新繪製或出現動畫時,你需要一幀幀使用這些功能,不過它會迅速增加你的堆的壓力。這兩種情況下,我們都制定瞭解決方案,可在短時間內創造大量的物件。根據創造的物件的量,或者每個物件的大小,你可能很快就消耗掉所有剩餘記憶體,導致垃圾收集強行開啟。隨著它們的開啟執行,會消耗更多寶貴的幀時間,所以,高效能的應用很有必要,你需要鑑別並從內迴圈裡,取消會被重複執行的程式碼配置。為了更好的尋找到這些程式碼配置,Android Studio為此特別打造了一個方便的工具.
使用Allocation Tracker
現在看一下你的應用記憶體分配圖,這能有效的獲悉大部分資料到底用在哪裡,以及正在分配哪種型別的資料,這能幫你找到現有的不必要分配的資料。可惜Heap Viewer不能顯示你的資料具體分配在程式碼的何處,為此,我們需要一個叫做分配追蹤器的工具。和以前一樣,我們開啟Android Studio Device Monitor,在前臺載入Sunshine,開啟DDMS檢視點選start allocation tracking按鈕,然後使用應用,隔一段時間在點選stop allocation tracking按鈕。停止之後在DDMS出現了一個列表,這個列表顯示了你在使用應用期間,所有的分配情況,這裡的每一行都代表不同的分配,allocation order這一欄會提示你,分配進行的具體時間,分配類別這一欄顯示了分配資料的型別,以及大小,還有其他資訊來告訴你哪個執行緒具體決定了這一分配。最後,分配站這一欄告訴你程式碼的哪一個功能實際分配了記憶體。比如,我們選擇整型,測試的值決定了這個整型的分配,如果你點選一個分配,你可以看見完整的呼叫堆疊。這個表格包含大量資訊!
通過Trace View找出記憶體抖動
本次練習,我們來執行記憶體抖動活動。下面點這個按鈕,對陣列來點有意思的事情,你會發現跳著舞的海盜會暫停,但最後都會接著跳舞。這就是討厭的卡頓,讓我們解決它吧。通過跟蹤顯示來剖析這個活動,開啟trace view的皮膚,注意短時間內發生的頻繁的垃圾收集活動,可能會傷害到應用的效能。記住,我們還可以採集這個記憶體監控器影象,這個截圖展示了記憶體抖動是怎樣通過Memory Monitor清晰顯示的。
什麼導致了記憶體抖動?
我們已經使用SDK工具採集足夠多的資料,能知道記憶體抖動情況出現的時間,現在來揪出導致這種情況的程式碼吧。Trace View給我們提供了一個方法,讓我們仔細看一下在主執行緒裡,選擇方式時的資料配置檔案,當你選擇主執行緒方式時,你會發現反覆出現的Java字串賦值操作,比如這個。再看呼叫堆疊,我們會更加確定資料佇列副本被運用於擴大字串緩衝。來看MemoryChurnActivity的原始碼,正如OnClickListener所顯示,我們稱此功能為imPrettySureSortingIsFree,讓我們來看這個程式碼。此處的方法叫作imPrettySureSortingIsFree,這個程式碼產生了新的字串,通過字串連線每次都有一個單元值,看一下我說的這個程式碼的指導提示,但是,出現連線的地方比較特別。這個初看起來似乎沒什麼問題,為什麼這個程式碼會導致記憶體抖動? 頻繁使用垃圾清理會造成兩種後果,一是,每個單元值的連結都會生成新的字元陣列,這是因為,在迴圈之內驟然接到重複指令組合而成,二是,通過定位追蹤器,確認字元陣列的膨脹,更新一下資料,在下一節中,向大家介紹所得的結果。
修改程式碼減少記憶體抖動
我們可以在我們的程式碼進行小的調整,以防止記憶體抖動。讓我們來看看對比圖,而不是在一個時間串聯一個單元格值打造每一行,讓我們使用一個StringBuilder例項,並用一個字串構造每一行,需要注意的是StringBuilder中的例項化的迴圈外。因此它的記憶體分配一次,然後,我們只是作為一個緩衝,在每次迴圈我們先清除它,然後我們追加,整數的一個字串來表示對於迴圈迭代的行。更多細節見導師的筆記到這個程式碼段,執行memory_churn_optimized,確認我們減少的GC在短期時間窗中發生的量,您也可以使用allocation tracker驗證。現在對於我們來說,即使修改了程式碼,海盜動畫仍然會出現卡頓的現象,這意味著該處理放到後臺處理可能更加合適。
工具的特色
1)Memory Monitor:獲得記憶體的動態檢視
2)Heap Viewer:顯示堆記憶體中儲存了什麼
3)Allocation Tracke: 具體是哪些程式碼使用了記憶體
關注我的技術公眾號"程式設計師驛站",每天都有優質技術文章推送,微信掃一掃下方二維碼即可關注: