實踐App內存優化:如何有序地做內存分析與優化

由于項目里之前線上版本出現過一定比例的OOM,雖然比例并不大,但是還是暴露了一定的問題,所以打算對我們App分為幾個步驟進行內存分析和優化,當然內存的優化是個長期的過程,不是一兩個版本的事,每個版本都需要收集線上內存數據進行監控以及分析。

版本迭代過程中,內存增長過快,不僅會導致一定概率的OOM,運行時若出現內存抖動,導致頻繁GC,則會對App的流暢度以及用戶體驗造成很大影響。

本文主要會根據實際項目中優化步驟分為以下幾部分:

  1. Android內存分析基礎
  2. 內存泄漏
  3. 靜態內存分析優化
  4. 運行時內存分析優化
  5. 監控

1.Android內存分析基礎

這部分主要先介紹一些進行內存分析的基礎方法以及工具,對這部分比較熟悉的同學可以先跳過哈。

一.App的內存使用情況概覽

每個App進程可以分配到的最大內存是有限的,當然不同手機每個App進程可以分配到的最大內存有可能不一樣,可以通過以下命令進行查看:

//dvm最大可用內存:
adb shell getprop | grep dalvik.vm.heapsize
//單個程序限制最大可用內存:
adb shell getprop|grep heapgrowthlimit

超過單個程序限制最大內存則OOM,如果設置了開啟largeHeap,則可提高到dvm最大內存才OOM。

我們可以輸出我們App的內存使用情況概覽:

adb shell dumpsys meminfo 包名

我們就可以看到:

內存概覽

Pss: 該進程獨占的內存+與其他進程共享的內存(按比例分配,比如與其他3個進程共享9K內存,則這部分為3K)

Privete Dirty:該進程獨享內存

Heap Size:分配的內存

Heap Alloc:已使用的內存

Heap Free:空閑內存

二、Android Profiler

AndroidStduio3.0后Android Profiler變得比之前更強大,內存分析頁變得更加直觀更加方便,下面是截圖:

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

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}

2.內存泄漏

根據以往經驗,其實做內存優化最先要搞定的應該是內存中的大頭,這類大頭對內存的占用很大,也是內存問題的主要禍首,相對來說比較容易定位問題,且優化后效果也非常明顯,性價比非常高。

事實上很多優化都是這樣,比如減包大小的優化,也是要先分析出主要大頭禍首,比如可能你的包里包含了一張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,接下來開始做具體的內存分析與稍微細致一點的內存優化。

3.靜態內存分析優化

這邊說的靜態內存指的是在伴隨著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

4.運行時內存分析優化

接下來做一下每個頁面的運行時內存分析優化,這一部分就是隨著App運行過程增長以及回收的內存,這部分工作十分繁瑣,需要耐得住寂寞啊。

分析和優化運行時內存主要是通過以下兩個核心方式:

  • 從首頁開始用腳本dump出每個頁面的內存快照文件,然后利用MAT的對比功能,找出每個頁面相對于上個頁面內存里主要增加了哪些東西,做針對性優化
  • 利用Android Profiler實時觀察進入每個頁面后的內存變化情況,對產生的內存較大波峰做分析

首先介紹一下我們App中我們產線的主要核心頁面流程:搜索頁-->列表頁-->詳情頁-->信息頁-->支付,這里重點對列表頁和詳情頁做運行時內存分析優化。

(1)列表頁內存優化

下面是列表頁的內存快照與搜索頁的對比:
內存對比

可以看到,絕大部分的內存增加還是圖片,當然還有一些靜態緩存:

問題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加載同一張圖片時既實現在內存緩存里進行復用又不會在內存緩存里保留兩份緩存呢?

  1. 開啟denyCacheImageMultipleSizesInMemory()避免同一張圖片因為targetSize不同而存在多個內存緩存
  2. 將有輕微大小差異的ImageView加載圖片時手動設置一樣的targetSize,這樣緩存的Key就一致了,就可以實現在內存里進行復用了,而指定一樣的targetSize并不會有什么風險,因為上面說了,只有你指定的targetSize比圖片實際大小小2倍以上,采樣率才會生效,實際圖片才會被壓縮。

(2)詳情頁的內存分析優化

可以看看剛進入詳情頁后會有一個明顯的波峰,通過點擊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下發生,但是依然需要改進這種打印。

5.監控

