Android 性能優化 - 詳解內存優化的來龍去脈

前言

APP內存的使用,是評價一款應用性能高低的一個重要指標。雖然現在智能手機的內存越來越大,但是一個好的應用應該將效率發揮到極致,精益求精。

什麼是內存

通常情況下我們說的內存是指手機的RAM,它主要包括一下幾個部分: 
- 寄存器(Registers讀音:[ˈrɛdʒɪstɚ]) 
速度最快的存儲場所,因爲寄存器位於處理器內部,所以在程序中我們無法控制。 
- 棧(Stack) 
存放基本類型的對象和引用,但是對象本身不存放在棧中,而是存放在堆中。

變量其實是分爲兩部分的:一部分叫變量名,另外一部分叫變量值,對於局部變量(基本類型的變量和對象的引用變量)而言,統一都存放在棧中,但是變量值中存儲的內容就有在一定差異了:Java中存在8大基本類型,他們的變量值中存放的就是具體的數值,而其他的類型都叫做引用類型(對象也是引用類型,你只要記住除了基本類型,都是引用類型)他們的變量值中存放的是他們在堆中的引用(內存地址)。

在函數執行的時候,函數內部的局部變量就會在棧上創建,函數執行結束的時候這些存儲單元會被自動釋放。棧內存分配運算內置於處理器的指令集中是一塊連續的內存區域,效率很高,速度快,但是大小是操作系統預定好的所以分配的內存容量有限。

  • 堆(Heap) 
    在堆上分配內存的過程稱作 內存動態分配過程。在java中堆用於存放由new創建的對象和數組。堆中分配的內存,由java虛擬機自動垃圾回收器(GC)來管理(可見我們要進行的內存優化主要就是對堆內存進行優化)。堆是不連續的內存區域(因爲系統是用鏈表來存儲空閒內存地址,自然不是連續的),堆大小受限於計算機系統中有效的虛擬內存(32bit系統理論上是4G)

  • 靜態存儲區/方法區(Static Field) 
    是指在固定的位置上存放應用程序運行時一直存在的數據,java在內存中專門劃分了一個靜態存儲區域來管理一些特殊的數據變量如靜態的數據變量。

  • 常量池(Constant Pool) 
    顧名思義專門存放常量的。注意 String s = "java"中的“java”也是常量。JVM虛擬機爲每個已經被轉載的類型維護一個常量池。常量池就是該類型所有用到地常量的一個有序集合包括直接常量(基本類型,String)和對其他類型、字段和方法的符號引用。

總結:

  1. 定義一個局部變量的時候,java虛擬機就會在棧中爲其分配內存空間,局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。因爲它們屬於方法中的變量,生命週期隨方法而結束。

  2. 成員變量全部存儲與堆中(包括基本數據類型,引用和引用的對象實體),因爲它們屬於類,類對象終究是要被new出來使用的。當堆中對象的作用域結束的時候,這部分內存也不會立刻被回收,而是等待系統GC進行回收。

  3. 所謂的內存分析,就是分析Heap中的內存狀態。

 

Android中的沙盒機制

大家可能都聽說過iOS中有沙盒機制(sandbox),但是我們的Android系統中也存在沙盒機制,只不過沒有IOS中的嚴格,所以常常被人忽略。

由於Android是建立在Linux系統之上的,所以Android系統繼承了Linux的 類Unix繼承進程隔離機制與最小權限原則,並且在原有Linux的進程管理基礎上對UID的使用做了改進,形成了Android應用的”沙箱“機制。

普通的Linux中啓動的應用通常和登陸用戶相關聯,同一用戶的UID相同。但是Android中給不同的應用都賦予了不同的UID,這樣不同的應用將不能相互訪問資源。對應用而言,這樣會更加封閉,安全。 
引文來自Android的SandBox(沙箱)

在Android系統中,應用(通常)都在一個獨立的沙箱中運行,即每一個Android應用程序都在它自己的進程中運行,都擁有一個獨立的Dalvik虛擬機實例。Dalvik經過優化,允許在有限的內存中同時高效地運行多個虛擬機的實例,並且每一個Dalvik應用作爲一個獨立的Linux進程執行。Android這種基於Linux的進程“沙箱”機制,是整個安全設計的基礎之一。 
引文來自淺析Android沙箱模型

