在Android應(yīng)用程序中,經(jīng)常會(huì)出現(xiàn)內(nèi)存泄露的問題,可能一兩次的啟動(dòng)沒有對(duì)應(yīng)用產(chǎn)生什么崩潰呀之類的問題,但是長(zhǎng)時(shí)間使用或者頻繁退出進(jìn)入使用,因?yàn)閮?nèi)存泄露,應(yīng)用超過了系統(tǒng)允許的內(nèi)存范圍就會(huì)出現(xiàn)ANR,卡頓,OOM等問題。有經(jīng)驗(yàn)和注重性能的程序員們都會(huì)在編程過程中,注意代碼的編寫,避免內(nèi)存泄漏。這邊文章也是我自己邊學(xué)習(xí)邊總結(jié)邊應(yīng)用??偨Y(jié)下來,給自己解疑,好記性不如爛筆頭,用了之后再總結(jié)一下,真的更有助于對(duì)知識(shí)的吸收,更希望能夠幫助跟我一樣還在學(xué)習(xí)路上的人解疑,有什么不對(duì)的地方,多多提出來哈!開篇啦!
為什么會(huì)有內(nèi)存泄露?
Android程序中,對(duì)象與對(duì)象之間存在引用的關(guān)系,當(dāng)一個(gè)對(duì)象因?yàn)楸黄渌麑?duì)象引用,在用完之后無法GC回收一直存在內(nèi)存中的時(shí)候就會(huì)出現(xiàn)內(nèi)存泄漏,因?yàn)閷?duì)象無法被回收,所以一直存在內(nèi)存里面,隨著使用時(shí)間的增長(zhǎng),同一個(gè)對(duì)象在內(nèi)存中存在很多對(duì)象,占用了應(yīng)用的內(nèi)存越來越大。
所以內(nèi)存泄露的原因就是本該回收的對(duì)象沒有被GC回收,一直存留在內(nèi)存中。
從這點(diǎn)我們也可以知道我們的要解決內(nèi)存泄露的方法就是讓應(yīng)該回收的對(duì)象可以正常的被回收。
在這里我們簡(jiǎn)單的提一下那些對(duì)象需要由GC來回收。方法里面的定義的局部變量存儲(chǔ)在棧中,變量使用完之后就會(huì)自動(dòng)釋放回收變量。而new出來的對(duì)象都存放在堆里面,有Java回收器回收。
對(duì)象引用
在一起學(xué)習(xí)什么時(shí)候?qū)ο笾g會(huì)存在引用關(guān)系之前,首先來將一下Android中四種引用的弱引用,強(qiáng)引用,軟引用,虛引用。
弱引用
關(guān)鍵詞WeakReference.定義。弱引用當(dāng)只有弱引用在引用時(shí),GC會(huì)回收該弱引用。強(qiáng)引用
直接new的對(duì)象都是強(qiáng)引用,強(qiáng)引用,只有沒有對(duì)象在引用他的時(shí)候,才會(huì)釋放掉。軟引用
關(guān)鍵詞SoftReference定義。軟引用的比較少,如果一個(gè)對(duì)象只具有軟引用,那么在內(nèi)存空間足夠時(shí),垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對(duì)象的內(nèi)存。只要垃圾回收器沒有回收它,該對(duì)象就可以被使用。虛引用
PhantomReference引用。
對(duì)象的引用這個(gè)概念之前也知道一些,但是經(jīng)過這次對(duì)內(nèi)存泄露的學(xué)習(xí)之后,對(duì)于自己以前不知道的引用的情況有了新的理解。下面我們來一起學(xué)習(xí)下引用吧。對(duì)了,這是基礎(chǔ)知識(shí)的講解。對(duì)于引用有深刻理解的同仁們可以直接跳下一節(jié)。
首先我們首先來看看最簡(jiǎn)單的引用的情況:
假設(shè)有兩個(gè)類,類A和類B。
① class A {
private B b;
public A() {
b = new B();
}
}
class B{
}
如上述代碼所示, 在A里面創(chuàng)建了一個(gè)B的實(shí)例,并且賦給了b,這樣A就引用b。因?yàn)閎是A的成員變量所以當(dāng)A釋放的時(shí)候,b也會(huì)被釋放。
② 當(dāng)我們?cè)贏中分別定義一個(gè)內(nèi)部靜態(tài)類和一個(gè)內(nèi)部非靜態(tài)類,以及添加一個(gè)外部接口。RefenrenceListener如下:
interface RefenrenceListener {
void onClick();
}
類A修改成這個(gè)樣子:
class A{
private C c ;
private D d;
// 非靜態(tài)匿名內(nèi)部類會(huì)引用外部類
class C {
}
//靜態(tài)匿名內(nèi)部類不會(huì)引用外部類
static class D{
}
public A(){
c = new C();
//拿有外部類的引用
d = new D(); //不引用外部類
setRefenrenceListener(new RefenrenceListenr(){
void onClick(){
Log.i("doris", "onclick");
} );
} //匿名內(nèi)部類RefenrenceListener會(huì)有外部類的引用
}
/* 靜態(tài)的匿名內(nèi)部類成員變量不引用外部類 */
private static RefenrenceListenr mRefenrenceListenr = new RefenrenceListenr() {
@Override
public void onclick() {
}
};
}
/***關(guān)于引用關(guān)系可以用個(gè)方法獲取到某個(gè)對(duì)象的引用情況
用法: getAllFieldName(c.getClass())***/
private void getAllFieldName(Class<?> clazz) {
System.out.println("className: ======> " + clazz.getSimpleName());
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
}
從上面總結(jié)可以得到:
- 靜態(tài)匿名內(nèi)部類成員變量不引用外部類。其他的非靜態(tài)匿名類成員變量,以及匿名內(nèi)部類局部變量都會(huì)引用外部類。
- 非靜態(tài)內(nèi)部類一定引用外部類。
- 靜態(tài)內(nèi)部類不引用外部類
- 方法內(nèi)的局部?jī)?nèi)部類和匿名內(nèi)部類都會(huì)引用外部類。
- 當(dāng)某個(gè)對(duì)象是某個(gè)類的成員變量的時(shí)候那么就一定引用了這個(gè)對(duì)象。
從上面我們知道了,對(duì)象之間引用關(guān)系的形成,但是并不是所有的應(yīng)用都會(huì)引起內(nèi)存泄露。主要看,引用對(duì)象與被引用對(duì)象那個(gè)生命周期比較長(zhǎng),如果引用對(duì)象生命周期長(zhǎng)于本來應(yīng)該釋放的對(duì)象的話,那就會(huì)引起內(nèi)存泄露。注意這句話,不管是那種引用其實(shí)都不是百分之百的會(huì)引起內(nèi)存泄露,只有當(dāng)引用對(duì)象生命周期夠長(zhǎng)的時(shí)候才會(huì)引起內(nèi)存泄露。 曾經(jīng)以為只要有非靜態(tài)內(nèi)部類就一定會(huì)引起內(nèi)存泄露、而且當(dāng)我知道回調(diào)也引用外部類的時(shí)候,我開始想那這內(nèi)存泄露是避免不了了,總不能不用回調(diào)吧。是不是很傻。還好學(xué)習(xí)更多,讓我知道了更多。所以記住哦?。。?!
常見內(nèi)存泄露的原因
- 非靜態(tài)匿名內(nèi)部類和非靜態(tài)內(nèi)部類會(huì)引用外部類。
- 回調(diào)有可能會(huì)引起內(nèi)存泄露,如果回調(diào)對(duì)象被靜態(tài)對(duì)象引用或者其他原因引用而無法釋放,就會(huì)導(dǎo)致內(nèi)存泄露。
- Dialog有可能引發(fā)泄露
- 非靜態(tài)Handler引用外部類引起內(nèi)存泄露
- 線程,動(dòng)畫等無限循環(huán)執(zhí)行,引用了需要釋放的對(duì)象,也會(huì)引起內(nèi)存泄露
- 靜態(tài)成員集合類和靜態(tài)View對(duì)象 以及靜態(tài)的非靜態(tài)成員變量
- 單例類
- 資源未關(guān)閉導(dǎo)致的泄露。BroadcastReceiver未解除注冊(cè)
當(dāng)然要記住 不是說有強(qiáng)引用就一定會(huì)引起內(nèi)存泄露,關(guān)鍵看可能泄露的對(duì)象和引用他的對(duì)象那個(gè)生命力更能頑強(qiáng)。所以我們要做的就是保證不該被強(qiáng)引用的不被強(qiáng)引用, 被強(qiáng)引用了也不會(huì)釋放。
經(jīng)典案例
下面我將舉例一些常見的內(nèi)存泄露的例子,并給出相應(yīng)的解決方法。
- 最最經(jīng)典的Handler內(nèi)存泄露
在程序中,我們通常會(huì)在一個(gè)類里面定義個(gè)內(nèi)部類Handler用來對(duì)消息的處理,從而達(dá)到異步處理。如果我們定義一個(gè)非靜態(tài)匿名內(nèi)部類的話,那么Handler就會(huì)持有外部類的引用,假設(shè)外部類是Activity。那么就會(huì)導(dǎo)致當(dāng)Activity退出之后,Handler如果還持有消息的話,就會(huì)無法釋放Activity。所以我們應(yīng)該定義一個(gè)靜態(tài)的內(nèi)部Handler類。但是我們要使用外部類的方法以及變量那該怎么辦呢,那也好辦這個(gè)時(shí)候就要用到弱引用了。弱引用就是當(dāng)對(duì)象只被弱引用引用的時(shí)候,會(huì)被GC回收。
代碼如下:
static class MyHandler extends WeakHandler<MainActivity> {
public MyHandler(MainActivity owner) {
super(owner);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (getOwner() == null) {
return;
}
if (msg.what == MSG_SHOW_REPLAYEPG_WINDOW) {
if (getOwner().mInfoBar != null) {
getOwner().mInfoBar.dismisss();
}
if (getOwner().mLookBackView == null) {
getOwner().mLookBackView = new LookBackView(getOwner());
getOwner().mLookBackView.setOnScheduleListItemListener(getOwner().mOnListItemClickListener);
}
getOwner().mLookBackView.showLookBackWindow();
}
}
}
其實(shí)追其根本,非靜態(tài)的Handler之所以會(huì)導(dǎo)致內(nèi)存泄漏就是因?yàn)椋庆o態(tài)的內(nèi)部類會(huì)引用外部類,當(dāng)外部類銷毀,而Handler仍然有消息要處理,未銷毀的時(shí)候,外部類就無法銷毀而引發(fā)了Activity的內(nèi)存泄漏。所以觸類旁通,所有的非靜態(tài)的內(nèi)部類如果引用了Activity,都有可能引起內(nèi)存泄漏,這個(gè)時(shí)候我們就應(yīng)該像Handler這樣處理,改成靜態(tài)類并且使用弱引用引用。如果可以當(dāng)然能不引用,外部類就不引用。
- Thread
在線程中可能發(fā)引發(fā)內(nèi)存泄漏的原因有,應(yīng)用退出之后,線程沒有停止,并且引用了資源,導(dǎo)致資源無法釋放,從而引起內(nèi)存泄漏,所以要注意線程的停止以及資源的釋放。 這里就不列舉代碼了。 - 單例
單例中比較容易引發(fā)內(nèi)存泄漏的情況是靜態(tài)變量引用了本需要被釋放掉的資源,比如Activity,導(dǎo)致Activity無法釋放。下面舉例說明:
public class ServerManager {
public static ServerManager mServerManager;
private Context mContext;
private ServerManager(Context context) {
mContext = context;
}
public static ServerManager getInstance(Context context) {
if (mServerManager == null) {
mServerManager = new ServerManager(context);
}
return mServerManager;
}
}
外部調(diào)用:
public class MainActivity extends AppCompatActivity {
private Button mBtnNotification;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ServerManager serverManager = ServerManager.getInstance(this);
}
}
上面的context引用有問題,問題在哪里呢,單例類ServerManager的實(shí)例是一個(gè)靜態(tài)變量,然后引用了context。context是一個(gè)指向MainActivity類的對(duì)象,那么
回調(diào)被別的靜態(tài)對(duì)象引用。當(dāng)MainActivity銷毀的時(shí)候就出現(xiàn)問題了,由于mServerManager是一個(gè)靜態(tài)變量所以一直存在于內(nèi)存中,并持有mainactivity的引用,所以MainActvity無法釋放。這是一個(gè)典型的以及我們很容易出錯(cuò)的地方,但是也是很容易修改的地方,那就是將MainActivity中的this改成
getApplicationContext(),從而ServerManager就不會(huì)影響Activity的釋放。
這里也引申除了一個(gè)新的點(diǎn),那就是能用ApplicationContext的地方,千萬不要用Activity的context,這樣就可以避免不必要的內(nèi)存泄漏。靜態(tài)成員變量引用類引發(fā)的內(nèi)存泄漏
靜態(tài)變量如果引用了資源,也會(huì)導(dǎo)致資源無法釋放。
總結(jié)
其實(shí)內(nèi)存泄漏的本質(zhì),就是要釋放的對(duì)象由于被GC-ROOt引用而導(dǎo)致無法釋放,這就有可能引起內(nèi)存泄漏,之所以說有可能還是因?yàn)椋惨匆尫诺膶?duì)象和引用他的對(duì)象那個(gè)對(duì)象生命長(zhǎng)。如果引用他的GC-root對(duì)象的生命長(zhǎng)于要釋放的對(duì)象,那么就會(huì)引發(fā)內(nèi)存泄漏。 而解決內(nèi)存泄漏的方法的本質(zhì)就是1.避免造成可能引發(fā)內(nèi)存泄漏的情況。2.當(dāng)不可避免的要引用的時(shí)候,記得在使用完之后及時(shí)的釋放資源,切換引用鏈中導(dǎo)致內(nèi)存泄漏的關(guān)鍵點(diǎn) 3.圖片資源等用完之后記得及時(shí)釋放。
這是我自己學(xué)習(xí)查閱了相關(guān)的內(nèi)存泄漏以及實(shí)踐之后的一些總結(jié),如果有哪里寫的不對(duì)的話,歡迎指出,以防他繼續(xù)遺丑萬年。哈哈。
最后貼出我覺得寫的很不錯(cuò)的文章,雖然人家已經(jīng)寫的很不錯(cuò)了,但是自己總結(jié)一番對(duì)自己吸收這個(gè)知識(shí)是很有幫助的。所以總結(jié)的博客 ,不只是為了給更多的人看,其實(shí)也是個(gè)人知識(shí)體系構(gòu)建的過程。你也可以一起來建立,貴在堅(jiān)持,簡(jiǎn)單的一句加油,共勉!
參考鏈接:
Android內(nèi)存泄露案例分析