前言
對(duì)于C++來(lái)說(shuō)眼虱,內(nèi)存泄漏就是new出來(lái)的對(duì)象沒(méi)有delete马胧,俗稱(chēng)野指針宴卖;
對(duì)于Java來(lái)說(shuō)滋将,就是new出來(lái)的Object 放在Heap上無(wú)法被GC回收;
本文通過(guò)QQ和Qzone中內(nèi)存泄漏實(shí)例來(lái)講android中內(nèi)存泄漏分析解法和編寫(xiě)代碼應(yīng)注意的事項(xiàng)症昏。
Java 中的內(nèi)存分配
靜態(tài)儲(chǔ)存區(qū):編譯時(shí)就分配好随闽,在程序整個(gè)運(yùn)行期間都存在。它主要存放靜態(tài)數(shù)據(jù)和常量肝谭;
棧區(qū):當(dāng)方法執(zhí)行時(shí)橱脸,會(huì)在棧區(qū)內(nèi)存中創(chuàng)建方法體內(nèi)部的局部變量,方法結(jié)束后自動(dòng)釋放內(nèi)存分苇;
堆區(qū):通常存放 new 出來(lái)的對(duì)象。由 Java 垃圾回收器回收屁桑。
四種引用類(lèi)型的介紹
強(qiáng)引用(StrongReference):JVM 寧可拋出 OOM 医寿,也不會(huì)讓 GC 回收具有強(qiáng)引用的對(duì)象;
軟引用(SoftReference):只有在內(nèi)存空間不足時(shí)蘑斧,才會(huì)被回的對(duì)象靖秩;
弱引用(WeakReference):在 GC 時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象竖瘾,不管當(dāng)前內(nèi)存空間足夠與否沟突,都會(huì)回收它的內(nèi)存;
虛引用(PhantomReference):任何時(shí)候都可以被GC回收捕传,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí)惠拭,如果發(fā)現(xiàn)它還有虛引用,就會(huì)在回收對(duì)象的內(nèi)存之前,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中职辅。程序可以通過(guò)判斷引用隊(duì)列中是否存在該對(duì)象的虛引用棒呛,來(lái)了解這個(gè)對(duì)象是否將要被回收∮蛐可以用來(lái)作為GC回收Object的標(biāo)志簇秒。
我們常說(shuō)的內(nèi)存泄漏是指new出來(lái)的Object無(wú)法被GC回收,即為強(qiáng)引用:
內(nèi)存泄漏發(fā)生時(shí)的主要表現(xiàn)為內(nèi)存抖動(dòng)秀鞭,可用內(nèi)存慢慢變少:
Andriod 中分析內(nèi)存泄漏的工具M(jìn)AT
MAT(Memory Analyzer Tools)是一個(gè) Eclipse 插件趋观,它是一個(gè)快速、功能豐富的JAVA heap分析工具锋边,它可以幫助我們查找內(nèi)存泄漏和減少內(nèi)存消耗皱坛。
MAT 插件的下載地址:
MAT 使用方法介紹:
http://www.cnblogs.com/larack/p/6071209.html
QQ 和 Qzone內(nèi)存泄漏如何監(jiān)控
QQ和Qzone 的內(nèi)存泄漏采用SNGAPM解決方案,SNGAPM是一個(gè)性能監(jiān)控宠默、分析的統(tǒng)一解決方案麸恍,它從終端收集性能信息,上報(bào)到一個(gè)后臺(tái)搀矫,后臺(tái)將監(jiān)控類(lèi)信息聚合展示為圖表抹沪,將分析類(lèi)信息進(jìn)行分析并提單,通知開(kāi)發(fā)者瓤球;
1.SNGAPM由App(MagnifierApp)和 web server(MagnifierServer)兩部分組成融欧;
2.MagnifierApp在自動(dòng)內(nèi)存泄漏檢測(cè)中是一個(gè)銜接檢測(cè)組件(LeakInspector)和自動(dòng)化云分析(MagnifierCloud)的中間性平臺(tái),它從LeakInspector的內(nèi)存dump自動(dòng)化上傳MagnifierServer卦羡;
3.MagnifierServer后臺(tái)會(huì)定時(shí)提交分析任務(wù)到MagnifierCloud噪馏;
4.MagnifierCloud分析結(jié)束之后會(huì)更新數(shù)據(jù)到magnifier web上,同時(shí)以bug單形式通知開(kāi)發(fā)者绿饵。
常見(jiàn)的內(nèi)存泄漏案例
case 1. 單例造成的內(nèi)存泄露
單例的靜態(tài)特性導(dǎo)致其生命周期同應(yīng)用一樣長(zhǎng)欠肾。
解決方案:
將該屬性的引用方式改為弱引用;
如果傳入Context,使用ApplicationContext;
example:泄漏代碼片段
private static ScrollHelper mInstance; private ScrollHelper() {
} public static ScrollHelper getInstance() {
if (mInstance == null) {
synchronized (ScrollHelper.class) {
if (mInstance == null) {
mInstance = new ScrollHelper();
}
}
}
return mInstance;
} /**
* 被點(diǎn)擊的view
*/
private View mScrolledView = null; public void setScrolledView(View scrolledView) {
mScrolledView = scrolledView;
}
Solution:使用WeakReference
private static ScrollHelper mInstance; private ScrollHelper() {
} public static ScrollHelper getInstance() {
if (mInstance == null) {
synchronized (ScrollHelper.class) {
if (mInstance == null) {
mInstance = new ScrollHelper();
}
}
}
return mInstance;
} /**
* 被點(diǎn)擊的view
*/
private WeakReference<View> mScrolledViewWeakRef = null; public void setScrolledView(View scrolledView) {
mScrolledViewWeakRef = new WeakReference<View>(scrolledView);
}
case 2. InnerClass匿名內(nèi)部類(lèi)
在Java中拟赊,非靜態(tài)內(nèi)部類(lèi) 和 匿名類(lèi) 都會(huì)潛在的引用它們所屬的外部類(lèi)刺桃,但是,靜態(tài)內(nèi)部類(lèi)卻不會(huì)吸祟。如果這個(gè)非靜態(tài)內(nèi)部類(lèi)實(shí)例做了一些耗時(shí)的操作瑟慈,就會(huì)造成外圍對(duì)象不會(huì)被回收,從而導(dǎo)致內(nèi)存泄漏屋匕。
解決方案:
將內(nèi)部類(lèi)變成靜態(tài)內(nèi)部類(lèi);
如果有強(qiáng)引用Activity中的屬性葛碧,則將該屬性的引用方式改為弱引用;
在業(yè)務(wù)允許的情況下,當(dāng)Activity執(zhí)行onDestory時(shí)过吻,結(jié)束這些耗時(shí)任務(wù);
example:
public class LeakAct extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aty_leak);
test();
}
//這兒發(fā)生泄漏
public void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
Solution:
public class LeakAct extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aty_leak);
test();
}
//加上static进泼,變成靜態(tài)匿名內(nèi)部類(lèi)
public static void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
case 3. Activity Context 的不正確使用
在Android應(yīng)用程序中通常可以使用兩種Context對(duì)象:Activity和Application。當(dāng)類(lèi)或方法需要Context對(duì)象的時(shí)候常見(jiàn)的做法是使用第一個(gè)作為Context參數(shù)缘琅。這樣就意味著View對(duì)象對(duì)整個(gè)Activity保持引用粘都,因此也就保持對(duì)Activty的所有的引用。
假設(shè)一個(gè)場(chǎng)景刷袍,當(dāng)應(yīng)用程序有個(gè)比較大的Bitmap類(lèi)型的圖片翩隧,每次旋轉(zhuǎn)是都重新加載圖片所用的時(shí)間較多。為了提高屏幕旋轉(zhuǎn)是Activity的創(chuàng)建速度呻纹,最簡(jiǎn)單的方法時(shí)將這個(gè)Bitmap對(duì)象使用Static修飾堆生。 當(dāng)一個(gè)Drawable綁定在View上,實(shí)際上這個(gè)View對(duì)象就會(huì)成為這份Drawable的一個(gè)Callback成員變量雷酪。而靜態(tài)變量的生命周期要長(zhǎng)于Activity淑仆。導(dǎo)致了當(dāng)旋轉(zhuǎn)屏幕時(shí),Activity無(wú)法被回收哥力,而造成內(nèi)存泄露蔗怠。
解決方案:
使用ApplicationContext代替ActivityContext,因?yàn)锳pplicationContext會(huì)隨著應(yīng)用程序的存在而存在吩跋,而不依賴(lài)于activity的生命周期寞射;
對(duì)Context的引用不要超過(guò)它本身的生命周期,慎重的對(duì)Context使用“static”關(guān)鍵字锌钮。Context里如果有線(xiàn)程桥温,一定要在onDestroy()里及時(shí)停掉。
example:
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
Solution:
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getApplicationContext().getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
case 4. Handler引起的內(nèi)存泄漏
當(dāng)Handler中有延遲的的任務(wù)或是等待執(zhí)行的任務(wù)隊(duì)列過(guò)長(zhǎng)梁丘,由于消息持有對(duì)Handler的引用侵浸,而Handler又持有對(duì)其外部類(lèi)的潛在引用,這條引用關(guān)系會(huì)一直保持到消息得到處理氛谜,而導(dǎo)致了Activity無(wú)法被垃圾回收器回收掏觉,而導(dǎo)致了內(nèi)存泄露。
解決方案:
可以把Handler類(lèi)放在單獨(dú)的類(lèi)文件中值漫,或者使用靜態(tài)內(nèi)部類(lèi)便可以避免泄露;
如果想在Handler內(nèi)部去調(diào)用所在的Activity,那么可以在handler內(nèi)部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式來(lái)達(dá)到斷開(kāi)Handler與Activity之間存在引用關(guān)系的目的履腋。
Solution:
@Override
protected void doOnDestroy() {
super.doOnDestroy();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
mHandler = null;
mRenderCallback = null;
}
case 5. 注冊(cè)監(jiān)聽(tīng)器的泄漏
系統(tǒng)服務(wù)可以通過(guò)Context.getSystemService 獲取,它們負(fù)責(zé)執(zhí)行某些后臺(tái)任務(wù)惭嚣,或者為硬件訪(fǎng)問(wèn)提供接口。如果Context 對(duì)象想要在服務(wù)內(nèi)部的事件發(fā)生時(shí)被通知悔政,那就需要把自己注冊(cè)到服務(wù)的監(jiān)聽(tīng)器中晚吞。然而,這會(huì)讓服務(wù)持有Activity 的引用谋国,如果在Activity onDestory時(shí)沒(méi)有釋放掉引用就會(huì)內(nèi)存泄漏槽地。
解決方案:
使用ApplicationContext代替ActivityContext;
在Activity執(zhí)行onDestory時(shí),調(diào)用反注冊(cè);
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
Solution:
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
下面是容易造成內(nèi)存泄漏的系統(tǒng)服務(wù):
InputMethodManager imm = (InputMethodManager) context.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
Solution:
protected void onDetachedFromWindow() {
if (this.mActionShell != null) { this.mActionShell.setOnClickListener((OnAreaClickListener)null);
}
if (this.mButtonShell != null) {
this.mButtonShell.setOnClickListener((OnAreaClickListener)null);
}
if (this.mCountShell != this.mCountShell) { this.mCountShell.setOnClickListener((OnAreaClickListener)null);
}
super.onDetachedFromWindow();
}
case 6. Cursor,Stream沒(méi)有close捌蚊,View沒(méi)有recyle
資源性對(duì)象比如(Cursor集畅,F(xiàn)ile文件等)往往都用了一些緩沖,我們?cè)诓皇褂玫臅r(shí)候缅糟,應(yīng)該及時(shí)關(guān)閉它們挺智,以便它們的緩沖及時(shí)回收內(nèi)存。它們的緩沖不僅存在于 java虛擬機(jī)內(nèi)窗宦,還存在于java虛擬機(jī)外赦颇。如果我們僅僅是把它的引用設(shè)置為null,而不關(guān)閉它們,往往會(huì)造成內(nèi)存泄漏赴涵。因?yàn)橛行┵Y源性對(duì)象媒怯,比如SQLiteCursor(在析構(gòu)函數(shù)finalize(),如果我們沒(méi)有關(guān)閉它,它自己會(huì)調(diào)close()關(guān)閉)髓窜,如果我們沒(méi)有關(guān)閉它扇苞,系統(tǒng)在回收它時(shí)也會(huì)關(guān)閉它,但是這樣的效率太低了寄纵。因此對(duì)于資源性對(duì)象在不使用的時(shí)候鳖敷,應(yīng)該調(diào)用它的close()函數(shù),將其關(guān)閉掉擂啥,然后才置為null. 在我們的程序退出時(shí)一定要確保我們的資源性對(duì)象已經(jīng)關(guān)閉哄陶。
Solution:
調(diào)用onRecycled()
@Override
public void onRecycled() {
reset();
mSinglePicArea.onRecycled();
}
在View中調(diào)用reset()
public void reset() { if (mHasRecyled) {
return;
}
...
SubAreaShell.recycle(mActionBtnShell);
mActionBtnShell = null;
...
mIsDoingAvatartRedPocketAnim = false;
if (mAvatarArea != null) {
mAvatarArea.reset();
}
if (mNickNameArea != null) {
mNickNameArea.reset();
}
}
case 7. 集合中對(duì)象沒(méi)清理造成的內(nèi)存泄漏
我們通常把一些對(duì)象的引用加入到了集合容器(比如ArrayList)中,當(dāng)我們不需要該對(duì)象時(shí)哺壶,并沒(méi)有把它的引用從集合中清理掉屋吨,這樣這個(gè)集合就會(huì)越來(lái)越大。如果這個(gè)集合是static的話(huà)山宾,那情況就更嚴(yán)重了至扰。
所以要在退出程序之前,將集合里的東西clear资锰,然后置為null敢课,再退出程序。
解決方案:
在Activity退出之前绷杜,將集合里的東西clear直秆,然后置為null,再退出程序鞭盟。
Solution
private List<EmotionPanelInfo> data; public void onDestory() {
if (data != null) {
data.clear();
data = null;
}
}
case 8. WebView造成的泄露
當(dāng)我們不要使用WebView對(duì)象時(shí)圾结,應(yīng)該調(diào)用它的destory()函數(shù)來(lái)銷(xiāo)毀它,并釋放其占用的內(nèi)存齿诉,否則其占用的內(nèi)存長(zhǎng)期也不能被回收筝野,從而造成內(nèi)存泄露晌姚。
解決方案:
為webView開(kāi)啟另外一個(gè)進(jìn)程,通過(guò)AIDL與主線(xiàn)程進(jìn)行通信歇竟,WebView所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(jī)進(jìn)行銷(xiāo)毀挥唠,從而達(dá)到內(nèi)存的完整釋放。
case 9. 構(gòu)造Adapter時(shí)焕议,沒(méi)有使用緩存的ConvertView
初始時(shí)ListView會(huì)從Adapter中根據(jù)當(dāng)前的屏幕布局實(shí)例化一定數(shù)量的View對(duì)象宝磨,同時(shí)ListView會(huì)將這些View對(duì)象 緩存起來(lái)。
當(dāng)向上滾動(dòng)ListView時(shí)号坡,原先位于最上面的List Item的View對(duì)象會(huì)被回收懊烤,然后被用來(lái)構(gòu)造新出現(xiàn)的最下面的List Item。
這個(gè)構(gòu)造過(guò)程就是由getView()方法完成的宽堆,getView()的第二個(gè)形參View ConvertView就是被緩存起來(lái)的List Item的View對(duì)象(初始化時(shí)緩存中沒(méi)有View對(duì)象則ConvertView是null)腌紧。