簡單點說就是在Android的世界中每一個應用相當與一個Linux中的用戶,他們相互獨立,不能相互共享與訪問,(這也就解釋了Android系統中爲什麼需要進程間通信),正是由於沙盒機制的存在最大程度的保護了應用之間的安全,但是也帶來了每一個應用所分配的內存大小是有限制的問題。

 

Generational Heap Memory內存模型的概述

在Android和Java中都存在着一個Generational(讀音:[ˌdʒenəˈreɪʃənl]) Heap Memory模型,系統會根據內存中不同的內存數據類型分別執行不同的GC操作。Generational Heap Memory模型主要由:Young Generation(新生代)、Old Generation(舊生代)、Permanent(讀音:[ˈpɜ:rmənənt]) Generation三個區域組成,而且這三個區域存在明顯的層級關係。所以此模型也可以成爲三級Generation的內存模型

其中Young Generation區域存放的是最近被創建對象,此區域最大的特點就是創建的快,被銷燬的也很快。當對象在Young Generation區域停留的時間到達一定程度的時候,它就會被移動到Old Generation區域中,同理,最後他將會被移動到Permanent Generation區域中。

在三級Generation內存模型中,每一個區域的大小都是有固定值的,當進入的對象總大小到達某一級內存區域閥值的時候就會觸發GC機制,進行垃圾回收,騰出空間以便其他對象進入。

不僅如此,不同級別的Generation區域GC是需要的時間也是不同的。同等對象數目下,Young Generation GC所需時間最短,Old Generation次之,Permanent Generation 需要的時間最長。當然GC執行的長短也和當前Generation區域中的對象數目有關。遍歷查找20000個對象比起遍歷50個對象自然是要慢很多的。

GC機制概述

與C++不用,在Java中,內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不需要通過調用函數來釋放內存,但也隨之帶來了內存泄漏的可能。簡單點說:對於 C++ 來說,內存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對於 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收

Android使用的主要開發語言是Java所以二者的GC機制原理也大同小異,所以我們只對於常見的JVM GC機制的分析,就能達到我們的目的。我還是先看看那二者的不同之處吧。

  • Dalvik 和標準Java虛擬機的主要區別

Dalvik虛擬機(DVM)是Android系統在java虛擬機(JVM)基礎上優化得到的,DVM是基於寄存器的,而JVM是基於棧的,由於寄存器高效快速的特性,DVM的性能相比JVM更好。

  • Dalvik 和 java 字節碼的區別

Dalvik執行.dex格式的字節碼文件,JVM執行的是.class格式的字節碼文件,Android程序在編譯之後產生的.class 文件會被aapt工具處理生成R.class等文件,然後dx工具會把.class文件處理成.dex文件,最終資源文件和.dex文件等打包成.apk文件。

  • 對於Young Generation(新生代)的GC

由於Young Generation通常存活的時間比較短,所以Young Generation採用了Copying算法進行回收,Copying算法就是掃描出存活的對象,並複製到一塊新的空間中,這個過程就是下圖Eden與Survivor Space之間的複製過程。Young Generation採用空閒指針的方式來控制GC觸發,指針保存最後一個分配在Young Generation中分配空間地對象的位置。當有新的對象要分配內存空間的時候,就會主動檢測空間是否足夠,不夠的情況下就出觸發GC,當連續分配對象時,對象會逐漸從Eden移動到Survivor,最後移動到Old Generation。

  • 對於Old Generation(舊生代)的GC

Old Generation與Young Generation不同,對象存活的時間比較長,比較穩固,因此採用標記(Mark)算法來進行回收。所謂標記就是掃描出存活的對象,然後在回收未必標記的對象。回收後的剩餘空間要麼進行合併,要麼標記出來便於下次進行分配,總之就是要減少內存碎片帶來的效率損耗。

  • 如何判斷對象是否可以被回收

從上面的一小節中我們知道了不同的區域GC機制是有所不同的,那麼這些垃圾是如何被發現的呢?下面我們就看一下兩種常見的判斷方法:引用計數、對象引用遍歷。

  • 引用計數器

