因爲項目裏以前線上版本出現過必定比例的OOM,雖然比例並不大,可是仍是暴露了必定的問題,因此打算對咱們App分爲幾個步驟進行內存分析和優化,固然內存的優化是個長期的過程,不是一兩個版本的事,每一個版本都須要收集線上內存數據進行監控以及分析。前端
版本迭代過程當中,內存增加過快,不只會致使必定機率的OOM,運行時若出現內存抖動,致使頻繁GC,則會對App的流暢度以及用戶體驗形成很大影響。java
本文主要會根據實際項目中優化步驟分爲如下幾部分:android
這部分主要先介紹一些進行內存分析的基礎方法以及工具,對這部分比較熟悉的同窗能夠先跳過哈。算法
每一個App進程能夠分配到的最大內存是有限的,固然不一樣手機每一個App進程能夠分配到的最大內存有可能不同,能夠經過如下命令進行查看:shell
//dvm最大可用內存:
adb shell getprop | grep dalvik.vm.heapsize
複製代碼
//單個程序限制最大可用內存:
adb shell getprop|grep heapgrowthlimit
複製代碼
超過單個程序限制最大內存則OOM,若是設置了開啓largeHeap,則可提升到dvm最大內存才OOM。數據庫
咱們能夠輸出咱們App的內存使用狀況概覽:json
adb shell dumpsys meminfo 包名
複製代碼
咱們就能夠看到:緩存
Pss
: 該進程獨佔的內存+與其餘進程共享的內存(按比例分配,好比與其餘3個進程共享9K內存,則這部分爲3K)性能優化
Privete Dirty
:該進程獨享內存網絡
Heap Size
:分配的內存
Heap Alloc
:已使用的內存
Heap Free
:空閒內存
AndroidStduio3.0後Android Profiler變得比以前更強大,內存分析頁變得更加直觀更加方便,下面是截圖:
進程佔用總內存
javaHeap
:這部份內存大小是有限制的,溢出則會OOM,這部份內存也是咱們分析優化的重點NativeHeap
:native層的 so 中調用malloc或new建立的內存,對於單個進程來講大小沒有限制,因此能夠利用在native層分配內存來緩解javaHeap的壓力(好比2.3.3以前Android Bitmap的內存分配就是在native層,以後移到javaHeap, 8.0又回到native)Graphics
:這部分通常遊戲app中用的較多,OpenGL和SurfaceFlinger相關的內存,若沒有直接調用到OpenGL,則通常不會涉及到這塊內存Stack
:棧,瞭解jvm內存模型的應該都知道Code
: 代碼,主要是dex以及so等佔用的內存Others
:就是others啦因此咱們能夠看到事實上咱們能夠優化的點有:JavaHeap、NativeHeap、Stack、Code所佔用的內存
MAT是作比較細緻的內存分析的利器了,功能十分強大,其中的:
Hisogram
:Lists number of instances per class
Dominator Tree
:List the biggest objects and what they keep alive.
能夠很是方便的排序查看當前內存中最佔內存的class或者實體對象,並且有一條很是清晰的引用鏈來查看該對象的持有者,這對內存的分析以及內存泄漏的分析都是很是友好的。
同時MAT支持compare對比功能
,將兩個.hprof文件導入,都Add to Compare Basket以後便可進行對比,這對於對比某個頁面相較與前一頁面的內存增量來講是很是有意義的。
有一點比較不友好的是,MAT須要標準的.hprof文件,因此在AndroidStduio的Profiler中GC後dump出的內存快照還要本身手動利用android sdk platform-tools下的hprof-conv進行轉換一下才能被MAT打開。 固然若是以爲麻煩的話也能夠本身寫個腳本執行幾條命令來直接完成GC->dump java heap->轉換.hprof文件 這個流程:
//adb and hprof-conv
ADB=${ANDROID_HOME}/platform-tools/adb
HPROF_CONV=${ANDROID_HOME}/platform-tools/hprof-conv
複製代碼
//GC
${ADB} shell pkill -l 10 $(PACKAGE_NAME)
複製代碼
//dump java heap
${ADB} shell "am dumpheap $(PACKAGE_NAME) $(OUT_PATH)"
複製代碼
//conv hprof
${HPROF_CONV} -z ${FILE_NAME} droid-${FILE_NAME}
複製代碼
根據以往經驗,其實作內存優化最早要搞定的應該是內存中的大頭,這類大頭對內存的佔用很大,也是內存問題的主要禍首,相對來講比較容易定位問題,且優化後效果也很是明顯,性價比很是高。
事實上不少優化都是這樣,好比減包大小的優化,也是要先分析出主要大頭禍首,好比可能你的包裏包含了一張3M大小的無用圖片,若是你沒找到這種禍首,可能你作了大量的工做去想辦法減小無用代碼等,最終可能只有幾百K的收益。
相對內存來講,這個大頭就是:
內存泄漏
圖片
因此首先你要確保你的應用裏沒有存在內存泄漏,而後再去作其餘的內存優化。
如今內存泄漏的檢測已經變得很是簡便了,使用App後在Android Profiler中先觸發GC而後dump內存快照,以後點擊按package分類,就能夠迅速查看到你的App目前在內存中殘留的class,點擊class便可在右邊查看到對應的實例以及引用對象。
固然你也能夠在debug下集成LeakCanary作內存泄漏監控警告
排除內存泄漏後,圖片就是另外一個佔用內存大頭的對象了。
對於圖片來講一個是顏色模式
,檢查一下項目裏的圖片的顏色模式,是否能夠下降,好比從RGB_8888降到RGB_565,則每張圖片能夠節省1/2的內存,若是沒有使用到透明通道等的話基本上肉眼看不出差異。
還有一個是下降圖片的大小
,可能你的ImageView只有你圖片的一半大,則這部份內存就大大浪費了,咱們項目服務端會根據前端的參數作動態切圖。
前端也能夠經過下降採樣率(inSampleSize)
來達到下降圖片佔用內存大小的目的,可是這個採樣率InSampleSize只能是整數(甚至只能是2的次方),若是inSampleSize=2,則最終內存佔用就會是原來的1/4,適用於圖片過大不少的狀況,對於只是想作小幅度壓縮的話,基本沒用。
ok,接下來開始作具體的內存分析與稍微細緻一點的內存優化。
這邊說的靜態內存指的是在伴隨着App的整個生命週期一直存在的那部份內存,也就是打底的,具體獲取這部份內存快照的方式是: 打開App開始重度使用App,基本打開每個主要頁面主要功能,而後回到首頁,進開發者選項打開"不保留後臺活動",而後將咱們的app退到後臺。最後GC,dump出內存快照。 下面是咱們app dump出的內存快照,進行分析後製圖以下:
經過對靜態內存數據的分析,主要發現瞭如下幾個問題:
問題1: App首頁的主圖有兩張(一張是保底圖,一張是動態加載的圖),都比較大,並且動態加載的圖回來後,保底圖並無及時被釋放
優化:首先是對首頁的主圖進行顏色通道的改變以及壓縮,能夠大大下降這兩張圖所佔的內存,而後在動態加載圖回來後及時釋放掉保底圖 -5M
問題2: 首頁底部的輪播背景圖佔用內存1.6M,且在圖片加載回來後,背景圖一直沒有置空
優化:首先通常來講對背景圖的質量並無很高的要求,因此這張背景圖是能夠被成倍壓縮的,而且在圖片加載回來後,背景圖要及時的釋放掉。同時首頁的多張輪播圖以及其餘圖片均可以進行顏色模式的改變以及質量壓縮。 -1.6M -4M
問題3: 項目會在App啓動時拉一個接口獲取一些實驗配置,放進單例,在內存分析時發現,這些實驗配置居然接近1M
優化:排查後發現,接口拉的是整個公司全部部門的實驗配置,上千個,這也給遍歷拿一個實驗配置帶來必定的性能損耗,推進接口去改進,只獲取當前部門業務須要的實驗配置,可節省內存90%以上 -700K
問題4: 發現幾個lottie動畫一直沒有被回收,而且同一個lottie動畫會有幾個不一樣的實例存在,總共佔用內存450K
優化:首先要肯定幾個lottie動畫爲何在頁面退出後沒有被回收,而且同一個動畫有幾個不一樣的實例,很容易就聯想到內存泄漏,因爲頁面沒有被銷燬,因此致使幾個lottie動畫也沒有被回收,排查下來是項目裏的RN頁面存在內存泄漏,解決後大概能夠節省3-5M內存
問題5: SharePreference在內存裏佔用了700K的內存
優化:因爲SP中的東西是會一次性加載到內存裏而且保存爲靜態的,直到App進程結束纔會被銷燬,因此SP中千萬別放大的對象,別圖一時方便把對象序列化成json後保存到SP裏,優化點就是把已經保存在SP中的一些較大的json字符串或者對象遷移到文件或者數據庫緩存。 -400K
問題6: 埋點數據
優化:產品或者運營爲了統計數據會在每一個版本不斷的增長新埋點,可是也須要按期去清理掉一些過期的不須要的埋點,來適當優化內存以及CPU的壓力。
問題7: 還有就是一些App裏的單例以及一些靜態緩存
優化:整個看下來在咱們項目中這部分佔總體的靜態內存其實較小,綜合考慮內存狀況以及使用的高效性能夠進行必定程度的優化,不過這部份內存在App內存緊張時能夠選擇清理掉他們
咱們能夠選擇在App退到後臺後內存緊張即將被Kill掉時選擇釋放掉一些內存,如圖片的緩存,靜態緩存等來自保,具體作法是在Activity中重寫onTrimMemory()
方法(4.0以前是onLowMemory()),在這裏面來作內存的釋放。
靜態內存優化:約15M
接下來作一下每一個頁面的運行時內存分析優化,這一部分就是隨着App運行過程增加以及回收的內存,這部分工做十分繁瑣,須要耐得住寂寞啊。
分析和優化運行時內存主要是經過如下兩個核心方式:
首先介紹一下咱們App中咱們產線的主要核心頁面流程:搜索頁-->列表頁-->詳情頁-->信息頁-->支付,這裏重點對列表頁和詳情頁作運行時內存分析優化。
下面是列表頁的內存快照與搜索頁的對比:
能夠看到,絕大部分的內存增長仍是圖片,固然還有一些靜態緩存:
問題1:列表item被回收時還持有圖片的引用
優化:應該在item被回收不可見時釋放掉對圖片的引用,這裏注意RecyclerView與ListView的區別,若是是ListView,由於每次item被回收後再次利用都會從新綁定數據,只需在ImageView onDetchFromWindow的時候釋放掉圖片引用便可。而對於RecyclerView來講,由於被回收不可見時第一選擇是放進mCacheView中,而這裏面的item被複用時並不會執行bindViewHolder來從新綁定數據,只有被回收進mRecyclePool中後拿出來複用纔會從新綁定數據,因此若是是RecyclerView,咱們釋放圖片引用的時機應該是item被回收進RecyclePool的時候,只要重寫Adapter中的onViewRecycled
方法便可:
@Override
public void onViewRecycled(@Nullable VH holder) {
super.onViewRecycled(holder);
if (holder != null) {
//作釋放圖片引用的操做
}
}
複製代碼
問題2:圖片大小有優化空間
優化:這個由於我司在服務端會對圖片進行動態切圖,因此最簡單的方法就是根據實際狀況來改變更態切圖的大小達到節省內存的做用,固然若是從服務端請求回來的圖片實在大(通常不要比裝載的ImageView要大),前端就能夠採用下降採樣率的方式來進行壓縮,固然這個上面說了採樣率(inSampleSize)只支持2的次方,因此對圖片佔用內存大小的壓縮是很是大的,若是你只是想小幅度的壓縮,基本上這個是沒用的。
問題3:對ImageLoader圖片緩存策略的思考
①對於UIL這個圖片框架,他的緩存策略是內存緩存+磁盤緩存,內存緩存默認的數據結構是LruMemoryCache,對圖片是強引用,默認最大Size是內存的1/8,滿後會按照LRU算法對最近最不經常使用的圖片進行移除,看起來比較合理,可是會有一個問題,就是當圖片緩存達到1/8後則圖片所佔的內存一直會保持在接近1/8,它沒有自我清理的能力,可能長時間過去了這1/8內存裏的有些圖片都再也不須要了,它也依然會保留在內存裏不會被清除,因此咱們能夠考慮對緩存的圖片作一個有效期的管理,圖片過時後則自動清理一波,這樣能夠優化很大一部份內存空間。
②因爲UIL對於內存緩存圖片是以「url+targetWidth+targetHeight」做爲key,若是咱們加載圖片的時候沒有設置targetSize,則框架裏默認會以ImageView的大小做爲targetSize,那麼就會出現一種狀況,同一張圖片,因爲放在大小有輕微差別的ImageView上顯示,則因爲targetSize不同,會在內存中被緩存兩份,固然要解決這個問題也很簡單,只要設置denyCacheImageMultipleSizesInMemory()
便可避免這種狀況,這樣同一張圖片在內存裏就只會有一份緩存(以前的會被以後的替換掉)。 設置完denyCacheImageMultipleSizesInMemory()
後又會出現一個新問題,雖然內存裏同一張圖片只有一份了,但這也意味着有輕微差別的ImageView加載的同一張圖片在內存裏沒辦法被複用了,每次都要去磁盤緩存裏從新加載(磁盤緩存是隻以url做爲key的)。
那麼如何作到讓有輕微大小差別的ImageView加載同一張圖片時既實如今內存緩存裏進行復用又不會在內存緩存裏保留兩份緩存呢?
denyCacheImageMultipleSizesInMemory()
避免同一張圖片由於targetSize不一樣而存在多個內存緩存能夠看看剛進入詳情頁後會有一個明顯的波峯,經過點擊Adnroid Profiler上的紅色圓點來記錄查看這段波峯裏的內存分配。
首先詳情頁依然有大量的圖片,因此對於圖片的大小以及複用上的優化上面已經說了,這裏就不重複說了。
問題1:在內存裏發現兩個極少機率出現的empty view,佔用了接近2M的內存
優化:用ViewStub對empty view作了懶加載,對於這些沒有立刻用到的資源要作延遲加載,還有不少大機率不會出現的View更加要作懶加載。 -2M
問題2:發現詳情頁的輪播大圖的Viewpager用的Adapter是FragmentPagerAdapter,致使了全部的page都會被保存,當圖片頁數多的時候,日後翻內存會不斷上升。
優化:這種頁數多的ViewPager使用FragmentStatePagerAdapter來替代,它只會保留先後pager,在頁數多的時候能夠 節省大量內存
。
問題3:對於一些實在大的圖而且複用頻率並不高的大圖只採用文件緩存就好了,不作內存緩存。
問題4:咱們項目在debug下會打印網絡請求的reqeust和response,而且會用String.subString()對較長的response json進行截取
優化:自己subString()就比較耗內存,因此在response較大的時候就會申請大量的內存,好在這種狀況只會在debug下發生,可是依然須要改進這種打印。
內存的分析優化並非一兩個版本的事,而是一個必須每一個版本持續進行的工做,這須要一套完善的線上用戶內存使用狀況監測系統來進行數據上傳、數據分析、數據整理、數據對比,方便咱們明確的瞭解每一個版本線上App內存的具體狀況。公司的一套性能監控平臺,能夠在這方面給咱們App開發人員提供很直觀的監控數據和版本迭代對比。
經過上面咱們項目的內存分析,能夠發現圖片絕對是內存中的一塊大頭,因此對於圖片的使用監控就顯得尤其重要,咱們自定義了一個簡單的能夠監控加載的圖片是否過大的ImageView,能夠在debug階段發出警告,方便開發人員及早發現過大的圖片。
固然要作的工做還有不少,好比當咱們發現佔用內存太高時,能夠嘗試來釋放一些靜態的緩存,一次來緩存內存的壓力。
這個版本利用了點時間對項目的內存佔用作了以上分析以及優化,還須要作的還有不少,以後的版本會繼續跟進,總得來講作內存分析和優化仍是比較辛苦的,特別是各類內存快照的分析以及對代碼問題的排查,固然時間有限,可能不少地方說的可能也有疏漏或者錯誤,紙上得來終覺淺,絕知此事要躬行,對於性能優化特別內存優化這一塊,實踐遠比理論獲得的要多。
目前項目裏關於流暢度以及耗電量還沒發現太大的問題,由於每一個版本或多或少都會作一些優化,線上也有數據監測,以後仍是想整理一下關於卡頓流程度的分析優化
以及耗電量的分析優化
實踐。