前言
Android App優化這個問題,我相信是Android開發者一個永恆的話題。本篇文章也不例外,也是來講解一下Android記憶體優化。那麼本篇文章有什麼不同呢? 本篇文章主要是從最基礎的Android系統記憶體管理方面出發再到App優化方法,讓你能更加清楚地理解、處理Android記憶體優化問題,下面進入正題。
Android記憶體的管理方式
Android系統分配和回收方式
通常情況下,一個APP就是一個程式或者說是一個虛擬機器。也就是說我們一個APP執行的時候那麼就有一個單獨的程式在執行。但是也有很多的大公司在Mainfest指定process程式名字,所以會看到一個APP對應多個程式的情況。
我們用實際的程式碼來演示看一下: 這是我執行的一個App的包名:
我們在Windows上看一下他的程式:
UID表示:使用者 PID表示:程式ID PPID表示:程式父ID CMD表示:名字 可以看一下我們的App執行的程式在上面可以找到 程式ID:12768 程式父ID:1509 通過父ID我們可以找到: 我們通過這張圖可以清楚的看到一個我們熟悉的名字:zygote 這個是什麼呢?zygote程式是由init程式啟動起來,在Android中,zygote是整個系統建立新程式的核心程式,換句話說就是zygote程式是android的孵化程式也就是父程式。
通過命令 dumpsys meminfo + 程式名字,可以獲取具體資訊:
簡單介紹下我們需要知道:Pss Total : 當前使用實體記憶體的大小
Heap Size : 堆空間
Heap Alloc : 分配多少堆空間
Heap Free :空閒堆空間
一般來說:Heap Size = Heap Alloc + Heap Free
Native Heap:指的JIN開發所佔的堆空間 Dalvik Heap : 虛擬機器的堆空間 Dalvik Other : 虛擬機器其他所佔空間 stack : 堆疊佔多少
其他還有很多的有用資訊,就不一一解釋了,感興趣的可以多去了解這方面的知識,我這裡就主要說一下我們經常記憶體洩漏主要在:Pss Total 中的TOTAL不斷的變大就可以看出記憶體洩漏
GC就是垃圾收集器,只有在Heap剩餘空間不夠的時候才會觸發垃圾回收。
Java的垃圾回收機制就是你在開發的時候不用去關注記憶體是否去釋放,這個是一個優點,但是也有缺點就是當前的變數不使用了,放在一邊,只有當記憶體不夠的時候才會觸發GC去回收這些不使用的記憶體。為什麼說是個缺點呢?
**因為在GC觸發垃圾回收的時候,所有的執行緒都會被暫停,此時就會我們經常出現的卡頓現象。
APP記憶體限制機制
首先我們要知道一個理論:每個APP分配的記憶體最大限制,是隨著裝置的不同而改變的。因此,我們才需要我們去管理我們的記憶體,有一點要明白的就是系統分配的記憶體,一般情況下是肯定夠使用的,如果出現OOM這種情況,那麼必定是你的APP優化的不夠好。
最常說的吃記憶體的: 高清圖片,現在的手機拍照動不動就是以M為單位。但是就我們目前的開發來說,大多數人使用的是Glide、Picasso的框架,其實都是框架給我們處理了管理圖片的問題。
為什麼要限制記憶體?
假如我們每個App都不限制記憶體的大小,那麼各自的APP都不管理,讓記憶體一直增大,Android系統是允許多個APP同時執行的,總的空間是固定的,最終導致結果必然有些APP沒有記憶體可以分配。
切換後臺是APP清理機制
APP切換的時候採用的是LRU Cache這種演算法。
什麼是LRU Cache演算法?
LRU Cache是一個Cache置換演算法,含義是“最近最少使用”,當Cache滿(沒有空閒的cache塊)時,把滿足“最近最少使用”的資料從Cache中置換出去,並且保證Cache中第一個資料是最近剛剛訪問的。由“區域性性原理”,這樣的資料更有可能被接下來的程式訪問。 切換到實際場景就是,我們APP切換的時候會把剛剛訪問的放在第一個。當我們記憶體不足的時候我們就會置換出最近最少使用、或者最久未使用的。
而最近使用的APP,最不容易被清理掉。
當我們的應用要被清理掉的時候,或者是我們的記憶體出現不夠的時候,我們的APP中會回撥一個方法
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
}
複製程式碼
我們解釋一下Level這引數的意義:
1.當你的app在後臺時:
TRIM_MEMORY_COMPLETE :當前程式在LRU列表的尾部,如果沒有足夠的記憶體,它將很快被殺死。這時候你應該釋放任何不影響app執行的資源。
TRIM_MEMORY_MODERATE :當前程式在LRU列表的中部,如果系統進一步需要記憶體,你的程式可能會被殺死。
TRIM_MEMORY_BACKGROUND:當前程式在LRU列表的頭部,雖然你的程式不會被高優殺死,但是系統已經開始準備殺死LRU列表中的其他程式了, 因此你應該儘量的釋放能夠快速回復的資源,以保證當使用者返回你的app時可以快速恢復。 。
2.當你的app的可見性改變時:
TRIM_MEMORY_UI_HIDDEN:當前程式的介面已經不可見,這時是釋放UI相關的資源的好時機。
3.當你的app正在執行時:
TRIM_MEMORY_RUNNING_CRITICAL:雖然你的程式不會被殺死,但是系統已經開始準備殺死其他的後臺程式了,這時候你應該釋放無用資源以防止效能下降。下一個階段就是呼叫”onLowMemory()”來報告開始殺死後臺程式了,特別是狀況已經開始影響到使用者。
TRIM_MEMORY_RUNNING_LOW:雖然你的程式不會被殺死,但是系統已經開始準備殺死其他的後臺程式了,你應該釋放不必要的資源來提供系統效能,否則會 影響使用者體驗。
TRIM_MEMORY_RUNNING_MODERATE:系統已經進入了低記憶體的狀態,你的程式正在執行但是不會被殺死。
我們可以用過這個Level的引數來判斷當前APP的情況,來優化記憶體。
監控記憶體的幾種演示方法
1.在我們的程式碼中動態列印出我們的內
private void printMemorySize() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
//以M為單位 當前APP最大限制記憶體
int memoryClass = activityManager.getMemoryClass();
//通過在Manifest <application>標籤中largeHeap屬性的值為"true" 為應用分配的最大的記憶體
int largeMemoryClass = activityManager.getLargeMemoryClass();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("memoryClass===" + memoryClass+"\n")
.append("largeMemoryClass===" + largeMemoryClass);
Logger.e(stringBuilder.toString());
//以M為單位輸出當前可用總的Memory大小
float totalMemory = Runtime.getRuntime().totalMemory() * 1.0f / (1014 * 1024);
// 以M為單位輸出當前空閒的Memory大小
float freeMemory = Runtime.getRuntime().freeMemory() * 1.0f / (1024 * 1024);
// 以M為單位輸出虛擬機器限制的最大記憶體
float maxMemory = Runtime.getRuntime().maxMemory() * 1.0f / (1024 * 1024);
StringBuilder builder = new StringBuilder();
builder.append("totalMemory==" + totalMemory + "\n")
.append("freeMemory==" + freeMemory + "\n")
.append("maxMemory==" + maxMemory + "\n");
Logger.e(builder.toString());
}
複製程式碼
通過上面的程式碼和圖我們就可以清楚的知道我們的應用的記憶體的情況。
這種方法也是最複雜的方法,需要程式碼去列印,下面兩種是我們的Android studio提供的工具。
2.Android studio 3.1 工具 Android profiler
這裡寫圖片描述
這種方式是方便檢視的,直接在下方點選 Android profiler就可以了,方便快捷
3.DDMS
開啟DDMS
這也是看我們的APP的記憶體使用情況,標記了的假如你的APP在執行的時候 data object和 class object 不斷的變化,那就說明你的應用可能有記憶體洩露了,這個時候你就需要檢查一下。
Android記憶體優化
資料結構的優化
1.字串拼接
我們都知道我們的如果使用string 的 “+”方式來拼接字串,會產生字串中間記憶體塊,這些記憶體塊是無用的,造成記憶體浪費,這種方式低效、而且耗時。我們就是實際看看:
int length = 20;
int rawLength = 300;
int[][] intMatrix = new int[length][rawLength];
for (int i = 0; i < length; i++) {
for (int j = 0; j < rawLength; j++) {
intMatrix[i][j] = ran.nextInt();
}
}
複製程式碼
初始化一個兩維的矩陣,得到隨機數。
/**
* 用StringBuilder連線起來
*/
private void strBuild() {
StringBuilder builder = null;
Log.e("test", "builder start:");
for (int i = 0; i < length; i++) {
for (int j = 0; j < rawLength; j++) {
builder.append(intMatrix[i][j]+"").append(",");
}
Log.e("test", "builder:" + i);
}
Log.e("test", "add finish:" + builder.toString().length());
}
複製程式碼
/**
* 字串用 “+” 連線起來
*/
private void strAdd() {
String str = null;
Log.e("test", "add start:");
for (int i = 0; i < length; i++) {
for (int j = 0; j < rawLength; j++) {
str = str + intMatrix[i][j];
str = str + ",";
}
Log.e("test", "add:" + i);
}
Log.e("test", "add finish:" + str.length());
}
複製程式碼
得到的用 “+”連結的結果:
耗時2.6s
用stringBuilder的結果:
耗時0.06s
這就可以看出我們的String和StringBuilder的使用效率的對比了。
2.替換HashMap
還有值得一提的就是JAVA裡面的HashMap,這個使用的效率是不高的,我們要用ArrayMap、SparseArray替換。
3.記憶體抖動
記憶體都用的主要原因是我們記憶體變數的使用不當造成的
/**
* 試驗記憶體抖動
*/
private void doChurn() {
Log.e("test", "doChurn start: ");
int len = 10;
int rawLen = 450000;
for (int i = 0; i < rawLen; i++) {
String[] strings = new String[len];
for (int j = 0; j < len; j++) {
strings[j] = String.valueOf(ran.nextInt());
}
Log.e("test", "doChurn : " + i);
}
Log.e("test", "doChurn end: ");
}
複製程式碼
重點就是在建立string陣列那裡,是放在第一個for迴圈裡面,rawLen=450000,因此會建立450000個物件。
這一塊就是我們的記憶體抖動的情況。
分析一下原因:
我們在for迴圈裡面建立了45000個string物件,然後再裡面新增了資料之後就沒有使用了,當建立的物件達到記憶體限制的時候就會觸發GC回收,接下來又建立,又回收,這樣就導致了記憶體抖動的情況。
記憶體的複用
-
複用系統自帶的資源
-
ListView/GridView中的ConvertView的複用,當然我們現在ListView和GridView使用已經很少了,都被RecyclerView給取代了
-
我們在自定義View的要避免在onDraw中去建立物件,因為onDraw方法會經常執行
記憶體洩露
記憶體洩露已經是老生常談了,但是我們還是要舉一些簡單的例子讓大家知道怎樣會造成記憶體洩露。
什麼是記憶體洩露?
記憶體洩露:由於你程式碼的問題,導致某一塊記憶體雖然已經不使用了,但是依然被其他的東西(物件或者其他)引用著,使得GC沒發對它回收。 所以記憶體洩露會導致APP剩餘可用的Heap越來越少,頻繁觸發GC。
1.內部內造成的記憶體洩露
/**
* 建立一個執行緒
*/
private class MyThread extends Thread {
@Override
public void run() {
try {
//休眠5分鐘
Thread.sleep(1000 * 60 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
上面這個是一個Activity的內部內,每次啟動這個activity都會開啟這個執行緒。點選按鈕開啟這個Activity 觸發執行緒休眠 5min,然後按返回鍵,再點選按鈕開啟這個Activity觸發執行緒休眠5min,就這樣依次反覆操作多次。我們5min中內可以重複這樣的N次操作,我們的操作會頻繁的觸發GC回收,但是由於我們的執行緒還在執行,這個內部類是預設持有外部類物件,因此這個Activity就不會被回收,就造成了記憶體洩露。
**內部內又分很多種,靜態內部類、非靜態內部類、匿名內部類,這些內部類我們都應該注意不要長時間引用Activity。
2.單例造成的記憶體洩露
建立一個單例
Activity獲取單例物件,並將Activity傳入單例中:
我們假設這樣一個場景,我們開啟應用,然後點手機返回,等待一段時間假設10s,這樣就會造成記憶體洩露。
為什麼會造成記憶體洩露呢?
AppManager appManager=AppManager.getInstance(this) 這句傳入的是Activity的Context,我們都知道,Activty是間接繼承於Context的,當這Activity退出時,Activity應該被回收, 但是單例中又持有它的引用,導致Activity回收失敗,造成記憶體洩漏。 像這種情況我們應該怎麼避免呢? 我們將傳入的this改成getApplicationContext(),因為我們Application的生命週期是和APP的生命週期一直的所以就不存在記憶體洩露的問題。
Android 圖片優化之OOM
現在我們的APP基本上都會有圖片顯示,那麼有圖片顯示必然就會出現圖片的優化問題,如果處理不得當就會出現OOM。
1.什麼是OOM?
我們程式申請需要10485776byte太大了,虛擬機器無法滿足我們,羞愧的shutdown自殺了
2.為什麼會有OOM?
因為android系統的app的每個程式或者每個虛擬機器有個最大記憶體限制,如果申請的記憶體資源超過這個限制,系統就會丟擲OOM錯誤。跟整個裝置的剩餘記憶體沒太大關係。比如比較早的android系統的一個虛擬機器最多16M記憶體,當一個app啟動後,虛擬機器不停的申請記憶體資源來裝載圖片,當超過記憶體上限時就出現OOM。 這一小節說的圖片優化OOM,為什麼說圖片會造成OOM呢?因為我們在網路請求載入圖片的時候,我們要申請記憶體來裝載圖片,然後我們的一張圖片原本1M,但是下載下來之後轉換成Bitmap顯示到我們的控制元件的話,那麼我們的Bitmap此時的大小估計是好幾M,會翻好幾倍。當你下載多了,不注意回收這些Bitmap的話,就會造成OOM。
總結有一下三種情況:
- 直接載入 超大尺寸 圖片;
- 圖片載入後 未及時釋放;
- 在頁面中,同時載入 非常多 的圖片;
解決載入圖片出現OOM有幾種方法:
- 對圖片進行裁剪之後再載入圖片。
- 採用LruCache來快取圖片
- 對圖片進行適當的縮小之後再載入顯示
為什麼這塊我們沒有細講,主要是因為我們現在的圖片載入主要都是使用這框架Glide、Picasso、Fresco 來載入圖片,我們現在就像是傻瓜似的操作,直接傳入個Url就好了,圖片的優化問題框架已經給我做的很好了,無需我們考慮那麼多。如果說有必要的話,我之後可以來一篇框架的載入圖片原理,原始碼解析,如有需要的可以在後臺留言。
原創不易,如果覺得寫得好,掃碼關注一下點個贊,是我最大的動力。
關注我,一定會有意想不到的東西等你: 每天專注分享Android、JAVA乾貨
備註:程式圈LT