1. 程式人生 > >Android記憶體優化之——記憶體洩漏篇

Android記憶體優化之——記憶體洩漏篇

原文地址:http://blog.csdn.net/ys408973279/article/details/50389200

Android開發中,我們經常會使用到static來修飾我們的成員變數,其本意是為了讓多個物件共用一份空間,節省記憶體,或者是使用單例模式,讓該類只生產一個例項而在整個app中使用。然而在某些時候不恰當的使用或者是程式設計的不規範卻會造成了記憶體洩露現象(Java上的記憶體洩漏指記憶體得不到gc的及時回收,從而造成記憶體佔用過多的現象) 
    本文中我們主要分析的是static變數對activtiy的不恰當引用而造成的記憶體洩漏,因為對於同一個Activity頁面一般每次開啟時系統都會重新生成一個該activity的物件(standard模式下),而每個activity物件一般都含有大量的檢視物件和bitmap物件,如果之前的activity物件不能得到及時的回收,從而就造成了記憶體的洩漏現象。 
下面一邊看程式碼一邊講解。

單例模式不正確的獲取context

public class LoginManager {

    private Context context;
    private static LoginManager manager;

    public static LoginManager getInstance(Context context) {
        if (manager == null)
            manager = new LoginManager(context);
        return manager;
    }

