性能優化-內存優化

內存優化

雖然Android有有優秀的內存管理機制,內存釋放有垃圾收集器(GC)來回收。但內存的不合理使用還是會造成一系列的性能問題,比如短時間分配大量內存對象、內存泄漏等問題。本篇講述如何檢測內存問題和解決,希望在內存優化方面能夠提供一些幫助。

Android內存管理機制

首先學習Android內存管理機制,瞭解系統如何分配和回收內存。

Java對象生命週期

Java對象在虛擬機上運行有7個階段,也就是對象的生命週期

  1. 創建階段(Created)
  • 爲對象分配存儲空間
  • 構造對象
  • 初始化
  1. 應用階段(InUse)
  • 對象至少被一個強引用持有,除非在系統顯示地使用來軟、弱、虛引用。
  1. 不可見階段(Invisible)
  • 處於不可見階段的對象在虛擬機的對象引用根集合再也找不到直接或間接的強引用,這些對象一般是所有線程棧中的臨時變量。
  • 當一個對象處於不可見階段時,說明程序本身不再持有該對象的任何強引用,雖然該對象仍然存在。
  1. 不可達階段(Unreachable)
  • 指該對象不再被任何強引用持有,且垃圾回收器發現給對象已經不可達。
  1. 收集階段(Collected)
  • 垃圾收集器發現該對象已經處於“不可達階段”並且垃圾回收器已經對該 i對象的內存空間重新分配做好準備,對象進入“收集階段”。(如果重寫來finalize()方法,則執行該方法)
  1. 終結階段(Finalized)
  • 當對象執行完finalize()方法後仍然處於不可達狀態時,該對象進入終結階段,等待垃圾回收器回收該對象空間。
  1. 對象空間重新分配階段(Deallocated)
  • 若垃圾回收器對該對象佔用的內存空間進行回收或者再分配,則該對象徹底消失,這個階段稱爲“對象空間重新分配階段”。

注意:在創建對象後,在確定不再需要使用該對象時,使對象置空,這樣更符合垃圾回收標準,比如Object = null,可以提供內存使用效率。

內存分配

在Android系統中,堆實際上是一塊匿名共享內存,Android虛擬機並沒有直接管理這塊匿名共享內存,而是把它封裝成一個mSpace,由底層C庫來管理。

爲了整個系統的內存控制需要,在Android系統爲每一個應用程序都設置一個硬性的Dalvik Heap Size最大限制閾值(視設備而定)。如果應用佔用內存空間接近閾值時,再嘗試分配內存很容易OOM。Android系統的內存堆被劃分爲不同的區塊,根據對數據配置對類型分配不同的區域內存,垃圾回收時,也會根據這些配置執行不同的垃圾回收處理過程,並且每一個區塊都有指定的單位大小。

Android Rumtime有兩種虛擬機,Dalvik和ART,他們分配的內存區域塊是不同的:

  • Dalvik: Linear Alloc、Zygote Space、Alloc Space
  • ART:Non Moving Space、Zygote Space、Alloc Space、Image Alloc、Large Obj Space

其中Image Alloc和Zygote Alloc在Zygote進程和應用程序進程之間共享,而Allocation Space是每個進程都獨立擁有一份。但Image Space的對象只創建一次,而Zygote Space的對象需要在系統每次啓動時,根據運行情況都重新創建一遍。

內存回收機制

整個內存分爲三個區域:年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。

1. Young Generation

年輕代分爲三個區,一個Eden區和兩個Survivor區S0和S1(S0和S1只是爲了好區分,兩者實質一樣,角色可互換)。

2. Old Generating

年老代存放的是上面年輕代複製過來的對象,也就是在年輕代還存活的對象並且區滿了複製過來的。一般來說,年老點中的對象生命週期都比較長。

3. Permanent Generation

用於存放靜態的類和方法,以及年老代移動過來的對象。持久代對垃圾回收沒有顯著影響。