內存的分析優化并不是一兩個版本的事,而是一個必須每個版本持續進行的工作,這需要一套完善的線上用戶內存使用情況監測系統來進行數據上傳、數據分析、數據整理、數據對比,方便我們明確的了解每個版本線上App內存的具體情況。公司的一套性能監控平臺,可以在這方面給我們App開發人員提供很直觀的監控數據和版本迭代對比。

通過上面我們項目的內存分析,可以發現圖片絕對是內存中的一塊大頭,所以對于圖片的使用監控就顯得尤為重要,我們自定義了一個簡單的可以監控加載的圖片是否過大的ImageView,可以在debug階段發出警告,方便開發人員及早發現過大的圖片。

當然要做的工作還有很多,比如當我們發現占用內存過高時,可以嘗試來釋放一些靜態的緩存,一次來緩存內存的壓力。

6.總結

這個版本利用了點時間對項目的內存占用做了以上分析以及優化,還需要做的還有很多,之后的版本會繼續跟進,總得來說做內存分析和優化還是比較辛苦的,特別是各種內存快照的分析以及對代碼問題的排查,當然時間有限,可能很多地方說的可能也有疏漏或者錯誤,紙上得來終覺淺,絕知此事要躬行,對于性能優化特別內存優化這一塊,實踐遠比理論得到的要多。

目前項目里關于流暢度以及耗電量還沒發現太大的問題,因為每個版本或多或少都會做一些優化,線上也有數據監測,之后還是想整理一下關于卡頓流程度的分析優化以及耗電量的分析優化實踐。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/281431.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/281431.shtml
英文地址,請注明出處:http://en.pswp.cn/news/281431.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php OpenSSL 加解密

2018-1-6 17:10:19 星期六 1 $data 123456;2 $openssl_method AES-256-CBC;3 $openssl_iv_length openssl_cipher_iv_length($openssl_method);4 $openssl_iv openssl_random_pseudo_bytes($openssl_iv_length);5 $openssl_password openssl_random_pseudo_bytes(16);6 7 …

前端應該掌握的網絡知識(1)