    private
LoginManager(Context context) { this.context = context; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在LoginActivity中

public class LoginActivity extends Activity  {

    private LoginManager loginManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        loginManager = LoginManager.getInstance(this
); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

這種方式大家應該一看就明白問題在哪裡了,在LoginManager的單例中context持有了LoginActivity的this物件,即使登入成功後我們跳轉到了其他Activity頁面,LoginActivity的物件仍然得不到回收因為他被單例所持有,而單例的生命週期是同Application保持一致的。

正確的獲取context的方式

public class LoginManager {

    private Context context;
    private static LoginManager manager;

    public static LoginManager getInstance(Context context) {
        if (manager == null)
            manager = new LoginManager(context);
        return manager;
    }

    private LoginManager(Context context) {
        this.context = context.getApplicationContext();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

修改方式也非常簡單我們單例中context不再持有Activity的context而是持有Application的context即可,因為Application本來就是單例,所以這樣就不會存在記憶體洩漏的的現象了。

單例模式中通過內部類持有activity物件 
    第一種方式記憶體洩漏太過與明顯,相信大家都不會犯這種錯誤,接下來要介紹的這種洩漏方式會比較不那麼容易發現,內部類的使用造成activity物件被單例持有。

還是看程式碼再分析,下面是一個單例的類:

public class TestManager {
    public static final TestManager INSTANCE = new TestManager();
    private List<MyListener> mListenerList;

    private TestManager() {
        mListenerList = new ArrayList<MyListener>();
    }

    public static TestManager getInstance() {
        return INSTANCE;
    }

    public void registerListener(MyListener listener) {
        if (!mListenerList.contains(listener)) {
            mListenerList.add(listener);
        }
    }
    public void unregisterListener(MyListener listener) {
        mListenerList.remove(listener);
    }
}

interface MyListener {
    public void onSomeThingHappen();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

然後是activity:

public class TestActivity extends AppCompatActivity {

    private MyListener mMyListener=new MyListener() {
        @Override
        public void onSomeThingHappen() {
        }
    };
    private TestManager testManager=TestManager.getInstance();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        testManager.registerListener(mMyListener);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

    我們知道在java中,非靜態的內部類的物件都是會持有指向外部類物件的引用的,因此我們將內部類物件mMyListener讓單例所持有時,由於mMyListener引用了我們的activity物件,因此造成activity物件也不能被回收了,從而出現記憶體洩漏現象。

修改以上程式碼,避免記憶體洩漏,在activity中新增以下程式碼:

@Override
    protected void onDestroy() {
        testManager.unregisterListener(mMyListener);
        super.onDestroy();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

AsyncTask不正確使用造成的記憶體洩漏

介紹完以上兩種情況的記憶體洩漏後,我們在來看一種更加容易被忽略的記憶體洩漏現象,對於AsyncTask不正確使用造成記憶體洩漏的問題。

  mTask=new AsyncTask<String,Void,Void>()
        {
            @Override
            protected Void doInBackground(String... params) {
                //doSamething..
                return null;
            }
        }.execute("a task");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

    一般我們在主執行緒中開啟一個非同步任務都是通過實現一個內部類其繼承自AsyncTask類然後實現其相應的方法來完成的,那麼自然的mTask就會持有對activity例項物件的引用了。檢視AsyncTask的實現,我們會通過一個SerialExecutor序列執行緒池來對我們的任務進行排隊,而這個SerialExecutor物件就是一個static final的常量。 
具體的引用關係是: 
1.我們的任務被封裝在一個FutureTask的物件中(它充當一個runable的作用),FutureTask的實現也是通過內部類來實現的,因此它也為持有AsyncTask物件,而AsyncTask物件引用了activity物件,因此activity物件間接的被FutureTask物件給引用了。 
2.futuretask物件會被新增到一個ArrayDeque型別的任務佇列的mTasks例項中 
3.mTasks任務佇列又被SerialExecutor物件所持有,剛也說了這個SerialExecutor物件是一個static final的常量。 
    具體AsyncTask的實現大家可以去參照下其原始碼,我這裡就通過文字描述一下其新增任務的實現過程就可以了,總之分析了這麼多通過層層引用後我們的activity會被一個static變數所引用到。所以我們在使用AsyncTask的時候不宜在其中執行太耗時的操作,假設activity已經退出了,然而AsyncTask裡任務還沒有執行完成或者是還在排隊等待執行,就會造成我們的activity物件被回收的時間延後,一段時間內記憶體佔有率變大。

解決方法在activity退出的時候應該呼叫cancel()函式

    @Override
    protected void onDestroy() {
        //mTask.cancel(false);
        mTask.cancel(true);
        super.onDestroy();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

具體cancel()裡傳遞true or false依實際情況而定:

1.當我們的任務還在排隊沒有被執行,呼叫cancel()無論true or false,doInBackground不會被呼叫,而是在輪到執行該任務時轉而呼叫oncancelled函式。 
    請注意這裡的描述:即使呼叫cancel函數了,並不會從排隊中移除任務,任務還是會被執行只是執行時呼叫的是oncancelled函式而不是doInBackground函式。 
2.當我們的任務已經開始執行了(doInBackground被呼叫),傳入引數為false時並不會打斷doInBackground的執行,傳入引數為true時,如果我們的執行緒處於休眠或阻塞(如:sleep,wait)狀況是會打斷其執行。

這裡具體解釋下cancle(true)的意義:

mTask=new AsyncTask<String,Void,Void>()
        {
         @Override
            protected Void doInBackground(String... params) {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.d("test", "task is running");
                return null;
            }
        }.execute("a task");
        try {
        //保證task得以執行
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mTask.cancel(true);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在這樣的情況下我們的執行緒處於休眠狀態呼叫cancel(true)方法會打斷doInBackground的執行——即不會看到log語句的輸出。

但在下面的這種情況的時候卻打斷不了

mTask=new AsyncTask<String,Void,Void>()
        {
         @Override
            protected Void doInBackground(String... params) {

                Boolean loop=true;
                while (loop) {
                    Log.d("test", "task is running");
                }
                return null;
            }
        }.execute("a task");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mTask.cancel(true);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

由於我們的執行緒不處於等待或休眠的狀況及時呼叫cancel(true)也不能打斷doInBackground的執行——現象:log函式一直在列印輸出。

解決方法:

 mTask=new AsyncTask<String,Void,Void>()
        {
            @Override
            protected Void doInBackground(String... params) {
                //doSomething..
                Boolean loop=true;
                while (loop) {
                    if(isCancelled())
                        return null;
                    Log.d("test", "task is running");
                }
                return null;
            }
        }.execute("a task");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mTask.cancel(true);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

這裡我們通過在每次迴圈是檢查任務是否已經被cancle掉,如果是則退出。 
因此對於AsyncTask我們也得注意按照正確的方式進行使用,不然也會造成程式記憶體洩漏的現象。

    以上內容就是在使用static時,我們需要怎麼做才能優化記憶體的使用,當然對於以上3種情況是我們程式設計中使用static經常遇到的記憶體洩漏的情況,但仍然還有很多情況我們不易察覺到。比如:如果不做介紹,上面的第三種情況就很難察覺到,這時我們最終的記憶體洩漏優化方法就是:使用記憶體洩漏分析工具。