內存對象的處理過程如下

  • 對象創建後在Eden區。
  • 執行GC時,如果對象仍然存活,則複製到S0區。
  • 當S0區滿時,該區存活對象將複製到S1區,然後S0清空,接下來S0和S1角色互換。
  • 當上一步達到一定次數(視系統版本差異)後,存活對象將被複制到Old Generation。
  • 當這個對象在Old Generation區域停留的時間達到一定程度時,它會被移動到Old Generation,最後積累一定時間再移動到Permanent Generation區域。

回收機制

系統在Young Generation和Old Generation上採用不同的回收機制。每一個Generation的內存區域都有固定的大小。隨着對象陸續被分配到此區域,當對象總的大小臨近這一級別內存區域的閾值時,會觸發GC操作,以便騰出空間來存放其他新的對象。

  • Young Generation:通常存活時間較短,因此基於Copying算法來回收,所謂Copying算法,就是掃描出存活的對象,並複製到一塊新的完成未使用的空間中。當連續分配對象時,對象會逐漸從Eden到Survivor,最後到Old Generation。
  • Old Generation:與Young Generation不同,對象存活的時間比較長,比較穩定,因此採用標記(Mark)算法來回收。所謂標記,就是掃描出存活的對象,然後在回收未被標記的對象,回收後對空出的空間要麼合併,要麼標記出來便於下次分配,以減少內存碎片帶來的效率損耗。

詳細內容可參考我另一篇文章

GC類型

Android系統中,GC有以下三種類型:

  • kGcCauseForAlloc:在分配內存時發現內存不夠的情況下引起的GC,這種情況下的GC會Stop World。Stop World是由於併發GC時,其他線程都會停止,直到GC完成。
  • kGcCauseBackground:當內存達到一定的閾值時觸發GC,這個時候是一個後臺GC,不會引起Stop World。
  • kGcCauseExplicit:顯式調用時進行的GC,如果ART打開了這個選項,在system.gc時會進行GC。

內存優化的意義

在GC過程中,任何其他在工作的線程(包括負責繪製的線程)都可能會被暫停,一旦GC消耗的時間超過16ms的閾值,就會出現丟幀。也就是說頻繁的GC會增加應用的卡頓

如果內存在某以階段的峯值達到了內存空間的閾值,或者頻繁地發生內存峯值(毛刺現象),剛好在這個峯值時,需要申請一塊較大的內存,就會由於對內存空間不足而導致OOM異常

內存泄漏是指應用已經不會再使用的內存對象,但垃圾回收時沒有把這些辨認出來,不能及時地回收,仍然一直保留在內存中,佔用了一定的空間,並且最終會到GC耗時最長的Old Generation,不釋放給其他對象。

內存優化主要有以下幾個意義:

  • 減少OOM,提高應用穩定性
  • 減少卡頓,提高應用流暢度
  • 減少內存佔用,提高應用後臺運行時的存活率
  • 減少異常發生,減少代碼邏輯隱患

內存分析工具

Memory Monitor

Memory Monitor是一款使用非常簡單的圖形化工具,可以很好地監控系統或應用的內存使用情況。可以快速發現內存抖動、大內存分配,甚至由於GC導致的卡頓。

(AS3.0以上的Android Profiler)

  1. 典型場景:內存分配與釋放、大內存申請與內存抖動。

Heap Viewer

Heap Viewer的主要功能是查看不同數據類型在內存中的使用情況。通過分析這些

 

image.png
  1. Heap Viewer啓動:在ADM面板,在進程列表選擇要查看的進程,單擊Update Heap按鈕。
  2. Heap Viewer面板

     
    image.png
  • data object:數據對象,Java類類型對象,是最主要的觀察對象。
  • class object:Java類類型的引用對象。

Allocation Tracker

Allocation Tracker可以分配跟蹤記錄應用程序的內存分配,並列出了他們的調用堆棧,可以查看所有對象內存分配的週期。

可以先用Memory Monitor或者Heap Viewer找到內存異常的場景,然後使用Allocation Tracker分析這個場景的內存使用情況。

  1. Allocation Tracker的使用:
  • 在Allocation Tracker選項卡,單擊Start Allocation Tracking;
  • 操作應用,懷疑內存有問題的操作;
  • 點擊Stop Allocation Tracking;
  • 自動生成一個alloc結尾的文件,記錄了追蹤到的所有內存數據。
  1. 查看面板信息