引用計數器是垃圾收集器中的早起策略。這種方法中,每個對象實體(不是它的引用)都有一個引用計數器。當一個對象創建的時候,且將該對象分配給一個每分配給一個變量,計數器就+1,當一個對象的某個引用超過了生命週期或者被設置一個新值時,對象計數器就-1,任何引用計數器爲 0 的對象可以被當作垃圾收集。當一個對象被垃圾收集時,引用的任何對象技術 - 1。 
優點:執行快,交織在程序運行中,對程序不被長時間打斷的實時環境比較有利。 
缺點:無法檢測出循環引用。比如:對象A中有對象B的引用,而B中同時也有A的引用。

  • 跟蹤收集器

現在的垃圾回收機制已經不太使用引用計數器的方法判斷是否可回收,而是使用跟蹤收集器方法。

現在大多數JVM採用對象引用遍歷機制從程序的主要運行對象(如靜態對象/寄存器/棧上指向的堆內存對象等)開始檢查引用鏈,去遞歸判斷對象收否可達,如果不可達,則作爲垃圾回收,當然在便利階段,GC必須記住那些對象是可達的,以便刪除不可到達的對象,這稱爲標記(marking)對象。

下一步,GC就要刪除這些不可達的對象,在刪除時未必標記的對象,釋放它們的內存的過程叫做清除(sweeping),而這樣會造成內存碎片化,佈局已分配給新的對象,但是他們集合起來還很大。所以很多GC機制還要重新組織內存中的對象,並進行壓縮,形成大塊、可利用的空間。

爲了達到這個目的,GC需要停止程序的其他活動,阻塞進程。這裏我們要注意的是:不要頻繁的引發GC,執行GC操作的時候,任何線程的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續運行, 故而如果程序頻繁GC, 自然會導致界面卡頓. 通常來說,單個的GC並不會佔用太多時間,但是大量不停的GC操作則會顯著佔用幀間隔時間(16ms)。如果在幀間隔時間裏面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了。

Android內存泄露分析

對於 C++ 來說,內存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對於 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收

GC過程與對象的引用類型是嚴重相關的,下面我們就看看Java中(Android中存在差異)對於引用的四種分類: 

- 強引用(Strong Reference):JVM寧願拋出OOM,也不會讓GC回收的對象 
- 軟引用(Soft Reference) :只有內存不足時,纔會被GC回收。 
- 弱引用(weak Reference):在GC時,一旦發現弱引用,立即回收 
- 虛引用(Phantom Reference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作爲GC回收Object的標誌。 

注意Android中存在的差異 
但是在2.3以後版本中,系統會優先將SoftReference的對象提前回收掉, 即使內存夠用,其他和Java中是一樣的。所以谷歌官方建議用LruCache(least recentlly use 最少最近使用算法)。會將內存控制在一定的大小內, 超出最大值時會自動回收, 這個最大值開發者自己定。其實LruCache就是用了很多的HashMap,三百多行的代碼

在開發過程中,保存對象,這時我很可以直接使用LruCache來代替,Bitmap對象:

在Android開發過程中,我們常常使用HasMap保存對象,但是爲了防止內存泄漏,在保存內存佔用較大、生命週期較長的對象的時候,儘量使用LruCache代替HasMap用於保存對象。

//指定最大緩存空間   
private static final int MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);    
LruCache<String,Bitmap> mBitmapLruCache = new LruCache<>(MAX_SIZE);

而造成不能回收的根本原因就是:堆內存中長生命週期的對象持有短生命週期對象的強/軟引用,儘管短生命週期對象已經不再需要,但是因爲長生命週期對象持有它的引用而導致不能被回收

如何監聽系統發送GC

那麼怎樣才能去監聽系統的GC過程呢?其實非常簡單,系統每進行一次GC操作時,都會在LogCat中打印一條日誌,我們只要去分析這條日誌就可以了,日誌的基本格式如下所示: 
DVM中

D/dalvikvm(30615): GC FOR ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms , total 24ms 

ART中

I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms

 

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>  

 

