Android記憶體管理機制和記憶體洩漏分析及優化
Android中的記憶體管理機制
分配機制
Android為每個程序分配記憶體的時候,採用了彈性的分配方式,也就是剛開始並不會一下分配很多記憶體給每個程序,而是給每一個程序分配一個“夠用”的量。這個量是根據每一個裝置實際的實體記憶體大小來決定的。隨著應用的執行,可能會發現當前的記憶體可能不夠使用了,這時候Android又會為每個程序分配一些額外的記憶體大小。但是這些額外的大小並不是隨意的,也是有限度的,系統不可能為每一個App分配無限大小的記憶體。
Android系統的宗旨是最大限度的讓更多的程序存活在記憶體中,因為這樣的話,下一次使用者再啟動應用,不需要重新建立程序,只需要恢復已有的程序就可以了,減少了應用的啟動時間,提高了使用者體驗。
回收機制
Android對記憶體的使用方式是“盡最大限度的使用”,這一點繼承了Linux的優點。Android會在記憶體中儲存儘可能多的資料,即使有些程序不再使用了,但是它的資料還被儲存在記憶體中,所以Android現在不推薦顯式的“退出”應用。因為這樣,當用戶下次再啟動應用的時候,只需要恢復當前程序就可以了,不需要重新建立程序,這樣就可以減少應用的啟動時間。只有當Android系統發現記憶體不夠使用,需要回收記憶體的時候,Android系統就會需要殺死其他程序,來回收足夠的記憶體。但是Android也不是隨便殺死一個程序,比如說一個正在與使用者互動的程序,這種後果是可怕的。所以Android會有限清理那些已經不再使用的程序,以保證最小的副作用。
Android殺死程序有兩個參考條件:
程序優先順序:
Android為每一個程序分配了優先順序的概念,優先順序越低的程序,被殺死的概率就更大。Android中總共有5個程序優先順序。具體含義這裡不再給出。
- 前臺程序:正常不會被殺死
- 可見程序:正常不會被殺死
- 服務程序:正常不會被殺死
- 後臺程序:存放於一個LRU快取列表中,先殺死處於列表尾部的程序
- 空程序:正常情況下,為了平衡系統整體效能,Android不儲存這些程序
回收收益:
當Android系統開始殺死LRU快取中的程序時,系統會判斷每個程序殺死後帶來的回收收益。因為Android總是傾向於殺死一個能回收更多記憶體的程序,從而可以殺死更少的程序,來獲取更多的記憶體。殺死的程序越少,對使用者體驗的影響就越小。
官方推薦的App記憶體使用方式
- 當Service完成任務後,儘量停止它。因為有Service元件的程序,優先順序最低也是服務程序,這會影響到系統的記憶體回收。IntentService可以很好地完成這個任務。
- 在UI不可見的時候,釋放掉一些只有UI使用的資源。系統會根據onTrimMemory()回撥方法d的TRIM_MEMORY_UI_HIDDEN等級的事件,來通知App UI已經隱藏了。
- 在系統記憶體緊張的時候,儘可能多的釋放掉一些非重要資源。系統會根據onTrimMemory()回撥方法來通知記憶體緊張的狀態,App應該根據不同的記憶體緊張等級,來合理的釋放資源,以保證系統能夠回收更多記憶體。當系統回收到足夠多的記憶體時,就不用殺死程序了。
- 檢查自己最大可用的記憶體大小。這對一些快取框架很有用,因為正常情況下,快取框架的快取池大小應當指定為最大記憶體的百分比,這樣才能更好地適配更多的裝置。通過getMemoryClass()和getLargeMemoryClass()來獲取可用記憶體大小的資訊。
- 避免濫用Bitmap導致的記憶體浪費。
根據當前裝置的解析度來壓縮Bitmap是一個不錯的選擇,在使用完Bitmap後,記得要使用recycle()來釋放掉Bitmap。使用軟引用或者弱引用來引用一個Bitmap,使用LRU快取來對Bitmap進行快取。 - 使用針對記憶體優化過的資料容器。針對移動裝置記憶體有限的問題,Android提供了一套針對記憶體優化過的資料容器,來替代JDK原生提供的資料容器。但是缺點就是,時間複雜度被提高了。比如SparseArray、SparseBooleanArray、LongSparseArray、
- 意識到記憶體的過度消耗。Enum型別佔用的記憶體是常量的兩倍多,所以避免使用enum,直接使用常量。
每一個Java的類(包括匿名內部類)都需要500Byte的程式碼。每一個類的例項都有12-16 Byte的額外記憶體消耗。注意類似於HashMap這種,內部還需要生成Class的資料容器,這會消耗更多記憶體。 - 抽象程式碼也會帶來更多的記憶體消耗。如果你的“抽象”設計實際上並沒有帶來多大好處,那麼就不要使用它。
- 使用nano protobufs 來序列化資料。Google設計的一個語言和平臺中立打的序列化協議,比XML更快、更小、更簡單。
- 避免使用依賴注入的框架。依賴注入的框架需要開啟額外的服務,來掃描App中程式碼的Annotation,所以需要額外的系統資源。
- 使用ZIP對齊的APK。對APK做Zip對齊,會壓縮其內部的資源,執行時會佔用更少的記憶體。
- 合理使用多程序。
Android記憶體洩漏分析及優化
記憶體洩漏的根本原因
如上圖所示,GC會選擇一些它瞭解還存活的物件作為記憶體遍歷的根節點(GC Roots),比方說thread stack中的變數,JNI中的全域性變數,zygote中的物件(class loader載入)等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。如下圖藍色部分。
記憶體洩漏指的是程序中某些物件(垃圾物件)已經沒有使用價值了,但是它們卻可以直接或間接地引用到gc roots導致無法被GC回收。無用的物件佔據著記憶體空間,使得實際可使用記憶體變小,形象地說法就是記憶體洩漏了。下面分析一些可能導致記憶體洩漏的情景。
Android中常見的記憶體洩漏原因
1.使用static變數引起的記憶體洩漏
因為static變數的生命週期是在類載入時開始 類解除安裝時結束,也就是說static變數是在程式程序死亡時才釋放,如果在static變數中引用了Activity那麼這個Activity由於被引用,便會隨static變數的生命週期一樣,一直無法被釋放,造成記憶體洩漏。
一般解決辦法:
想要避免context相關的記憶體洩漏,需要注意以下幾點:
- 不要對activity的context長期引用(一個activity的引用的生存週期應該和activity的生命週期相同)
- 如果可以的話,儘量使用關於application的context來替代和activity相關的context
- 如果一個acitivity的非靜態內部類的生命週期不受控制,那麼避免使用它;正確的方法是使用一個靜態的內部類,並且對它的外部類有一WeakReference,就像在ViewRootImpl中內部類W所做的那樣,使用弱引用private final WeakReference mViewAncestor;。
下面的程式碼存在記憶體洩漏的問題,非靜態內部類的靜態例項導致記憶體洩漏。
/**
mDemo會獲得並一直持有MemoryLeakActivity的引用。當MemoryLeakActivity銷燬後重建,因為mDemo持有引用,無法被GC回收的,程序中會存在2個MemoryLeakActivity例項。所以,對於lauchMode不是singleInstance的Activity, 應該避免在activity裡面例項化其非靜態內部類的靜態例項。
*/
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
static Demo mDemo;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("啟動一個持有本物件的執行緒");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
mDemo = new Demo();
mDemo.run();
}
class Demo{
void run(){
Log.i(TAG, "run: ");
}
}
}
解決方法:將Demo改成靜態內部類
因為普通的內部類物件隱含地儲存了一個引用,指向建立它的外圍類物件。然而,當內部類是static的時,就不是這樣了。巢狀類意味著: 1. 巢狀類的物件,並不需要其外圍類的物件。 2. 不能從巢狀類的物件中訪問非靜態的外圍類物件。
2.執行緒引起的記憶體洩漏
下面的程式碼存在記憶體洩漏的問題,啟動執行緒的匿名內部類會持有MemoryLeakActivity.this的引用。如果執行緒還沒有結束,Activity已經銷燬那就會造成記憶體洩漏。
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("啟動一個持有本物件的執行緒");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(8000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(runnable).start();
}
}
解決辦法:
1.合理安排執行緒執行的時間,控制執行緒在Activity結束前結束。
2.將內部類改為靜態內部類,並使用弱引用WeakReference來儲存Activity例項 因為弱引用 只要GC發現了 就會回收它 ,因此可儘快回收。
將匿名內部類改成靜態類,避免了Activity context的記憶體洩漏問題
/**
* 示例通過將執行緒類宣告為私有的靜態內部類避免了 Activity context 的記憶體洩漏問題,但
* 在配置發生改變後,執行緒仍然會執行。原因在於,DVM 虛擬機器持有所有執行執行緒的引用,無論
* 這些執行緒是否被回收,都與 Activity 的生命週期無關。執行中的執行緒只會繼續執行,直到
* Android 系統將整個應用程序殺死
*/
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("啟動一個持有本物件的執行緒");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
new MyThread().start();
}
private static class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(8000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在Activity生命週期的onDestory中結束執行緒執行
/**
* 除了我們需要實現銷燬邏輯以保證執行緒不會發生記憶體洩漏。在退出當前
* Activity 前使用 onDestroy() 方法結束你的執行中執行緒。
*/
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
private static boolean mRunnale = false;
private MyThread mThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("啟動一個持有本物件的執行緒");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
new Thread(runnable).start();
new MyThread().start();
}
@Override
protected void onDestroy() {
super.onDestroy();
mThread.closeThread();
}
private static class MyThread extends Thread{
@Override
public void run() {
mRunnale = true;
while (true){
//TODO
Log.i(TAG, "run: do something");
}
}
public void closeThread(){
mRunnale = false;
}
}
}
3.Handler的使用造成的記憶體洩漏
由於在Handler的使用中,handler會發送message物件到 MessageQueue中 然後 Looper會輪詢MessageQueue 然後取出Message執行,但是如果一個Message長時間沒被取出執行,那麼由於 Message中有 Handler的引用,而 Handler 一般來說也是內部類物件,Message引用 Handler ,Handler引用 Activity 這樣 使得 Activity無法回收。或者說Handler在Activity退出時依然還有訊息需要處理,那麼這個Activity就不會被回收。
解決辦法:
依舊使用 靜態內部類+弱引用的方式 可解決
例如下面的程式碼
public class MemoryLeakActivity extends AppCompatActivity{
private TextView view;
private static final String TAG = "MemoryLeakActivity";
private MyHandler mHandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new TextView(MemoryLeakActivity.this);
view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setText("啟動一個持有本物件的執行緒");
view.setTextSize(40);
view.setTextColor(Color.parseColor("#0000ff"));
setContentView(view);
mHandler = new MyHandler(this);
mHandler.sendEmptyMessage(0);
}
@Override
protected void onDestroy() {
super.onDestroy();
//第三步,在Activity退出的時候移除回撥
mHandler.removeCallbacksAndMessages(null);
}
//第一步,將Handler改成靜態內部類。
static class MyHandler extends Handler{
//第二步,將需要引用Activity的地方,改成弱引用。
private WeakReference<MemoryLeakActivity> mActivityRef;
public MyHandler(MemoryLeakActivity activity){
mActivityRef = new WeakReference<MemoryLeakActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MemoryLeakActivity mla = mActivityRef == null ? null : mActivityRef.get();
if(mla == null || mla.isFinishing()){
return;
}
//TODO
mla.view.setText("do something");
}
}
}
4.資源未被及時關閉造成的記憶體洩漏
比如一些Cursor 沒有及時close 會儲存有Activity的引用,導致記憶體洩漏
解決辦法:
在onDestory方法中及時 close即可
5.BitMap佔用過多記憶體
bitmap的解析需要佔用記憶體,但是記憶體只提供8M的空間給BitMap,如果圖片過多,並且沒有及時 recycle bitmap 那麼就會造成記憶體溢位。
解決辦法:
及時recycle 壓縮圖片之後載入圖片
其中還有一些關於 集合物件沒移除,註冊的物件沒反註冊,程式碼壓力的問題也可能產生記憶體洩漏,但是使用上述的幾種解決辦法一般都是可以解決的。