避免內存泄漏

GC會選擇一些還存活的對象作爲內存遍歷的根節點GC Roots,通過對GC Roots對可達性來判斷是否需要回收。GC Roots是系統選擇的對象根節點,對Heap進行遍歷,沒有被直接或間接遍歷到的引用會被GC 回收,能遍歷到的能被回收。這類在當前應用週期內不再使用的對象被GC Roots引用,導致不能回收,使實際可使用內存變小,這種現象在Android應用中稱爲內存泄漏。

使用MAT查找內存泄漏

MAT是一個快速、功能豐富的Java heap分析工具,可以幫助開發者定位導致內存泄漏的對象,以發現大的內存對象,然後解決內存泄漏並優化。

1. 使用步驟

  • AS並沒有集成MAT,需下載MAT客戶端
  • 獲取HPROF文件,進入ADM選擇要分析的應用進程,單擊Update Heap按鈕,操作幾次GC後點Dump HPROF File按鈕保存文件;
  • 右鍵文件彈出菜單選中Export standard .hprof選項,轉成標準的HPROF文件;
  • 用MAT打開轉換後的標準HPROF文件。

2. MAT視圖

分析內存最常用的是Histogram和Dominator Tree兩個視圖

(具體使用自行搜索哈哈)

場景內存泄漏場景

  1. 資源性對象未關閉:比如讀寫文件、操作數據庫等,如果僅僅把引用置null,而不關閉他們,往往會造成內存泄漏。
  2. 註冊對象未註銷:事件註冊後未註銷,會導致觀察者列表中維持着對象的引用,阻止垃圾回收。
  3. 類的靜態變量持有大數據對象:靜態變量長期維持對象的引用,阻止垃圾回收,如果持有的是如Bitmap等大的數據對象很容易引起內存問題。
  4. 非靜態內部類的靜態實例:非靜態內部類會維持一個到外部類對象的引用,如果非靜態內部類的實例是靜態,就會間接長期維持着外部類的引用,阻止被系統回收。
public class TestActivity extends Activity{
    private static TestModule mTestModule = null;
    @Override
    protected void onCreate(Bundle b){
        //...
        mTestModule = new TestModule(this);
    }
    class TestModule{
        private Context mContext = null;
        public TestModule(Context ctx){
            mContext = ctx;
        }
    }
}

上例中靜態實例mTestModule會一直持有該Activity的引用,導致Activity的內存資源不能正常回收。

  1. Handler臨時性內存泄漏:如果Handler是非靜態的,會持有外部類Activity的引用。有一種情況,當Activity退出時,如果消息隊列中還是未處理或正在處理的消息,並且消息隊列中的Message持有Handler實例的引用,會導致Activity資源無法被回收,引發內存泄漏。
    爲避免這種情況要修改兩個地方:
  • 使用一個靜態Handler內部類,然後對Handler持有的對象使用弱引用;
  • 在Activity的Destroy或stop時,移除消息隊列中的消息,避免Looper線程的消息隊列還有消息待處理。
public class TestActivity extends Activity{
    private NewHandler mHandler = new NewHandler(this);
    private static class NewHandler extends Handler{
        private WeakReference<Context>mContext = null;
        public NewHandler(Context ctx){
            mContext = new WeakReference<Context>(ctx);
        }
    }
    @Override
    protected void onDestroy(){
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}
  1. 容器中的對象沒清理造成的內存泄漏:通常把一些對象的引用加入集合中,在不需要該對象時,如果沒有把它的引用從集合中清掉,這個集合會越來越大。如果集合是static,情況更嚴重。
  2. 未正確使用Context:對於非一定要使用Activity的Context的情況可以考慮Application Context來代替,避免Activity 一泄漏,比如下面的單例:
public class Appsettings{
    private Context mAppContext;
    private static AppSettings mAppSettings = new AppSettings();
    public static AppSettings getInstance(){
        return mAppSettings;
    }
    public final void setup(Context context){
        mAppContext = context;
        //mAppContext = context.getApplicationContext(); 用這個代替
    }
}

如果setup(Context context)傳入的是Activity的Context,使得Activity被一個單例持有,mAppSettings作爲靜態變量,生命週期大於Activity,產生內存泄漏。

