淺談Android性能優化之內存優化

1、Android內存管理機制

1.1 Java內存分配模型

先上一張JVM將內存劃分區域的圖

程序計數器:存儲當前線程執行目標方法執行到第幾行。

棧內存:Java棧中存放的是一個個棧幀,每個棧幀對應一個被調用的方法。棧幀包括局部標量表,
操作數棧。

本地方法棧:本地方法棧主要是為執行本地方法服務的。而Java棧是為執行Java方法服務的。
方法區:該區域被線程共享。主要存儲每個類的信息(類名,方法信息,字段信息等)、靜態變量,常量,以及編譯器編譯後的代碼等。

堆:Java中的堆是被線程共享的,且JVM中隻有一個堆內存,主要存儲對象本身及數組

1.2 Dalvik和ART介紹

Dalvik:Dalvik是Google公司自己設計用於Android平臺的Java虛擬機。它可以支持已轉換為.dex格式的Java應用程序的運行,.dex格式是專門為Dalvik應用設計的一種壓縮格式,適合內存和處理器速度有限的系統,Dalvik經過優化,允許在有限的內存中同時運行多個虛擬機實例,並且每一個Dalvik應用做為獨立的Linux進程執行,獨立的進程可以防止在虛擬機崩潰的時候所有程序都被關閉。

ART:ART表示Android Runtime,Dalvik是依靠一個just-In -Time編譯器去解釋字節碼,運行時編譯後的應用都需要通過一個解釋器在用戶的設備上運行,這一機制並不是特別高效,但是能讓應用更容易在不同的硬件和架構上運行。ART則是完全改變瞭這種做法,在安裝應用的時候就預編譯字節碼到機器語言,這一機制叫預編譯。在移除解釋代碼這一過程,應用程序執行將更有效率,啟動速度更快。

ART優點:

1.系統性能更高

2.應用啟動速度,運行更快,體驗更好,觸感反饋更加及時。

3.更長的電池續航能力

4.支持更低的硬件

ART缺點:

1.儲存空間占用更大。

2.應用安裝時間更長。

Dalvik與ART區別

1.Dalvik每次都要編譯在運行,art隻會安裝時啟動編譯

2.art占用的空間比Dalvik要大,就是用空間換時間

3.art減少編譯,減少CPU使用頻率,使用明顯改善電池續航

4.art啟動,運行更快,體驗更好,觸感反饋更及時。

1.3 為什麼要進行內存優化

1.減少oom,提高應用的穩定性
2.減少卡頓,體驗更好
3.減少內存占用,應用存活率更高
4.提前處理掉一些異常的隱患

2、Java內存回收算法

2.1判斷Java中對象是否存活的算法

2.1.1 引用計數算法

堆內存的每個對象都有一個引用計數器,當對象被引用的時候,計數器+1,當引用失效時計數器-1,當計數器的值為0時,說明該對象沒有被引用,就會被認為是垃圾對象,系統將會將其回收內存重新分配。

優點:引用計數器執行簡單,判定效率高。

缺點:對於循環引用的對象難以判斷出來,同時引用計數器增加瞭程序執行的開銷,在jdk1.1後,就不在使用瞭。

2.1.1 根搜索法

GC Roots的對象做為起點,然後向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則該對象不可達,也就是說該對象為為垃圾對象,可以被回收。
在Java中,可以做為GC Roots的對象包括一下四種:

1.虛擬機棧中引用的對象

2.方法區中的類靜態屬性引用的對象

3.方法區中常量引用的對象

4.本地方法棧中JNI的引用對象

2.2 JVM垃圾回收算法

2.2.1 標記清除法

最基礎的垃圾收集算法,算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成之後統一回收掉所有被標記的對象。

缺點:效率低,其次會產生大量的不連續的內存碎片,導致提前觸發另一次垃圾收集動作。

2.2.2 復制回收算法

