特效視頻製作軟件app(特效怎麽做)
特效視頻製作(zuò)軟件app(特效怎(zěn)麽做)西瓜視頻穩定性治理體係建設(shè)二:Raphael 原理(lǐ)及實踐
摘要
Raphael [1]是西瓜視頻基礎技術團隊開發的(de)一款(kuǎn) native 內存泄漏檢測工具,廣泛用於字節跳動旗下各大 App 的 native 內存泄漏治理,收益顯(xiǎn)著。工具現已開源,本文將(jiāng)通過原理、方案和實踐來剖析 Raphael 的相關(guān)細節。
背景androids 平(píng)台上的內存問題一(yī)直是性能優化(huà)和穩定性治理(lǐ)的焦點和痛點,Java 堆內存因為有比較成熟的工具和方法論,加上 hprof 快照作為補充,定位和治理都很方便。而 native 內存問題一直缺乏穩定、高(gāo)效的工具,僅有的 malloc debug [6]不僅性能和穩定性難以滿足需要(yào),還存在 androids 版本兼容的問題。
現狀(zhuàng)事實上(shàng),native 內存泄漏治理(lǐ)一直不乏優秀的工具,已知的可用於調查 native 內存泄漏問題的工具主要有:LeakTracer、MTrace、MemWatch、Valgrind-memcheck、TCMalloc、LeakSanitizer 等。但由於 androids 平台的特殊性,這些工具要麽不兼容,要麽接入成本過高,很難在 androids 平台上落地。這些工具的(de)原理基本都是:先代(dài)理內存分配/釋放相關的函數(如:malloc/calloc/realloc/memalign/free),再通(tōng)過 unwind 回溯調用堆棧,最後借助緩存管理過濾出未(wèi)釋(shì)放的內存分配記(jì)錄。因(yīn)此,這些工具(jù)的主(zhǔ)要差異也就體現在代理實現、棧回溯和緩存管理三個方麵。根據(jù)這些工具代理(lǐ)實現的差異,大致(zhì)可以分為 hook 和 LD_PRELOAD 兩大類,典型的(de)如 malloc debug [5] 和 LeakTracer。

malloc debug
malloc debug 是 androids 係統自帶的內存調試工具(jù)(官方(fāng) Native 內存調試 有相關介紹 ) ,雖然沒有額外的(de)接入代碼(mǎ),但開(kāi)啟方式和核心功能等都受(shòu) androids 版本限製。

我們在(zài)線下嚐試使用 malloc debug 監控西瓜視頻 App(配置 wrap.sh)時發現,正常(cháng)啟動時間小於 1s 的機型(Pixel 2 & androids 10),其冷啟動時間被(bèi)拉長到了 11s+。而且在正常使用過程中滑動時(shí)的卡頓感非常明(míng)顯(xiǎn),頁麵切換時耗(hào)時難以接(jiē)受(shòu),監(jiān)控(kòng)過程中應(yīng)用的使用體驗極差(chà)。不僅(jǐn)如此,西(xī)瓜視頻在 malloc debug 監控過(guò)程中還會(huì)遇到必現的棧回溯 crash(堆棧如下,《libunwind llvm 編年史》[8] 有相關分析)。

LeakTracer 是另一個比較知名的(de)內存泄漏監控工具,其原理是:通過 LD_PRELOAD 機製搶先加載一個定義了 malloc/calloc/realloc/memalign/free 等同名函數的代理庫,這樣就全局代理了應(yīng)用層內存的分配和釋放,通過 unwind 回溯調用棧(zhàn)並過濾出疑似的內存(cún)泄漏信息。androids 平(píng)台上的 LD_PRELOAD 是被嚴格(gé)限製的,因為其沒有獨立的 unwind 實現,依賴係統的(de) unwind 能力,也會遇到 malloc debug 遇(yù)到的棧幀兼(jiān)容問題;如果把 LeakTracer 集成到目標 so 裏通過 override 方式實現代(dài)理,隻(zhī)能攔(lán)截到本 so 裏顯式的內存分(fèn)配/釋放(fàng),無法攔截到其他 so 和跨 so 調用的內存分配/釋放。通過 native 插(chā)樁(zhuāng)的方式也是如此,隻能監控局部單純的內存泄漏,無法全局監控內存使(shǐ)用。
綜合以上分析和接入(rù)體驗,我(wǒ)們不難發現,這些內存泄(xiè)漏(lòu)監控(kòng)工具在 androids 平台上實際接入時(shí)基本都存在以下三個比較典型的問題:
流程繁瑣:需要配置 wrap.sh/root permission/setprop 等(děng),受 androids 版本限製兼容問題:unwind 庫存在嚴重的(de)兼容性問題,libunwind_llvm 無法正確回溯 GNU 編(biān)譯的棧幀性能問題:官方的(de) malloc debug 性能數據是損失 10 倍以上,實測西瓜開啟後在中高(gāo)端機上不可用(yòng)我們的需求西瓜視頻 App 是一(yī)個匯集了視頻播放、特效拍攝、視頻剪輯輯、P2P 加速等 native 代碼非常多的中大型應用,每個(gè) native 代碼相關的模塊背後都有(yǒu)一個專業團隊在高(gāo)速迭代,加上(shàng)日人均使用時長超過 100 分鍾的影響,西瓜視頻 App 的 native 內存問題治理難度非(fēi)常大。事實上,單純的內存泄漏問題相對較少,更多的是因為業務邏輯不合(hé)理帶來的內存使用問題,需(xū)要工具滲透到 App 運行的過程中進行監(jiān)控,無形中(zhōng)提高了對工(gōng)具性能和穩定性的要求。
線上(shàng) native 內存問題基本都(dōu)是以虛擬內存觸頂的形式(shì)暴露出來的。在西瓜視頻 App 裏,虛(xū)擬內存的消耗(hào)除了上述幾大模塊外,還有(yǒu)其他幾個消耗大戶,如線程、webview、Flutter、硬件加速、顯存(cún)等。事(shì)實上,malloc/calloc/realloc/memalign 等相(xiàng)對於 mmap/mmap64 直接(jiē)分配出的內存在整個虛擬(nǐ)內存空間中通常占比比較小。因為內存(cún)問題通常以虛擬內(nèi)存耗盡的形式表現出來,隻有盡可能多的收集各種內存消耗來無限逼近虛擬(nǐ)內存上限,才能準(zhǔn)確找出虛擬內存(cún)耗(hào)盡的原因。因此,像 malloc debug 這樣隻監控 malloc/calloc/realloc/memalign/free 等根本無法滿足內存治(zhì)理需要,覆(fù)蓋 mmap/mmap64/munmap 等盡可能多的內存分配形式是監控工具必須要做的。
綜合(hé)上麵的分析可(kě)以得出,西瓜視頻 App 乃至整個字節跳動旗下其(qí)他 App, 對於一個通用的 native 內(nèi)存泄漏監控工具的訴(sù)求主(zhǔ)要有以下幾個方麵:
接入層麵:不依(yī)賴(lài) androids 版本,無需 root,對業務滲透(tòu)盡可能低穩定性:不存在影響業務的穩定性問題,可以(yǐ)滿足線上使用的訴求性能層麵:沒有明顯的性能問題,達到可線上(shàng)使用的標準監控範圍:不局限於(yú) malloc/calloc/realloc/memalign/free,至少還能覆蓋 mmap/mmap64/munmapRaphael 核心設計通過前麵的(de)分(fèn)析可以知道,一個完整的 native 內存泄漏監控工具主要(yào)包含三部分(fèn):代理實現、棧回溯和緩存管理。代理實現是解決 androids 平台上接入問題的關鍵,棧回溯是性能和穩定性的核心,緩存邏輯在一(yī)定程度上也會直接(jiē)影響性能(néng)和穩定性。接(jiē)下來我們會從四個方(fāng)麵介紹 Raphael 的核心設計。
代理(lǐ)實(shí)現鑒於 wrap.sh 和 LD_PRELOAD 在 androids 平台上不具有通用性,首先被排除。又因 malloc hook 隻能代理(lǐ) malloc/calloc/realloc/free,無法覆蓋 mmap/mmap64/munmap,也被放棄。但受 malloc hook 實現方式的啟發(fā),借(jiè)助於 inline hook / PLT hook 工(gōng)具(jù)我們可以實現同樣的代理效果,這其中比較(jiào)有代表性的工具主要有 androids-Inline-Hook[3] 和(hé) xHook[1]。

xHook 是比較優秀的 PLT hook 工具代表,其穩定性可以達到上線標準。因其實現依賴正(zhèng)則,同時 hook 的 so 或函數比較多時(shí),hook 耗時會比較明顯。此外,其原生實現隻能 hook 當前已經(jīng)加載的(de) so,對於未加載的(de)沒做特殊處理(lǐ),如果用(yòng)來做長時間的進程級監控,需要解決增量 so hook 問題。不過這(zhè)種 hook 方式非常適合做(zuò) so 定向(xiàng)監控。
與(yǔ) PLT hook 原理不同,inline hook 則是(shì)在目標函數的頭部(bù)直接插入跳轉指令,其 hook 的是最終的函數實現,不存在增量 so hook 問題(tí),hook 效率高效直接。但 inline hook 在(zài) hook 那些可能正在執行(háng)的函數後,需要掛起相關線程進行指令修正,這個是 inline hook 的痛點(diǎn),現有(yǒu) hook 實(shí)現很多沒有做指令修複,或(huò)者在指令修複時或多或少都存在一(yī)些問題(tí)。
Raphael 在早期的驗證版本裏采用 xHook 來實現代理接入。後續(xù)為了實現長(zhǎng)時間進程級監控,以覆蓋更多(duō)的業務場景,Raphael 又通過 androids-Inline-Hook 解(jiě)決增量 so hook 問題,通過 xHook 實現定向監控。為了(le)進一步提升工具的性能和穩定性, Raphael 內部最新版本已切換到了 bytehook(字節跳動自研的 PLT hook 工具,可自動處理增量 so hook 問題)。
棧回溯定位一個對象(xiàng)或者一段內存(cún)通(tōng)常可以通過引用/依賴關係(xì),也可以通(tōng)過創建/分配時的堆棧。Java 堆內存(cún)因為有明確的組織形式和(hé)清晰的依賴關係,可以通過(guò)依賴關係靜態(tài)分析內存泄漏問(wèn)題(tí)。但 native 堆內存依賴/引用比較隱晦,也沒有 Java 堆內(nèi)存那樣明確的組織(zhī)格式(shì),無法(fǎ)通過依賴/引用關係進(jìn)行靜態分析(xī),隻能通過分配時的堆棧來(lái)輔助定位。棧回溯(unwind)是 native 層獲取調用堆棧的通用方式,是 native 內存泄漏(lòu)監控工具不可或缺的核心,同時也是工具(jù)性能和穩定性(xìng)的(de)瓶頸所在。接下來(lái)本文將從棧回溯工具選(xuǎn)取、限製棧回溯頻次、減少無用棧回溯三個方麵介紹 Raphael 在棧回溯上所做的工作。
棧回溯工具選取androids 平台上常用(yòng)的 32 位棧回溯庫主要有:libunwind_llvm、libunwind (nongnu)、libgcc_s、libudf、libbacktrace、libunwindstack 等,實踐證實這(zhè)些工具(jù)或多或少都存在一些問題,以(yǐ)下是我們基(jī)於三個主流的棧回溯庫做的(de)簡單對比分析(平(píng)台:Pixel 2 & androids 10,性能:Demo 裏統計 16 層棧幀回溯的總(zǒng)耗時;兼(jiān)容性:字節跳動旗下多個應用(yòng)長時間的(de)優化治理實踐)

棧回(huí)溯涉及到的東西比較多,想要(yào)自(zì)己短時間內實現一個在穩定性、回溯性能、回溯成功率等方麵都表現優異的 32 位棧回溯工具難度非(fēi)常大。為了快速驗證並解決實(shí)際機(jī)問題(tí),Raphael 在早期版本裏采用的是 libunwind_llvm,隨後切換到 libunwind_llvm & libunwind (nongnu),通過 libunwind_llvm 保證回溯性能,在回溯深度低於 2 層時切換到 libunwind (nongnu),以保證(zhèng)回溯成功率。最新(xīn)版本裏則采用的(de)是 libudf,兼具了性能和回溯成功率(lǜ)。相對而(ér)言,64 位下基於 FP 的棧回溯實現性能和穩(wěn)定(dìng)性基本都能(néng)滿足需求(qiú),這裏不做過多介紹。Rapahel 同時也在設計時(shí)做了充分的擴展考慮(lǜ),可以(yǐ)輕鬆切換到其他更優秀的棧回溯(sù)實現。
限製棧(zhàn)回溯頻次即(jí)便(biàn)是 libudf 實(shí)現(xiàn),其在 demo 裏(lǐ)回溯 16 層棧幀的平(píng)均耗時也需要 0.6ms,監控工(gōng)具實際運行時對 App 性能(néng)的影響(xiǎng)是很明顯的。提(tí)升監控性(xìng)能的途徑除(chú)了直(zhí)接優化(huà)棧(zhàn)回溯性能外,減少回溯頻次也是十分有效的手(shǒu)段。我們在西瓜視頻 App 的優化治理實踐中發現,多數場景小於 1024 byte 的內存分配其頻率約占(zhàn) 70% 以上,但線上遇到的 native 內存觸頂問題,卻很少是由小內(nèi)存泄漏引(yǐn)發的,監控小內存泄漏對於解決線上 native 內存觸頂問題沒有實質(zhì)效果。即便真的是由小內存引發的,這個需要高頻和必現(xiàn)的場(chǎng)景才(cái)能達到,這類問題通常在線下單測(定向監控(kòng))場景(jǐng)是完全可(kě)以覆蓋(gài)到的。基(jī)於此,Raphael 通過設定內存(cún)閾值來控製棧(zhàn)回(huí)溯頻次,可以大幅降低棧回溯的性能損耗(hào)。

受限於代(dài)理流程和棧回溯(sù)實現機製,從代理函數入口到回溯開始的路徑上會存在幾層跟分配堆棧無關的函數調用,這幾層調用最終會體現在最後回(huí)溯成功的堆棧上(下圖的紅色部分),每次內存分配都回溯(sù)這幾層無用的調用鏈是十分損耗性能的。解決這種問題的直觀方法(fǎ)就(jiù)是減少甚至完全規避這種無關的棧回溯,體現在代碼層麵就是減少代理入口到(dào)回溯開啟函數之間的調用層級。inline 是一種簡單直(zhí)接的實現方式,也可以直接在(zài)代理入口處提前構建(jiàn)回溯的 context 數(shù)據。

緩存管理作為 native 內存監(jiān)控的重要一環,對整個監控(kòng)工具性能的影響至關重要。以 malloc debug 和LeakTracer 為例,它們都是通過分配後的內存地址作為 key 來計算 hash 後散(sàn)列存儲的,並通過(guò)一個全(quán)局鎖來同步緩存更新的時序。兩者不同的(de)是,malloc debug 會通(tōng)過(guò)堆棧聚合調用鏈完全相同的內存分配記錄,其緩存的存(cún)儲(chǔ)單元通過(guò) malloc 動態分配;而 LeakTracer 則不會(huì)根(gēn)據堆棧聚合,其存儲單元會預(yù)先分配一部分,緩存不足時也會(huì)動態申請。通過以(yǐ)上分析和實測可以(yǐ)發現(xiàn),malloc debug 的實際性能比LeakTracer 低很多,原因主要體現在堆棧聚合(hé)和緩存動(dòng)態分配上。

對比 malloc debug 和 LeakTracer 的源碼也可以發現:運行時的堆棧聚合(hé)是完全(quán)沒有必(bì)要的;如(rú)果限製內存監控的閾值,緩存空間和緩存單元的上限都可以控製在一定範圍(wéi)內的,不需要動(dòng)態申請,可以減少動態分配的性能損耗;此(cǐ)外,由於 native 內存分配和釋放頻率比較高,全局鎖一定程序上會(huì)影響整體性能(néng),通(tōng)過 key 計算 hash 後再散列存儲時不需要全局鎖。
Raphael 是預先分配固定大小的(de)緩存空間,除了發生內存觸頂導致的 crash 外,緩存單元提前耗完也認為存在內(nèi)存泄漏問題。這(zhè)主要是因為:對於 32 位進程,其(qí)虛擬(nǐ)內存的上限通常是(shì) 4G,正常運(yùn)行時相對比較容易觸達(dá)上限,而 64 位進程(chéng)的虛(xū)擬地址空間非(fēi)常大,實際很難遇到虛(xū)擬內存觸頂的 case,但遇到物理內存不足(zú)的概率則要大很多,這與 32 位進程基本(běn)相反。通過控製 vmPeak 閾值和(hé)緩存單元餘量可以有(yǒu)效捕捉到內存泄漏數據,最終實現穩定可靠的全自動內存泄漏監控及消費流程(chéng)


通過前麵的分析可以(yǐ)知道,隻監控 malloc/calloc/realloc/memalign/free 是無法滿足治理需求的,這主要是因為 malloc/calloc/realloc/memalign/free 等分(fèn)配出的(de)內存(cún)通常在整個虛擬內存空間裏占比較小,常見的內存消耗大戶 Thread、webview、Flutter、硬件加速、顯存等,都不是通過這些函數分配(pèi)出的。為了能(néng)夠對 androids 平台上的 native 內存觸(chù)頂(dǐng)問題精準歸因,監控需(xū)要無限逼近虛(xū)擬內存的(de)上限,這就需要監(jiān)控盡(jìn)可能多的內存分配形(xíng)式。
androids 上的內存操作主要是 malloc/calloc/realloc/memalign/free 和 mmap/mmap64/munmap,同監控 malloc/calloc/realloc/memalign/free 相(xiàng)比,監控 mmap/mmap64/munmap 有兩點不同:一個是(shì)線程棧的釋放問題,雖然創建(jiàn)線(xiàn)程時是通(tōng)過 mmap/mmap64 分(fèn)配(pèi)的棧內存,但棧(zhàn)內存的釋放並不一定是通過顯(xiǎn)式調用 munmap 實現的;另一個(gè)是(shì)監控重入問題(tí),當(dāng)通過 malloc/calloc/realloc/memalign 等分(fèn)配大內(nèi)存時,底層通常是(shì)通過 mmap/mmap64 實現的,兩類接口同時監控(kòng)時會存在重入問題。
棧內存(cún)釋放線程的棧內存(cún)又分為信號棧和執行(háng)棧,信號棧在調用void pthread_exit(void *return_value) 接口時會通過 munmap 即刻釋放,而執行(háng)棧的釋放(fàng)則有兩種形式:
void pthread_exit(void return_value) 函數體裏,當線程狀態為 THREAD_DETACHED 時會直接(jiē)通過 void _exit_with_stack_teardown(voidstack, size_t sz) 釋放



綜上,最終通過 munmap 釋放的內存(cún)都可以被監控(kòng)到,而通過(guò)_exit_with_stack_teardown 釋放的內存則無法攔(lán)截到。我們針對這種情況做了特殊處理:在 Raphael 裏代理攔截了 void pthread_exit(void *) ,並判(pàn)斷此時線程狀(zhuàng)態是否為 THREAD_DETACHED,如果是則(zé)在監(jiān)控裏直接移除相(xiàng)關記錄,否則不移除。
重入問題下圖是一個典型的重入現場,其上層的 malloc 函數最終調用到了 mmap 函數,同時監控兩類內存接口時就會遇(yù)到此類問題。重入問題帶來的一個挑戰是緩存如何管理,同一個緩存(cún)裏隻能維護一個記錄(lù),維護(hù)兩個(gè)記錄的邏輯和性能過於複雜。此外,從 malloc 到 mmap 的堆棧是固定的,這幾層堆棧對分析內存泄漏完全沒用,因為這個時候關注的是 malloc 之上的堆(duī)棧。

解決重入問題(tí)的方案很直接,在檢測到 mmap/mmap64 之上(shàng)有 malloc/calloc/realloc 等棧幀時(shí),忽略本(běn)次分配。這樣不僅解決了重入問題,也避免了不必要的棧回溯。因為 androids 平台不支持 thread local storage(TLS),隻能通過 pthread_setspecific 和(hé) pthread_getspecific 實現。

相對於 malloc debug 和 LeakTracer,Raphael 不僅支持 malloc/calloc/realloc/memalign/free,也支持監控 mmap/mmap64/munmap 等,使監控範圍擴展到了線程、webview、Flutter、顯存(cún)等,基本完全(quán)覆蓋了 androids 平(píng)台上的 native 內存使用場(chǎng)景
性能androids 平台上(shàng)的 native 內存泄漏(lòu)檢測通常都是在程序運行過程中進行的,棧回溯和緩存管(guǎn)理會消耗部分 CPU 和內存(cún),帶來一定的性能損失。Raphael 可配置的監控能力有(yǒu)很大的伸縮性,性能影響(xiǎng)可以限製在可接受範圍(wéi)內,以下數據基於(yú)西(xī)瓜視頻 App 32 位模式評測(中高端機型和 64 位下的性能(néng)更高):
CPU:32 位(wèi)模式 & ≥1024 的監控閾值下,在低端機上 CPU 消耗< 3%內(nèi)存:32 位模(mó)式下默(mò)認會有約 16M 的虛擬內存消耗幀率:32 位模式 & ≥1024 的監控閾值下,低端機上幀率沒有明顯變化穩定性已開(kāi)源的(de)版本是(shì)基於開(kāi)源 inline hook 實(shí)現的,在部分 androids 6 機型上存在卡死問題,除此之外暫未發現其他穩定性問(wèn)題。此外,字節跳動這邊早期的治理實踐集中在線下,並基(jī)於 Raphael 建設完善了線下的防治體係,更(gèng)為穩定的版本(běn)可(kě)以滿足線上的監控需求,我們會在後續(xù)迭代開源。
治理實踐Raphael 在字節跳動內部使用非常廣泛,是字節跳動 native 協會指定的 native 內存(cún)泄漏檢測工(gōng)具。在治理(lǐ)實踐中,Raphael 覆蓋了幾乎所有的 native 內存使用場景,輔助解決了大量的 native 內存泄漏和內存使用不合理(lǐ)的問題。接下來通過四個(gè)典型的案(àn)例簡單介紹下 Raphael 的監控能力和基於 Raphael 的數據分析方法(應用自身的,Java 層(céng)的,webview 的,係統層的)
案例(lì) 1
下圖是西瓜視頻裏兩個比較典型的 native 內存問題(tí)現場,既有嚴格意義上的內存泄漏(用完(wán)之後未釋放),也有更為廣泛的內存不合理使用的問題(短暫泄漏、局部場景(jǐng)問(wèn)題、上層業(yè)務邏輯問題等)。針對內存(cún)泄漏問題,在明確了相關內存的生命周期之後,可以相對輕鬆地快速定位到。對於(yú)內存使用不合理的問題,則需要盡可能多的搜集未釋放的內存,來綜合(hé)評(píng)估影響。

早期在分析數據時,我們也會通(tōng)過 maps 來驗證 Raphael 的數據。通常通過(guò)分析 maps 可以大致知道內存觸(chù)頂的原因,下圖是一個典型的運(yùn)行時通過 malloc/calloc/realloc/memalign 和 mmap/mmap64 分配的內存(cún)過多導致的 OOM 現場。

案例 2
下圖是字節跳動內部一個業務遇到的 native 內存問題現場,未(wèi)接入 Raphael 前雖能輕鬆(sōng)複(fù)現 native 內存增長的問題,但(dàn)無法定位(wèi)內存增長的原因。在接入 Raphael 後(hòu),雖然攔(lán)截到的(de)內存並(bìng)不多,但問題暴露得非常明(míng)顯。排名(míng)第一個(gè)的堆棧是 Java 層創建 bitmap 對象時調用到 native 層(céng)堆(duī)棧(androids 8 以後(hòu) Bitmap 的數據(jù)是存(cún)儲在 native 層),該問題的調查(chá)最終轉移到了 Java 層。

基於以上分析,我們可以斷定 Java 層的堆內(nèi)存裏一定存在大量的 Bitmap 對象。因為該問題是線下可複現(xiàn)的,我們可以很容易地通過 Java 堆內存(cún)快照驗證並定位(wèi)到問題原因(如下圖所示)。如果是(shì)線(xiàn)上(shàng),我們需要抓取異常(cháng)現場(chǎng)的快照才(cái)能最終定位,這也正是 西(xī)瓜視頻穩定性治理體係建(jiàn)設一:Tailor 原理及實踐 裏所提到的(de)通(tōng)用異常數據搜集建設。

案例 3
一直(zhí)以來 androids 設備上 webview 消耗的內存很少被重視,隨著前端業(yè)務(wù)場(chǎng)景增多,webview 導致的內存問(wèn)題也越來越明顯、越來越頻繁。下圖是 Raphael 在西瓜視頻 App 裏監控到的一個前端活動頁導致的內存(cún)問題現場。由於(yú)係(xì)統 webview 自身的原因,工具無(wú)法回溯出(chū)完整的調用棧,無法直觀(guān)定位到問題原因。最終我們通過定向分析(xī)內存數據(jù),定(dìng)位到這些內存基本都是前端頁麵裏緩存的圖片資(zī)源,在對該頁麵的圖片緩存策(cè)略進行優化之後,相關的內存(cún)觸(chù)頂的異常大(dà)幅降低。

案例 4
下圖是(shì) androids 係統上長期存(cún)在的一類 Camera 內存泄漏現場。通過分析源碼可知,Camera 在拍(pāi)攝過程中會在(zài) native 層持(chí)續構造 CameraMetadata 實例,而每個 CameraMetadata 對象都會指向一塊不小(xiǎo)的 native 內存,這(zhè)塊 native 內存的釋放依賴 Java 層的 CameraMetadataNative 對象(xiàng)執行 finalize 函數(shù)。這個邏輯最終導致這部分 native 內存的回收間接依(yī)賴(lài) Java 層的 GC。如果一段時間內 Java 層沒有 GC ,這部分 native 內存就會因為沒有及時釋放而(ér)堆積,進而在觸頂後引發各種因 native 內(nèi)存不(bú)足(zú)而導致的異常。《androids Camera 內存問題剖析》裏有詳細的分析(xī)過程,《ART 視角 | 如何讓 GC 同(tóng)步回收 native 內存》針對此類問題也同步給(gěi)出了方案,通過溝通 androids 團隊表示會在後續版本裏徹底修複此(cǐ)問題。

Native 內存泄漏(lòu)監控的原理相對簡單,但想要做到完美通用卻很困難,最主要的考驗當屬性能和穩定性問題,例(lì)如 32 位棧(zhàn)回溯的性能和(hé)穩定性、緩存管理的性(xìng)能等。前期我(wǒ)們在調研和開發 Raphael 時,基於快速落地(dì)和解決緊迫問題的目的,複用了大量第三方代碼,並簡化了很多邏(luó)輯。經過(guò)長期的治理實踐,工具自(zì)身也暴露出一些問題和後續可以優(yōu)化的方向。
就代理邏輯而言,androids-Inline-Hook 和 And64InlineHook 雖然都是比較優秀(xiù)的 inline hook 工具,但實際使用時仍然存在(zài)兼容和卡死的問題。雖然 xHook 在兼(jiān)容性和性能上都可以達(dá)到上線標準,但不(bú)具(jù)有通用性,很難將(jiāng) native 內存泄漏監控(kòng)擴展到其他有(yǒu)上限的(de)資源上(如 JNI Reference Table)。我們也在(zài)調研優化(huà) inline hook,探(tàn)索更為穩定高效的 hook 方案。
棧回溯和緩存(cún)管理(lǐ)是 native 內存泄漏監控性能(néng)和穩定性的(de)瓶頸。相對而言,基於 FP 的 64 位棧回溯方案已經(jīng)到了(le)極(jí)致,但 32 位下目前仍沒有完美理想的(de)方案。在 32 位下,Raphael 通過限製棧回溯深度和控(kòng)製監控範圍來規避頻繁棧回溯帶來的性能影響,雖然可以大幅提升性能,但(dàn)也存在漏報問題(tí)。因此,32 位棧回溯性能也是我們後續的優化方向(xiàng)。此外,Raphael 已開源的版本其緩存管理仍然是通過全局(jú)鎖來實現同(tóng)步的,會(huì)有(yǒu)一定的性(xìng)能(néng)損失,這個我們也會在後(hòu)續的開源迭(dié)代裏同步最新的優化。
眾所(suǒ)周知,物理(lǐ)內存、虛擬內存、Thread、FD、JNI Reference Table 等(děng)都是典型的(de)有上限(xiàn)的(de)資源,不合理使用都會造成常規手段難(nán)以調查的穩定性問題。顯而易(yì)見,內存泄漏的監控邏輯, 同樣適用於其他這些有上限的(de)資源(yuán)。甚至於那些雖然沒有明確上限的(如 Binder、流(liú)量、耗時等),我們也可以構(gòu)造出相應的上限來實(shí)現監控和溯源。基(jī)於 Raphael 擴展其他的監控能力是(shì)我們(men)後續要高優完善的。
總(zǒng)結androids native 內存泄漏話題由來(lái)已久,在此之前(qián)業界一直沒有穩定可靠的工具可用(yòng),得益於 AOSP 和其他優秀的開源項目(androids-Inline-Hook、And64InlineHook、xHook、xDL),使得(dé)我們有機會進行相關的嚐試。Raphael 是西瓜視頻基礎技術(shù)團隊(duì)的初步探(tàn)索(suǒ)和(hé)嚐試,在字(zì)節跳動內部眾多 App (如(rú)西瓜、抖音、頭條)長期的治理實踐(jiàn)中,不(bú)僅解(jiě)決(jué)了大量疑難問題,也(yě)進一步完善(shàn)了工具和方法論。
雖然基於 Raphael 的 native 內存泄(xiè)漏監控方案目前已經足夠成熟和穩定,但其監控過程畢竟滲透到了 App 的運行過(guò)程(chéng),會(huì)有一定程度的性能損失和穩定性風險。我們倡導的方案(àn)是基於此來建設完善線(xiàn)下的(de)內存(cún)泄漏(lòu)防治體係,謹慎帶到(dào)線上。由於(yú)內(nèi)部迭(dié)代的 Raphael 版(bǎn)本比較多,且涉及其他未開源的項目,本次開源我們隻能選(xuǎn)擇其(qí)中一個穩定可用的版本,其他優(yōu)化(huà)會在後續逐步(bù)開源。
Raphael 隻是(shì)邁開了其中的一小步,方案還有很大的優化空間。開源不是終點,我們希望集思(sī)廣益、共同探索完善,在(zài) androids 穩定性治理上走(zǒu)得更快更遠。
相關資料Raphael 開源地址:https://github.com/bytedance/memory-leak-detectorxHook 鏈接:https://github.com/iqiyi/xHookxDL 鏈接:https://github.com/hexhacking/xDLandroids-Inline-Hook 鏈接:https://github.com/ele7enxxh/androids-Inline-HookAnd64InlineHook 鏈接:https://github.com/Rprop/And64InlineHookmalloc debug 鏈接:https://androids.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.mdLeakTracer 鏈接:http://www.andreasen.org/LeakTracer/ androids Camera 內存問(wèn)題剖析 libunwind llvm 編年史:https://zhuanlan.zhihu.com/p/33937283ART 視角 | 如何讓 GC 同(tóng)步回(huí)收 native 內存:https://juejin.cn/post/6894153239907237902加入我們
歡迎加入字節跳動西瓜視頻客戶端團(tuán)隊,我們專(zhuān)注於(yú)西瓜視頻 App 的開發和基礎技術建(jiàn)設,在客戶端架構、性能(néng)、穩定性、編譯構建、研發工具等方向都有投入。如果你也(yě)想一起攻克技術難題,迎接(jiē)更大的(de)技術挑戰,歡迎加入我(wǒ)們 !
西(xī)瓜視頻客(kè)戶端團隊(duì)正在熱招 androids、ioses 架構師和研發工程師,最 Nice 的工作氛圍和成長機(jī)會,各種福利各種機遇,在北京、杭州、上海三地均有職(zhí)位,歡迎投遞簡曆!聯係(xì)郵箱:tech@bytedance.com ;郵件標(biāo)題:姓名-工作年(nián)限-西瓜-androids/ioses/基礎技術。
歡迎關(guān)注「字節(jiē)跳動技術團隊」
網址格(gé)式不正確
網址(zhǐ)格式不正(zhèng)確
申明:如本站文章或轉稿涉及(jí)版權等問(wèn)題,請您及時聯係本站,我們會盡快處理!