  1. 靜態View:使用靜態View可以避免每次啓動Activity都去渲染View,當靜態View會持有Activity的引用,導致Activity無法被回收,解決方法是在onDestory方法中將靜態View置爲null。
public class TestActivity extends Activity{
    public static Button button;
    //...
    button = (Button)findViewById(R.id.btn);
    //...
    protected void onDestory(){
        super.onDestory();
        button = null;
    }
}
  1. Bitmap對象:Bitmap對象在轉換得到新Bitmap對象後,應該儘快回收原始的Bitmap釋放空間。避免靜態變量持有比較大的Bitmap對象或其他大的數據對象。
  2. WebView:WebView存在內存泄漏問題,在應用中只有使用一次WebView,內存就不會被釋放掉。通常解決辦法是爲WebView開啓一個獨立的進程,使用AIDL與應用的主進程進行通信。WebView所在進程根據業務在合適時機銷燬。

內存監控

LeakCanary是一個檢測內存的開源類庫,可以在發生內存泄漏時告警,並且生成leak trace分析泄漏位置,同時可以提供Dump文件。

  1. 實現監控
  • 首先在build.gradle文件配置導入LeakCanary的SDK
  • 引入相關依賴後,在應用的自定義Application中安裝LeakCanary
public class GmfApplication extends Application{
    @Override
    protected void onCreate(){
        super.onCreate();
        mRefWatcher = LeakCanary.install(this);
    }
}

LeakCanary.install(this)會安裝一個Leaks的Apk,同時也啓用一個ActivityRefWatcher,用於自動監控調用Activity.onDestroy()之後泄漏的對象。

默認情況下,只對Activity進行監控,如果需要對Fragment或Service等這類組件監控,可以在Fragment onDestroy方法中,或自定義組件的週期結束回調接口加入以下實現

GmfApplication.getRefWatcher().watch(this);
  1. 自定義處理結果

僅僅依靠默認的處理方式,體驗不是很好,可以自定義監控結果處理。

  • 首先繼承DisplayLeakService實現一個自定義的監控處理Service,重新afterDefaultHandling方法。
public class LeakService extends DisplayLeakService{
    private final String TAG = "LeakService";
    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo){
        //自定義的處理
        super.afterDefaultHandling(heapDump,result,leakInfo);
    }
}

heapDump:堆內存文件,可以拿到完成的hprof文件

result:監控到內存的狀態,如是否泄漏等

leakInfo:leak trace詳細信息

  • 然後在install時,使用自定義的LeakService
public class GmfApplication extends Application{
    @Override
    protected void onCreate(){
        super.onCreate();
        mRefWatcher = LeakCanary.install(this, LeakService.class, AndroidExcludedRefs.createAppDefaults().build());
    }
}
  • 需要在AndroidManifest中註冊LeakService。

優化內存空間

對象引用

根據業務需求,使用合適的引用類型

  • 強引用:如果沒有指定對象引用類型,默認是強引用。如果一個對象具有強引用,垃圾回收器(GC)就絕不會回收它,即使內存空間不足。因此如果強引用的對象,在不需要時要記得釋放或轉成弱引用以便系統回收。
  • 軟引用:在保存引用對象的同時,保證在虛擬機報告內存不足的情況之前,清除所有的軟引用。
  • 弱引用:垃圾收集器運行時如果掃描到弱可及對象,將釋放WeakReference引用的對象,不管當前內存是否足夠都會回收它的內存。
  • 虛引用:只能用於跟蹤即將對被引用對象進行的收集,使用戶能夠剛好在對象佔用的內存被回收之前採取行動。(如果一個對象盡持有虛引用,它就和沒有任何引用一樣,在任何時候都可能被垃圾收集器回收)

減少不必要的內存開銷

  1. 自動裝箱AutoBoxing
