2007年的波尔多:最常見的Android內存優化方式及防止泄漏造成OOM總結篇 [復制鏈接]

2019-10-9 10:20
IamCoder 閱讀:171 評論:0 贊:0
Tag:  內存優化 OOM

恋恋波尔多 www.luaogj.com.cn

前言

內存優化目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻不再被使用導致 GC 不能回收。既然說到內存泄漏和優化,就不得不先簡單了解一下內存分配策略,然后再舉常見泄漏例子和解決方法,最后做一下總結,這樣更直觀全面了解Android內存方面處理。

內存分配

內存分配策略有三種,分別是靜態、棧式和堆式。對應的的內存空間主要分別是靜態存儲區(也稱方法區)、棧區和堆區。如下:

  • 靜態存儲區:主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,并且在程序整個運行期間都存在。
  • 棧區 :當方法被執行時,方法體內的局部變量都在棧上創建,并在方法執行結束時這些局部變量所持有的內存將會自動被釋放。因為棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
  • 堆區 : 又稱動態內存分配,通常就是指在程序運行時直接 new 出來的內存。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收。

棧與堆 區別:

在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內存空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。堆內存用來存放所有由 new 創建的對象(包括該對象其中的所有成員變量)和數組。在堆中分配的內存,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數組。

舉個例子:

public class Sample() {

int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}

Sample mSample3 = new Sample();

Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的。

mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在于棧中。

小結

局部變量的基本數據類型和引用存儲于棧中,引用的對象實體存儲于堆中:因為它們屬于方法中的變量,生命周期隨方法而結束。 成員變量全部存儲與堆中(包括基本數據類型,引用和引用的對象實體):因為它們屬于類,類對象終究是要被new出來使用的。

管理內存

內存管理就是對象的分配和釋放問題。

在 Java 中,程序員需要通過關鍵字 new 為每個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執行的。在 Java 中,內存的分配是由程序完成的,而內存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是 Java 程序運行速度較慢的原因之一。因為,GC 為了能夠正確釋放對象,GC 必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都需要進行監控。監視對象狀態是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

內存泄漏

內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內存泄漏,這些對象不會被GC所回收,然而它卻占用內存。

在C++中,內存泄漏的范圍更大一些。有些對象被分配了內存空間,然后卻不可達,由于C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內存泄露。

通過分析,我們得知,對于C++,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。

因此,通過以上分析,我們知道在Java中也有內存泄漏,但范圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規范定義, 該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對于基于Web的實時系統,如網絡游戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那么我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。

Java 內存泄漏的典型例子:

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}

在這個例子中,我們循環申請Object對象,并將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置為 null。

常見內存泄漏

(本篇重點)

1、集合類泄漏

如果一個集合類是全局性變量(比如類中的靜態變量或全局性map即有靜態引用又或者final指向它)只有添加元素的方法,而沒有相應的清除機制,就會占用內存只增不減,造成內存泄漏。比如我們通常用HashMap做一些緩存之類的事,這種情況就多留點心,做好相應刪除機制。

2、單例造成泄漏

由于單例的靜態性使得生命周期跟應用的生命周期一樣長,很容易造成內存泄漏。

典型的例子

public class AppManager {
private static AppManager instance;
private Context context;

private AppManager(Context context) {
this.context = context;
}

public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}

這是一個普通的單例模式,當創建這個單例的時候,由于需要傳入一個Context,所以這個Context的生命周期的長短至關重要:

1:如果此時傳入的是 Application 的 Context,因為 Application 的生命周期就是整個應用的生命周期,所以這將沒有任何問題。

2:如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應用程序的生命周期,所以當前 Activity 退出時它的內存并不會被回收,這就造成泄漏了

3、非靜態內部類創建靜態實例造成的內存泄漏

有的時候我們可能會在啟動頻繁的Activity中,為了避免重復創建相同的數據資源,可能會出現這種寫法:

public class MainActivity extends AppCompatActivity { 
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
}
class TestResource {
//…
}
}

這樣就在Activity內部創建了一個非靜態內部類的單例TestResource,每次啟動Activity時都會使用該單例的數據,這樣雖然避免了資源的重復創建,不過這種寫法卻會造成內存泄漏,因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正?;厥?。

正確的做法為:將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對于有些地方則必須使用 Activity 的 Context,對于Application,Service,Activity三者的Context的應用場景如下:Application 和 Service 可以啟動一個 Activity,不過需要創建一個新的 task 任務隊列。而對于 Dialog 而言,只有在 Activity 中才能創建。

