Android效能優化——記憶體洩漏的根本原因
什麼是記憶體洩露
什麼是記憶體洩露,通俗的來說就是堆中的一些物件已經不會再被使用了,但垃圾收集器卻無法將它們從記憶體中清除。
記憶體洩漏很嚴重的問題,因為它會阻塞記憶體資源並隨著時間的推移降低系統性能。如果不進行有效的處理,最終的結果將會使應用程式耗盡記憶體資源,無法正常服務,導致程式崩潰,丟擲java.lang.OutOfMemoryError異常。
堆記憶體中通常有兩種型別的物件:被引用的物件和未被引用的物件。被引用的物件是應用程式中仍然具有活躍的引用,而未被引用的物件則沒有任何活躍的引用。
垃圾收集器會回收那些未被引用的物件,但不會回收那些還在被引用的物件。這也是記憶體洩露發生的源頭。
哪些操作會造成記憶體洩漏?
下面我們介紹幾種常見的造成記憶體洩露的情況
1、意外宣告全域性變數是最常見也最容易修復的記憶體洩漏問題,比如:
function fn() {
name = '張三';
}
直譯器在解釋上面的函式時,會把name當做全域性變數,即window.name = '張三'。只要window物件沒有被清理,那麼name屬性和屬性值將一直存在,造成記憶體洩露。
解決方法:
(1)只要在變數宣告前面加上var、let或const關鍵字即可,這樣變數就會在函式執行完畢後離開作用域。
(2)使用this關鍵字
function fn() {
this.name = '張三';
}
(3)可以在 JavaScript 檔案開頭新增 “use strict”,使用嚴格模式。這樣在嚴格模式下解析 JavaScript 可以防止意外的全域性變數
(4)在使用完之後,對其賦值為null或者重新分配
2、 定時器導致的洩露
let name = '張三';
setInterval(() => {
console.log(name);
}, 100);
上面的程式碼中,只要定時器一直執行,回撥函式中引用的name就會一直佔用記憶體。
3、閉包、控制檯日誌、迴圈(在兩個物件彼此引用且彼此保留時,就會產生一個迴圈),下面我們看一個JavaScript閉包導致的內訓洩露例子
let fun = function() {
let name = '張三';
return function() {
return name;
};
};
呼叫fun()會導致分配給name的記憶體被洩漏。以上程式碼執行後建立了一個內部閉包,只要返回的函式存在就不能清理name,因為閉包一直在引用著它。
常見記憶體洩露問題
1.資源性物件未關閉
資源性物件(如Cursor、File等一些Closeable物件),它們往往使用了緩衝區,緩衝區不僅在JVM內,JVM之外也有。如果僅僅把變數設定為null,而不關閉它們,緩衝區得不到釋放,往往造成記憶體洩露。
解決方案:一般在finally中關閉資源型物件,而後設定物件為null
2.註冊物件未登出
訂閱者模式中,如果註冊物件不再使用時,未及時登出,會導致訂閱者列表中維持這物件的引用,阻止垃圾回收,導致記憶體洩露。常見場景:動態註冊BroadcastReceiver,註冊PhoneStateListener,註冊EventBus等等,
還有自定義使用訂閱者模式的情形。
解決方案:一般在onDestroy()中進行解註冊
3.非靜態內部類的靜態例項
非靜態內部類持有外部類例項的引用,若非靜態內部類的例項是靜態的,便擁有app存活期整個生命週期,長期持有外部類的引用,阻止外部類例項被回收。
使用內部類的情況十分常見,尤其是匿名內部類:一些介面的匿名實現類,都是內部類。
解決方案:(1)改為靜態內部類,不再持有外部類例項的引用 (2)避免申明非靜態內部類的靜態例項 (3)將內部類抽取出來封裝成一個單例,如果需要Context,沒有特殊要求就使用Application Context;如果需要Activity Context,則使用完畢置空,或者使用弱引用
4.單例模式引起的記憶體洩露
由於單例模式的靜態特性,使得它的生命週期和我們的應用一樣長,如果讓單例無限制的持有Activity的強引用就會導致記憶體洩漏
解決方案:使用Activity的弱引用,或者沒特殊需求時使用Application Context
5.Handler臨時性記憶體洩露
非靜態Handler持有Activity或Service的引用,Message中的target指向Handler例項,所以當Message在MessageQueue中排隊,長時間未得到處理時,Activity邊不會被回收,導致臨時性記憶體洩露。
解決方案:(1)使用靜態Handler內部類,然後對Handler持有的物件(Activity或Service)使用弱引用 (2)在onDestroy()中移除訊息佇列中的訊息 mHandler.removeCallbacksAndMessages(null)
類似的:AsyncTask內部也是Handler機制,也存在同樣的臨時性記憶體洩露風險
6.容器中物件未及時清理導致記憶體洩露
容器類一般擁有較長的生命週期,若內部不再使用的物件不及時清理,內部物件邊一直被容器類引用。上述2中的訂閱者列表也屬於容器類這中情況。另外常見的容器類還有執行緒池、物件池、圖片快取池等。執行緒池中的執行緒若存在ThreadLocal物件,因為執行緒物件一直被迴圈使用,ThreadLocal物件便會一直被引用,要注意對value物件的置空釋放。
7.靜態View導致記憶體洩露
有時,當一個Activity經常啟動,但是對應的View讀取非常耗時,我們可以通過靜態View變數來保持對該Activity的rootView引用。這樣就可以不用每次啟動Activity都去讀取並渲染View了。這確實是一個提高Activity啟動速度的好方法!但是要注意,一旦View attach到我們的Window上,就會持有一個Context(即Activity)的引用。而我們的View有事一個靜態變數,所以導致Activity不被回收。 解決辦法:在使用靜態View時,需要確保在資源回收時,將靜態View detach掉。
8.屬性動畫未及時關閉導致記憶體洩露
在使用ValueAnimator或者ObjectAnimator時,如果沒有及時做cancel取消動畫,就可能造成記憶體洩露。 因為在cancel方法裡,最後呼叫了endAnimation(); ,在endAnimation裡,有個AnimationHandler的單例,會持有屬性動畫物件的引用
解決辦法:在在onDestory時,呼叫動畫的cancel方法
9.WebView記憶體洩露
目前Android中WebView的實現存在很大的相容性問題,Google支援各個ROM廠商自行定製自己的WebView實現,各個ROM間差異較大,且大多都存在記憶體洩露問題。除了呼叫其內部的clearCache()、clearHistory()、removeAllViews()、freeMemory()、destroy()和置null以外,一般比較粗暴有效的解決方法是:將包含WebView的Activity放在一個單獨的程序中,不需要時將程序銷燬,從而釋放所有所佔記憶體。
10.其他的系統控制元件以及自定義View
在 Android Lollipop 之前使用 AlertDialog 可能會導致記憶體洩漏
view中有執行緒或者動畫 要及時停止。這是為了防止記憶體洩漏,可以在onDetachedFromWindow方法中結束,這個方法回撥的時機是 當View的Activity退出或者當前View被移除的時候 會呼叫 這時候是結束動畫或者執行緒的好時機 另外還有一個對應的方法 onAttachedToWindow 這個方法呼叫的時機是在包含View的Activity啟動時 回撥 回撥在onDraw方法之前
11.其他常見的引起記憶體洩漏原因
- (1)構造Adapter時,沒有使用快取的 contentView
- (2)Bitmap在不使用的時候沒有使用recycle()釋放記憶體
- (3)警惕執行緒未終止造成的記憶體洩露;譬如在Activity中關聯了一個生命週期超過Activity的Thread,在退出Activity時切記結束執行緒。一個典型的例子就是HandlerThread的run方法是一個死迴圈,它不會自己結束,執行緒的生命週期超過了Activity生命週期,我們必須手動在Activity的銷燬方法中呼叫thread.getLooper().quit();才不會洩露
- (4)避免程式碼設計模式的錯誤造成記憶體洩露;譬如迴圈引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放
以上就是Android開發當中的效能優化重要部分;記憶體洩漏在開發中佔比很重要,也是一個老生常談的問題。除此之外還有一系列的Android效能優化學習。比喻:卡頓優化、記憶體優化、啟動優化等等。這裡想學習效能優化的推薦參考前往傳送直達↓↓↓ :link.juejin.cn/?target=htt…裡面記錄了7大塊效能優化學習。
文末
理解記憶體洩漏的危害,我們舉個簡單的例子。有一個賓館,有100間房間,顧客每次都是在前臺進行登記,然後拿到房間鑰匙。如果有些顧客不需要該房間了,也不歸還鑰匙,久而久之,前臺處可用房間越來越少,收入也越來越少,瀕臨倒閉。當程式申請了記憶體,而不進行歸還,久而久之,可用記憶體越來越少,OS就會進行自我保護,殺掉該程序,這就是我們常說的OOM(out of memory)。
- Android效能優化——記憶體洩漏的根本原因
- Android ViewStub的使用方法——邊走邊看邊學
- Android進階——sdk開發和apk開發有什麼區別?
- Android開發——RXBinding防抖機制與案件分析
- Android效能啟動優化——IO優化進階
- Android適配【入坑指南 解決痛點】
- android 開發——疑難雜症ANR簡單介紹與解析
- Android外掛化框架—— Atlas
- 一名合格的音影片工程師,技能樹狀分佈是怎樣形成的?
- Android核心技術—核心(Linux) 的IO棧
- Android前沿技術—— Jetpack Compose
- Android開發資料結構與演算法——ArrayList原始碼講解
- Flutter中如何構建顯式動畫 【教學】
- Android記憶體抖動(主要原因分析 6個優化小技巧)
- Android車載多媒體開發——MediaSession框架
- 車機空調系統開發(HVAC),溫暖一整個冬天!
- 大廠為什麼在招聘安卓架構師時,為啥都需要熟悉 framework 經驗?
- 一個擴充套件性極強的 Flutter MVVM 實用框架,完善你的技術棧
- 2021年,跨端是否已成趨勢?Android 開發還有必要學 Flutter 嗎?
- Jetpack的MVVM通訊 - LiveData的原理分析