android 效能優化詳解
非常好的一篇記憶體優化文章:
文章來源:https://www.cnblogs.com/xinmengwuheng/p/7232429.html
這一篇中我們將著重介紹Android的記憶體優化。本文的篇幅很長,但是請不要嫌煩,因為每看一節,你就多了一份在面試官面前裝X的資本。
什麼是記憶體
通常情況下我們說的記憶體是指手機的RAM,它主要包括一下幾個部分:
- 暫存器(Registers讀音:[ˈrɛdʒɪstɚ]
)
速度最快的儲存場所,因為暫存器位於處理器內部,所以在程式中我們無法控制。
- 棧(Stack)
存放基本型別的物件和引用,但是物件本身不存放在棧中,而是存放在堆中。
變數其實是分為兩部分的:一部分叫變數名,另外一部分叫變數值,對於區域性變數
在函式執行的時候,函式內部的區域性變數就會在棧上建立,函式執行結束的時候這些儲存單元會被自動釋放。棧記憶體分配運算內置於處理器的指令集中是一塊連續的記憶體區域,效率很高,速度快,但是大小是作業系統預定好的所以分配的記憶體容量有限。
-
堆(Heap)
在堆上分配記憶體的過程稱作 記憶體動態分配過程。在java中堆用於存放由new建立的物件和陣列。堆中分配的記憶體,由java虛擬機器自動垃圾回收器(GC)來管理(可見我們要進行的記憶體優化主要就是對堆記憶體進行優化)。堆是不連續的記憶體區域(因為系統是用連結串列來儲存空閒記憶體地址,自然不是連續的),堆大小受限於計算機系統中有效的虛擬記憶體(32bit系統理論上是4G) -
靜態儲存區/方法區(Static Field)
是指在固定的位置上存放應用程式執行時一直存在的資料,java在記憶體中專門劃分了一個靜態儲存區域來管理一些特殊的資料變數如靜態的資料變數。 -
常量池(Constant Pool)
顧名思義專門存放常量的。注意String s = "java"
中的“java”也是常量。JVM虛擬機器為每個已經被轉載的型別維護一個常量池。常量池就是該型別所有用到地常量的一個有序集合包括直接常量(基本型別,String)和對其他型別、欄位和方法的符號引用。
總結:
-
定義一個區域性變數的時候,java虛擬機器就會在棧中為其分配記憶體空間,區域性變數的基本資料型別和引用儲存於棧中,引用的物件實體儲存於堆中。因為它們屬於方法中的變數,生命週期隨方法而結束。
-
成員變數全部儲存與堆中(包括基本資料型別,引用和引用的物件實體),因為它們屬於類,類物件終究是要被new出來使用的。當堆中物件的作用域結束的時候,這部分記憶體也不會立刻被回收,而是等待系統GC進行回收。
-
所謂的記憶體分析,就是分析Heap中的記憶體狀態。
Android中的沙盒機制
大家可能都聽說過iOS中有沙盒機制(sandbox),但是我們的Android系統中也存在沙盒機制,只不過沒有IOS中的嚴格,所以常常被人忽略。
由於Android是建立在Linux系統之上的,所以Android系統繼承了Linux的 類Unix繼承程序隔離機制與最小許可權原則,並且在原有Linux的程序管理基礎上對UID的使用做了改進,形成了Android應用的”沙箱“機制。
普通的Linux中啟動的應用通常和登陸使用者相關聯,同一使用者的UID相同。但是Android中給不同的應用都賦予了不同的UID,這樣不同的應用將不能相互訪問資源。對應用而言,這樣會更加封閉,安全。
引文來自Android的SandBox(沙箱)在Android系統中,應用(通常)都在一個獨立的沙箱中執行,即每一個Android應用程式都在它自己的程序中執行,都擁有一個獨立的Dalvik虛擬機器例項。Dalvik經過優化,允許在有限的記憶體中同時高效地執行多個虛擬機器的例項,並且每一個Dalvik應用作為一個獨立的Linux程序執行。Android這種基於Linux的程序“沙箱”機制,是整個安全設計的基礎之一。
引文來自淺析Android沙箱模型
簡單點說就是在Android的世界中每一個應用相當與一個Linux中的使用者,他們相互獨立,不能相互共享與訪問,(這也就解釋了Android系統中為什麼需要程序間通訊),正是由於沙盒機制的存在最大程度的保護了應用之間的安全,但是也帶來了每一個應用所分配的記憶體大小是有限制的問題。
Generational Heap Memory記憶體模型的概述
在Android和Java中都存在著一個Generational(讀音:[ˌdʒenəˈreɪʃənl])
Heap Memory模型,系統會根據記憶體中不同的記憶體資料型別分別執行不同的GC操作。Generational Heap Memory模型主要由:Young Generation(新生代)、Old Generation(舊生代)、Permanent(讀音:[ˈpɜ:rmənənt])
Generation三個區域組成,而且這三個區域存在明顯的層級關係。所以此模型也可以成為三級Generation的記憶體模型。
其中Young Generation區域存放的是最近被建立物件,此區域最大的特點就是建立的快,被銷燬的也很快。當物件在Young Generation區域停留的時間到達一定程度的時候,它就會被移動到Old Generation區域中,同理,最後他將會被移動到Permanent Generation區域中。
在三級Generation記憶體模型中,每一個區域的大小都是有固定值的,當進入的物件總大小到達某一級記憶體區域閥值的時候就會觸發GC機制,進行垃圾回收,騰出空間以便其他物件進入。
不僅如此,不同級別的Generation區域GC是需要的時間也是不同的。同等物件數目下,Young Generation GC所需時間最短,Old Generation次之,Permanent Generation 需要的時間最長。當然GC執行的長短也和當前Generation區域中的物件數目有關。遍歷查詢20000個物件比起遍歷50個物件自然是要慢很多的。
GC機制概述
與C++不用,在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式設計師不需要通過呼叫函式來釋放記憶體,但也隨之帶來了記憶體洩漏的可能。簡單點說:對於 C++ 來說,記憶體洩漏就是new出來的物件沒有 delete,俗稱野指標;而對於 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收。
Android使用的主要開發語言是Java所以二者的GC機制原理也大同小異,所以我們只對於常見的JVM GC機制的分析,就能達到我們的目的。我還是先看看那二者的不同之處吧。
-
Dalvik 和標準Java虛擬機器的主要區別
Dalvik虛擬機器(DVM)是Android系統在java虛擬機器(JVM)基礎上優化得到的,DVM是基於暫存器的,而JVM是基於棧的,由於暫存器高效快速的特性,DVM的效能相比JVM更好。
-
Dalvik 和 java 位元組碼的區別
Dalvik執行.dex
格式的位元組碼檔案,JVM執行的是.class
格式的位元組碼檔案,Android程式在編譯之後產生的.class
檔案會被aapt
工具處理生成R.class
等檔案,然後dx
工具會把.class
檔案處理成.dex
檔案,最終資原始檔和.dex
檔案等打包成.apk
檔案。
-
對於Young Generation(新生代)的GC
由於Young Generation通常存活的時間比較短,所以Young Generation採用了Copying演算法進行回收,Copying演算法就是掃描出存活的物件,並複製到一塊新的空間中,這個過程就是下圖Eden與Survivor Space之間的複製過程。Young Generation採用空閒指標的方式來控制GC觸發,指標儲存最後一個分配在Young Generation中分配空間地物件的位置。當有新的物件要分配記憶體空間的時候,就會主動檢測空間是否足夠,不夠的情況下就出觸發GC,當連續分配物件時,物件會逐漸從Eden移動到Survivor,最後移動到Old Generation。
-
對於Old Generation(舊生代)的GC
Old Generation與Young Generation不同,物件存活的時間比較長,比較穩固,因此採用標記(Mark)演算法來進行回收。所謂標記就是掃描出存活的物件,然後在回收未必標記的物件。回收後的剩餘空間要麼進行合併,要麼標記出來便於下次進行分配,總之就是要減少記憶體碎片帶來的效率損耗。
-
如何判斷物件是否可以被回收
從上面的一小節中我們知道了不同的區域GC機制是有所不同的,那麼這些垃圾是如何被發現的呢?下面我們就看一下兩種常見的判斷方法:引用計數、物件引用遍歷。
-
引用計數器
引用計數器是垃圾收集器中的早起策略。這種方法中,每個物件實體(不是它的引用)都有一個引用計數器。當一個物件建立的時候,且將該物件分配給一個每分配給一個變數,計數器就+1,當一個物件的某個引用超過了生命週期或者被設定一個新值時,物件計數器就-1,任何引用計數器為 0 的物件可以被當作垃圾收集。當一個物件被垃圾收集時,引用的任何物件技術 - 1。
優點:執行快,交織在程式執行中,對程式不被長時間打斷的實時環境比較有利。
缺點:無法檢測出迴圈引用。比如:物件A中有物件B的引用,而B中同時也有A的引用。
-
跟蹤收集器
現在的垃圾回收機制已經不太使用引用計數器的方法判斷是否可回收,而是使用跟蹤收集器方法。
現在大多數JVM採用物件引用遍歷機制從程式的主要執行物件(如靜態物件/暫存器/棧上指向的堆記憶體物件等)開始檢查引用鏈,去遞迴判斷物件收否可達,如果不可達,則作為垃圾回收,當然在便利階段,GC必須記住那些物件是可達的,以便刪除不可到達的物件,這稱為標記(marking)物件。
下一步,GC就要刪除這些不可達的物件,在刪除時未必標記的物件,釋放它們的記憶體的過程叫做清除(sweeping),而這樣會造成記憶體碎片化,佈局已分配給新的物件,但是他們集合起來還很大。所以很多GC機制還要重新組織記憶體中的物件,並進行壓縮,形成大塊、可利用的空間。
為了達到這個目的,GC需要停止程式的其他活動,阻塞程序。這裡我們要注意的是:不要頻繁的引發GC,執行GC操作的時候,任何執行緒的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續執行, 故而如果程式頻繁GC, 自然會導致介面卡頓. 通常來說,單個的GC並不會佔用太多時間,但是大量不停的GC操作則會顯著佔用幀間隔時間(16ms)。如果在幀間隔時間裡面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了。
Android記憶體洩露分析
對於 C++ 來說,記憶體洩漏就是new出來的物件沒有 delete,俗稱野指標;而對於 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收
GC過程與物件的引用型別是嚴重相關的,下面我們就看看Java中(Android中存在差異)對於引用的四種分類:
- 強引用(Strong Reference):JVM寧願丟擲OOM,也不會讓GC回收的物件
- 軟引用(Soft Reference) :只有記憶體不足時,才會被GC回收。
- 弱引用(weak Reference):在GC時,一旦發現弱引用,立即回收
- 虛引用(Phantom Reference):任何時候都可以被GC回收,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否存在該物件的虛引用,來了解這個物件是否將要被回收。可以用來作為GC回收Object的標誌。
注意Android中存在的差異
但是在2.3以後版本中,系統會優先將SoftReference的物件提前回收掉, 即使記憶體夠用,其他和Java中是一樣的。所以谷歌官方建議用LruCache(least recentlly use 最少最近使用演算法)。會將記憶體控制在一定的大小內, 超出最大值時會自動回收, 這個最大值開發者自己定。其實LruCache就是用了很多的HashMap,三百多行的程式碼
在開發過程中,儲存物件,這時我很可以直接使用LruCache來代替,Bitmap物件:
在Android開發過程中,我們常常使用HasMap儲存物件,但是為了防止記憶體洩漏,在儲存記憶體佔用較大、生命週期較長的物件的時候,儘量使用LruCache代替HasMap用於儲存物件。
//指定最大快取空間 private static final int MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8); LruCache<String,Bitmap> mBitmapLruCache = new LruCache<>(MAX_SIZE);
而造成不能回收的根本原因就是:堆記憶體中長生命週期的物件持有短生命週期物件的強/軟引用,儘管短生命週期物件已經不再需要,但是因為長生命週期物件持有它的引用而導致不能被回收。
如何監聽系統傳送GC
那麼怎樣才能去監聽系統的GC過程呢?其實非常簡單,系統每進行一次GC操作時,都會在LogCat中列印一條日誌,我們只要去分析這條日誌就可以了,日誌的基本格式如下所示:
DVM中
D/dalvikvm(30615): GC FOR ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms , total 24ms
ART中
I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
原因,一般情況下一共有以下幾種觸發GC操作的原因:
-
GC_CONCURRENT: 當我們應用程式的堆記憶體快要滿的時候,系統會自動觸發GC操作來釋放記憶體。
-
GC_FOR_MALLOC: 當我們的應用程式需要分配更多記憶體,可是現有記憶體已經不足的時候,系統會進行GC操作來釋放記憶體。
-
GC_HPROF_DUMP_HEAP: 當生成HPROF檔案的時候,系統會進行GC操作,關於HPROF檔案我們下面會講到。
-
GC_EXPLICIT: 這種情況就是我們剛才提到過的,主動通知系統去進行GC操作,比如呼叫System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的。
接下來第二部分Amount_freed,表示系統通過這次GC操作釋放了多少記憶體。
然後Heap_stats中會顯示當前記憶體的空閒比例以及使用情況(活動物件所佔記憶體 / 當前程式總記憶體)。
最後Pause_time表示這次GC操作導致應用程式暫停的時間。
關於這個暫停的時間,Android在2.3的版本當中進行過一次優化,在2.3之前GC操作是不能併發進行的,也就是系統正在進行GC,那麼應用程式就只能阻塞住等待GC結束。雖說這個阻塞的過程並不會很長,也就是幾百毫秒,但是使用者在使用我們的程式時還是有可能會感覺到略微的卡頓。
而自2.3之後,GC操作改成了併發的方式進行,就是說GC的過程中不會影響到應用程式的正常執行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間,不過優化到這種程度,使用者已經是完全無法察覺到了。
導致GC頻繁執行有兩個原因
由於GC會阻塞程序,所以我們不避免頻繁的GC。
1. Memory Churn(記憶體抖動),記憶體抖動是因為大量的物件被建立又在短時間內馬上被釋放。
2. 瞬間產生大量的物件會嚴重佔用Young Generation的記憶體區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即使每次分配的物件佔用了很少的記憶體,但是他們疊加在一起會增加 Heap的壓力,從而觸發更多其他型別的GC。這個操作有可能會影響到幀率,並使得使用者感知到效能問題。
解決上面的問題有簡潔直觀方法,如果你在Memory Monitor裡面檢視到短時間發生了多次記憶體的漲跌,這意味著很有可能發生了記憶體抖動。
記憶體洩露的檢測與處理
幹說不練假把式,說這麼多的記憶體知識,下面就讓我們看看Android給我們提供了那些工具來解決記憶體洩漏的問題。例如
熟悉Android Studio介面
工欲善其事,必先利其器。我們接下來先來熟悉下Android Studio的介面
一般分析記憶體洩露, 首先執行程式,開啟日誌控制檯,有一個標籤Memory ,我們可以在這個介面分析當前程式使用的記憶體情況, 一目瞭然, 我們再也不需要苦苦的在logcat中尋找記憶體的日誌了。
圖中藍色區域,就是程式使用的記憶體, 灰色區域就是空閒記憶體, 當然,Android記憶體分配機制是對每個應用程式逐步增加, 比如你程式當前使用30M記憶體, 系統可能會給你分配40M, 當前就有10M空閒, 如果程式使用了50M了,系統會緊接著給當前程式增加一部分,比如達到了80M, 當前你的空閒記憶體就是30M了。 當然,系統如果不能再給你分配額外的記憶體,程式自然就會OOM(記憶體溢位)了。 每個應用程式最高可以申請的記憶體和手機密切相關,比如我當前使用的華為Mate7,極限大概是200M,算比較高的了,
一般128M 就是極限了, 甚至有的手機只有可憐的16M或者32M,這樣的手機相對於記憶體溢位的概率非常大了。
如何檢測記憶體洩露
首先需要明白一個概念, 記憶體洩露就是指,本應該回收的記憶體,還駐留在記憶體中。 一般情況下,高密度的手機,一個頁面大概就會消耗20M記憶體,如果發現退出介面,程式記憶體遲遲不降低的話,可能就發生了嚴重的記憶體洩露。 我們可以反覆進入該介面,然後點選dump Java heap 這個按鈕,然後Android Studio就開始幹活了,下面的圖就是正在dump
dump成功後會自動開啟 hprof檔案,檔案以Snapshot+時間來命名
MAT
通過Android Studio自帶的介面,檢視記憶體洩露還不是很智慧,我們可以藉助第三方工具,常見的工具就是MAT了,下載地址 http://eclipse.org/mat/downloads.php ,這裡我們需要下載獨立版的MAT. 下圖是MAT一開始開啟的介面, 這裡需要提醒大家的是,MAT並不會準確地告訴我們哪裡發生了記憶體洩漏,而是會提供一大堆的資料和線索,我們需要自己去分析這些資料來去判斷到底是不是真的發生了記憶體洩漏。
接下來我們需要用MAT開啟記憶體分析的檔案, 上文給大家介紹了使用Android Studio生成了 hprof檔案, 這個檔案在呢, 在Android Studio中的Captrues這個目錄中,可以找到
注意,這個檔案不能直接交給MAT, MAT是不識別的, 我們需要右鍵點選這個檔案,轉換成MAT識別的。
然後用MAT開啟匯出的hprof(File->Open heap dump) MAT會幫我們分析記憶體洩露的原因
LeakCanary
上面介紹了MAT檢測記憶體洩露, 再給大家介紹LeakCanary。 專案地址:https://github.com/square/leakcanary
LeakCanary會檢測應用的記憶體回收情況,如果發現有垃圾物件沒有被回收,就會去分析當前的記憶體快照,也就是上邊MAT用到的.hprof檔案,找到物件的引用鏈,並顯示在頁面上。這款外掛的好處就是,可以在手機端直接檢視記憶體洩露的地方,可以輔助我們檢測記憶體洩露
使用: 在build.gradle檔案中新增,不同的編譯使用不同的引用:
dependencies { debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
在應用的Application onCreate方法中新增LeakCanary.install(this),如下
public class ExampleApplication extends Application @Override public void onCreate() { super.onCreate(); LeakCanary.install(this); } }
應用執行起來後,LeakCanary會自動去分析當前的記憶體狀態,如果檢測到洩漏會發送到通知欄,點選通知欄就可以跳轉到具體的洩漏分析頁面。 Tips:就目前使用的結果來看,絕大部分洩漏是由於使用單例模式hold住了Activity的引用,比如傳入了context或者將Activity作為listener設定了進去,所以在使用單例模式的時候要特別注意,還有在Activity生命週期結束的時候將一些自定義監聽器的Activity引用置空。 關於LeakCanary的更多分析可以看專案主頁的介紹,還有這裡http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/
追蹤記憶體分配
如果我們想了解記憶體分配更詳細的情況,可以使用Allocation Traker來檢視記憶體到底被什麼佔用了。 用法很簡單:
點一下是追蹤, 再點一下是停止追蹤, 停止追蹤後 .alloc檔案會自動開啟,開啟後介面如下: 當你想檢視某個方法的原始碼時,右鍵選擇的方法,點選Jump to source就可以了
查詢方法執行的時間
Android Studio 功能越來越強大了, 我們可以藉助AS觀測各種效能,如下圖:
如果我們要觀測方法執行的時間,就需要來到CPU介面
點選Start Method Tracking, 一段時間後再點選一次, trace檔案被自動開啟,
非獨佔時間: 某函式佔用的CPU時間,包含內部呼叫其它函式的CPU時間。 獨佔時間: 某函式佔用CPU時間,但不含內部呼叫其它函式所佔用的CPU時間。
我們如何判斷可能有問題的方法?
通過方法的呼叫次數和獨佔時間來檢視,通常判斷方法是:
如果方法呼叫次數不多,但每次呼叫卻需要花費很長的時間的函式,可能會有問題。
如果自身佔用時間不長,但呼叫卻非常頻繁的函式也可能會有問題。
常見記憶體洩露分析
1. 永遠的單例(Singleton)
為了完美解決我們在程式中反覆建立同一物件的問題,我們選用了單例模式,單例在我們的程式中隨處可見,但是由於單例模式的靜態特性,使得它的生命週期和我們的應用一樣長,一不小心讓單例無限制的持有Activity的強引用就會導致記憶體洩漏。例如:
public class SingleTon{
private Context context;
private static SingleTon singleTon;
public static final SingleTon getInstance(Context context){
this.context = context;
return SingleHolder.INSTANCE;
}
private static class SingleHolder{
private static final SingleTon INSTANCE = new SingleTon();
}
}
解決辦法:
這個錯誤很普遍,這個是一個很正常的單利模式,但是由於傳入了一個Context,而這個Context的生命週期就的長短就尤為重要了。如果我們傳入的是某個Activity的Context,而當這個Activity推出的時候,由於該Context的強引用被單例持有,那麼這個Activity就等同於擁有了整個程式的生命週期。這種情況下,當Activity退出的時候記憶體並沒有被回收,這就造成了記憶體洩漏。
正確的做法就是應該把傳入的Context改為同應用生命週期一樣長的Application中的Context。
public class BaseApplication extends Application{
private static BaseApplication baseApplication;
@Override
public void onCreate(){
super.onCreate();
baseApplication = this;
}
public static Context getContext{
baseApplication.getApplicationContext();
}
}
當然我們可以直接重寫Application,提供getContext方法,不必在依靠傳入的引數:
public static final SingleTon getInstance(Context context) {
this.context = context.getApplicationContext;
return SingleHolder.INSTANCE;
}
6.2 Handler引起的記憶體洩漏
Handler引起的記憶體洩漏在我們開發中最為常見的。我們知道Handler、Message、MessageQueue都是相互關聯在一起的,萬一Handler傳送的Message尚未被處理,那麼該Message以及傳送它的Handler物件都會被執行緒MessageQueue一直持有。
由於Handler屬於TLS(Thread Local Storage)變數,生命週期和Activity是不一致的,因此這種實現方式很難保證跟Activity的生命週期一直,所以很容易無法釋放記憶體。比如:
public class HandlerBadActivity extends AppCompatActivity {
private final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_bad);
// 延遲5min傳送一個訊息
handler.postDelayed(new Runnable() {
@Override
public void run() {
// write something
}
},1000*60*5);
this.finish();
}
}
我們在例子中生命了一個延時5分鐘執行的Message,當該Activity退出的時候,延時任務(Message)還在主線成的MessageQueue中等待,此時的Message持有Handler的強引用,並且由於Handler是HandlerBadActivity的非靜態內部類,所以Handler會持有HandlerBadActivity的強引用,此時HandlerBadActivity退出時無法進行記憶體回收,造成記憶體洩漏。
解決辦法:
將Handler生命為靜態內部類,這樣它就不會持有外部來的引用了。這樣以來Handler的的生命週期就與Activity無關了。不過倘若用到Context等外部類的非static物件,還是應該通過使用Application中與應用同生命週期的Context比較合適。比如:
public class HandlerGoodActivity extends AppCompatActivity {
private static final class MyHandler extends Handler {
private Context mActivity;
public MyHandler(HandlerGoodActivity activity) {
//使用生命週期與應用同長的getApplicationContext
this.mActivity = activity.getApplicationContext();
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mActivity != null) {
// write something
}
}
}
private final MyHandler myHandler = new MyHandler(this);
// 匿名內部類在static的時候絕對不會持有外部類的引用
private static final Runnable RUNNABLE = new Runnable() {
@Override
public void run() {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_good);
myHandler.postDelayed(RUNNABLE, 1000 * 60 * 5);
}
雖然我們結局了Activity的記憶體洩漏問題,但是經過Handler傳送的延時訊息還在MessageQueue中,Looper也在等待處理訊息,所以我們要在Activity銷燬的時候處理掉佇列中的訊息。
@Override
protected void onDestroy() {
super.onDestroy();
//傳入null,就表示移除所有Message和Runnable
myHandler.removeCallbacksAndMessages(null);
}
6.3 匿名內部類在非同步執行緒中的使用
它們方便卻暗藏殺機。Android開發經常會繼承實現 Activity 或者 Fragment 或者 View。如果你使用了匿名類,而又被非同步執行緒所引用,那得小心,如果沒有任何措施同樣會導致記憶體洩漏的:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_bad);
Runnable runnable1 = new MyRunnable();
Runnable runnable2 = new Runnable() {
@Override
public void run() {
}
};
}
private static class MyRunnable implements Runnable{
@Override
public void run() {
}
}
}
runnable1 和 runnable2的區別就是,runnable2使用了匿名內部類,我們看看引用時的引用記憶體
可以看到,runnable1是沒有什麼特別的。但runnable2多出了一個MainActivity的引用,若是這個引用再傳入到一個非同步執行緒,此執行緒在和Activity生命週期不一致的時候,也就造成了Activity的洩露。
6.4 善用static成員變數
從前面的介紹我們知道,static修飾的變數位於記憶體的靜態儲存區,此變數與App的生命週期一致
這必然會導致一系列問題,如果你的app程序設計上是長駐記憶體的,那即使app切到後臺,這部分記憶體也不會被釋放。按照現在手機app記憶體管理機制,佔記憶體較大的後臺程序將優先回收,因為如果此app做過程序互保保活,那會造成app在後臺頻繁重啟。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被使用者解除安裝或者靜默。
這裡修復的方法是:
不要在類初始時初始化靜態成員。可以考慮lazy初始化(延遲載入)。架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此物件的生命週期你有責任管理起來。
6.5 避免使用
在我們的日常程式碼中,這樣的情況似乎很常見,及直接寫一個class就這麼光禿禿的情況
這樣就在Activity內部建立了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏,因為非靜態內部類預設會持有外部類的引用,而該非靜態內部類又建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。正確的做法為:
將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景如下:
其中: NO1表示 Application 和 Service 可以啟動一個 Activity,不過需要建立一個新的 task 任務佇列。而對於 Dialog 而言,只有在 Activity 中才能建立
6.6 集合引發的記憶體洩漏
相關推薦
android 效能優化詳解
非常好的一篇記憶體優化文章: 文章來源:https://www.cnblogs.com/xinmengwuheng/p/7232429.html 這一篇中我們將著重介紹Android的記憶體優化。本文的篇幅很長,但是請不要嫌煩,因為每看一節,你就多了一份在面試官面前
kafka效能優化詳解
KAFKA Cluster模式最大的優點:可擴充套件性和容錯性。下圖是關於Kafka叢集的結構圖: 一、Kafka Broker個數決定因素 二、作業系統優化 大部分Linux釋出版本預設的核心引數配置能讓大部分應用工作的相當好。但對於實際的Kafka br
資料庫效能優化詳解
一、問題的提出 在應用系統開發初期,由於開發資料庫資料比較少,對於查詢SQL語句,複雜檢視的的編寫等體會不出SQL語句各種寫法的效能優劣,但是如果將應用系統提交實際應用後,隨著資料庫中資料的增加,系統的響應速度就成為目前系統需要解決的最主要的問題之一。系統優化中一個很重要的方面就是SQL語句的優化。對於
SQL--資料庫效能優化詳解
出處:1.資料庫訪問優化法則要正確的優化SQL,我們需要快速定位能性的瓶頸點,也就是說快速找到我們SQL主要的開銷在哪裡?而大多數情況效能最慢的裝置會是瓶頸點,如下載時網路速度可能會是瓶頸點,本地複製檔案時硬碟可能會是瓶頸點,為什麼這些一般的工作我們能快速確認瓶頸點呢,因為我
Android記憶體優化:詳解記憶體分析工具MAT
前言在這個系列的前四篇文章中,我分別介紹了DVM、ART、記憶體洩漏和記憶體檢測工具的相關知識點,這一篇我們通過一個小例子,來學習如何使用記憶體分析工具MAT。1.概述在進行記憶體分析時,我們可以使用Memory Monitor和Heap Dump來觀察記憶體的使用情況、使用
Qt Embedded效能優化詳解
Qt Embedded是挪威公司的圖形化介面開發工具QT的嵌入式版本,它通過QTAPI與LinuxI/O以及Framebuffer直接互動,擁有較高的執行效率,而且整體採用面向物件程式設計,擁有良好地體系架構和程式設計模式. Qt Embedded和Qt一樣,在4.5版
Android應用開發之PNG、IconFont、SVG圖示資源優化詳解
1 背景 最近因為一些個人私事導致好久沒寫部落格了,多事之年總算要過去了,突然沒了動力,所以趕緊先拿個最近專案中重構的一個小知識點充下數,老題重談。 在我們App開發中大家可能都會有過如下痛疾(程式設計師和設計妹妹注意嘍): 好多小的圖示好煩人,又佔體積
Android效能優化二 電量優化、網路優化、物件池、bitmap解位元速率
大綱 電量優化、網路優化、Android Wear上如何做優化、使用物件池來提高效率、LRU Cache、Bitmap的縮放、快取、重用、PNG壓縮、自定義View的效能、提升設定alpha之後View的渲染效能,以及Lint、StictMode等工具的使用技巧 對於手機程
nginx配置項優化詳解
nginx優化(1)nginx運行工作進程個數,一般設置cpu的核心或者核心數x2如果不了解cpu的核數,可以top命令之後按1看出來,也可以查看/proc/cpuinfo文件 grep ^processor /proc/cpuinfo | wc -l [[email protected]/* *
lucene、lucene.NET詳細使用與優化詳解[轉]
構造 bitset 更多 隱患 .net wrapper 屬性設置 似的 擔心 1 lucene簡介1.1 什麽是luceneLucene是一個全文搜索框架,而不是應用產品。因此它並不像www.baidu.com 或者google Desktop那麽拿來就能用,它只是提供了
MySQL批量SQL插入性能優化詳解
inno user val 事務 優化 好的 維護 排列 測試結果 對於一些數據量較大的系統,數據庫面臨的問題除了查詢效率低下,還有就是數據入庫時間長。特別像報表系統,每天花費在數據導入上的時間可能會長達幾個小時或十幾個小時之久。因此,優化數據庫插入性能是很有意義的。經過對
aNDROID之MEDIapLaYER詳解
iap music media 詳解 list oid aid 5% layer %E8%BD%AC%E8%BD%BD%E4%B8%80%E4%B8%AA%E5%9B%BE%E7%89%87%E5%A4%84%E7%90%86%E5%B7%A5%E5%85%B7%E7%B1
Android RxJava操作符詳解系列: 變換操作符
urn 原因 轉換 需要 生產 依賴 reat 入門 所有 Rxjava,由於其基於事件流的鏈式調用、邏輯簡潔 & 使用簡單的特點,深受各大 Android開發者的歡迎。Github截圖 如果還不了解 RxJava,請看文章:Android:這是一篇 清晰 &
android:exported 屬性詳解
itl fas 默認 之前 綁定 四大 nbsp ring ins http://blog.csdn.net/watermusicyes/article/details/46460347 昨天在用360掃描應用漏洞時,掃描結果,出來一個android:exported屬性,
大數據量高並發的數據庫優化詳解(MSSQL)
臨時表 引用 edi 強制 順序存儲 實現 好的 float 空間 轉載自:http://www.jb51.net/article/71041.htm 如果不能設計一個合理的數據庫模型,不僅會增加客戶端和服務器段程序的編程和維護的難度,而且將會影響系統實際運行的性能。所以,
Java Tomcat7性能監控與優化詳解
無限 bin esp mpat 活躍 壓縮 常見 定向 rms 1. 目的 通過優化tomcat提高網站的並發能力。 2. 服務器資源 服務器所能提供CPU、內存、硬盤的性能對處理能力有決定性影響。 3. 優化配置 3.1. 配置tomcat管理員賬戶 在con
web網站集群之企業級Nginx Web服務優化詳解(二)
監牢模式 優雅顯示 防盜鏈 非法解析 12 配置Nginx gzip壓縮實現性能優化 100k ---- 1s 90k 100k ---- 5s 10k gzip on; gzip_min_length 1k; gzip_buffers
Android SDK Manager詳解
eba 調試 測試工具 使用說明 服務 能力 bar 部分 以及 Android基礎知識——Android SDK Manager詳解 做Android開發時,免不了使用Android SDK Manager,安裝需要的sdk版本、buildTool
nginx.conf 配置優化詳解
nginx優化 高並發 user www www;——配置nginx運行用戶和用戶組,使用之前創建用戶useradd www -s /sbin/nologin -M worker_processes 4;——配置nginx worker進程數,根據cpu內核數設置,也可以設置成auto worker
python之模塊定義、導入、優化詳解
無需 文件中 就是 系統路徑 python 順序 使用方式 一行 系列 一、模塊 1.模塊的定義 模塊是一組包含了一組功能的python文件,比如test.py,模塊名為test,可以通過import test進行調用。模塊可以分為以下四個通用類別 1 使用python