android 內(nèi)存泄漏(多文章摘錄)

內(nèi)存泄漏

gc沒有辦法回收activity的內(nèi)存。

垃圾回收(GC)

垃圾回收或GC(Garbage Collection),是一種自動的存儲管理機制,它是Java語言的一大特性,把內(nèi)存釋放工作的壓力都轉(zhuǎn)讓到了系統(tǒng),故而是以消耗系統(tǒng)性能為代價的。C++編碼的時候,我們需要自己實現(xiàn)析構(gòu)函數(shù)來進行內(nèi)存釋放,很麻煩,而且非常容易遺漏而最終導(dǎo)致程序崩掉。所以Java語言就引入了自動內(nèi)存管理的機制,也就是垃圾回收機制,針對的主要的內(nèi)存的堆區(qū)域,關(guān)于內(nèi)存的分配機制。

一些常見的內(nèi)存泄漏及解決方案

一:單例設(shè)計模式造成的內(nèi)存泄漏:

單例設(shè)計模式的靜態(tài)特性會使他的生命周期和應(yīng)用程序的生命周期一樣長,這就說明了如果一個對象不在使用了,而這時單例對象還在持有該對象的引用,這時GC就會無法回收該對象,造成了內(nèi)存泄露的情況。
下面是錯誤的單例設(shè)計模式的代碼:

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;
    }
}

上面的代碼是一個最普通的單例模式,但是需要注意兩個問題:
1、如果我們傳入的Context是Application的Context的話,就沒有任何問題,因為Application的Context生命周期和應(yīng)用程序生命周期一樣長。
2、如果我們傳入的Context是Activity的Context的話,這時如果我們因為需求銷毀了該Activity的話,Context也會隨著Activity被銷毀,但是單例還在持有對該類對象的引用,這時就會造成內(nèi)存泄漏。

所以,正確的單例模式寫法應(yīng)該是這樣的:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

這樣的話不管我們傳入什么樣的Context,最終使用的都是Application的Context,單例的生命周期和應(yīng)用一樣長,這樣就不會造成內(nèi)存泄漏了。

二、靜態(tài)變量導(dǎo)致內(nèi)存泄露

靜態(tài)變量存儲在方法區(qū),它的生命周期從類加載開始,到整個進程結(jié)束。一旦靜態(tài)變量初始化后,它所持有的引用只有等到進程結(jié)束才會釋放。

比如下面這樣的情況,在Activity中為了避免重復(fù)的創(chuàng)建info,將sInfo作為靜態(tài)變量:

public class MainActivity extends AppCompatActivity {

    private static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInfo != null) {
            sInfo = new Info(this);
        }
    }
}

class Info {
    public Info(Activity activity) {
    }
}

Info作為Activity的靜態(tài)成員,并且持有Activity的引用,但是sInfo作為靜態(tài)變量,生命周期肯定比Activity長。所以當Activity退出后,sInfo仍然引用了Activity,Activity不能被回收,這就導(dǎo)致了內(nèi)存泄露。

在Android開發(fā)中,靜態(tài)持有很多時候都有可能因為其使用的生命周期不一致而導(dǎo)致內(nèi)存泄露,所以我們在新建靜態(tài)持有的變量的時候需要多考慮一下各個成員之間的引用關(guān)系,并且盡量少地使用靜態(tài)持有的變量,以避免發(fā)生內(nèi)存泄露。當然,我們也可以在適當?shù)臅r候講靜態(tài)量重置為null,使其不再持有引用,這樣也可以避免內(nèi)存泄露。

三、非靜態(tài)內(nèi)部類創(chuàng)建的靜態(tài)實例造成的內(nèi)存泄漏

有時候因為需求我們會去頻繁的啟動一個Activity,這時為了避免頻繁的創(chuàng)建相同的數(shù)據(jù)源,我們通常會做如下處理:

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 {
 
        //...
 
    }
 
}