4、匿名內部類線程異步導致泄漏

在繼承實現Activity/Fragment/View時,此時如果你使用了匿名類,并被異步線程持有了,那要小心了,沒有任何措施一定會導致泄露。

舉個栗子:

public class MainActivity extends Activity { 
...{
Runnable re1 = new MyRunable();
Runnable re2 = new Runnable() {
@Override
public void run() {
}
};
}

re1和re2的區別是,re2使用了匿名內部類。運行時這兩個引用的內存可以看到,re1沒什么特別的。

但ref2這個匿名類的實現對象里面多了一個引用:this$0這個引用指向MainActivity.this。

也就是說當前的MainActivity實例會被re2持有,如果將這個引用再傳入一個異步線程,此線程和此Acitivity生命周期不一致的時候,就造成了Activity的泄露。

5、Handler 造成的內存泄漏

handler為了避免ANR而不在主線程進行耗時操作,去處理網絡任務或者封裝一些請求回調等api等。我們知道 Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有造成內存泄漏。

由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實現方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導致無法正確釋放。

知識點:在java里 ,非靜態內部類 和匿名類都會潛在的引用它們所屬的外部類。但是靜態內部類卻不會。

接下里看個案例:

public class SampleActivity extends Activity {

private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(new Runnable() {
@Override
public void run() { ... }}, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

分析:

當activity結束(finish)時,里面的延時消息在得到處理前,會一直保存在主線程的消息隊列里持續10分鐘。而且,由上文可知這個message持有handler引用,而handler又持有對其外部類(activity)的潛引用。這條引用關系會一直保持直到消息得到處理,從而阻止了acitivty被垃圾回收器回收,造成應用程序的泄漏。另外非靜態匿名類Runnable同樣持有外部類,導致泄漏。總結2條原因:

小結:

  • 只要有未處理的消息,那么消息會引用handler,非靜態的handler又會引用外部類,即Activity,導致Activity無法被回收,造成泄漏;
  • Runnable類屬于非靜態匿名類,同樣會引用外部類。

解決方案:

  • 我們可以把handler類放在單獨的類文件中,或者使用靜態內部類便可以避免泄漏。另外,如果想要在handler內部去調用所在的外部類Activity,那么可以在handler內部使用弱引用的方式指向所在Activity,這樣統一不會導致內存泄漏。
  • 對于匿名類Runnable,同樣可以將其設置為靜態類。因為靜態的匿名類不會持有對外部類的引用。

再看源碼:

public class SampleActivity extends Activity {

private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity); //弱引用
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}

private final MyHandler mHandler = new MyHandler(this);


private static final Runnable sRunnable = new Runnable() { //靜態匿名類
@Override
public void run() { /* ... */ }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

如果一個內部類實例的生命周期比Activity更長,那么我們千萬不要使用非靜態的內部類。最好的做法是,使用靜態內部類,然后在該類里使用弱引用來指向所在的Activity。綜述,推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。

知識點:

前面提到了 WeakReference,所以這里就簡單的說一下 Java 對象的幾種引用類型。

Java對引用的分類有強(Strong reference),軟(SoftReference),弱 (WeakReference),虛 PhatomReference 四種。

在Android應用的開發中,為了防止內存溢出,在處理一些占用內存大而且聲明周期較長的對象時候,可以盡量應用軟引用和弱引用技術。

軟/弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用。

假設我們的應用會用到大量的默認圖片,比如應用中有默認的頭像,默認游戲圖標等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由于讀取文件需要硬件操作,速度較慢,會導致性能較低。所以我們考慮將圖片緩存起來,需要的時候直接從內存中讀取。但是,由于圖片占用內存空間比較大,緩存很多圖片需要很多的內存,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術來避免這個問題發生。以下就是高速緩沖器的雛形:首先定義一個HashMap,保存軟引用對象——private Map。

6、盡量避免使用 static 成員變量

如果成員變量被聲明為 static,那我們都知道其生命周期將與整個app進程生命周期一樣。

這會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到后臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,占內存較大的后臺進程將優先回收,如果此app做過進程互保?;?,那會造成app在后臺頻繁重啟。當手機安裝了你參與開發的app以后一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

修復的方法:

  • 不要在類初始時初始化靜態成員??梢鑰悸莑azy初始化。

7、AsyncTask對象造成的泄漏

AsyncTask確實需要額外注意一下。它的泄露原理和前面Handler,Thread泄露的原理差不多,它的生命周期和Activity不一定一致。

解決方案:在activity退出的時候,終止AsyncTask中的后臺任務。

但是,問題是如何終止?

AsyncTask提供了對應的API:public final boolean cancel (boolean mayInterruptIfRunning)。

它的說明有這么一句話:

// Attempts to cancel execution of this task. This attempt will fail if the task has already completed, already been cancelled, or could not be cancelled for some other reason.
// If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.

cancel是不一定成功的,如果正在運行,它可能會中斷后臺任務。怎么感覺這話說的這么不靠譜呢?

是的,就是不靠譜。

那么,怎么才能靠譜點呢?我們看看官方的示例:

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
// 注意下面這行,如果檢測到cancel,則及時退出
if (isCancelled()) break;
}
return totalSize;
}

protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}

