常見的內存泄漏原因及解決方法

(Memory Leak,內存泄漏)

為什么會產生內存泄漏?

當一個對象已經不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。

內存泄漏對程序的影響?

內存泄漏是造成應用程序OOM的主要原因之一。我們知道Android系統為每個應用程序分配的內存是有限的,而當一個應用中產生的內存泄漏比較多時,這就難免會導致應用所需要的內存超過系統分配的內存限額,這就造成了內存溢出從而導致應用Crash。

如何檢查和分析內存泄漏?

因為內存泄漏是在堆內存中,所以對我們來說并不是可見的。通常我們可以借助MAT、LeakCanary等工具來檢測應用程序是否存在內存泄漏。
1、MAT是一款強大的內存分析工具,功能繁多而復雜。
2、LeakCanary則是由Square開源的一款輕量級的第三方內存泄漏檢測工具,當檢測到程序中產生內存泄漏時,它將以最直觀的方式告訴我們哪里產生了內存泄漏和導致誰泄漏了而不能被回收。

常見的內存泄漏及解決方法

1、單例造成的內存泄漏

由于單例的靜態特性使得其生命周期和應用的生命周期一樣長,如果一個對象已經不再需要使用了,而單例對象還持有該對象的引用,就會使得該對象不能被正常回收,從而導致了內存泄漏。
示例:防止單例導致內存泄漏的實例

// 使用了單例模式
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

這樣不管傳入什么Context最終將使用Application的Context,而單例的生命周期和應用的一樣長,這樣就防止了內存泄漏。???

2、非靜態內部類創建靜態實例造成的內存泄漏

例如,有時候我們可能會在啟動頻繁的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(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
    
    class TestResource {
    //...
    }
}

這樣在Activity內部創建了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的數據。雖然這樣避免了資源的重復創建,但是這種寫法卻會造成內存泄漏。因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,從而導致Activity的內存資源不能被正常回收。
解決方法:將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,就使用Application的Context。

3、Handler造成的內存泄漏

示例:創建匿名內部類的靜態對象

public class MainActivity extends AppCompatActivity {

    private final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...
                handler.sendEmptyMessage(0x123);
            }
        });
    }
}

1、從Android的角度
當Android應用程序啟動時,該應用程序的主線程會自動創建一個Looper對象和與之關聯的MessageQueue。當主線程中實例化一個Handler對象后,它就會自動與主線程Looper的MessageQueue關聯起來。所有發送到MessageQueue的Messag都會持有Handler的引用,所以Looper會據此回調Handle的handleMessage()方法來處理消息。只要MessageQueue中有未處理的Message,Looper就會不斷的從中取出并交給Handler處理。另外,主線程的Looper對象會伴隨該應用程序的整個生命周期。
2、 Java角度
在Java中,非靜態內部類和匿名類內部類都會潛在持有它們所屬的外部類的引用,但是靜態內部類卻不會。

對上述的示例進行分析,當MainActivity結束時,未處理的消息持有handler的引用,而handler又持有它所屬的外部類也就是MainActivity的引用。這條引用關系會一直保持直到消息得到處理,這樣阻止了MainActivity被垃圾回收器回收,從而造成了內存泄漏。
解決方法:將Handler類獨立出來或者使用靜態內部類,這樣便可以避免內存泄漏。

4、線程造成的內存泄漏

示例:AsyncTask和Runnable

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        new Thread(new MyRunnable()).start();
        new MyAsyncTask(this).execute();
    }

    class MyAsyncTask extends AsyncTask<Void, Void, Void> {

        // ...

        public MyAsyncTask(Context context) {
            // ...
        }

        @Override
        protected Void doInBackground(Void... params) {
            // ...
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            // ...
        }
    }

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            // ...
        }
    }
}

AsyncTask和Runnable都使用了匿名內部類,那么它們將持有其所在Activity的隱式引用。如果任務在Activity銷毀之前還未完成,那么將導致Activity的內存資源無法被回收,從而造成內存泄漏。
解決方法:將AsyncTask和Runnable類獨立出來或者使用靜態內部類,這樣便可以避免內存泄漏。

5、資源未關閉造成的內存泄漏

對于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,從而造成內存泄漏。

1)比如在Activity中register了一個BraodcastReceiver,但在Activity結束后沒有unregister該BraodcastReceiver。
2)資源性對象比如Cursor,Stream、File文件等往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。它們的緩沖不僅存在于 java虛擬機內,還存在于java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄漏。
3)對于資源性對象在不使用的時候,應該調用它的close()函數將其關閉掉,然后再設置為null。在我們的程序退出時一定要確保我們的資源性對象已經關閉。
4)Bitmap對象不在使用時調用recycle()釋放內存。2.3以后的bitmap應該是不需要手動recycle了,內存已經在java層了。

6、使用ListView時造成的內存泄漏

初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化一定數量的View對象,同時ListView會將這些View對象緩存起來。當向上滾動ListView時,原先位于最上面的Item的View對象會被回收,然后被用來構造新出現在下面的Item。這個構造過程就是由getView()方法完成的,getView()的第二個形參convertView就是被緩存起來的Item的View對象(初始化時緩存中沒有View對象則convertView是null)。

構造Adapter時,沒有使用緩存的convertView。
解決方法:在構造Adapter時,使用緩存的convertView。

7、集合容器中的內存泄露

我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,并沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
解決方法:在退出程序之前,將集合里的東西clear,然后置為null,再退出程序。

8、WebView造成的泄露

當我們不要使用WebView對象時,應該調用它的destory()函數來銷毀它,并釋放其占用的內存,否則其長期占用的內存也不能被回收,從而造成內存泄露。
解決方法:為WebView另外開啟一個進程,通過AIDL與主線程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

如何避免內存泄漏?

1、在涉及使用Context時,對于生命周期比Activity長的對象應該使用Application的Context。凡是使用Context優先考慮Application的Context,當然它并不是萬能的,對于有些地方則必須使用Activity的Context。對于Application,Service,Activity三者的Context的應用場景如下:



其中,NO1表示Application和Service可以啟動一個Activity,不過需要創建一個新的task任務隊列。而對于Dialog而言,只有在Activity中才能創建。除此之外三者都可以使用。

2、對于需要在靜態內部類中使用非靜態外部成員變量(如:Context、View ),可以在靜態內部類中使用弱引用來引用外部類的變量來避免內存泄漏。
3、對于不再需要使用的對象,顯示的將其賦值為null,比如使用完Bitmap后先調用recycle(),再賦為null。
4、保持對對象生命周期的敏感,特別注意單例、靜態對象、全局性集合等的生命周期。
5、對于生命周期比Activity長的內部類對象,并且內部類中使用了外部類的成員變量,可以這樣做避免內存泄漏:
1)將內部類改為靜態內部類
2)靜態內部類中使用弱引用來引用外部類的成員變量

參考

Android內存泄漏總結

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,654評論 0 8
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,415評論 0 12
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    apkcore閱讀 1,237評論 2 7
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 802評論 0 5
  • ###集合類泄漏 集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變...
    RunningTeemo閱讀 585評論 0 0