這樣就在Activity中創(chuàng)建了非靜態(tài)內(nèi)部類,非靜態(tài)內(nèi)部類默認持有Activity類的引用,但是他的生命周期還是和應(yīng)用程序一樣長,所以當Activity銷毀時,靜態(tài)內(nèi)部類的對象引用不會被GC回收,就會造成了內(nèi)存溢出,解決辦法:
1、將內(nèi)部類改為靜態(tài)內(nèi)部類。
2、將這個內(nèi)部類封裝成一個單例,Context使用Application的Context

四、Handler造成的內(nèi)存泄漏:

不規(guī)范的Handler寫法:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //...
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

這里的handler也是一個非靜態(tài)匿名內(nèi)部類,他跟上面的一樣,也會持有Activity的引用,我們知道handler是運行在一個Looper線程中的,而Looper線程是輪詢來處理消息隊列中的消息的,假設(shè)我們處理的消息有十條,而當他執(zhí)行到第6條的時候,用戶點擊了back返回鍵,銷毀了當前的Activity,這個時候消息還沒有處理完,handler還在持有Activity的引用,這個時候就會導(dǎo)致無法被GC回收,造成了內(nèi)存泄漏。正確的做法是:

public class MainActivity extends AppCompatActivity {
//new一個自定義的Handler
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
 
//自定義靜態(tài)內(nèi)部類繼承自Handler
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
//在構(gòu)造函數(shù)中使用弱引用來引用context對象
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }
  
    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
 
@Override
  protected void onDestroy() {
      super.onDestroy();
//移除隊列中所有的Runable和消息
//這里也可以使用mHandler.removeMessage和mHandler.removeCallBacks來移除指定的Message和Runable
      mHandler.removeCallbacksAndMessages(null);
  }
}

創(chuàng)建一個靜態(tài)內(nèi)部類繼承自handler,然后再在構(gòu)造參數(shù)中對handler持有的對象做弱引用,這樣在回收時就會回收了handler持有的對象,這里還做了一處修改,就是當我們的回收了handler持有的對向,即銷毀了該Activity時,這時如果handler中的還有未處理的消息,我們就需要在OnDestry方法中移除消息隊列中的消息。

五、線程造成的內(nèi)存泄漏

線程使用不恰當造成的內(nèi)存泄漏也是很常見的,下面舉兩個例子:

      //——————test1
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();
        //——————test2
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();

上面是兩個內(nèi)部類,當我們的Activity銷毀時,這兩個任務(wù)沒有執(zhí)行完畢,就會使Activity的內(nèi)存資源無法被回收,造成了內(nèi)存泄漏。
正確的做法是使用靜態(tài)內(nèi)部類:如下

static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;
  
        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }
  
        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }
  
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
//——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();

這樣就避免了內(nèi)存泄漏,當然在Activity銷毀時也要記得在OnDestry中調(diào)用AsyncTask.cancal()方法來取消相應(yīng)的任務(wù)。避免在后臺運行浪費資源。

六、資源未關(guān)閉造成的內(nèi)存泄漏

在使用完BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源時,一定要在Activity中的OnDestry中及時的關(guān)閉、注銷或者釋放內(nèi)存,

否則這些資源不會被GC回收,就會造成內(nèi)存泄漏。

七、Timer和TimerTask導(dǎo)致內(nèi)存泄露

Timer和TimerTask在Android中通常會被用來做一些計時或循環(huán)任務(wù),比如實現(xiàn)無限輪播的ViewPager:

public class MainActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private PagerAdapter mAdapter;
    private Timer mTimer;
    private TimerTask mTimerTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
        mTimer.schedule(mTimerTask, 3000, 3000);
    }

    private void init() {
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mAdapter = new ViewPagerAdapter();
        mViewPager.setAdapter(mAdapter);

        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        loopViewpager();
                    }
                });
            }
        };
    }

    private void loopViewpager() {
        if (mAdapter.getCount() > 0) {
            int curPos = mViewPager.getCurrentItem();
            curPos = (++curPos) % mAdapter.getCount();
            mViewPager.setCurrentItem(curPos);
        }
    }

    private void stopLoopViewPager() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopLoopViewPager();
    }
}

