Android 性能優化 - 詳解內存優化的前因後果

前言php

APP內存的使用,是評價一款應用性能高低的一個重要指標。雖然如今智能手機的內存愈來愈大,可是一個好的應用應該將效率發揮到極致,精益求精。java

 

這一篇中咱們將着重介紹Android的內存優化。本文的篇幅很長,可是請不要嫌煩,由於每看一節,你就多了一份在面試官面前裝X的資本。android

什麼是內存git

一般狀況下咱們說的內存是指手機的RAM,它主要包括一下幾個部分: 
- 寄存器(Registers讀音:[ˈrɛdʒɪstɚ]) 
速度最快的存儲場所,由於寄存器位於處理器內部,因此在程序中咱們沒法控制。 
- 棧(Stack) 
存放基本類型的對象和引用,可是對象自己不存放在棧中,而是存放在堆中。程序員

變量實際上是分爲兩部分的:一部分叫變量名,另一部分叫變量值,對於局部變量(基本類型的變量和對象的引用變量)而言,統一都存放在棧中,可是變量值中存儲的內容就有在必定差別了:Java中存在8大基本類型,他們的變量值中存放的就是具體的數值,而其餘的類型都叫作引用類型(對象也是引用類型,你只要記住除了基本類型,都是引用類型)他們的變量值中存放的是他們在堆中的引用(內存地址)。github

在函數執行的時候,函數內部的局部變量就會在棧上建立,函數執行結束的時候這些存儲單元會被自動釋放。棧內存分配運算內置於處理器的指令集中是一塊連續的內存區域,效率很高,速度快,可是大小是操做系統預約好的因此分配的內存容量有限。web

  • 堆(Heap) 
    在堆上分配內存的過程稱做 內存動態分配過程。在java中堆用於存放由new建立的對象和數組。堆中分配的內存,由java虛擬機自動垃圾回收器(GC)來管理(可見咱們要進行的內存優化主要就是對堆內存進行優化)。堆是不連續的內存區域(由於系統是用鏈表來存儲空閒內存地址,天然不是連續的),堆大小受限於計算機系統中有效的虛擬內存(32bit系統理論上是4G)面試

  • 靜態存儲區/方法區(Static Field) 
    是指在固定的位置上存放應用程序運行時一直存在的數據,java在內存中專門劃分了一個靜態存儲區域來管理一些特殊的數據變量如靜態的數據變量。算法

  • 常量池(Constant Pool) 
    顧名思義專門存放常量的。注意 String s = "java"中的「java」也是常量。JVM虛擬機爲每一個已經被轉載的類型維護一個常量池。常量池就是該類型全部用到地常量的一個有序集合包括直接常量(基本類型,String)和對其餘類型、字段和方法的符號引用。shell

總結:

  1. 定義一個局部變量的時候,java虛擬機就會在棧中爲其分配內存空間,局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。由於它們屬於方法中的變量,生命週期隨方法而結束。

  2. 成員變量所有存儲與堆中(包括基本數據類型,引用和引用的對象實體),由於它們屬於類,類對象終究是要被new出來使用的。當堆中對象的做用域結束的時候,這部份內存也不會馬上被回收,而是等待系統GC進行回收。

  3. 所謂的內存分析,就是分析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 集合引起的內存泄漏

咱們一般會把一些對象的引用加入到集合容器(好比ArrayList)中,當咱們再也不須要該對象時,並無把它的引用從集合中清理掉,當集合中的內容過於大的時候,而且是static的時候就形成了內存泄漏,全部咱們最好在onDestory狀況並讓其不可達

private List<String> nameList;

    private List<Fragment> list;

 

    @Override

    public void onDestroy() {

        super.onDestroy();

        if (nameList != null){

            nameList.clear();

            nameList = null;

        }

        if (list != null){

            list.clear();

            list = null;

        }

    }

6.7 webView引起的內存泄漏

WebView解析網頁時會申請Native堆內存用於保存頁面元素,當頁面較複雜時會有很大的內存佔用。若是頁面包含圖片,內存佔用會更嚴重。而且打開新頁面時,爲了能快速回退,以前頁面佔用的內存也不會釋放。有時瀏覽十幾個網頁,都會佔用幾百兆的內存。這樣加載網頁較多時,會致使系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啓。

因爲佔用的都是Native堆內存,因此實際佔用的內存大小不會顯示在經常使用的DDMS Heap工具中(這裏看到的只是Java虛擬機分配的內存,通常即便Native堆內存已經佔用了幾百兆,這裏顯示的還只是幾兆或十幾兆)。只有使用adb shell中的一些命令好比dumpsys meminfo 包名,或者在程序中使用Debug.getNativeHeapSize()才能看到。

聽說因爲WebView的一個BUG,即便它所在的Activity(或者Service)結束也就是onDestroy()以後,或者直接調用WebView.destroy()以後,它所佔用這些內存也不會被釋放。

解決這個問題最直接的方法是:把使用了WebView的Activity(或者Service)放在單獨的進程裏。而後在檢測到應用佔用內存過大有可能被系統幹掉或者它所在的Activity(或者Service)結束後,調用System.exit(0),主動Kill掉進程。因爲系統的內存分配是以進程爲準的,進程關閉後,系統會自動回收全部內存。

關於WebView的跟多內容請參見 : Android WebView Memory Leak WebView內存泄漏

6.8其餘常見的引發內存泄漏緣由

  • 構造Adapter時,沒有使用緩存的 convertView

  • Bitmap在不使用的時候沒有使用recycle()釋放內存

  • 非靜態內部類的靜態實例容易形成內存泄漏:即一個類中若是你不可以控制它其中內部類的生命週期(譬如Activity中的一些特殊Handler等),則儘可能使用靜態類和弱引用來處理(譬如ViewRoot的實現)。

  • 警戒線程未終止形成的內存泄露;譬如在Activity中關聯了一個生命週期超過Activity的Thread,在退出Activity時切記結束線程。一個典型的例子就是HandlerThread的run方法是一個死循環,它不會本身結束,線程的生命週期超過了Activity生命週期,咱們必須手動在Activity的銷燬方法中中調運thread.getLooper().quit();纔不會泄露。

  • 對象的註冊與反註冊沒有成對出現形成的內存泄露;譬如註冊廣播接收器、註冊觀察者(典型的譬如數據庫的監聽)等。

  • 建立與關閉沒有成對出現形成的泄露;譬如Cursor資源必須手動關閉,WebView必須手動銷燬,流等對象必須手動關閉等。

  • 不要在執行頻率很高的方法或者循環中建立對象(好比onMeasure),可使用HashTable等建立一組對象容器從容器中取那些對象,而不用每次new與釋放。

  • 避免代碼設計模式的錯誤形成內存泄露;譬如循環引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。

總結

    • Android內存優化主要是針對堆(Heap)而言的,當堆中對象的做用域結束的時候,這部份內存也不會馬上被回收,而是等待系統GC進行回收。

    • Java中形成內存泄漏的根本緣由是:堆內存中長生命週期的對象持有短生命週期對象的強/軟引用,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收。

相關文章
相關標籤/搜索