上一篇文章中本文我們講解了一個Android產品研發中可能會碰到的一個問題:如何在App中保存靜態秘鑰以及保證其安全性。許多的移動app需要在app端保存一些靜態字符串常量,其可能是靜態秘鑰、第三方appId等。在保存這些字符串常量的時候就涉及到了如何保證秘鑰的安全性問題。如何保證在App中靜態秘鑰唯一且正確安全,這是一個很重要的問題,公司的產品中就存在著靜態字符串常量類型的秘鑰,所以一個明顯的問題就是如何生成秘鑰,保證秘鑰的安全性?上一篇文章中我們做了一個簡單的介紹。
本文我們將講解一下關于Android開發過程中常見的內存泄露場景與檢測方案。Android系統為每個應用程序分配的內存是有限的,當一個應用中產生的內存泄漏的情況比較多時,這就會導致應用所需要的內存超過這個系統分配的內存限額,進而造成了內存溢出而導致應用崩潰。在實際的開發過程中我們由于對程序代碼的不當操作隨時都有可能造成內存泄露。
**(1)什么是內存泄露**
當一個對象已經不需要再使用了,本該被回收時,而有另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。
**(2)系統分配的應用內存大小**
ActivityManager的getMemoryClass()獲得內用正常情況下內存的大小
ActivityManager的getLargeMemoryClass()可以獲得開啟largeHeap最大的內存大小
~~~
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryClass();
activityManager.getLargeMemoryClass();
~~~
需要指出的是這里獲取的內存大小是JVM為進程分配的內存大小,而當我們的應用中存在多個進程的時候,該應用理論上的內存大小限制:
- 應用內存 = 進程內存大小 * 進程個數
所以當我們應用需要較大內存的時候也可以考慮通過多進程的方式進而獲取更多的系統內存。
這樣獲取到的應用內存大小就是應用所能獲取到的最大內存大小,當應用需要更多內存以支持其運行的時候,系統無法為其分配更多的內存,這樣就造成了OOM的異常。
**(3)內存泄露的常見場景**
- 非靜態內部類,靜態實例化
~~~
/**
* 自定義實現的Activity
*/
public class MyActivity extends AppCompatActivity {
/**
* 靜態成員變量
*/
public static InnerClass innerClass = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
innerClass = new InnerClass();
}
class InnerClass {
public void doSomeThing() {
}
}
}
~~~
這里內部類InnerClass隱式的持有外部類MyActivity的引用,而在MyActivity的onCreate方法中調用了
~~~
innerClass = new InnerClass();
~~~
這樣innerClass就會在MyActivity創建的時候是有了他的引用,而innerClass是靜態類型的不會被垃圾回收,MyActivity在執行onDestory方法的時候由于被innerClass持有了引用而無法被回收,所以這樣MyActivity就總是被innerClass持有而無法回收造成內存泄露。
- 不正確的使用Context對象造成內存泄露
~~~
/**
* 自定義單例對象
*/
public class Single {
private static Single instance;
private Context context;
private Object obj = new Object();
private Single(Context context) {
this.context = context;
}
/**
* 初始化獲取單例對象
*/
public static Single getInstance(Context context) {
if (instance == null) {
synchronized(obj) {
if (instance == null) {
instance = new Single(context);
}
}
}
return instance;
}
}
~~~
我們通過懶漢模式創建單例對象,并且在創建的時候需要傳入一個Context對象,而這時候如果我們使用Activity、Service等Context對象,由于單例對象的生命周期與進程的生命周期相同,會造成我們傳入的Activity、Service對象無法被回收,這時候就需要我們傳入Application對象,或者在方法中使用Application對象,上面的代碼可以改成:
~~~
/**
* 自定義單例對象
*/
public class Single {
private static Single instance;
private Context context;
private Object obj = new Object();
private Single(Context context) {
this.context = context;
}
/**
* 初始化獲取單例對象
*/
public static Single getInstance(Context context) {
if (instance == null) {
synchronized(obj) {
if (instance == null) {
instance = new Single(context.getApplication());
}
}
}
return instance;
}
}
~~~
這樣就不會有內存泄露的問題了。
- 使用Handler異步消息通信
在日常開發中我們通常都是這樣定義Handler對象:
~~~
/**
* 定義Handler成員變量
*/
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
dosomething();
}
};
~~~
但是這樣也存在著一個隱藏的問題:在Activity中使用Handler創建匿名內部類會隱式的持有外部Activity對象的引用,當子線程使用Handler暫時無法完成異步任務時,handler對象無法銷毀,同時由于隱式的持有activity對象的引用,造成activity對象以及相關的組件與資源文件同樣無法銷毀,造成內存泄露。
好吧,那么如何解決這個問題呢?具體可以參考:[Android中使用Handler造成內存泄露的分析和解決](http://blog.csdn.net/qq_23547831/article/details/46881941)
- 使用資源文件結束之后未關閉
在使用一些資源性對象比如(Cursor,File,Stream,ContentProvider等)往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。它們的緩沖不僅存在于Java虛擬機內,還存在于Java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄露。
因為有些資源性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對于資源性對象在不使用的時候,應該立即調用它的close()函數,將其關閉掉,然后再置為null.在我們的程序退出時一定要確保我們的資源性對象已經關閉。
~~~
/**
* 初始化Cursor對象
*/
Cursor cursor = getContentResolver().query(uri...);
if (cursor.moveToNext()) {
/**
* 執行自設你的業務代碼
*/
doSomeThing();
}
~~~
這時候我們應當在doSomeThing之后執行cursor的close方法,關閉資源對象。
~~~
```
/**
* 初始化Cursor對象
*/
Cursor cursor = getContentResolver().query(uri...);
if (cursor.moveToNext()) {
/**
* 執行自設你的業務代碼
*/
doSomeThing();
}
if (cursor != null) {
cursor.close();
}
~~~
* Bitmap使用不當
bitmap對象使用的內存較大,當我們不再使用Bitmap對象的時候一定要執行recycler方法,這里需要指出的是當我們在代碼中執行recycler方法,Bitmap并不會被立即釋放掉,其只是通知虛擬機該Bitmap可以被recycler了。
當然了現在項目中使用的一些圖片庫已經幫我們對圖片資源做了很好的優化緩存工作,是我們省去了這些操作。
* 一些框架使用了注冊方法而未反注冊
比如我們時常使用的事件總線框架-EventBus,具體的實現原理可參考:[Android EventBus源碼解析 帶你深入理解EventBus](http://blog.csdn.net/lmj623565791/article/details/40920453)當我們需要注冊某個Activity時需要在onCreate中:
~~~
EventBus.getDefault().register(this);
~~~
然后這樣之后就沒有其他操作的話就會出現內存泄露的情況,因為EventBus對象會是有該Activity的引用,即使執行了改Activity的onDestory方法,由于被EventBus隱式的持有了該對象的引用,造成其無法被回收,這時候我們需要在onDestory方法中執行:
~~~
EventBus.getDefault().unregister(this);
~~~
- 集合中的一些方法的錯誤使用
(1)比如List列表靜態化,只是添加元素而不再使用時不清楚元素;
(2)map對象只是put,而無remove操作等等;
**(4)關于內存泄露檢測的兩個開源方案**
在項目中使用到了兩個開源的內存泄露檢測庫:
[LeakCanary ](https://github.com/square/leakcanary)
[BlockCanary](https://github.com/fengcunhan/BlockCanary)
推薦使用一下這兩個庫檢測一下項目,或許會有意想不到的收獲(曾檢測出一個主流第三方SDK的內存泄露BUG)。
關于LeakCanary,可參考我的:[Android內存泄露監測之leakcanary](http://blog.csdn.net/qq_23547831/article/details/50536690),大概講解了一下LeakCanary的使用方式。
BlockCanary庫的使用方式和LeakCanary類似,更多關于其使用方式的介紹可查看其github文檔。
除了以上兩個開源庫之外,還可以考慮使用軟引用的方式,更多關于Java引用類型的知識,可參考我的:[Java中的四種引用](http://blog.csdn.net/qq_23547831/article/details/46505287)
**(5)關于屏蔽內存泄露的建議**
- 正確的保證內存對象的生命周期,就是盡量保證內存對象在其生命周期內創建于結束,比如Android中的“上帝對象Context”,要保證不同的場景下使用不同的Context對象,下面是一張Context對象的使用場景圖:

* 對資源對象的使用要在使用完成之后保證調用其資源的關閉方法,而非僅僅是對資源引用的關閉操作;
* 靜態化資源對象其生命周期就會變成與進程的生命周期相同,在使用靜態化時一定要考慮清楚該對象靜態化是否存在內存泄露的可能;
* 對Android開發中常見的內存泄露場景要做到了然于胸,了解一些Android中常見的內存泄露檢測方法;
**總結**:
關于內存泄露其實主要記住一個原則就好:確保對象能夠在正確的時機被回收掉。然后我們根據具體內存泄露的場景具體解決就好了。
另外對產品研發技術,技巧,實踐方面感興趣的同學可以參考我的:
[Android產品研發(十一)–>應用內跳轉scheme協議](http://blog.csdn.net/qq_23547831/article/details/51685310)
[Android產品研發(十二)–>App長連接實現](http://blog.csdn.net/qq_23547831/article/details/51719389)
[Android產品研發(十三)–>App輪詢操作](http://blog.csdn.net/qq_23547831/article/details/51764773)
[Android產品研發(十四)–>App升級與更新](http://blog.csdn.net/qq_23547831/article/details/51764773)
[Android產品研發(十五)–>內存對象序列化](http://blog.csdn.net/qq_23547831/article/details/51779528)
[Android產品研發(十六)–>開發者選項](http://blog.csdn.net/qq_23547831/article/details/51809497)
[Android產品研發(十七)–>hybrid開發](http://blog.csdn.net/qq_23547831/article/details/51812985)
[Android產品研發(十八)–>webview問題集錦](http://blog.csdn.net/qq_23547831/article/details/51820139)
[Android產品研發(十九)–>Android studio中的單元測試](http://blog.csdn.net/qq_23547831/article/details/51868451)
[Android產品研發(二十)–>代碼Review](http://blog.csdn.net/qq_23547831/article/details/51833080)
[Android產品研發(二十一)–>Android中的UI優化](http://blog.csdn.net/qq_23547831/article/details/51868453)
[Android產品研發(二十二)–>Android實用調試技巧](http://blog.csdn.net/qq_23547831/article/details/51868496)
[Android產品研發(二十三)–>Android中保存靜態秘鑰實踐](http://blog.csdn.net/qq_23547831/article/details/51953926)
- 前言
- (一)–>實用開發規范
- (二)-->啟動頁優化
- (三)-->基類Activity
- (四)-->減小Apk大小
- (五)-->多渠道打包
- (六)-->Apk混淆
- (七)-->Apk熱修復
- (八)-->App數據統計
- (九)-->App網絡數據解析
- (十)-->盡量不使用靜態變量保存數據
- (十一)-->應用內跳轉Scheme協議
- (十二)-->App長連接實現
- (十三)-->App輪詢操作
- (十四)-->App升級與更新
- (十五)-->內存對象序列化
- (十六)-->開發者選項
- (十七)-->Hybrid開發
- (十八)-->webview問題集錦
- (十九)-->Android studio中的單元測試
- (二十)-->代碼Review
- (二十一)-->Android中的UI優化
- (二十二)-->Android實用調試技巧
- (二十三)-->Android中保存靜態秘鑰實踐
- (二十四)-->內存泄露場景與檢測
- (二十五)-->MVC/MVVM/MVP簡單理解