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