Integer num = 0;
for(int i=0; i < 100; i++){
    num += I;
}

考慮上面的情況,在自動裝箱轉化時,都會產生一個新的對象,這些對象比基礎數據類型要大,這樣會產生更多內存和性能開銷。(int只有4字節,而Integer對象有16字節)

  1. 內存複用
  • 有效利用系統自帶的資源:比如一些通用的字符串、顏色定義、常用Icon等。
  • 視圖複用:重複子組件,可以使用ViewHolder實現ConvertView複用
  • 對象池:顯示在程序創建對象池,實現複用邏輯,對相同類型數據使用同一內存空間
  • Bitmap對象複用:利用Bitmap中的inBitmap的高級特性。
  1. 使用最優的數據類型
  • ArrayMap與HashMap

HashMap是一個散列鏈表,先HashMap中put元素時,先根據key的HashCode重新計算hash值,根據hash值得到這個元素在數組中的位置,如果數組位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭。爲了減少hash衝突,會配置一個大的數組,從內存節省的角度是非常不理想的。爲了解決這個問題,Android提供了一個替代容器ArrayMap。

ArrayMap提供了和HashMap一樣的功能,但避免了過多的內存開銷,方法是使用兩個小數組而不是一個大數組。其中一個數組記錄對象Key Hash過後的順序列表,另外一個數組按Key的順序記錄Key-Value值,根據Key數組的順序,交織在一起。在獲取某個value時,ArrayMap會計算輸入Key轉換後的hash值,然後使用二分查找法對Hash數組尋找到對應的index,然後通過這個index在另外一個數組中直接訪問需要的鍵值對。如果在第二個數組鍵值對中的key和前面輸入的查詢key不一致,就認爲發生了碰撞衝突。ArrayMap會以該key爲中心點,分別上下展開,逐個對比查找,直到找到匹配的值。

ArrayMap中執行插入或刪除時,性能比HashMap要差一點,但如果設計對象數少,比如1000以下,不用擔心這個問題。用ArrayMap能節省內存。

  • 枚舉類型和替代方案

枚舉的優點是類型安全,可讀性高,但是枚舉的內存開銷是直接定義常量的三倍以上。官方也提醒儘量避免使用枚舉類型,同時提供註解的方式檢測類型安全,目前提供了int和String兩者類型註解方式:IntDef和StringDef。即使用“常量定義+註解”替代枚舉。

public static final int UI_LEVEL_0 = 0;
public static final int UI_LEVEL_1 = 1;

@IntDef({UI_LEVEL_0, UI_LEVEL_1})
@Retention(RetentionPolicy.SOURCE)
public @interface PER_LEVEL{
    
}

public static int getLevel(@PER_LEVEL int level){
    switch(level){
        case UI_LEVEL_0: return 0;
        case UI_LEVEL_1: return 1;
        default:
            throw new IllegalArgumentException("UnKonw");
    }
}

使用IntDef和StringDef需要在Gradle引入依賴

compile 'com.android.support:support-annotation:22.0.0'
  1. LruCache

圖片內存優化

Android設備上顯示圖片需要把圖片解碼成位圖格式,佔用的內存只和位圖的質量和大小相關。下面介紹幾種減少圖片內存開銷的方法:

  1. 設置位圖規格

系統默認位圖格式是RGB_8888佔用內存較高,一般用RGB_565或RGB_4444代替。
RGB_8888佔32bit、GB_565和RGB_4444都是16bit、ALPHA_8佔8bit

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap.Factory.decodeStream(is, null, options);
  1. 縮放inSampleSize

如果內存中的圖片大於屏幕需顯示圖片的大小,這些高分辨率圖片會導致性能問題。可以通過重置這些圖片大小,讓它們符合實際顯示大小。Bitmap的inSampleSize屬性能實現位圖縮放功能。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; //實際要根據寬高比例計算縮放比例
Bitmap.Factory.decodeStream(is, null, options);
  1. 三級緩存

可參考郭霖博客

本文參考書籍《Android應用性能優化最佳實踐》

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