Android App優化這個問題,我相信是Android開發者一個永恆的話題。本篇文章也不例外,也是來說解一下Android內存優化。那麼本篇文章有什麼不一樣呢? 本篇文章主要是從最基礎的Android系統內存管理方面出發再到App優化方法,讓你能更加清楚地理解、處理Android內存優化問題,下面進入正題。android
一般狀況下,一個APP就是一個進程或者說是一個虛擬機。也就是說咱們一個APP運行的時候那麼就有一個單獨的進程在運行。可是也有不少的大公司在Mainfest指定process進程名字,因此會看到一個APP對應多個進程的狀況。算法
咱們用實際的代碼來演示看一下: 這是我運行的一個App的包名:數組
咱們在Windows上看一下他的進程:緩存
zygote進程是由init進程啓動起來,在Android中,zygote是整個系統建立新進程的核心進程,換句話說就是zygote進程是android的孵化進程也就是父進程。bash
經過命令 dumpsys meminfo + 進程名字,能夠獲取具體信息:網絡
Pss Total : 當前使用物理內存的大小數據結構
Heap Size : 堆空間app
Heap Alloc : 分配多少堆空間框架
Heap Free :空閒堆空間ide
通常來講:Heap Size = Heap Alloc + Heap Free
Native Heap:指的JIN開發所佔的堆空間 Dalvik Heap : 虛擬機的堆空間 Dalvik Other : 虛擬機其餘所佔空間 stack : 堆棧佔多少
其餘還有不少的有用信息,就不一一解釋了,感興趣的能夠多去了解這方面的知識,我這裏就主要說一下咱們常常內存泄漏主要在:Pss Total 中的TOTAL不斷的變大就能夠看出內存泄漏
GC就是垃圾收集器,只有在Heap剩餘空間不夠的時候纔會觸發垃圾回收。
Java的垃圾回收機制就是你在開發的時候不用去關注內存是否去釋放,這個是一個優勢,可是也有缺點就是當前的變量不使用了,放在一邊,只有當內存不夠的時候纔會觸發GC去回收這些不使用的內存。爲何說是個缺點呢?
**由於在GC觸發垃圾回收的時候,全部的線程都會被暫停,此時就會咱們常常出現的卡頓現象。
首先咱們要知道一個理論:每一個APP分配的內存最大限制,是隨着設備的不一樣而改變的。所以,咱們才須要咱們去管理咱們的內存,有一點要明白的就是系統分配的內存,通常狀況下是確定夠使用的,若是出現OOM這種狀況,那麼一定是你的APP優化的不夠好。
最常說的吃內存的: 高清圖片,如今的手機拍照動不動就是以M爲單位。可是就咱們目前的開發來講,大多數人使用的是Glide、Picasso的框架,其實都是框架給咱們處理了管理圖片的問題。
爲何要限制內存?
假如咱們每一個App都不限制內存的大小,那麼各自的APP都無論理,讓內存一直增大,Android系統是容許多個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());
}
複製代碼
2.Android studio 3.1 工具 Android profiler
這裏寫圖片描述
這種方式是方便查看的,直接在下方點擊 Android profiler就能夠了,方便快捷
3.DDMS
打開DDMS
這也是看咱們的APP的內存使用狀況,標記了的假如你的APP在運行的時候 data object和 class object 不斷的變化,那就說明你的應用可能有內存泄露了,這個時候你就須要檢查一下。
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的生命週期一直的因此就不存在內存泄露的問題。
如今咱們的APP基本上都會有圖片顯示,那麼有圖片顯示必然就會出現圖片的優化問題,若是處理不得當就會出現OOM。
1.什麼是OOM?
咱們程序申請須要10485776byte太大了,虛擬機沒法知足咱們,羞愧的shutdown自殺了
2.爲何會有OOM?
由於android系統的app的每一個進程或者每一個虛擬機有個最大內存限制,若是申請的內存資源超過這個限制,系統就會拋出OOM錯誤。跟整個設備的剩餘內存沒太大關係。好比比較早的android系統的一個虛擬機最多16M內存,當一個app啓動後,虛擬機不停的申請內存資源來裝載圖片,當超過內存上限時就出現OOM。 這一小節說的圖片優化OOM,爲何說圖片會形成OOM呢?由於咱們在網絡請求加載圖片的時候,咱們要申請內存來裝載圖片,而後咱們的一張圖片本來1M,可是下載下來以後轉換成Bitmap顯示到咱們的控件的話,那麼咱們的Bitmap此時的大小估計是好幾M,會翻好幾倍。當你下載多了,不注意回收這些Bitmap的話,就會形成OOM。
總結有一下三種狀況:
解決加載圖片出現OOM有幾種方法:
爲何這塊咱們沒有細講,主要是由於咱們如今的圖片加載主要都是使用這框架Glide、Picasso、Fresco 來加載圖片,咱們如今就像是傻瓜似的操做,直接傳入個Url就行了,圖片的優化問題框架已經給我作的很好了,無需咱們考慮那麼多。若是說有必要的話,我以後能夠來一篇框架的加載圖片原理,源碼解析,若有須要的能夠在後臺留言。
原創不易,若是以爲寫得好,掃碼關注一下點個贊,是我最大的動力。
關注我,必定會有意想不到的東西等你: 天天專一分享Android、JAVA乾貨
備註:程序圈LT