Android 效能優化 ---- 記憶體優化
阿新 • • 發佈:2020-07-17
### 1、Android記憶體管理機制
#### 1.1 Java記憶體分配模型
先上一張JVM將記憶體劃分區域的圖
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091650518-110380858.png)
程式計數器:儲存當前執行緒執行目標方法執行到第幾行。
棧記憶體: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 標記清除法
最基礎的垃圾收集演算法,演算法分為標記和清除兩個階段:首先標記出所有需要回收的物件,在標記完成之後統一回收掉所有被標記的物件。
缺點:效率低,其次會產生大量的不連續的記憶體碎片,導致提前觸發另一次垃圾收集動作。
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091714627-97965139.jpg)
##### 2.2.2 複製回收演算法
複製回收演算法是將可用記憶體按容量分成大小相等的兩塊,每次只使用其中的一塊,當這塊記憶體使用完了,就將存活的物件複製到另一塊記憶體上去,然後把使用過的記憶體空間一次清理掉,這樣使得每都次都是對其中一塊記憶體進行回收,記憶體分配時不用考慮記憶體碎片等複雜情況。
缺點:可使用記憶體降為原來的一半。
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091737259-1262745247.jpg)
##### 2.2.3 標記整理法
標記-整理演算法在標記-清除演算法的基礎上做了改進,標記階段將可回收的物件標記出來,標記完成後不是直接對可回收的物件進行清理,而是讓所有存活的物件都向一端移動,在移動的過程中清理掉可回收的物件。
優點:相比於標記清除法來說,標記整理法不會大量產生不連續記憶體碎片問題。
缺點:如果是在物件存活率較高的情況下會執行較多的複製操作,效率將會降低很多,而在存活率較低的情況下,效率會大大提高。
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091749776-1503117187.jpg)
##### 2.2.4 分代收集回收演算法
當前商業虛擬機器都是採用的是分代收集演算法,根據物件存活的週期不同將記憶體劃分為幾塊,一般是將java堆分為年輕代,老年代和永久代。然後根據各個年代的特點來採取不同收集演算法,年輕代存活率較低,採用複製回收演算法,老年代物件存活率較高,採用標記清除法或者是標記整理法來進行回收。
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091802605-254796353.jpg)
### 3、記憶體問題表現形式
#### 3.1 記憶體抖動
記憶體波動圖呈鋸齒狀,gc頻繁導致卡頓。
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091814683-1208578109.png)
#### 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檔案
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091830307-1991541877.png)
匯出來的Dump是沒法直接使用mat開啟的,Android SDK自帶了一個轉換工具在SDK的platform-tools下,其中轉換語句為:
``` stylus
cd D:\aa\sdk\platform-tools
hprof-conv aaa.hprof bbb.hprof
```
注:aaa.hprof表示從profiler中匯出來的dump檔案,bbb.hprof 表示轉化出來的dump檔案
使用mat開啟轉化出來的dump
MAT檢視
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091841824-1096903747.png)
在MAT視窗上,OverView是一個總體概覽,顯示總體的記憶體消耗情況和疑似問題。
1、Histogram:列出記憶體中的所有例項物件和個數以及大小,在頂部regex區域支撐正則表示式查詢
2、Dominator Tree:列出最大的物件及其依賴存活的Object,相比於Histogram,能更方便的看出引用關係。
3、Top Consumers:通過影象列出最大的Object
4、Leak Suspects:通過MAT自動分析記憶體洩露的原因和洩露的一份總體報告
其中分析記憶體情況,我們基本用到的就是Histogram和Dominator Tree
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091856106-271174751.png)
Class Name:類名。
Objects:物件例項個數。
Shallow Heap:物件自身佔用記憶體大小,不包括它引用的物件
Retained Heap:是當前物件大小和直接或者間接引用到的物件大小總和,包括遞迴釋放的。、
查詢記憶體洩露方式
步驟1:在Regex通過包名進行匹配,當然也可以通過其他方式進行匹配
步驟二:右鍵選中懷疑物件,List objects --> with incoming references
注 with outgoing references 他引用了那些物件
with incoming references 那些物件引用了他
步驟三:選擇當前的一個 Path to GC Roots/Merge to GC Roots 的 exclude All 弱軟虛引用。
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200717091920715-1286382404.png)
圖示的左下角出現這個,則表示出現了記憶體洩露。然後回撥程式碼中分析即可。
#### 4.3 LeakCanary
使用
``` stylus
implementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
```
application中
``` stylus
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()方法呼叫
``` stylus
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、記憶體複用
資源複用:通用的字串,顏色定義,簡單頁面佈局的複用
檢視