protected void onPostExecute(Long result) {
showDialog("Downloaded " + result + " bytes");
}
}

官方的例子是很好的,在后臺循環中時刻監聽cancel狀態,防止沒有及時退出。

為了提醒大家,google特意在AsyncTask的說明中撂下了一大段英文:

// AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent pacakge such as Executor, ThreadPoolExecutor and FutureTask.

可憐我神州大陸幅員遼闊,地大物博,什么都不缺,就是缺對英語閱讀的敏感。

AsyncTask適用于短耗時操作,最多幾秒鐘。如果你想長時間耗時操作,請使用其他java.util.concurrent包下的API,比如Executor, ThreadPoolExecutor 和 FutureTask.

學好英語,避免踩坑!

8、 BroadcastReceiver對象

種種原因沒有調用到unregister()方法。

解決方法很簡單,就是確保調用到unregister()方法。

順帶說一下,我在工作中碰到一種相反的情況,receiver對象沒有registerReceiver()成功(沒有調用到),于是unregister的時候提示出錯:

// java.lang.IllegalArgumentException: Receiver not registered ...

解決方案:

方案一:在registerReceiver()后設置一個FLAG,根據FLAG判斷是否unregister()。網上搜到的文章幾乎都這么寫,我以前碰到這種bug,也是一直都這么解。但是不可否認,這種代碼看上去確實有點丑陋。

方案二:我后來無意中聽到某大牛提醒,在Android源碼中看到一種更通用的寫法:

// just sample, 可以寫入工具類
// 第一眼我看到這段代碼,靠,太粗暴了,但是回頭一想,要的就是這么簡單粗暴,不要把一些簡單的東西搞的那么復雜。
private void unregisterReceiverSafe(BroadcastReceiver receiver) {
try {
getContext().unregisterReceiver(receiver);
} catch (IllegalArgumentException e) {
// ignore
}
}

9、BitMap對象造成的泄漏

Bitmap 對象不用的時候最好調用一下recycle 方法再賦值null,清空資源的直接或間接引用,但是有人要問,android源碼里面好多地方也沒有調用???

是的,我這里說的是最好,如果不調用的話,只能依賴于Java GC 執行的時候,調用Bitmap 的 finalize方法,

這里面會執行navtive的方法 nativeDestructor() 去釋放資源,其實查看一下那個函數,就是一句 delete bitmap。

總結

1.對 Activity 等組件的引用應該控制在 Activity 的生命周期之內; 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命周期的對象引用而泄露。

2.盡量不要在靜態變量或者靜態內部類中使用非靜態外部成員變量(包括context ),即使要使用,也要考慮適時把外部成員變量置空;也可以在內部類中使用弱引用來引用外部類的變量。

3.對于生命周期比Activity長的內部類對象,并且內部類中使用了外部類的成員變量,可以將內部類改為靜態內部類、靜態內部類中使用弱引用來引用外部類的成員變量 。

4.Handler 的持有的引用對象最好使用弱引用,資源釋放時也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable。

5.在 Java 的實現過程中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦值為 null,比如使用完Bitmap 后先調用 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的數組(使用 array.clear() ; array = null)等,最好遵循誰創建誰釋放的原則。

6.正確關閉資源,對于使用了BraodcastReceiver,ContentObserver,File,游標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷。

7.保持對對象生命周期的敏感,特別注意單例、靜態對象、全局性集合等的生命周期。


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反?。[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 恋恋波尔多 )