1、客戶端:通過發送請求獲取服務器資源的web瀏覽器等。 2、TCP/IP協議族按層次分為:應用層、傳輸層、網絡層和數據鏈路層。 應用層決定了向用戶提供應用服務時通信的活動。比如:FTP(文本傳輸協議)和DNS(域名…

WinForm(十四)窗體滾動日志

在桌面程序里,一般日志記錄到文件里就可以了,但有的時間,也需要在窗體上動態滾動顯示,這時,就需要引入日志框架了。這里引入的依舊是NLog(在我的Mini API系統里,用的也是NLog)。首先…

xp計算機找不到音量調節,WinXP電腦沒聲音且小喇叭不見了如何解決?

有用戶在使用電腦聽音樂的時候,突然發現電腦沒有聲音了,本來以為只是被禁了音,想著調節音量即可解決問題。但是當他想要點開音量小喇叭的時候,發現桌面任務欄通知區域的小喇叭不見了,這該怎么辦呢?下面小編…

2018-2019-1 20165211 實驗四 外設驅動程序設計

2018-2019-1 20165211 實驗四 外設驅動程序設計 任務一 1.實驗要求 學習資源中全課中的“hqyj.嵌入式Linux應用程序開發標準教程.pdf”中的第十一章 提交康奈爾筆記的照片(可以多張) 2. 任務完成 任務二 1. 實驗要求 在Ubuntu完成資源中全課中的“hqyj.嵌…

《ASP.NET Core 6框架揭秘》實例演示[31]:路由高階用法

ASP.NET的路由是通過EndpointRoutingMiddleware和EndpointMiddleware這兩個中間件協作完成的,它們在ASP.NET平臺上具有舉足輕重的地位,MVC和gRPC框架,Dapr的Actor和發布訂閱編程模式都建立在路由系統之上。Minimal API更是將提升到了前所未有…

java中文亂碼解決之道(五)—–java是如何編碼解碼的

編碼&解碼 1:I/O操作 2:內存 3:數據庫 4:javaWeb 下面主要介紹前面兩種場景,數據庫部分只要設置正確編碼格式就不會有什么問題,javaWeb場景過多需要了解URL、get、POST的編碼,servlet的解碼…

java反射--Class類

面向對象的世界里,萬事萬物皆對象。 1)類是誰的對象呢? 類是對象,類是java.lang.Class類的實例對象。 2)這個對象如何表示呢? package com.reflect;public class ClassDemo1 {public static void main(Stri…

win10系統按esc會彈出計算機,win10系統版本2004控制面板多出ESC是什么原因?

如果我們的電腦在升級了win102004控制面板多出ESC什么情況方法一:“干凈啟動”,排除第三方軟體的影響1.停止非核心的程序運作(包括第三方殺毒、優化軟體)2.情況允許的話,卸載設備中的第三方殺毒、管家、優化軟件3.同時按【4.點擊【服務】>…

CentOS6/7 配置守護進程

CentOS6.xCentOS6中轉用Upstrat代替以前的init.d/rcX.d的線性啟動方式。一、相關命令通過initctl help可以查看相關命令[rootlocalhost ~]# initctl help Job commands:start Start job.stop Stop job.restart …

Vue源碼解析之數組變異

力有不逮的對象 眾所周知,在 Vue 中,直接修改對象屬性的值無法觸發響應式。當你直接修改了對象屬性的值,你會發現,只有數據改了,但是頁面內容并沒有改變。 這是什么原因? 原因在于: Vue 的響應式…

linux守護進程的編寫

linux監控一個進程進行 代碼如下: #!/bin/shcd /home/autoprocess/ autopgrep -f autoProcessNew.php | wc -l if [ "$auto" 0 ] then nohup php autoProcessNew.php & fi 監視autoProcessNew.php,使他一直監視轉載于:https://www.cnblogs.com/matengfei123/p/…

微軟2014編程之美初賽第一場——題目3 : 活動中心

【來源】 題目3 : 活動中心 【分析】 本題採用的是三分法。 輸入的一組點中找出左右邊界。作為起始邊界。 while(右邊界-左邊界<精度){將左右邊界構成的線段均勻分成3段&#xff0c;推斷切割點的距離關系&#xff0c;抹去距離大的一段。更新左右邊界。 } 輸出左(右)邊界 【…

windows10計算機里輸入法,win10電腦上輸入法不見了怎么辦

好的輸入法可以加快我們的工作效率&#xff0c;當電腦上輸入法不見時&#xff0c;你會調出來嗎?下面小編告訴你win10電腦上輸入法不見時弄出來的一些訣竅吧。win10電腦上輸入法不見了的解決方法win10電腦上輸入法不見了的解決方法&#xff1a;Win10系統輸入法圖標不見了的找回…

Java并發(二十一):線程池實現原理

一、總覽 線程池類ThreadPoolExecutor的相關類需要先了解&#xff1a; &#xff08;圖片來自&#xff1a;https://javadoop.com/post/java-thread-pool#%E6%80%BB%E8%A7%88&#xff09; Executor&#xff1a;位于最頂層&#xff0c;只有一個 execute(Runnable runnable) 方法&a…

進程池

轉自&#xff1a;https://www.cnblogs.com/kaituorensheng/p/4465768.html 在利用Python進行系統管理的時候&#xff0c;特別是同時操作多個文件目錄&#xff0c;或者遠程控制多臺主機&#xff0c;并行操作可以節約大量的時間。當被操作對象數目不大時&#xff0c;可以直接利用…

gulp版本號管理插件注意事項

2019獨角獸企業重金招聘Python工程師標準>>> 打開node_modules\gulp-rev\index.js 第144行 manifest[originalFile] revisionedFile; 更新為: manifest[originalFile] originalFile ?v file.revHash; 打開node_modules\rev-path\index.js 第10行 return filena…

bigfile.to服務器位置,Cloudera Manager 遷移服務器

Cloudera Manager還是比較耗資源的&#xff0c;想把Cloudera Manager&#xff0c;移動到比較好的機器上。在這篇文章中&#xff0c;Cloudera Manager安裝在bigserver1上面&#xff0c;bigserver1是奔騰雙核的CPU。1&#xff0c;Cloudera Manager占資源比較多cloudera manager占…

vue定時ajax獲取數據,vue 中使用 AJAX獲取數據的方法

在VUE開發時&#xff0c;數據可以使用jquery和vue-resource來獲取數據。在獲取數據時&#xff0c;一定需要給一個數據初始值。看下例&#xff1a;new Vue({el:#app,data:{data:""},created:function(){var url"json.jsp";var _selfthis;$.get(url,function…

轉:shell awk

簡單使用&#xff1a; awk &#xff1a;對于文件中一行行的獨處來執行操作 。 awk -F &#xff1a;{print $1,$4} :使用‘&#xff1a;’來分割這一行&#xff0c;把這一行的第一第四個域打印出來 。 詳細介紹&#xff1a; AWK命令介紹 awk語言的最基本功能是在文件或字符串中基…