內存優化目的就是讓咱們在開發中怎麼有效的避免咱們的應用出現內存泄漏的問題。內存泄漏你們都不陌生了,簡單粗俗的講,就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻再也不被使用致使 GC 不能回收。既然說到內存泄漏和優化,就不得不先簡單瞭解一下內存分配策略,而後再舉常見泄漏例子和解決方法,最後作一下總結,這樣更直觀全面瞭解Android內存方面處理。android
內存分配策略有三種,分別是靜態、棧式和堆式。對應的的內存空間主要分別是靜態存儲區(也稱方法區)、棧區和堆區。以下:程序員
在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中爲該變量分配內存空間,當超過該變量的做用域後,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間能夠被從新使用。堆內存用來存放全部由 new 建立的對象(包括該對象其中的全部成員變量)和數組。在堆中分配的內存,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象後,還能夠在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是咱們上面說的引用變量。咱們能夠經過這個引用變量來訪問堆中的對象或者數組。web
舉個例子:算法
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 指向的對象是存在於堆上的。api
mSample3 指向的對象實體存放在堆上,包括這個對象的全部成員變量 s1 和 mSample1,而它本身存在於棧中。數組
小結緩存
局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中:由於它們屬於方法中的變量,生命週期隨方法而結束。 成員變量所有存儲與堆中(包括基本數據類型,引用和引用的對象實體):由於它們屬於類,類對象終究是要被new出來使用的。bash
內存管理就是對象的分配和釋放問題。
在 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。
(本篇重點)
一、集合類泄漏
若是一個集合類是全局性變量(好比類中的靜態變量或全局性map即有靜態引用又或者final指向它)只有添加元素的方法,而沒有相應的清除機制,就會佔用內存只增不減,形成內存泄漏。好比咱們一般用HashMap作一些緩存之類的事,這種狀況就多留點心,作好相應刪除機制。
二、單例形成泄漏
因爲單例的靜態性使得生命週期跟應用的生命週期同樣長,很容易形成內存泄漏。
典型的例子
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 退出時它的內存並不會被回收,這就形成泄漏了
三、非靜態內部類建立靜態實例形成的內存泄漏
有的時候咱們可能會在啓動頻繁的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 中才能建立。
四、匿名內部類線程異步致使泄漏
在繼承實現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的泄露。
五、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條緣由:
小結:
解決方案:
再看源碼:
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。
六、儘可能避免使用 static 成員變量
若是成員變量被聲明爲 static,那咱們都知道其生命週期將與整個app進程生命週期同樣。
這會致使一系列問題,若是你的app進程設計上是長駐內存的,那即便app切到後臺,這部份內存也不會被釋放。按照如今手機app內存管理機制,佔內存較大的後臺進程將優先回收,若是此app作過進程互保保活,那會形成app在後臺頻繁重啓。當手機安裝了你參與開發的app之後一晚上時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
修復的方法:
七、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.
學好英語,避免踩坑!
八、 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 }}複製代碼
九、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.保持對對象生命週期的敏感,特別注意單例、靜態對象、全局性集合等的生命週期。
更多文章
前言
目前hybrid開發模式:
1.經過WebView來進行原生和web交互
2.爲了解決WebView性能差的問題,以React Native爲表明的一類框架將最終渲染工做交還給了系統,雖然一樣使用類HTML+JS的UI構建邏輯,可是最終會生成對應的自定義原生控件,以充分利用原生控件相對於WebView的較高的繪製效率……閱讀全文