當我們Activity銷毀的時,有可能Timer還在繼續(xù)等待執(zhí)行TimerTask,它持有Activity的引用不能被回收,因此當我們Activity銷毀的時候要立即cancel掉Timer和TimerTask,以避免發(fā)生內(nèi)存泄漏。

集合中的對象未清理造成內(nèi)存泄露

這個比較好理解,如果一個對象放入到ArrayList、HashMap等集合中,這個集合就會持有該對象的引用。當我們不再需要這個對象時,也并沒有將它從集合中移除,這樣只要集合還在使用(而此對象已經(jīng)無用了),這個對象就造成了內(nèi)存泄露。并且如果集合被靜態(tài)引用的話,集合里面那些沒有用的對象更會造成內(nèi)存泄露了。所以在使用集合時要及時將不用的對象從集合remove,或者clear集合,以避免內(nèi)存泄漏。

資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露

在使用IO、File流或者Sqlite、Cursor等資源時要及時關(guān)閉。這些資源在進行讀寫操作時通常都使用了緩沖,如果及時不關(guān)閉,這些緩沖對象就會一直被占用而得不到釋放,以致發(fā)生內(nèi)存泄露。因此我們在不需要使用它們的時候就及時關(guān)閉,以便緩沖能及時得到釋放,從而避免內(nèi)存泄露。

屬性動畫造成內(nèi)存泄露

動畫同樣是一個耗時任務(wù),比如在Activity中啟動了屬性動畫(ObjectAnimator),但是在銷毀的時候,沒有調(diào)用cancle方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用Activity,這就造成Activity無法正常釋放。因此同樣要在Activity銷毀的時候cancel掉屬性動畫,避免發(fā)生內(nèi)存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

WebView造成內(nèi)存泄露

關(guān)于WebView的內(nèi)存泄露,因為WebView在加載網(wǎng)頁后會長期占用內(nèi)存而不能被釋放,因此我們在Activity銷毀后要調(diào)用它的destory()方法來銷毀它以釋放內(nèi)存。

另外在查閱WebView內(nèi)存泄露相關(guān)資料時看到這種情況:

Webview下面的Callback持有Activity引用,造成Webview內(nèi)存無法釋放,即使是調(diào)用了Webview.destory()等方法都無法解決問題(Android5.1之后)。

最終的解決方案是:在銷毀WebView之前需要先將WebView從父容器中移除,然后在銷毀WebView。詳細分析過程請參考這篇文章:WebView內(nèi)存泄漏解決方法。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 先從父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}

構(gòu)造單例的時候盡量別用Activity的引用;
靜態(tài)引用時注意應(yīng)用對象的置空或者少用靜態(tài)引用;
使用靜態(tài)內(nèi)部類+軟引用代替非靜態(tài)內(nèi)部類;
及時取消廣播或者觀察者注冊;
耗時任務(wù)、屬性動畫在Activity銷毀時記得cancel;
文件流、Cursor等資源及時關(guān)閉;
Activity銷毀時WebView的移除和銷毀。


本文參考了以下博客:
http://www.itdecent.cn/p/4b6adee12682
https://blog.csdn.net/qq_35373333/article/details/74909811
http://www.itdecent.cn/p/ab4a7e353076
https://blog.csdn.net/baidu_33396702/article/details/50337875
https://blog.csdn.net/lvwenbo0107/article/details/51458885

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 內(nèi)存管理的目的 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。簡單粗...
    晨光光閱讀 1,373評論 1 4
  • 【Android 內(nèi)存泄漏】 引用: ★★★ 【知識必備】內(nèi)存泄漏全解析,從此拒絕ANR,讓OOM遠離你的身邊,跟...
    Rtia閱讀 987評論 0 2
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,492評論 0 12
  • 琪33閱讀 582評論 0 0
  • 1 有人說,要么閱讀,要么運動,身體與靈魂總要有一個在路上!我不記得曾經(jīng)在哪里看到這句話的,自從看到以后,一直很喜...
    我撿了九條貓閱讀 469評論 2 2

友情鏈接更多精彩內(nèi)容