05.21 Android 性能優化之內存優化

前言

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.在我們的代碼中動態打印出我們的內

通過上面的代碼和圖我們就可以清楚的知道我們的應用的內存的情況。

這種方法也是最複雜的方法,需要代碼去打印,下面兩種是我們的Android studio提供的工具。

2.Android studio 3.1 工具 Android profiler

這種方式是方便查看的,直接在下方點擊 Android profiler就可以了,方便快捷

3.DDMS

打開DDMS

這也是看我們的APP的內存使用情況,標記了的假如你的APP在運行的時候 data object和 class object 不斷的變化,那就說明你的應用可能有內存洩露了,這個時候你就需要檢查一下。

Android內存優化

數據結構的優化

1.字符串拼接

我們都知道我們的如果使用string 的 “+”方式來拼接字符串,會產生字符串中間內存塊,這些內存塊是無用的,造成內存浪費,這種方式低效、而且耗時。我們就是實際看看:

初始化一個兩維的矩陣,得到隨機數。

得到的用 “+”鏈接的結果:

耗時2.6s

用stringBuilder的結果:

耗時0.06s

這就可以看出我們的**String和StringBuilde**r的使用效率的對比了。

2.替換HashMap

還有值得一提的就是JAVA裡面的HashMap,這個使用的效率是不高的,我們要用ArrayMap、SparseArray替換。

3.內存抖動

內存都用的主要原因是我們內存變量的使用不當造成的

重點就是在創建string數組那裡,是放在第一個for循環裡面,rawLen=450000,因此會創建450000個對象。

這一塊就是我們的內存抖動的情況。

分析一下原因:

我們在for循環裡面創建了45000個string對象,然後再裡面添加了數據之後就沒有使用了,當創建的對象達到內存限制的時候就會觸發GC回收,接下來又創建,又回收,這樣就導致了內存抖動的情況。

內存的複用

複用系統自帶的資源ListView/GridView中的ConvertView的複用,當然我們現在ListView和GridView使用已經很少了,都被RecyclerView給取代了我們在自定義View的要避免在onDraw中去創建對象,因為onDraw方法會經常執行

內存洩露

內存洩露已經是老生常談了,但是我們還是要舉一些簡單的例子讓大家知道怎樣會造成內存洩露。

什麼是內存洩露?

內存洩露:由於你代碼的問題,導致某一塊內存雖然已經不使用了,但是依然被其他的東西(對象或者其他)引用著,使得GC沒發對它回收。

所以內存洩露會導致APP剩餘可用的Heap越來越少,頻繁觸發GC。

1.內部內造成的內存洩露

上面這個是一個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就好了,圖片的優化問題框架已經給我做的很好了,無需我們考慮那麼多。如果說有必要的話,我之後可以來一篇框架的加載圖片原理,源碼解析,如有需要的可以在後臺留言。