編輯:Android資訊
在Android開發中,我們經常會使用到static來修飾我們的成員變量,其本意是為了讓多個對象共用一份空間,節省內存,或者是使用單例模式,讓該類只生產一個實例而在整個app中使用。然而在某些時候不恰當的使用或者是編程的不規范卻會造成了內存洩露現象(java上的內存洩漏指內存得不到gc的及時回收,從而造成內存占用過多的現象)
本文中我們主要分析的是static變量對activtiy的不恰當引用而造成的內存洩漏,因為對於同一個Activity頁面一般每次打開時系統都會重新生成一個該activity的對象(standard模式下),而每個activity對象一般都含有大量的視圖對象和bitmap對象,如果之前的activity對象不能得到及時的回收,從而就造成了內存的洩漏現象。
下面一邊看代碼一邊講解。
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; }
在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); }
這種方式大家應該一看就明白問題在哪裡了,在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(); }
修改方式也非常簡單我們單例中context不再持有Activity的context而是持有Application的context即可,因為Application本來就是單例,所以這樣就不會存在內存洩漏的的現象了。
第一種方式內存洩漏太過與明顯,相信大家都不會犯這種錯誤,接下來要介紹的這種洩漏方式會比較不那麼容易發現,內部類的使用造成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(); }
然後是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); } }
我們知道在java中,非靜態的內部類的對象都是會持有指向外部類對象的引用的,因此我們將內部類對象mMyListener讓單例所持有時,由於mMyListener引用了我們的activity對象,因此造成activity對象也不能被回收了,從而出現內存洩漏現象。
修改以上代碼,避免內存洩漏,在activity中添加以下代碼:`
@Override protected void onDestroy() { testManager.unregisterListener(mMyListener); super.onDestroy(); }
介紹完以上兩種情況的內存洩漏後,我們在來看一種更加容易被忽略的內存洩漏現象,對於AsyncTask不正確使用造成內存洩漏的問題。
mTask=new AsyncTask<String,Void,Void>() { @Override protected Void doInBackground(String... params) { //doSamething.. return null; } }.execute("a task");
一般我們在主線程中開啟一個異步任務都是通過實現一個內部類其繼承自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(); }
具體cancel()裡傳遞true or false依實際情況而定:
1.當我們的任務還在排隊沒有被執行,調用cancel()無論true or false,任務會從排隊隊列中移除,即任務都不會被執行到了。
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);
在這樣的情況下我們的線程處於休眠狀態調用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);
由於我們的線程不處於等待或休眠的狀況及時調用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);
這裡我們通過在每次循環是檢查任務是否已經被cancle掉,如果是則退出。因此對於AsyncTask我們也得注意按照正確的方式進行使用,不然也會造成程序內存洩漏的現象。
以上內容就是在使用static時,我們需要怎麼做才能優化內存的使用,當然對於以上3種情況是我們編程中使用static經常遇到的內存洩漏的情況,但仍然還有很多情況我們不易察覺到。比如:如果不做介紹,上面的第三種情況就很難察覺到,這時我們最終的內存洩漏優化方法就是:使用內存洩漏分析工具,在下一篇文章裡我會參照第三種情況(AsyncTask)造成的內存洩漏,通過使用MAT工具進行分析,講解MAT排除內存洩漏的使用方法。
之前一直頭痛於沒有辦法在多個程序中共享資源,用作公共類庫的方法也是使用的導出jar再導入的辦法,現在終於初步搞明白了,可算解脫了~,分享出來。 建立公共庫 首先建
不廢話,先看效果,嗯…看起來有點卡,截圖軟件的問題: 圖中普通列表是ListView樣式,沒有設置Header和Footer時使用默認的下拉刷新和上
本文由碼農網 – 蘇耀東原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! 簡介 本文介紹一個Android手勢密碼開源庫的使用及實現的詳細過程
前言 開發過程中有些時候會遇到一些功能,自己不知道該怎麼做,然而別的軟件裡面已經有了,這個時候可以采用反編譯的方式,解開其他的程序,來了解一些它的做法,同時啊,還