原因,一般情況下一共有以下幾種觸發GC操作的原因:

  • GC_CONCURRENT: 當我們應用程序的堆內存快要滿的時候,系統會自動觸發GC操作來釋放內存。

  • GC_FOR_MALLOC: 當我們的應用程序需要分配更多內存,可是現有內存已經不足的時候,系統會進行GC操作來釋放內存。

  • GC_HPROF_DUMP_HEAP: 當生成HPROF文件的時候,系統會進行GC操作,關於HPROF文件我們下面會講到。

  • GC_EXPLICIT: 這種情況就是我們剛纔提到過的,主動通知系統去進行GC操作,比如調用System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的。

接下來第二部分Amount_freed,表示系統通過這次GC操作釋放了多少內存。 
然後Heap_stats中會顯示當前內存的空閒比例以及使用情況(活動對象所佔內存 / 當前程序總內存)。

最後Pause_time表示這次GC操作導致應用程序暫停的時間。

關於這個暫停的時間,Android在2.3的版本當中進行過一次優化,在2.3之前GC操作是不能併發進行的,也就是系統正在進行GC,那麼應用程序就只能阻塞住等待GC結束。雖說這個阻塞的過程並不會很長,也就是幾百毫秒,但是用戶在使用我們的程序時還是有可能會感覺到略微的卡頓。 
而自2.3之後,GC操作改成了併發的方式進行,就是說GC的過程中不會影響到應用程序的正常運行,但是在GC操作的開始和結束的時候會短暫阻塞一段時間,不過優化到這種程度,用戶已經是完全無法察覺到了。

導致GC頻繁執行有兩個原因

由於GC會阻塞進程,所以我們不避免頻繁的GC。 
1. Memory Churn(內存抖動),內存抖動是因爲大量的對象被創建又在短時間內馬上被釋放。 
2. 瞬間產生大量的對象會嚴重佔用Young Generation的內存區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即使每次分配的對象佔用了很少的內存,但是他們疊加在一起會增加 Heap的壓力,從而觸發更多其他類型的GC。這個操作有可能會影響到幀率,並使得用戶感知到性能問題。

解決上面的問題有簡潔直觀方法,如果你在Memory Monitor裏面查看到短時間發生了多次內存的漲跌,這意味着很有可能發生了內存抖動。 

常見內存泄露分析

1. 永遠的單例(Singleton)

爲了完美解決我們在程序中反覆創建同一對象的問題,我們選用了單例模式,單例在我們的程序中隨處可見,但是由於單例模式的靜態特性,使得它的生命週期和我們的應用一樣長,一不小心讓單例無限制的持有Activity的強引用就會導致內存泄漏。例如:

public class SingleTon{

    private Context context;
    private static SingleTon singleTon;


    public static final SingleTon getInstance(Context context){
        this.context = context;
        return SingleHolder.INSTANCE;

    }
    private static class SingleHolder{

        private static final SingleTon INSTANCE = new SingleTon();

    }

}

解決辦法:

這個錯誤很普遍,這個是一個很正常的單利模式,但是由於傳入了一個Context,而這個Context的生命週期就的長短就尤爲重要了。如果我們傳入的是某個Activity的Context,而當這個Activity推出的時候,由於該Context的強引用被單例持有,那麼這個Activity就等同於擁有了整個程序的生命週期。這種情況下,當Activity退出的時候內存並沒有被回收,這就造成了內存泄漏。

正確的做法就是應該把傳入的Context改爲同應用生命週期一樣長的Application中的Context。

public class BaseApplication extends Application{
    private static BaseApplication baseApplication;

    @Override
    public void onCreate(){
        super.onCreate();
        baseApplication = this;

    }
    public static Context getContext{
        baseApplication.getApplicationContext();
    }
}

當然我們可以直接重寫Application,提供getContext方法,不必在依靠傳入的參數:

 public static final SingleTon getInstance(Context context) {
        this.context = context.getApplicationContext;
        return SingleHolder.INSTANCE;
  }

2 Handler引起的內存泄漏

Handler引起的內存泄漏在我們開發中最爲常見的。我們知道Handler、Message、MessageQueue都是相互關聯在一起的,萬一Handler發送的Message尚未被處理,那麼該Message以及發送它的Handler對象都會被線程MessageQueue一直持有。