復制回收算法是將可用內存按容量分成大小相等的兩塊,每次隻使用其中的一塊,當這塊內存使用完瞭,就將存活的對象復制到另一塊內存上去,然後把使用過的內存空間一次清理掉,這樣使得每都次都是對其中一塊內存進行回收,內存分配時不用考慮內存碎片等復雜情況。

缺點:可使用內存降為原來的一半。

2.2.3 標記整理法

標記-整理算法在標記-清除算法的基礎上做瞭改進,標記階段將可回收的對象標記出來,標記完成後不是直接對可回收的對象進行清理,而是讓所有存活的對象都向一端移動,在移動的過程中清理掉可回收的對象。

優點:相比於標記清除法來說,標記整理法不會大量產生不連續內存碎片問題。

缺點:如果是在對象存活率較高的情況下會執行較多的復制操作,效率將會降低很多,而在存活率較低的情況下,效率會大大提高。

2.2.4 分代收集回收算法

當前商業虛擬機都是采用的是分代收集算法,根據對象存活的周期不同將內存劃分為幾塊,一般是將java堆分為年輕代,老年代和永久代。然後根據各個年代的特點來采取不同收集算法,年輕代存活率較低,采用復制回收算法,老年代對象存活率較高,采用標記清除法或者是標記整理法來進行回收。

3、內存問題表現形式

3.1 內存抖動

內存波動圖呈鋸齒狀,gc頻繁導致卡頓。

3.2 內存泄漏

內存泄露簡單來說就是系統分配出去的內存由於某種原因導致沒法釋放,內存會越來越小,最終導致oom。

3.3 內存溢出

即OOM,OOM時會導致程序異常。Android設備出廠以後,java虛擬機對單個應用的最大內存分配就確定下來瞭,超出這個值就會OOM。

4、內存優化常用工具

4.1 Memory Profiler

Memory Profiler是Android studio自帶的工具,實時圖表形式展示應用內存使用的情況,可以用來識別內存泄露,抖動等
註意:如果在控制臺中沒有找到Profiler,可View —–> Tool Windows —> Profiler 進行打開

優點:方便直觀,便於線下使用

4.2 Memory Analyzer(MAT)

1、強大的java heap分析工具,查找內存泄露及內存占用

2、生成整體報告,便於分析問題

3、可以在線下深入使用

MAT使用:

MAT下載地址:https://www.eclipse.org/mat/downloads.php

獲取hprof文件

導出來的Dump是沒法直接使用mat打開的,Android SDK自帶瞭一個轉換工具在SDK的platform-tools下,其中轉換語句為:

cd D:\aa\sdk\platform-tools

hprof-conv aaa.hprof  bbb.hprof

註:aaa.hprof表示從profiler中導出來的dump文件,bbb.hprof 表示轉化出來的dump文件

使用mat打開轉化出來的dump

MAT視圖

在MAT窗口上,OverView是一個總體概覽,顯示總體的內存消耗情況和疑似問題。

1、Histogram:列出內存中的所有實例對象和個數以及大小,在頂部regex區域支撐正則表達式查找

2、Dominator Tree:列出最大的對象及其依賴存活的Object,相比於Histogram,能更方便的看出引用關系。

3、Top Consumers:通過圖像列出最大的Object

4、Leak Suspects:通過MAT自動分析內存泄露的原因和泄露的一份總體報告

其中分析內存情況,我們基本用到的就是Histogram和Dominator Tree

Class Name:類名。

Objects:對象實例個數。

Shallow Heap:對象自身占用內存大小,不包括它引用的對象

Retained Heap:是當前對象大小和直接或者間接引用到的對象大小總和,包括遞歸釋放的。

查找內存泄露方式

步驟一:在Regex通過包名進行匹配,當然也可以通過其他方式進行匹配

步驟二:右鍵選中懷疑對象,List objects –> with incoming references

註 with outgoing references 他引用瞭那些對象

with incoming references 那些對象引用瞭他

步驟三:選擇當前的一個 Path to GC Roots/Merge to GC Roots 的 exclude All 弱軟虛引用。

圖標的左下角出現這個,則表示出現瞭內存泄露。然後回調代碼中分析即可。

