Android中記憶體洩漏超級精煉詳解
一、前期基礎知識儲備
(1)什麼是記憶體?
JAVA是在JVM所虛擬出的記憶體環境中執行的,JVM的記憶體可分為三個區:堆(heap)、棧(stack)和方法區(method)。
棧(stack):是簡單的資料結構,但在計算機中使用廣泛。棧最顯著的特徵是:LIFO(Last In, First Out, 後進先出)。後來者居上。(跟執行緒中佇列的順序恰好相反)棧中只存放基本型別和物件的引用(不是物件)。
堆(heap):堆記憶體用於存放由new建立的物件和陣列。在堆中分配的記憶體,由java虛擬機器自動垃圾回收器來管理。JVM只有一個堆區(heap),且被所有執行緒共享,堆中不存放基本型別和物件引用,只存放物件本身
方法區(method):又叫靜態區,跟堆一樣,被所有的執行緒共享。方法區包含所有的class和static變數。
(2)什麼是記憶體洩漏?
提及記憶體洩露,就不得不提到記憶體溢位,這兩個比較容易混淆的概念,我們來分析一下。
記憶體洩露:ML (Memory Leak),程式在向系統申請分配記憶體空間後(new),在使用完畢後未釋放。結果導致一直佔據該記憶體單元,我們和程式都無法再使用該記憶體單元,直到程式結束,這是記憶體洩露。
記憶體洩漏對程式的影響是:容易使得應用程式發生記憶體溢位,即 OOM 。
記憶體溢位:OOM(Out of Memory),程式向系統申請的記憶體空間超出了系統能給的。比如記憶體只能分配一個int型別,我卻要塞給他一個long型別,系統就出現OOM。小明向他同桌小紅要橡皮,週一要一塊,用一半沒還;週二要一塊,用一半沒還;週三要一塊,用一半沒還;週四週五也是一樣,然後到下週一他就失去這個同桌了,並且被叫了家長。
記憶體溢位對程式的影響是:導致ANR錯誤甚至應用Crush。OOM錯誤最終會導致ANR錯誤,即應用程式無響應,經常無響應的應用程式用著是會有點“上火”,比如“XXX掌遊寶”(撈錢倒是有一手,換頭像10元起步),這個ANR錯誤真的是每次開啟有會報錯,有時候真的是十分的惱火。
二、Android記憶體洩漏原因分析
(1)記憶體洩漏原因分析
需掌握的概念:①棧中只存放基本型別變數和物件的引用,棧(stack)可以自行清除不用的記憶體空間;②棧中引用的物件的本體存放在堆中,但在JAVA中堆記憶體不會隨著方法的結束而清空;③所以如果我們不停的建立新物件,堆(heap)的記憶體空間就會被消耗盡;
④聰明的Java引入了垃圾回收(garbage collection)去處理堆記憶體的回收,perfect;
⑤但是實際情況是,仍然有很多情況導致記憶體洩漏:如果物件一直被引用無法被回收,造成記憶體的浪費,無法再被使用。所以物件無法被GC回收就是造成記憶體洩露的原因!
(2)Java垃圾回收機制分析
垃圾回收(garbage collection,簡稱GC)可以自動清空堆中不再使用的物件。在JAVA中物件是通過引用使用的。如果再沒有引用指向該物件,那麼該物件就無從處理或呼叫該物件,這樣的物件稱為不可到達(unreachable)。垃圾回收用於釋放不可到達的物件所佔據的記憶體。
回收機制分析:我們將棧定義為root,遍歷棧中所有的物件的引用,再遍歷一遍堆中的物件。因為棧中的物件的引用執行完畢就刪除,所以我們就可以通過棧中的物件的引用,查詢到堆中沒有被指向的物件,這些物件即為不可到達物件,對其進行垃圾回收。
但是:如果持有物件的強引用,垃圾回收器GC是無法在記憶體中回收這個物件。
————————————————————我是重點分隔線——————————————————
(3)記憶體洩漏的真實原因
記憶體洩露的直接原因是:本該回收的物件,因為某些原因(物件的強引用被另外一個正在使用的物件所持有,且沒有及時釋放),進而造成記憶體單元一直被佔用,浪費空間,甚至可能造成記憶體溢位!
記憶體洩漏的本質原因是:持有引用者的生命週期>被引用者的生命週期!
強引用:實際編碼中最常見的一種引用型別。常見形式如:A a = new A();等。對於這類引用GC任何時候不會對其進行記憶體回收,在記憶體不足的情況下寧願丟擲Out of Memory(OOM記憶體溢位)。類似這樣的都是強引用:
private final MessageReceived mMessageReceived=new MessageReceived(this);
————————————————————我是重點分隔線——————————————————
其實在Android中會造成記憶體洩露的情景無外乎兩種:
全域性程序(process-global)的static變數。這個無視應用的狀態,持有Activity的強引用的怪物。
活在Activity生命週期之外的執行緒。沒有清空對Activity的強引用。
小結:雖然現在手機記憶體在不停的提升,記憶體洩露興許不會像dalvik時代由於虛擬機器記憶體過小造成各種花樣OOM。但是過量的記憶體洩露依然會造成記憶體溢位,影響使用者體驗,在如今定製系統層出不窮、機型花樣越來越多的情況下解決好記憶體洩露的問題會讓適配和穩定性進一步提高!
三、Android中什麼導致記憶體洩漏
有開發者總結了如下一張表,這裡供讀者參考:
①資源物件沒關閉造成的記憶體洩漏——描述: 資源性物件比如(Cursor遊標,Stream流,BroadCastReceiver等)往往都用了一些緩衝,我們在不使用的時候或者在使用完之後,應該及時關閉它們比如close()方法,以便它們的緩衝及時回收記憶體。
Cursor cursor = sqLiteDatabase.rawQuery(query, null);
try {
if (cursor.moveToFirst()) {
do {
try {
long time = cursor.getLong(cursor.getColumnIndex(KEY_HISTORY_ID));
String math = cursor.getString(cursor.getColumnIndex(KEY_HISTORY_INPUT));
String result = cursor.getString(cursor.getColumnIndex(KEY_HISTORY_RESULT));
ResultEntry resultEntry = new ResultEntry(math, result, color, time, type);
itemHistories.add(resultEntry);
Log.d(TAG, "txtResult: DatabaseHelper查詢了" + time + " " + math + " " + result);
} catch (Exception e) {
e.printStackTrace();
}
} while (cursor.moveToNext());
}
cursor.close(); //關閉cursor遊標物件
} catch (Exception e) {
Log.d(TAG, "Error query: " + e.getMessage());
}
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
File files = getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File outFile = new File(files, sdf.format(new Date()).concat(".").concat("jpg"));
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(outFile);
outStream.write(data);
outStream.flush();
outStream.close(); //關閉輸入流
Log.e(TAG, "image saved to file " + outFile.getAbsolutePath());
Glide.with(getContext()).load(outFile.getAbsolutePath()).into(mCameraSnapshot);
} catch (IOException e) {
e.printStackTrace();
}
//動態註冊的廣播需要反註冊
@Override
protected void onDestroy() {
super.onDestroy();
if (receiver != null) {
unregisterReceiver(receiver);
}
}
②構造Adapter時,沒有使用快取的convertView——常見於使用ListView,如果,不使用convertView,那麼每次列表載入都會載入一次物件,使用者反覆載入,就會造成大量的物件建立,得不到釋放,極容易發生記憶體洩漏。
public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}
修改為程式碼:
public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
};
③對於不再需要使用的物件,顯示的將其賦值為null,比如使用完Bitmap後先呼叫recycle(),再賦為null ——有時我們會手工的操作Bitmap物件,如果一個Bitmap物件比較佔記憶體,當它不在被使用的時候,可以呼叫Bitmap.recycle()方法回收此物件的畫素所佔用的記憶體,比如在Activity或者Fragment中的onDestory中新增以下程式碼:
if(bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
bitmap = null; //回收Bitmap
}
或者在使用的時候,就及時回收不再使用的Bitmap物件:
if (myBitmap != null) {
myBitmap.recycle();
myBitmap = null;
}
Bitmap original = BitmapFactory.decodeFile(myFile);
myBitmap = Bitmap.createScaledBitmap(original, displayWidth, displayHeight, true);
original.recycle();
original = null;
④對於生命週期比Activity長的物件如果需要應該使用ApplicationContext ——這是一個很隱晦的記憶體洩漏的情況。有一種簡單的方法來避免context相關的記憶體洩漏。最顯著地一個是避免context逃出他自己的範圍之外。使用Application context。這個context的生存週期和你的應用的生存週期一樣長,而不是取決於activity的生存週期。如果你想保持一個長期生存的物件,並且這個物件需要一個context,記得使用application物件。你可以通過呼叫 Context.getApplicationContext() or Activity.getApplication()來獲得。
⑤集合中物件沒清理造成的記憶體洩漏——我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。
⑥使用Service之後,沒有關閉服務——如果Service停止失敗也會導致記憶體洩漏。
Intent floatWinIntent = new Intent(BasicCalculatorActivity.this, FlowWindowService.class);
stopService(floatWinIntent);
因為系統會傾向於把這個Service所依賴的程序進行保留,如果這個程序很耗記憶體,就會造成記憶體洩漏。建議使用IntentService,可以自動停止。
記憶體洩漏對我們來說並不是可見的,因為它是在堆中活動,而要想檢測程式中是否有記憶體洩漏的產生,通常我們可以藉助LeakCanary、MAT等工具來檢測應用程式是否存在記憶體洩漏,當檢測到程式中有記憶體洩漏的產生時,它將告訴我們該記憶體洩漏是由誰產生的和該記憶體洩漏導致誰洩漏了而不能回收,供我們複查。
⑦使用WebView之後沒有進行回收 —— 推薦在程式碼裡動態新增WebView
//Fragment中新增
private WebView mWebView;
private FrameLayout frameWebView;
frameWebView = view.findViewById(R.id.webView);
mWebView=new WebView(getActivity());
frameWebView.addView(mWebView);
//回收
@Override
public void onDestroy() {
super.onDestroy();
if( mWebView!=null) {
mWebView.removeAllViews();
mWebView.destroy();
}
}
最後 祝各位開發者/讀者少bug少洩漏少溢位少crash!!!