由於Handler屬於TLS(Thread Local Storage)變量,生命週期和Activity是不一致的,因此這種實現方式很難保證跟Activity的生命週期一直,所以很容易無法釋放內存。比如:

 public class HandlerBadActivity extends AppCompatActivity {

    private final Handler handler = new Handler(){
       @Override
      public void handleMessage(Message msg) {
          super.handleMessage(msg);
     }
     };

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_handler_bad); 
         // 延遲5min發送一個消息
         handler.postDelayed(new Runnable() {
             @Override
             public void run() {
                 // write something
             }
         },1000*60*5);
         this.finish();
     }
 }

我們在例子中生命了一個延時5分鐘執行的Message,當該Activity退出的時候,延時任務(Message)還在主線成的MessageQueue中等待,此時的Message持有Handler的強引用,並且由於Handler是HandlerBadActivity的非靜態內部類,所以Handler會持有HandlerBadActivity的強引用,此時HandlerBadActivity退出時無法進行內存回收,造成內存泄漏。

解決辦法:

將Handler生命爲靜態內部類,這樣它就不會持有外部來的引用了。這樣以來Handler的的生命週期就與Activity無關了。不過倘若用到Context等外部類的非static對象,還是應該通過使用Application中與應用同生命週期的Context比較合適。比如:

public class HandlerGoodActivity extends AppCompatActivity {
    private static final class MyHandler extends Handler {
        private Context mActivity;
        public MyHandler(HandlerGoodActivity activity) {
            //使用生命週期與應用同長的getApplicationContext
            this.mActivity = activity.getApplicationContext();
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mActivity != null) {
                // write something
            }
        }
    }

    private final MyHandler myHandler = new MyHandler(this);
    // 匿名內部類在static的時候絕對不會持有外部類的引用
    private static final Runnable RUNNABLE = new Runnable() {
        @Override
        public void run() {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_good);
        myHandler.postDelayed(RUNNABLE, 1000 * 60 * 5);

    }

雖然我們結局了Activity的內存泄漏問題,但是經過Handler發送的延時消息還在MessageQueue中,Looper也在等待處理消息,所以我們要在Activity銷燬的時候處理掉隊列中的消息。

   @Override
    protected void onDestroy() {
        super.onDestroy();
        //傳入null,就表示移除所有Message和Runnable
        myHandler.removeCallbacksAndMessages(null);
    }

3 匿名內部類在異步線程中的使用

它們方便卻暗藏殺機。Android開發經常會繼承實現 Activity 或者 Fragment 或者 View。如果你使用了匿名類,而又被異步線程所引用,那得小心,如果沒有任何措施同樣會導致內存泄漏的:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inner_bad);
        Runnable runnable1 = new MyRunnable();
        Runnable runnable2 = new Runnable() {
            @Override
            public void run() {

            }
        };
    }

    private static class MyRunnable implements Runnable{
        @Override
        public void run() {

        }
    }
}

runnable1 和 runnable2的區別就是,runnable2使用了匿名內部類,我們看看引用時的引用內存 

可以看到,runnable1是沒有什麼特別的。但runnable2多出了一個MainActivity的引用,若是這個引用再傳入到一個異步線程,此線程在和Activity生命週期不一致的時候,也就造成了Activity的泄露。

4 善用static成員變量

從前面的介紹我們知道,static修飾的變量位於內存的靜態存儲區,此變量與App的生命週期一致 
這必然會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到後臺,這部分內存也不會被釋放。按照現在手機app內存管理機制,佔內存較大的後臺進程將優先回收,因爲如果此app做過進程互保保活,那會造成app在後臺頻繁重啓。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這裏修復的方法是: 
不要在類初始時初始化靜態成員。可以考慮lazy初始化(延遲加載)。架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此對象的生命週期你有責任管理起來。

5 避免使用

在我們的日常代碼中,這樣的情況似乎很常見,及直接寫一個class就這麼光禿禿的情況 

這樣就在Activity內部創建了一個非靜態內部類的單例,每次啓動Activity時都會使用該單例的數據,這樣雖然避免了資源的重複創建,不過這種寫法卻會造成內存泄漏,因爲非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命週期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收。正確的做法爲:

將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景如下:

其中: NO1表示 Application 和 Service 可以啓動一個 Activity,不過需要創建一個新的 task 任務隊列。而對於 Dialog 而言,只有在 Activity 中才能創建。

6 集合引發的內存泄漏

我們通常會把一些對象的引用加入到集合容器(比如ArrayList)中,當我們不再需要該對象時,並沒有把它的引用從集合中清理掉,當集合中的內容過於大的時候,並且是static的時候就造成了內存泄漏,所有我們最好在onDestory情況並讓其不可達

private List<String> nameList;
    private List<Fragment> list;
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (nameList != null){
            nameList.clear();
            nameList = null;
        }
        if (list != null){
            list.clear();
            list = null;
        }
    }

7 webView引發的內存泄漏

WebView解析網頁時會申請Native堆內存用於保存頁面元素,當頁面較複雜時會有很大的內存佔用。如果頁面包含圖片,內存佔用會更嚴重。並且打開新頁面時,爲了能快速回退,之前頁面佔用的內存也不會釋放。有時瀏覽十幾個網頁,都會佔用幾百兆的內存。這樣加載網頁較多時,會導致系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啓。

由於佔用的都是Native堆內存,所以實際佔用的內存大小不會顯示在常用的DDMS Heap工具中(這裏看到的只是Java虛擬機分配的內存,一般即使Native堆內存已經佔用了幾百兆,這裏顯示的還只是幾兆或十幾兆)。只有使用adb shell中的一些命令比如dumpsys meminfo 包名,或者在程序中使用Debug.getNativeHeapSize()才能看到。

據說由於WebView的一個BUG,即使它所在的Activity(或者Service)結束也就是onDestroy()之後,或者直接調用WebView.destroy()之後,它所佔用這些內存也不會被釋放。

解決這個問題最直接的方法是:把使用了WebView的Activity(或者Service)放在單獨的進程裏。然後在檢測到應用佔用內存過大有可能被系統幹掉或者它所在的Activity(或者Service)結束後,調用System.exit(0),主動Kill掉進程。由於系統的內存分配是以進程爲準的,進程關閉後,系統會自動回收所有內存。

8其他常見的引起內存泄漏原因

  • 構造Adapter時,沒有使用緩存的 convertView

  • Bitmap在不使用的時候沒有使用recycle()釋放內存

  • 非靜態內部類的靜態實例容易造成內存泄漏:即一個類中如果你不能夠控制它其中內部類的生命週期(譬如Activity中的一些特殊Handler等),則儘量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。

  • 警惕線程未終止造成的內存泄露;譬如在Activity中關聯了一個生命週期超過Activity的Thread,在退出Activity時切記結束線程。一個典型的例子就是HandlerThread的run方法是一個死循環,它不會自己結束,線程的生命週期超過了Activity生命週期,我們必須手動在Activity的銷燬方法中中調運thread.getLooper().quit();纔不會泄露。

  • 對象的註冊與反註冊沒有成對出現造成的內存泄露;譬如註冊廣播接收器、註冊觀察者(典型的譬如數據庫的監聽)等。

  • 創建與關閉沒有成對出現造成的泄露;譬如Cursor資源必須手動關閉,WebView必須手動銷燬,流等對象必須手動關閉等。

  • 不要在執行頻率很高的方法或者循環中創建對象(比如onMeasure),可以使用HashTable等創建一組對象容器從容器中取那些對象,而不用每次new與釋放。

  • 避免代碼設計模式的錯誤造成內存泄露;譬如循環引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。

總結

  • Android內存優化主要是針對堆(Heap)而言的,當堆中對象的作用域結束的時候,這部分內存也不會立刻被回收,而是等待系統GC進行回收。

  • Java中造成內存泄漏的根本原因是:堆內存中長生命週期的對象持有短生命週期對象的強/軟引用,儘管短生命週期對象已經不再需要,但是因爲長生命週期對象持有它的引用而導致不能被回收。

    原文:Android 性能優化 - 詳解內存優化的來龍去脈
    相關文章:https://www.cnblogs.com/lianghe01/p/6617275.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章