昨天是個好日子,10.24,程序員的節日,在這裏給全部的程序員送上一份遲到的祝福。本文原計劃是昨晚推送的,可是計劃趕不上變化,昨晚臨時有事耽擱了,因此只能推到今晚了。今天的主題是Android開發中的內存泄漏,之因此說這個是由於前幾天作了項目中的內存泄漏排查與解決,在這裏總結一下,被提供一種快速定位解決Android內存泄漏的方案,但願你們看完有所收穫。java
在介紹內存泄漏以前頗有必要說起一下Android系統的垃圾回收機制。Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,做爲Java開發者,通常不須要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題,也不須要像C程序員那樣戰戰兢兢。這是由於在Java虛擬機中,存在自動內存管理和垃圾清掃機制。歸納地說,該機制對虛擬機中的內存進行標記,並肯定哪些內存須要回收,根據必定的回收策略,自動的回收內存,永不停息(Nerver Stop)的保證虛擬機中的內存空間,防止出現內存泄露和溢出問題。Android系統的垃圾回收是基於可達性分析算法(根搜索算法)的。以下圖所示,從GC Roots(每種具體實現對GC Roots有不一樣的定義)做爲起點,向下搜索它們引用的對象,能夠生成一棵引用樹,樹的節點視爲可達對象,反之視爲不可達。android
不可達的對象(以下圖中的object5,6,7)會在系統GC的時候被回收,從而釋放內存空間。git
若是全部的對象均可以被順利回收就沒有本文的誕生了,舉個簡單的例子,咱們在開發中常用單例模式,單例的靜態特性致使其生命週期同應用同樣長。有時建立單例時若是咱們須要Context對象,若是傳入的是Application的Context那麼不會有問題。若是傳入的是Activity的Context對象,那麼當Activity生命週期結束時,該Activity的引用依然被單例持有,因此不會被回收,而單例的生命週期又是跟應用同樣長,這個狀況就叫作內存泄露(Memory Leak)。它指的是當你再也不須要某個實例後,可是這個對象卻仍然被引用,防止被垃圾回收(Prevent from being bargage collected)。程序員
public class Util {
private Context mContext;
private static Util sInstance;
private Util(Context context) {
thi s.mContext = context;
}
public static Util getInstance(Context context) {
if (sInstance == null) {
sInstance = new Util(context);
}
return sInstance;
}
}
本傑明 富蘭克林曾說:A small leak will sink a great ship.意即:小漏不補沉大船。基於Android系統的設備通常來講內存就不大,特別是早期的Android設備,內存泄漏是很致命的,內存泄漏積攢到必定程度會引起內存溢出(OOM),若是處理不當直接致使程序崩潰退出。github
通常來講在開發中咱們常常會犯下下面幾個錯誤,致使內存泄漏。這幾個都是前人踩坑總結出來的,很是有參考價值,至少我在排查解決內存泄漏的時候是這樣的。
1、單例形成的內存泄漏
Android的單例模式很是受開發者的喜好,不過使用的不恰當的話也會形成內存泄漏。由於單例的靜態特性使得單例的生命週期和應用的生命週期同樣長,這就說明了若是一個對象已經不須要使用了,而單例對象還持有該對象的引用,那麼這個對象將不能被正常回收,這就致使了內存泄漏。例子見上面那段代碼。
2、非靜態內部類建立靜態實例形成的內存泄漏
有的時候咱們可能會在啓動頻繁的Activity中,爲了不重複建立相同的數據資源,在Activity內部建立了一個非靜態內部類的單例,每次啓動Activity時都會使用該單例的數據,這樣雖然避免了資源的重複建立,不過這種寫法卻會形成內存泄漏,由於非靜態內部類默認會持有外部類的引用,而又使用了該非靜態內部類建立了一個靜態的實例,該實例的生命週期和應用的同樣長,這就致使了該靜態實例一直會持有該Activity的引用,致使Activity的內存資源不能正常回收。例子以下算法
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (mManager == null) {
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
3、Handler形成的內存泄漏
Handler的使用形成的內存泄漏問題應該說最爲常見了,平時在處理網絡任務或者封裝一些請求回調等api都應該會藉助Handler來處理,咱們常常在Activity裏面這樣定義一個私有的Handler對象並初始化,這種建立Handler的方式會形成內存泄漏,因爲mHandler是Handler的非靜態匿名內部類的實例,因此它持有外部類Activity的引用,咱們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏。api
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
4、資源未關閉形成的內存泄漏
對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,不然這些資源將不會被回收,形成內存泄漏。bash
見到這個標題有經驗的開發者可能要吐槽我是標題黨了,特別是從Eclipse時代走過來的開發者,覺得我一要開始貼那張像同樣的MAT內存模型圖或者AndroidStudio中Monitors下的實時內存佔用圖,又要開始分析那一條條剪不斷理還亂的內存引用鏈,而後費盡九牛二虎之力去查找項目中無數的內存泄漏中的一個。可是,我要告訴你,你錯了。其實,之前我看到內存泄漏分析文章的時候也是這樣的想法,看着恐怖的MAT內存模型圖,以爲內存泄漏的排查和解決簡直是Android開發中登峯造極的技能。直到我遇到了她——LeakCanary,我才直到原來內存泄漏的排查和解決能夠那麼的優雅。LeakCanary**是Square開源了一個內存泄露自動探測神器 。這是項目的github倉庫地址:https://github.com/square/leakcanary 。使用很是簡單,在build.gradle中引入包依賴:markdown
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
在Application中的onCreate方法中增長初始化代碼:網絡
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
//You should not init your app in this process.
return;
}
LeakCanary.install(this);
集成後什麼都不用作,按照正常測試,當有內存泄漏發生後,應用會經過系統通知欄發出通知,點擊通知就能夠進入查看內存泄漏的具體信息。在這裏舉個實踐中的例子。把LeakCanary集成到項目中後,等App啓動後一會,系統通知到了,點擊通知,跳轉到泄漏的詳情頁面進行查看:
很明顯,WebSiteQueryActivity泄露了。首先,static 的MaskHeadView.fLayout變量引用了FrameLayout.mContext對象,這個對象的引用就是指向了WebSiteQueryActivity的實例,致使了它的泄漏,在第二節中咱們說過static對象是內部的static對象是比較容易形成內存泄漏的,檢查代碼發現,MaskHeadView直接在WebSiteQueryActivity的xml文件中使用了,所以持有WebSiteQueryActivity的實例,由於fLayout對象是靜態的,所以它的生命週期與Application一樣長,所以WebSiteQueryActivity退出後,它的實例引用依然被fLayout持有,致使它沒法被回收從而內存泄露了。仔細檢查代碼,發現fLayout並無被外部使用到,應該是以前的開發者手抖加了個static字段上去或者是如今不用了,可是沒有去掉,在這裏我直接去掉了這個修飾符,在此build代碼,這個內存泄漏的現象消失了。
//去掉static修飾符,避免static對象引發的內存泄漏
private static FrameLayout fLayout;
public MaskHeadView(Context context, AttributeSet attrs){super(context, attrs);
this.context=context;
initView(context);
}
private void initView(Context context2) {
view = LayoutInflater.from(context).inflate(R.layout.mask_head_view, this);
fLayout=(FrameLayout) view.findViewById(R.id.mask_container);
}
這只是個極簡單的例子,但方法是同樣的。順便提一句,其實不管是MAT工具的內存分析,仍是AndroidStudio中自帶的分析工具亦或是LeakCanary,原理都是同樣的,都是dump java heap出來進行分析,找到泄漏的問題,只是LeakCanary幫咱們把分析的工做作了。
曾幾什麼時候,你覺得內存泄漏分析都是這樣的
可是如今你會發現其實也能夠是醬紫的: