微信 Android 终端内存优化实践
前言
內(nèi)存問題是軟件領(lǐng)域的經(jīng)典問題,平時(shí)藏得很深,在出現(xiàn)問題之前沒太多征兆。而一旦爆發(fā)問題,問題來源的多樣、不易重現(xiàn)、現(xiàn)場信息少、難以定位等困難,就會讓人頭疼不已。
?
微信在過去 N 多的版本迭代中,經(jīng)歷了各式各樣的內(nèi)存問題,這些問題包括但不限于 Activity 的泄漏、Cursor 未關(guān)閉、線程的過度使用、無節(jié)制的創(chuàng)建緩存、以及某個(gè) so 庫悄無聲息一點(diǎn)點(diǎn)的泄漏內(nèi)存,等等。有些問題甚至曾倒逼著我們改變了微信的架構(gòu)(2.x 時(shí)代 webview 內(nèi)核泄露催生了微信多進(jìn)程架構(gòu)的改變)。時(shí)至今日微信依然偶爾會受到內(nèi)存問題的挑戰(zhàn),在持續(xù)不斷的版本迭代中,總會有新的問題被引入并潛藏著。
?
在解決各種問題的過程中,我們積累了一些相對有效和多面的優(yōu)化手段及工具,從監(jiān)控上報(bào)到開發(fā)階段的測試檢查,為預(yù)防和解決問題提供幫助,并還在不斷的持續(xù)改進(jìn)。本文打算介紹一下這些工程上的優(yōu)化實(shí)踐經(jīng)驗(yàn),希望對大家有一些參考價(jià)值。
?
Activity 泄露檢測
Activity 泄漏,即因?yàn)楦鞣N原因?qū)е?Activity 被生命周期遠(yuǎn)比該 Activity 長的對象直接或間接以強(qiáng)引用持有,導(dǎo)致在此期間 Activity 無法被 GC 機(jī)制回收的問題。與其他對象泄漏相比,Android 上的 Activity 一方面提供了與系統(tǒng)交互的 Context,另一方面也是用戶與 App 交互的承載者,因此非常容易意外被系統(tǒng)或其他業(yè)務(wù)邏輯作為一個(gè)普通的工具對象長期持有,而且一旦發(fā)生泄漏,被牽連導(dǎo)致同樣被泄漏的對象也會非常多。此外,由于這類問題在大量爆發(fā)之前除了 App 內(nèi)存占用變大之外并沒有 crash 之類的明顯征兆,因此在測試階段主動檢測、排查 Activity 泄漏,避免線上出現(xiàn) OOM 或其他問題就顯得非常必要了。
?
早期我們曾通過自動化測試階段在內(nèi)存占用達(dá)到閾值后自動觸發(fā) Hprof Dump,將得到的 Hprof 存檔后由人工通過 MAT 進(jìn)行分析。在新代碼提交速度還不太快的時(shí)候,這樣做確實(shí)也能湊合著解決問題,但隨著微信新業(yè)務(wù)代碼越來越多,人工排查后反饋給各 Activity 的負(fù)責(zé)人,各負(fù)責(zé)人修復(fù)之后再人工確認(rèn)一遍是否已經(jīng)修復(fù),這個(gè)過程需要反復(fù)的情況也越來越多,人工解決的方案已力不從心。
?
后來我們嘗試了 LeakCanary。這款工具除了能給出可讀性非常好的檢測結(jié)果外,對于排查出的問題,還會展示開源社區(qū)維護(hù)的解決方案,在 Activity 泄漏檢測、分析上完全可以代替人力。唯一美中不足的是 LeakCanary 把檢測和分析報(bào)告都放到了一起,流程上更符合開發(fā)和測試是同一人的情況,對批量自動化測試和事后分析就不太友好了。
?
為此我們在 LeakCanary 的基礎(chǔ)上研發(fā)了一套 Activity 泄漏的檢測分析方案 —— ResourceCanary,作為我們內(nèi)部質(zhì)量監(jiān)控平臺 Matrix 的一部分參與到每天的自動化測試流程中。與 LeakCanary 相比 ResourceCanary 做了以下改進(jìn):
?
事實(shí)上這兩部分本來就可以獨(dú)立運(yùn)作,檢測部分負(fù)責(zé)檢測和產(chǎn)生 Hprof 及一些必要的附加信息,分析部分處理這些產(chǎn)物即可得到引發(fā)泄漏的強(qiáng)引用鏈。這樣一來檢測部分就不再和分析、故障解決相耦合,自動化測試由測試平臺進(jìn)行,分析則由監(jiān)控平臺的服務(wù)端離線完成,再通知相關(guān)開發(fā)同學(xué)解決問題。三者互不打斷對方進(jìn)程,保證了自動化流程的連貫性。
-
分離檢測和分析兩部分邏輯。
?
-
裁剪 Hprof 文件,降低后臺存檔 Hprof 的開銷。
就 Activity 泄漏分析而言,我們只需要 Hprof 中類和對象的描述和這些描述所需的字符串信息,其他數(shù)據(jù)都可以在客戶端就地裁剪。由于 Hprof 中這些數(shù)據(jù)比重很低,這樣處理之后能把 Hprof 的大小降至原來的 1/10 左右,極大降低了傳輸和存儲開銷。
?
實(shí)際運(yùn)行中通過 ResourceCanary,我們排查了一些非常典型的泄漏場景,部分列舉如下:
?
-
匿名內(nèi)部類隱式持有外部類的引用導(dǎo)致的泄漏
?
?
public?class?ChattingUI?extends?MMActivity?{@Overrideprotected?void?onCreate(Bundle?savedInstanceState)?{super.onCreate(savedInstanceState);setContentView(R.layout.activity_chatting_ui);EventCenter.addEventListener(new?IListener<IEvent>()?{//?這個(gè)?IListener?內(nèi)部類里有個(gè)隱藏成員?this$?持有了外部的?ChattingUI@Overridepublic?void?onEvent()?{//?...}});}}public?class?EventCenter?{//?此?ArrayList?實(shí)例的生命周期為?App?的生命周期private?static?List<IListener>?sListeners?=?new?ArrayList();public?void?addEventListener(IListener?cb)?{//?ArrayList?對象持有?cb,cb.this$?持有?ChattingUI,導(dǎo)致?ChattingUI?泄漏sListeners.add(cb);}}?
-
各種原因?qū)е碌姆醋院瘮?shù)未按預(yù)期被調(diào)用導(dǎo)致的Activity泄漏
?
-
系統(tǒng)組件導(dǎo)致的 Activity 泄漏,如 LeakCanary 中提到的 SensorManager 和 InputMethodManager 導(dǎo)致的泄漏。
?
還有特別耗時(shí)的 Runnable 持有 Activity,或者此 Runnable 本身并不耗時(shí),但在它前面有個(gè)耗時(shí)的 Runnable 堵塞了執(zhí)行線程導(dǎo)致此 Runnable 一直沒機(jī)會從等待隊(duì)列里移除,也會引發(fā) Activity 泄漏等等。從來源上這類例子是舉不完的,總之任何能構(gòu)造長期持有 Activity 的強(qiáng)引用的場景都能泄漏掉 Activity,從而泄漏 Activity 持有的大量 View 和其他對象。
?
事實(shí)上,ResourceCanary 將檢測與分析分離和大幅裁剪了 Hprof 文件體積的改進(jìn)是相當(dāng)重要的,這使我們將 Activity 檢查做成自動化變得更容易。我們將 ResourceCanary 的 sdk 植入在微信中,通過每日常規(guī)的自動化測試,將發(fā)現(xiàn)的問題上報(bào)到微信的 Matrix 平臺,自動進(jìn)行統(tǒng)計(jì)、棧提取、歸責(zé)、建單,然后系統(tǒng)會自動通知相關(guān)開發(fā)同學(xué)進(jìn)行修復(fù),并可以持續(xù)跟進(jìn)修復(fù)情況。對有效解決問題的意義非常大。
?
?
除了開發(fā)同學(xué)每天根據(jù) Matrix 平臺的報(bào)告進(jìn)行確認(rèn)修復(fù)外,對于這些泄漏,微信客戶端還會采取一些主動措施規(guī)避掉無法立即解決的泄漏,大致包括:
?
-
主動切斷 Activity 對 View 的引用、回收 View 中的 Drawable,降低 Activity 泄漏帶來的影響
-
盡量用 Application Context 獲取某些系統(tǒng)服務(wù)實(shí)例,規(guī)避系統(tǒng)帶來的內(nèi)存泄漏
-
對于已知的無法通過上面兩步解決的來自系統(tǒng)的內(nèi)存泄漏,參考 LeakCanary 給出的建議進(jìn)行規(guī)避
?
Bitmap 分配及回收追蹤
Bitmap 一直以來都是 Android App 的內(nèi)存消耗大戶,很多 Java 甚至 native 內(nèi)存問題的背后都是不當(dāng)持有了大量大小很大的 Bitmap。
?
與此同時(shí),Bitmap 有幾個(gè)特點(diǎn)方便我們對它們進(jìn)行監(jiān)控:
?
-
創(chuàng)建場景較為單一。?Bitmap 通常通過在 Java 層調(diào)用?Bitmap.create?直接創(chuàng)建,或者通過?BitmapFactory?從文件或網(wǎng)絡(luò)流解碼。正好,我們有一層對 Bitmap 創(chuàng)建接口調(diào)用的封裝,基本囊括微信內(nèi)創(chuàng)建 Bitmap 的全部場景(包括調(diào)用外部庫產(chǎn)生 Bitmap 也封裝在這層接口內(nèi))。這層統(tǒng)一接口有利于我們在創(chuàng)建 Bitmap 時(shí)進(jìn)行統(tǒng)一監(jiān)控,而不需要進(jìn)行插樁或 hook 等較為 hack 的方法。
-
創(chuàng)建頻率較低。?Bitmap 創(chuàng)建的行為不如 malloc 等通用內(nèi)存分配頻繁,本身往往也伴隨著耗時(shí)較長的解碼或處理,因此在創(chuàng)建 Bitmap 時(shí)加入監(jiān)控邏輯,其性能要求不會特別高。即使是獲取完整的 Java 堆棧甚至做一些篩選,其耗時(shí)相比起解碼或者其他圖像處理也是微不足道,我們可以執(zhí)行稍微復(fù)雜的邏輯。
-
Java 對象的生命周期。?Bitmap 對象的生命周期和普通 Java 對象一樣服從 JVM 的 GC,因此我們可以通過?WeakReference?等手段來跟蹤 Bitmap 的銷毀,而不用像創(chuàng)建一樣對銷毀也一并跟蹤。
?
針對上述特點(diǎn),我們加入了一個(gè)針對 Bitmap 的高性價(jià)比監(jiān)控:在接口層中將所有被創(chuàng)建出來的 Bitmap 加入一個(gè)?WeakHashMap,同時(shí)記錄創(chuàng)建 Bitmap 的時(shí)間、堆棧等信息,然后在適當(dāng)?shù)臅r(shí)候查看這個(gè)?WeakHashMap?看看哪些 Bitmap 仍然存活來判斷是否出現(xiàn) Bitmap 濫用或泄漏。
?
這個(gè)監(jiān)控對性能消耗非常低,可以在發(fā)布版進(jìn)行。判斷是否泄漏則需要耗費(fèi)一點(diǎn)性能,且目前還需要人工處理。收集泄漏的時(shí)機(jī)包括:
?
-
如果是測試環(huán)境,比如 Monkey Test 過程中,則使用 “激進(jìn)模式”,即每次進(jìn)行 Bitmap 創(chuàng)建的數(shù)秒后都檢查一次 Java 堆的情況,Java 內(nèi)存占用超過某個(gè)閾值即觸發(fā)收集邏輯,將所有存活的 Bitmap 信息輸出到文件,另外還輸出 hprof 輔助查找別的內(nèi)存泄漏。
-
發(fā)布版則采用 “保守模式”,只有在出現(xiàn) OOM 了之后,才將內(nèi)存占用 1 MB 以上的 Bitmap 信息輸出到 xlog,避免 xlog 過大。
?
激進(jìn)模式中閾值目前定為 200 MB,這是因?yàn)槲覀冎С值?Android 設(shè)備中,最容易出現(xiàn) OOM 的一批手機(jī)的 large heap 限制為 256 MB,一旦 Heap 峰值達(dá)到 200 MB 以上且回收不及時(shí),在一些需要類似解碼大圖的場景下就會出現(xiàn)無法臨時(shí)分配數(shù)十 MB 的內(nèi)存供圖片顯示而導(dǎo)致 OOM,因此在 Monkey Test 時(shí)認(rèn)為 Java Heap 占用超過 200 MB 即為異常。
?
Bitmap 追蹤嘗試投入到 Monkey Test 后,發(fā)現(xiàn)問題最多最突出的,是緩存的濫用問題,最為典型的是使用 static LRUCache 來緩存大尺寸 Bitmap。
?
private?static?LruCache<String,?Bitmap>?sBitmapCache?=?new?LruCache<>(20);public?static?Bitmap?getBitmap(String?path)?{Bitmap?bitmap?=?sBitmapCache.get(path);if?(bitmap?!=?null)?{return?bitmap;}bitmap?=?decodeBitmapFromFile(path);sBitmapCache.put(path,?bitmap);return?bitmap;}?
比如上面的代碼,作用是緩存一些重復(fù)使用的 Bitmap 避免重復(fù)解碼損失性能,但由于?sBitmapCache?是靜態(tài)的且沒有清理邏輯,緩存在其中的圖片將永遠(yuǎn)無法釋放,除非 20 個(gè)的配額用盡或圖片被替換。LruCache?對緩存對象的?個(gè)數(shù)?進(jìn)行了限制,但沒有對對象的?總大小?進(jìn)行限制(Java的對象模型也不支持直接獲取對象占用內(nèi)存大小),因此如果緩存里面存放了數(shù)個(gè)大圖或者長圖,將長期占用大量內(nèi)存。此外,不同業(yè)務(wù)之間不太可能提前考慮緩存可能造成的相互擠壓,進(jìn)一步加劇問題。也正因如此我們還開始推動了內(nèi)部使用統(tǒng)一的緩存管理組件,從整體上,控制使用緩存的策略和數(shù)量。
?
Native 內(nèi)存泄漏檢測
Native 層內(nèi)存泄漏通常是指各種原因?qū)е碌囊逊峙鋬?nèi)存未得到有效釋放,導(dǎo)致可用內(nèi)存越來越少直到 crash 的問題。由于Native 層沒有 GC 機(jī)制,內(nèi)存管理行為非常可控,檢測起來確實(shí)也簡單許多——直接攔截內(nèi)存分配和釋放相關(guān)的函數(shù)看一下是否配對即可。
?
我們首先在單個(gè) so 上嘗試了一些成熟的方案:
?
-
valgrind
App 明顯變得卡頓,檢測結(jié)果沒有太大幫助,而且 valgrind 在 Android 上的部署太麻煩了,要在幾百臺測試機(jī)器上部署是個(gè)很大的問題。
-
asan
跟文檔描述得差不多,檢測階段開銷確實(shí)比 valgrind 少,但是 App 還是變卡了,自動化測試時(shí)容易 ANR。回溯堆棧階段容易 crash。另外我們的一些歷史悠久的 so 按 asan 的要求用 clang 編譯之后可能存在 bug,這點(diǎn)也成為了采用此方案的阻礙。
?
對上述結(jié)果我們的猜想是這些工具除了本身開銷之外,大而全的功能,諸如雙重釋放,地址合法性檢測,越界訪問檢測也增加了運(yùn)行時(shí)開銷。按此思路我們又改用系統(tǒng)自帶的 malloc_debug 進(jìn)行檢測,但 malloc_debug 在堆棧回溯階段會產(chǎn)生一個(gè)必現(xiàn)的 crash,按照網(wǎng)上資料和廠商的反饋的說法,應(yīng)該是它依賴 stl 庫里的 __Unwind 系列函數(shù)需要的數(shù)據(jù)結(jié)構(gòu)在不同的 stl 庫里定義不同導(dǎo)致的,然而由于一些原因,被檢測的 so 里有些已經(jīng)不具備換 stl 庫重編的條件了。這樣的狀況迫使我們自研一套方案解決問題。
?
根據(jù)之前的嘗試,實(shí)際上我們需要研發(fā)兩個(gè)方案組合使用。對于不方便重編的庫,我們采用一個(gè)不需要重編的方案舍棄一些信息以換取對泄漏的定位能力;對于易于重編的庫,我們采用一個(gè)不需要 clang 環(huán)境的方案保證能在不引入 bug 的情況下拿到 asan 能拿到的泄漏內(nèi)存分配位置的堆棧信息。當(dāng)然,兩個(gè)方案都要足夠輕,保證不會產(chǎn)生 ANR 中斷自動化測試過程。
?
限于篇幅,這里不再展開介紹方案原理,只大概說明兩個(gè)方案的思路:
?
-
無法重編的情況:PLT hook 攔截被測庫的內(nèi)存分配函數(shù),重定向到我們自己的實(shí)現(xiàn)后記錄分配的內(nèi)存地址、大小、來源 so 庫路徑等信息,定期掃描分配與釋放是否配對,對于不配對的分配輸出我們記錄的信息。
-
可重編的情況:通過 gcc 的 -finstrument-functions 參數(shù)給所有函數(shù)插樁,樁中模擬調(diào)用棧入棧出棧操作;通過 ld 的 --wrap 參數(shù)攔截內(nèi)存分配和釋放函數(shù),重定向到我們自己的實(shí)現(xiàn)后記錄分配的內(nèi)存地址、大小、來源 so?以及插樁記錄的調(diào)用棧此刻的內(nèi)容,定期掃描分配與釋放是否配對,對于不配對的分配輸出我們記錄的信息。
?
實(shí)測中這兩個(gè)方案為每次內(nèi)存分配帶來的額外開銷小于 10ns,總體開銷的變化幾乎可忽略不計(jì)。我們通過這兩套方案組合除了發(fā)現(xiàn)一個(gè)棘手的新問題外,還順便檢測了使用多年的基礎(chǔ)網(wǎng)絡(luò)協(xié)議 so 庫,并成功找出隱藏多年的十多處小內(nèi)存泄漏點(diǎn),降低內(nèi)存地址的碎片化。
?
線程監(jiān)控
常見的 OOM 情況大多數(shù)是因?yàn)閮?nèi)存泄漏或申請大量內(nèi)存造成的,比較少見的有下面這種跟線程相關(guān)情況,但在我們 crash 系統(tǒng)上有時(shí)能發(fā)現(xiàn)一些這樣的問題。
?
java.lang.OutOfMemoryError:?pthread_create?(1040KB?stack)?failed:?Out?of?memory?
原因分析
OutOfMemoryError 這種異常根本原因在于申請不到足夠的內(nèi)存造成的,直接的原因是在創(chuàng)建線程時(shí)初始 stack size 的時(shí)候,分配不到內(nèi)存導(dǎo)致的。這個(gè)異常是在 /art/runtime/thread.cc 中線程初始化的時(shí)候 throw 出來的。
?
void?Thread::CreateNativeThread(JNIEnv*?env,?jobject?java_peer,?size_t?stack_size,?bool?is_daemon)?{...int?pthread_create_result?=?pthread_create(&new_pthread,?&attr,?Thread::CreateCallback,?child_thread);if?(pthread_create_result?!=?0)?{env->SetLongField(java_peer,?WellKnownClasses::java_lang_Thread_nativePeer,?0);{std::string?msg(StringPrintf("pthread_create?(%s?stack)?failed:?%s",PrettySize(stack_size).c_str(),?strerror(pthread_create_result)));ScopedObjectAccess?soa(env);soa.Self()->ThrowOutOfMemoryError(msg.c_str());}}}?
調(diào)用這個(gè) pthread_create 的方法去 clone 一個(gè)線程,如果返回 pthread_create_result 不為 0,則代表初始化失敗。什么情況下會初始化失敗,pthread_create 的具體邏輯是在 /bionic/libc/bionic/pthread_create.cpp 中完成:
?
int?pthread_create(pthread_t*?thread_out,?pthread_attr_t?const*?attr,void*?(*start_routine)(void*),?void*?arg)?{...pthread_internal_t*?thread?=?NULL;void*?child_stack?=?NULL;int?result?=?__allocate_thread(&thread_attr,?&thread,?&child_stack);if?(result?!=?0)?{return?result;}...}static?int?__allocate_thread(pthread_attr_t*?attr,?pthread_internal_t**?threadp,?void**?child_stack)?{size_t?mmap_size;uint8_t*?stack_top;...attr->stack_base?=?__create_thread_mapped_space(mmap_size,?attr->guard_size);if?(attr->stack_base?==?NULL)?{return?EAGAIN;?//?EAGAIN?!=?0}...return?0;}?
可以看到每個(gè)線程初始化都需要 mmap 一定的 stack size,在默認(rèn)的情況下一般初始化一個(gè)線程需要 mmap 1M 左右的內(nèi)存空間,在 32bit 的應(yīng)用中有 4g 的 vmsize,實(shí)際能使用的有 3g+,按這種估算,一個(gè)進(jìn)程最大能創(chuàng)建的線程數(shù)可達(dá) 3000+,當(dāng)然這是理想的情況,在 linux 中對每個(gè)進(jìn)程可創(chuàng)建的線程數(shù)也有一定的限制(/proc/pid/limits)而實(shí)際測試中,我們也發(fā)現(xiàn)不同廠商對這個(gè)限制也有所不同,而且當(dāng)超過系統(tǒng)進(jìn)程線程數(shù)限制時(shí),同樣會拋出這個(gè)類型的 OOM。
可見對線程數(shù)量的限制,可以一定程度避免 OOM 的發(fā)生。所以我們也開始對微信的線程數(shù)進(jìn)行了監(jiān)控統(tǒng)計(jì)。
?
監(jiān)控上報(bào)
我們在灰度版本中通過一個(gè)定時(shí)器 10 分鐘 dump 出應(yīng)用所有的線程,當(dāng)線程數(shù)超過一定閾值時(shí),將當(dāng)前的線程上報(bào)并預(yù)警,通過對這種異常情況的捕捉,我們發(fā)現(xiàn)微信在某些特殊場景下,確實(shí)存在線程泄漏以及短時(shí)間內(nèi)線程暴增,導(dǎo)致線程數(shù)過大(500+)的情況,這種情況下再創(chuàng)建線程往往容易出現(xiàn) OOM。
在定位并解決這幾個(gè)問題后,我們的 crash 系統(tǒng)和廠商的反饋中這種類型 OOM 確實(shí)降低了不少。所以監(jiān)控線程數(shù),收斂線程也成為我們降低 OOM 的有效手段之一。
?
內(nèi)存監(jiān)控
Android 系統(tǒng)中,需要關(guān)注兩類內(nèi)存的使用情況,物理內(nèi)存和虛擬內(nèi)存。通常我們使用 Memory Profiler 的方式查看 APP 的內(nèi)存使用情況。
?
? ? ? ??
在默認(rèn)視圖中,我們可以查看進(jìn)程總內(nèi)存占用、JavaHeap、NativeHeap,以及 Graphics、Stack、Code 等細(xì)分類型的內(nèi)存分配情況。當(dāng)系統(tǒng)內(nèi)存不足時(shí),會觸發(fā) onLowMemory。在 API Level 14及以上,則有更細(xì)分的 onTrimMemory。實(shí)際測試中,我們發(fā)現(xiàn) onTrimMemory 的 ComponentCallbacks2.TRIM_MEMORY_COMPLETE 并不等價(jià)于 onLowMemory,因此推薦仍然要監(jiān)聽 onLowMemory 回調(diào)。
?
除了舊有的大盤粗粒度內(nèi)存上報(bào),我們正在建設(shè)相對精細(xì)的內(nèi)存使用情況監(jiān)控并集成到 Matrix 平臺上。進(jìn)行監(jiān)控方案前,我們需要運(yùn)行時(shí)獲得各項(xiàng)內(nèi)存使用數(shù)據(jù)的能力。通過 ActivityManager 的 getProcessMemoryInfo,我們獲得微信進(jìn)程的 Debug.MemoryInfo 數(shù)據(jù)(注意:這個(gè)接口在低端機(jī)型中可能耗時(shí)較久,不能在主線程中調(diào)用,且監(jiān)控調(diào)用耗時(shí),在耗時(shí)過大的機(jī)型上,屏蔽內(nèi)存監(jiān)控模塊)。通過 hook Debug.MemoryInfo 的 getMemoryStat 方法(需要 23 版本及以上),我們可以獲得等價(jià)于 Memory Profiler 默認(rèn)視圖中的多項(xiàng)數(shù)據(jù),從而獲得細(xì)分內(nèi)存使用情況。此外,通過 Runtime 可獲得 DalvikHeap;通過 Debug.getNativeHeapAllocatedSize 可獲得 NativeHeap。至此,我們可以獲得低內(nèi)存發(fā)生時(shí),微信的虛擬內(nèi)存、物理內(nèi)存的各項(xiàng)數(shù)據(jù),從而實(shí)現(xiàn)監(jiān)控。
?
內(nèi)存監(jiān)控將分為常規(guī)監(jiān)控和低內(nèi)存監(jiān)控兩個(gè)場景。
?
-
常規(guī)內(nèi)存監(jiān)控 —— 微信使用過程中,內(nèi)存監(jiān)控模塊會根據(jù)斐波那契數(shù)列的特性,每隔一段時(shí)間(最長30分鐘)獲取內(nèi)存的使用情況,從而獲得微信隨使用時(shí)間而變化的內(nèi)存曲線。
-
低內(nèi)存監(jiān)控 —— 通過 onLowMemory 的回調(diào),或者通過 onTrimMemory 回調(diào)且不同的標(biāo)記位,結(jié)合 ActivityManager.MemoryInfo 的 lowMemory 標(biāo)記,我們可以獲得低內(nèi)存的發(fā)生時(shí)機(jī)。這里需要注意,只有物理內(nèi)存不足時(shí),才會引起 onLowMemory 回調(diào)。超過虛擬內(nèi)存的大小限制則直接觸發(fā) OOM 異常。因此我們也監(jiān)聽虛擬內(nèi)存的占用情況,當(dāng)虛擬內(nèi)存占用超過最大限制的 90% 時(shí),觸發(fā)為低內(nèi)存告警。低內(nèi)存監(jiān)控將監(jiān)控低內(nèi)存的發(fā)生頻率、發(fā)生時(shí)各項(xiàng)內(nèi)存使用情況監(jiān)控、發(fā)生時(shí)微信的當(dāng)前場景等。
?
兜底保護(hù)
除了上面的各種問題及解決手段外,面對各種未知的、難以及時(shí)發(fā)現(xiàn)問題,目前我們也提出了一個(gè)兜底保護(hù)策略進(jìn)行嘗試。
?
從大盤統(tǒng)計(jì)的數(shù)據(jù)上看,我們發(fā)現(xiàn)微信主進(jìn)程存活的時(shí)間超過一天的用戶達(dá)千萬級別,占比 1.5%+,倘若應(yīng)用本身或系統(tǒng)底層存在細(xì)微的內(nèi)存泄漏,短時(shí)間上不會造成 OOM,但在長時(shí)間的使用中,會使得應(yīng)用占用內(nèi)存越積越大,最終也會造成 OOM 情況發(fā)生。在這種情況下,我們也在思考,如果可以提前知道內(nèi)存的占用情況,以及用戶當(dāng)前的使用場景,那么我們可以將這種異常的情況進(jìn)行兜底保護(hù),來避免不可控的且容易讓用戶感知到的 OOM 現(xiàn)象。
?
?
如何兜底
OOM 會使得進(jìn)程被殺,實(shí)際上也是系統(tǒng)處理異常所拋出來的信號及處理方式。如果應(yīng)用本身也充當(dāng)起這個(gè)角色,相比系統(tǒng)而言,我們可以根據(jù)具體場景,更加靈活的提前處理這種異常情況。其中最大的好處在于,可以在用戶無感知的情況下,在接近觸發(fā)系統(tǒng)異常前,選擇合適的場景殺死進(jìn)程并將其重啟,使得應(yīng)用的內(nèi)存占用回到正常情況,這不為是一種好的兜底方式。
這里我們主要考慮了幾種條件:
?
-
微信是否在主界面退到后臺 且 位于后臺的時(shí)間超過 30 分鐘
-
當(dāng)前時(shí)間為凌晨 2~5 點(diǎn)
-
不存在前臺服務(wù)(存在通知欄,音樂播放欄等情況)
-
java heap 必須大于當(dāng)前進(jìn)程最大可分配的 85% || native 內(nèi)存大于 800M || vmsize 超過了 4G(微信 32bit)的 85%
-
非大量的流量消耗(每分鐘不超過 1M) && 進(jìn)程無大量 CPU 調(diào)度情況?
?
在滿足以上幾種條件下,殺死當(dāng)前微信主進(jìn)程并通過 push 進(jìn)程重新拉起及初始化,來進(jìn)行兜底保護(hù)。在用戶角度,當(dāng)用戶將微信切回前臺時(shí),不會看到初始化界面,還是位于主界面中,所以也不會感到突兀。從本地測試及灰度的結(jié)果上看,應(yīng)用上該兜底策略,可以有效的減少用戶出現(xiàn) OOM 的情況,在灰度的 5w 用戶中,有 3、4 個(gè)是命中了這個(gè)兜底策略,但具體兜底的策略是否合理,還需要經(jīng)過更嚴(yán)格的測試才能確認(rèn)上線。
?
總結(jié)
通過上面的文章,我們盡可能多的介紹了多個(gè)方面的內(nèi)存問題優(yōu)化手段和工程實(shí)踐。因?yàn)槠邢?#xff0c;一些不那么顯著的問題和不少細(xì)節(jié)無法詳細(xì)展開。總的來說,我們優(yōu)化實(shí)踐的思路是在研發(fā)階段不斷實(shí)現(xiàn)更多的工具和組件,系統(tǒng)性的并逐步提升自動化程度從而提升發(fā)現(xiàn)問題的效率。
?
當(dāng)然不得不提的是,即使做了這么多努力,內(nèi)存問題仍沒有徹底消滅,仍有問題會因?yàn)槿鄙傩畔⒍y以定位原因、或因?yàn)闇y試路徑無法覆蓋而無法提前發(fā)現(xiàn),還有兼容性的問題、引入的外部組件有泄漏等等,以及我們還需要更多的系統(tǒng)化和自動化,這是我們還在不斷優(yōu)化和改進(jìn)的方向。
?
希望已有的這些經(jīng)驗(yàn)?zāi)軐Υ蠹矣兴鶐椭?#xff0c;優(yōu)化沒有盡頭。
現(xiàn)在加Android高級開發(fā)群;701740775,可免費(fèi)領(lǐng)取一份最新Android高級架構(gòu)技術(shù)體系大綱和進(jìn)階視頻資料,以及這些年年積累整理的所有面試資源筆記。加群請備注csdn領(lǐng)取xx資料
總結(jié)
以上是生活随笔為你收集整理的微信 Android 终端内存优化实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序人生丨如何体现测试工程师的价值
- 下一篇: 令人心动的HTTP知识点大全