4.3 LeakCanary

使用

implementation ‘com.squareup.leakcanary:leakcanary-android:1.5.4’

application中

public class App extends Application {

    private RefWatcher mRefWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
     mRefWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        App application = (App) context.getApplicationContext();
        return application.mRefWatcher;
    }
 }

在activity或者fragment中的onDestory()方法調用

RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);

原理

主要是通過WeakReference + ReferenceQueue來判斷對象是否被系統GC回收,WeakReference創建時,傳入一個ReferenceQueue對象,當WeakReference引用的對象生命周期結束後,會被添加到ReferenceQueue中,當GC過後,對象一直沒有被添加進入到ReferenceQueue,可能就會存在內存泄露,再次觸發GC,二次確認。

5、常見的內存泄露

1、資源性對象未關閉

對於資源性對象不再使用時,應該立即調用它的close()函數,將其關閉,然後再置為null。例如Bitmap等資源未關閉會造成內存泄漏,此時我們應該在Activity銷毀時及時關閉。

2、註冊對象未註銷

例如BraodcastReceiver、EventBus未註銷造成的內存泄漏,我們應該在Activity銷毀時及時註銷。

3、類的靜態變量持有大數據對象

盡量避免使用靜態變量存儲數據,特別是大數據對象,建議使用數據庫存儲。

4、單例造成的內存泄漏

優先使用Application的Context,如需使用Activity的Context,可以在傳入Context時使用弱引用進行封裝,然後,在使用到的地方從弱引用中獲取Context,如果獲取不到,則直接return即可。

5、非靜態內部類的靜態實例

該實例的生命周期和應用一樣長,這就導致該靜態實例一直持有該Activity的引用,Activity的內存資源不能正常回收。此時,我們可以將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,盡量使用Application Context,如果需要使用Activity Context,就記得用完後置空讓GC可以回收,否則還是會內存泄漏。

6、Handler臨時性內存泄漏

Message發出之後存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引用,Message在Queue中存在的時間過長,就會導致Handler無法被回收。如果Handler是非靜態的,則會導致Activity或者Service不會被回收。並且消息隊列是在一個Looper線程中不斷地輪詢處理消息,當這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,並且消息隊列中的Message持有Handler實例的引用,Handler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。解決方案如下所示:

  • 1、使用一個靜態Handler內部類,然後對Handler持有的對象(一般是Activity)使用弱引用,這樣在回收時,也可以回收Handler持有的對象。
  • 2、在Activity的Destroy或者Stop時,應該移除消息隊列中的消息,避免Looper線程的消息隊列中有待處理的消息需要處理。需要註意的是,AsyncTask內部也是Handler機制,同樣存在內存泄漏風險,但其一般是臨時性的。對於類似AsyncTask或是線程造成的內存泄漏,我們也可以將AsyncTask和Runnable類獨立出來或者使用靜態內部類。

7、容器中的對象沒清理造成的內存泄漏

在退出程序之前,將集合裡的東西clear,然後置為null,再退出程序

8、WebView

WebView都存在內存泄漏的問題,在應用中隻要使用一次WebView,內存就不會被釋放掉。我們可以為WebView開啟一個獨立的進程,使用AIDL與應用的主進程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,達到正常釋放內存的目的。

9、使用ListView時造成的內存泄漏

在構造Adapter時,使用緩存的convertView。

6、優化內存空間的方式

6.1、java對象的引用

強引用:我們平時開發寫的代碼,基本百分之九十九的都是強引用。

軟引用:如果一個對象具有軟引用,那麼當內存不足時,就會回收它。

弱引用:GC時,隻要發現有弱引用,那麼就會回收它,當然,有可能存在GC多次才發現

虛引用:虛引用必須要和引用隊列關聯起來使用。任何時候都有可能被垃圾回收器回收。一般可以用來判斷GC的頻率,GC頻率過高,那麼說明內存出瞭問題。同時也可以監聽某個重要的對象是否被回收。

所以,在平時我們編寫代碼的時候,適當的使用軟引用,弱引用,對我們的內存優化也能起到重要的作用。

6.2、減少不必要的內存開銷

1、AutoBoxing

自動裝箱的核心是吧基礎數據類型轉換成對應的包裝類,比如int 類型隻是占用4字節,但是Integer對象占用16字節。

2、內存復用

資源復用:通用的字符串,顏色定義,簡單頁面佈局的復用

視圖復用:進行佈局復用

3、使用優化過的數據類型

如 SparseArray、SparseBooleanArray、LongSparseArray,使用這些API可以讓我們的程序更加高效。HashMap 工具類會相對比較 低效,因為它 需要為每一個鍵值對都提供一個對象入口,而 SparseArray 就 避免 掉瞭 基本數據類型轉換成對象數據類型的時間。

4、項目中少用枚舉

枚舉占用內存是常量三倍。

5、在應用可以內存過低時主動釋放內存

在application中的 onTrimMemory/onLowMemory,內存緊張時會回調該方法,我們可以在這個方法中釋放掉圖片緩存,靜態緩存來避免被kill。

6、避免創建一些不必要的對象

如在字符串拼接時不要用“+”來進行拼接,而是使用StringBuffer,StringBuilder來替代。因為String 內部是被final修飾的,不可繼承,使用+進行拼接是會產生一個新的對象,而占用內存。

7、盡量不要在一些循環的地方創建對象。

如自定義的時候在onDraw()方法。

7、優雅的檢測大圖

項目中會經常遇到這樣的情況,我們的佈局中,控件的寬高可能隻是50 * 50 但是從服務器給過來的圖片或者是UI給過來的圖片往往會大很多,而如果圖片在資源文件下還好,可以直接查看寬高,但是如果從服務器上獲取到的呢,這是我們經常會忽略的。而圖片過大,占用的內存就更多,這是沒有必要的。那麼我們怎麼檢測出服務器給過來的圖片過大的呢?

7.1、繼承ImageView 重新實現onDraw()

這種方法我們可以重新測量圖片的寬高,超過一定的范圍,我們就可以輸出警告。但是這種方法對代碼侵入性很強。如果是有新同學加入,容易造成代碼混亂。

7.2、ARTHook

Hook的意思是鉤子,也就是在消息過去之前可以把消息勾住,不讓其傳遞,能夠針對不同的消息或者api在執行之前,先執行我們自己的操作。

這裡推薦使用Epic 框架:https://github.com/tiann/epic

添加依賴

implementation ‘me.weishu:epic:0.3.6’

創建一個ImageHook類

package com.optimize.performance.memory;

import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

import com.optimize.performance.utils.LogUtils;
import com.taobao.android.dexposed.XC_MethodHook;

public class ImageHook extends XC_MethodHook {

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        // 實現我們的邏輯
        ImageView imageView = (ImageView) param.thisObject;
        checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
    }

    private static void checkBitmap(Object thiz, Drawable drawable) {
        if (drawable instanceof BitmapDrawable && thiz instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                final View view = (View) thiz;
                int width = view.getWidth();
                int height = view.getHeight();
                if (width > 0 && height > 0) {
                    // 圖標寬高都大於view帶下的2倍以上,則警告
                    if (bitmap.getWidth() >= (width << 1)
                            && bitmap.getHeight() >= (height << 1)) {
                        warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                    }
                } else {
                    final Throwable stackTrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int w = view.getWidth();
                            int h = view.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1)
                                        && bitmap.getHeight() >= (h << 1)) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
                                }
                                view.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }
    
    
    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();

        LogUtils.i(warnInfo);
    }

}

在application中

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
            }
        });

這樣在開發者調用setImageBitmap 來設置圖片的時候,都會進行對圖片的寬高進行比如,如果超出一定的范圍則進行提示。

以上就是淺談Android性能優化之內存優化的詳細內容,更多關於Android性能優化之內存優化的資料請關註WalkonNet其它相關文章!

推薦閱讀: