Raphael 是西瓜視頻基礎技術團隊開發的一款 native 內存泄漏檢測工具,普遍用於字節跳動旗下各大 App 的 native 內存泄漏治理,收益顯著。工具現已開源,本文將經過原理、方案和實踐來剖析 Raphael 的相關細節。html
Android 平臺上的內存問題一直是性能優化和穩定性治理的焦點和痛點,Java 堆內存由於有比較成熟的工具和方法論,加上 hprof 快照做爲補充,定位和治理都很方便。而 native 內存問題一直缺少穩定、高效的工具,僅有的 malloc debug 不只性能和穩定性難以知足須要,還存在 Android 版本兼容的問題。前端
事實上,native 內存泄漏治理一直不乏優秀的工具,已知的可用於調查 native 內存泄漏問題的工具主要有:LeakTracer、MTrace、MemWatch、Valgrind-memcheck、TCMalloc、LeakSanitizer 等。但因爲 Android 平臺的特殊性,這些工具要麼不兼容,要麼接入成本太高,很難在 Android 平臺上落地。這些工具的原理基本都是:先代理內存分配/釋放相關的函數(如:malloc/calloc/realloc/memalign/free),再經過 unwind 回溯調用堆棧,最後藉助緩存管理過濾出未釋放的內存分配記錄。所以,這些工具的主要差別也就體如今代理實現、棧回溯和緩存管理三個方面。根據這些工具代理實現的差別,大體能夠分爲 hook 和 LD_PRELOAD 兩大類,典型的如 malloc debug 和 LeakTracer。android
工具
|
原理
|
---|---|
malloc debug
|
it works by adding a shim layer that replaces the normal allocation calls
|
LeakTracer
|
經過LD_PRELOAD機制實現代理,或將源碼集成到目標庫裏,經過 override 實現代理
|
malloc debug 是 Android 系統自帶的內存調試工具(官方 Native 內存調試 有相關介紹 ) ,雖然沒有額外的接入代碼,但開啓方式和核心功能等都受 Android 版本限制。git
API level
|
官方描述
|
---|---|
API <= 18
|
Note: malloc debug was full of bugs and was not fully functional until API level 19.
|
API 19-23
|
This isn't very useful since it tends to display a lot of false positive because many programs do not free everything before terminating.
|
API 24-26
|
You must be able to set system properties using the setprop command from the shell. This requires the ability to run as root on the device.
|
API 27-28
|
App developers should check the NDK documentation about wrap.sh for the best way to use malloc debug in Android O or later on non-rooted devices.
|
咱們在線下嘗試使用 malloc debug 監控西瓜視頻 App(配置 wrap.sh)時發現,正常啓動時間小於 1s 的機型(Pixel 2 & Android 10),其冷啓動時間被拉長到了 11s+。並且在正常使用過程當中滑動時的卡頓感很是明顯,頁面切換時耗時難以接受,監控過程當中應用的使用體驗極差。不只如此,西瓜視頻在 malloc debug 監控過程當中還會遇到必現的棧回溯 crash(堆棧以下,libunwind llvm 編年史 有相關分析)github
LeakTracer 是另外一個比較知名的內存泄漏監控工具,其原理是:經過 LD_PRELOAD 機制搶先加載一個定義了 malloc/calloc/realloc/memalign/free 等同名函數的代理庫,這樣就全局代理了應用層內存的分配和釋放,經過 unwind 回溯調用棧並過濾出疑似的內存泄漏信息。Android 平臺上的 LD_PRELOAD 是被嚴格限制的,由於其沒有獨立的 unwind 實現,依賴系統的 unwind 能力,也會遇到 malloc debug 遇到的棧幀兼容問題;若是把 LeakTracer 集成到目標 so 裏經過 override 方式實現代理,只能攔截到本 so 裏顯式的內存分配/釋放,沒法攔截到其餘 so 和跨 so 調用的內存分配/釋放。經過 native 插樁的方式也是如此,只能監控局部單純的內存泄漏,沒法全局監控內存使用。web
綜合以上分析和接入體驗,咱們不難發現,這些內存泄漏監控工具在 Android 平臺上實際接入時基本都存在如下三個比較典型的問題:shell
西瓜視頻 App 是一個聚集了視頻播放、特效拍攝、視頻剪輯輯、P2P 加速等 native 代碼很是多的中大型應用,每一個 native 代碼相關的模塊背後都有一個專業團隊在高速迭代,加上日人均使用時長超過 100 分鐘的影響,西瓜視頻 App 的 native 內存問題治理難度很是大。事實上,單純的內存泄漏問題相對較少,更多的是由於業務邏輯不合理帶來的內存使用問題,須要工具滲透到 App 運行的過程當中進行監控,無形中提升了對工具性能和穩定性的要求。緩存
線上 native 內存問題基本都是以虛擬內存觸頂的形式暴露出來的。在西瓜視頻 App 裏,虛擬內存的消耗除了上述幾大模塊外,還有其餘幾個消耗大戶,如線程、webview、Flutter、硬件加速、顯存等。事實上,malloc/calloc/realloc/memalign 等相對於 mmap/mmap64 直接分配出的內存在整個虛擬內存空間中一般佔比比較小。由於內存問題一般以虛擬內存耗盡的形式表現出來,只有儘量多的收集各類內存消耗來無限逼近虛擬內存上限,才能準確找出虛擬內存耗盡的緣由。 所以,像 malloc debug 這樣只監控 malloc/calloc/realloc/memalign/free 等根本沒法知足內存治理須要,覆蓋 mmap/mmap64/munmap 等儘量多的內存分配形式是監控工具必需要作的。性能優化
綜合上面的分析能夠得出,西瓜視頻 App 乃至整個字節跳動旗下其餘 App, 對於一個通用的 native 內存泄漏監控工具的訴求主要有如下幾個方面:markdown
經過前面的分析能夠知道,一個完整的 native 內存泄漏監控工具主要包含三部分:代理實現、棧回溯和緩存管理。代理實現是解決 Android 平臺上接入問題的關鍵,棧回溯是性能和穩定性的核心,緩存邏輯在必定程度上也會直接影響性能和穩定性。接下來咱們會從四個方面介紹 Raphael 的核心設計。
鑑於 wrap.sh 和 LD_PRELOAD 在 Android 平臺上不具備通用性,首先被排除。又因 malloc hook 只能代理 malloc/calloc/realloc/free,沒法覆蓋 mmap/mmap64/munmap,也被放棄。但受 malloc hook 實現方式的啓發,藉助於 inline hook / PLT hook 工具咱們能夠實現一樣的代理效果,這其中比較有表明性的工具主要有 Android-Inline-Hook 和 xHook
模式
|
原理
|
優勢
|
缺點
|
---|---|---|---|
malloc hook
|
原生支持,開關控制
|
沒有性能/穩定性問題
|
接口不暴露,不支持 mmap/mmap64
|
LD_PRELOAD
|
同名函數搶先佔位
|
沒有性能/穩定性問題
|
Android 平臺開啓難,不具備通用性
|
PLT hook
|
修改GOT表中的數據
|
單點hook,成熟可靠
|
效率低,須要解決增量so hook的問題
|
inline hook
|
目標函數裏插入跳轉指令
|
全局hook,效率最高
|
有兼容性問題,會面臨指令修復問題
|
xHook 是比較優秀的 PLT hook 工具表明,其穩定性能夠達到上線標準。因其實現依賴正則,同時 hook 的 so 或函數比較多時,hook 耗時會比較明顯。此外,其原生實現只能 hook 當前已經加載的 so,對於未加載的沒作特殊處理,若是用來作長時間的進程級監控,須要解決增量 so hook 問題。不過這種 hook 方式很是適合作 so 定向監控。
與 PLT hook 原理不一樣,inline hook 則是在目標函數的頭部直接插入跳轉指令,其 hook 的是最終的函數實現,不存在增量 so hook 問題,hook 效率高效直接。但 inline hook 在 hook 那些可能正在執行的函數後,須要掛起相關線程進行指令修正,這個是 inline hook 的痛點,現有 hook 實現不少沒有作指令修復,或者在指令修復時或多或少都存在一些問題。
Raphael 在早期的驗證版本里採用 xHook 來實現代理接入。後續爲了實現長時間進程級監控,以覆蓋更多的業務場景,Raphael 又經過 Android-Inline-Hook 解決增量 so hook 問題,經過 xHook 實現定向監控。爲了進一步提高工具的性能和穩定性, Raphael 內部最新版本已切換到了 bytehook(字節跳動自研的 PLT hook 工具,可自動處理增量 so hook 問題)。
定位一個對象或者一段內存一般能夠經過引用/依賴關係,也能夠經過建立/分配時的堆棧。Java 堆內存由於有明確的組織形式和清晰的依賴關係,能夠經過依賴關係靜態分析內存泄漏問題。但 native 堆內存依賴/引用比較隱晦,也沒有 Java 堆內存那樣明確的組織格式,沒法經過依賴/引用關係進行靜態分析,只能經過分配時的堆棧來輔助定位。棧回溯(unwind)是 native 層獲取調用堆棧的通用方式,是 native 內存泄漏監控工具不可或缺的核心,同時也是工具性能和穩定性的瓶頸所在。接下來本文將從棧回溯工具選取、限制棧回溯頻次、減小無用棧回溯三個方面介紹 Raphael 在棧回溯上所作的工做。
Android 平臺上經常使用的 32 位棧回溯庫主要有:libunwind_llvm、libunwind (nongnu)、libgcc_s、libudf、libbacktrace、libunwindstack 等,實踐證明這些工具或多或少都存在一些問題,如下是咱們基於三個主流的棧回溯庫作的簡單對比分析(平臺:Pixel 2 & Android 10,性能:Demo 裏統計 16 層棧幀回溯的總耗時;兼容性:字節跳動旗下多個應用長時間的優化治理實踐)
unwind
|
性能
|
兼容性
|
---|---|---|
libunwind (nongnu)
|
總回溯耗時9.0ms以上,性能最差
|
未發現穩定性問題,兼容性最好,回溯成功率最高
|
libunwind_llvm
|
總回溯耗時0.8ms之內,性能較好
|
GNU編譯的棧幀存在兼容性問題,回溯成功率最低
|
libudf
|
總回溯耗時0.6ms之內,性能最好
|
未發現穩定性問題,兼容性較好,回溯成功率較高
|
棧回溯涉及到的東西比較多,想要本身短期內實現一個在穩定性、回溯性能、回溯成功率等方面都表現優異的 32 位棧回溯工具難度很是大。爲了快速驗證並解決實際機問題,Raphael 在早期版本里採用的是 libunwind_llvm,隨後切換到 libunwind_llvm & libunwind (nongnu),經過 libunwind_llvm 保證回溯性能,在回溯深度低於 2 層時切換到 libunwind (nongnu),以保證回溯成功率。最新版本里則採用的是 libudf,兼具了性能和回溯成功率。相對而言,64 位下基於 FP 的棧回溯實現性能和穩定性基本都能知足需求,這裏不作過多介紹。Rapahel 同時也在設計時作了充分的擴展考慮,能夠輕鬆切換到其餘更優秀的棧回溯實現。
即使是 libudf 實現,其在 demo 裏回溯 16 層棧幀的平均耗時也須要 0.6ms,監控工具實際運行時對 App 性能的影響是很明顯的。提高監控性能的途徑除了直接優化棧回溯性能外,減小回溯頻次也是十分有效的手段。咱們在西瓜視頻 App 的優化治理實踐中發現,多數場景小於 1024 byte 的內存分配其頻率約佔 70% 以上,但線上遇到的 native 內存觸頂問題,卻不多是由小內存泄漏引起的,監控小內存泄漏對於解決線上 native 內存觸頂問題沒有實質效果。即使真的是由小內存引起的,這個須要高頻和必現的場景才能達到,這類問題一般在線下單測(定向監控)場景是徹底能夠覆蓋到的。基於此,Raphael 經過設定內存閾值來控制棧回溯頻次,能夠大幅下降棧回溯的性能損耗。
受限於代理流程和棧回溯實現機制,從代理函數入口到回溯開始的路徑上會存在幾層跟分配堆棧無關的函數調用,這幾層調用最終會體如今最後回溯成功的堆棧上(下圖的紅色部分),每次內存分配都回溯這幾層無用的調用鏈是十分損耗性能的。解決這種問題的直觀方法就是減小甚至徹底規避這種無關的棧回溯,體如今代碼層面就是減小代理入口到回溯開啓函數之間的調用層級。inline 是一種簡單直接的實現方式,也能夠直接在代理入口處提早構建回溯的 context 數據。
緩存管理做爲 native 內存監控的重要一環,對整個監控工具性能的影響相當重要。以 malloc debug 和LeakTracer 爲例,它們都是經過分配後的內存地址做爲 key 來計算 hash 後散列存儲的,並經過一個全局鎖來同步緩存更新的時序。二者不一樣的是,malloc debug 會經過堆棧聚合調用鏈徹底相同的內存分配記錄,其緩存的存儲單元經過 malloc 動態分配;而 LeakTracer 則不會根據堆棧聚合,其存儲單元會預先分配一部分,緩存不足時也會動態申請。經過以上分析和實測能夠發現,malloc debug 的實際性能比LeakTracer 低不少,緣由主要體如今堆棧聚合和緩存動態分配上。
對比 malloc debug 和 LeakTracer 的源碼也能夠發現:運行時的堆棧聚合是徹底沒有必要的;若是限制內存監控的閾值,緩存空間和緩存單元的上限均可以控制在必定範圍內的,不須要動態申請,能夠減小動態分配的性能損耗;此外,因爲 native 內存分配和釋放頻率比較高,全局鎖必定程序上會影響總體性能,經過 key 計算 hash 後再散列存儲時不須要全局鎖。
Raphael 是預先分配固定大小的緩存空間,除了發生內存觸頂致使的 crash 外,緩存單元提早耗完也認爲存在內存泄漏問題。這主要是由於:對於 32 位進程,其虛擬內存的上限一般是 4G,正常運行時相對比較容易觸達上限,而 64 位進程的虛擬地址空間很是大,實際很難遇到虛擬內存觸頂的 case,但遇到物理內存不足的機率則要大不少,這與 32 位進程基本相反。經過控制 vmPeak 閾值和緩存單元餘量能夠有效捕捉到內存泄漏數據,最終實現穩定可靠的全自動內存泄漏監控及消費流程
經過前面的分析能夠知道,只監控 malloc/calloc/realloc/memalign/free 是沒法知足治理需求的,這主要是由於 malloc/calloc/realloc/memalign/free 等分配出的內存一般在整個虛擬內存空間裏佔比較小,常見的內存消耗大戶 Thread、webview、Flutter、硬件加速、顯存等,都不是經過這些函數分配出的。爲了可以對 Android 平臺上的 native 內存觸頂問題精準歸因,監控須要無限逼近虛擬內存的上限,這就須要監控儘量多的內存分配形式。
Android 上的內存操做主要是 malloc/calloc/realloc/memalign/free 和 mmap/mmap64/munmap,同監控 malloc/calloc/realloc/memalign/free 相比,監控 mmap/mmap64/munmap 有兩點不一樣:一個是線程棧的釋放問題,雖然建立線程時是經過 mmap/mmap64 分配的棧內存,但棧內存的釋放並不必定是經過顯式調用 munmap 實現的;另外一個是監控重入問題,當經過 malloc/calloc/realloc/memalign 等分配大內存時,底層一般是經過 mmap/mmap64 實現的,兩類接口同時監控時會存在重入問題。
線程的棧內存又分爲信號棧和執行棧,信號棧在調用void pthread_exit(void *return_value) 接口時會經過 munmap 即刻釋放,而執行棧的釋放則有兩種形式:
綜上,最終經過 munmap 釋放的內存均可以被監控到,而經過_exit_with_stack_teardown 釋放的內存則沒法攔截到。咱們針對這種狀況作了特殊處理:在 Raphael 裏代理攔截了 void pthread_exit(void *) ,並判斷此時線程狀態是否爲 THREAD_DETACHED,若是是則在監控裏直接移除相關記錄,不然不移除。
下圖是一個典型的重入現場,其上層的 malloc 函數最終調用到了 mmap 函數,同時監控兩類內存接口時就會遇到此類問題。重入問題帶來的一個挑戰是緩存如何管理,同一個緩存裏只能維護一個記錄,維護兩個記錄的邏輯和性能過於複雜。此外,從 malloc 到 mmap 的堆棧是固定的,這幾層堆棧對分析內存泄漏徹底沒用,由於這個時候關注的是 malloc 之上的堆棧。
解決重入問題的方案很直接,在檢測到 mmap/mmap64 之上有 malloc/calloc/realloc 等棧幀時,忽略本次分配。這樣不只解決了重入問題,也避免了沒必要要的棧回溯。由於 Android 平臺不支持 thread local storage(TLS),只能經過 pthread_setspecific 和 pthread_getspecific 實現。
相對於 malloc debug 和 LeakTracer,Raphael 不只支持 malloc/calloc/realloc/memalign/free,也支持監控 mmap/mmap64/munmap 等,使監控範圍擴展到了線程、webview、Flutter、顯存等,基本徹底覆蓋了 Android 平臺上的 native 內存使用場景
Android 平臺上的 native 內存泄漏檢測一般都是在程序運行過程當中進行的,棧回溯和緩存管理會消耗部分 CPU 和內存,帶來必定的性能損失。Raphael 可配置的監控能力有很大的伸縮性,性能影響能夠限制在可接受範圍內,如下數據基於西瓜視頻 App 32 位模式評測(中高端機型和 64 位下的性能更高):
已開源的版本是基於開源 inline hook 實現的,在部分 Android 6 機型上存在卡死問題,除此以外暫未發現其餘穩定性問題。此外,字節跳動這邊早期的治理實踐集中在線下,並基於 Raphael 建設完善了線下的防治體系,更爲穩定的版本能夠知足線上的監控需求,咱們會在後續迭代開源。
Raphael 在字節跳動內部使用很是普遍,是字節跳動 native 協會指定的 native 內存泄漏檢測工具。在治理實踐中,Raphael 覆蓋了幾乎全部的 native 內存使用場景,輔助解決了大量的 native 內存泄漏和內存使用不合理的問題。接下來經過四個典型的案例簡單介紹下 Raphael 的監控能力和基於 Raphael 的數據分析方法(應用自身的,Java 層的,webview 的,系統層的)
案例 1
下圖是西瓜視頻裏兩個比較典型的 native 內存問題現場,既有嚴格意義上的內存泄漏(用完以後未釋放),也有更爲普遍的內存不合理使用的問題(短暫泄漏、局部場景問題、上層業務邏輯問題等)。針對內存泄漏問題,在明確了相關內存的生命週期以後,能夠相對輕鬆的快速定位到。對於內存使用不合理的問題,則須要儘量多的蒐集未釋放的內存,來綜合評估影響。
早期在分析數據時,咱們也會經過 maps 來驗證 Raphael 的數據。一般經過分析 maps 能夠大體知道內存觸頂的緣由,下圖是一個典型的運行時經過 malloc/calloc/realloc/memalign 和 mmap/mmap64 分配的內存過多致使的 OOM 現場。
案例 2
下圖是字節跳動內部一個業務遇到的 native 內存問題現場,未接入 Raphael 前雖能輕鬆復現 native 內存增加的問題,但沒法定位內存增加的緣由。在接入 Raphael 後,雖然攔截到的內存並很少,但問題暴露的很是明顯。排名第一個的堆棧是 Java 層建立 bitmap 對象時調用到 native 層堆棧(Android 8 之後 Bitmap 的數據是存儲在 native 層),該問題的調查最終轉移到了 Java 層。
基於以上分析,咱們能夠判定 Java 層的堆內存裏必定存在大量的 Bitmap 對象。由於該問題是線下可復現的,咱們能夠很容易的經過 Java 堆內存快照驗證並定位到問題緣由(以下圖所示)。若是是線上,咱們須要抓取異常現場的快照才能最終定位,這也正是 西瓜視頻穩定性治理體系建設一:Tailor 原理及實踐 裏所提到的通用異常數據蒐集建設。
案例 3
一直以來 Android 設備上 webview 消耗的內存不多被重視,隨着前端業務場景增多,webview 致使的內存問題也愈來愈明顯、愈來愈頻繁。下圖是 Raphael 在西瓜視頻 App 裏監控到的一個前端活動頁致使的內存問題現場。因爲系統 webview 自身的緣由,工具沒法回溯出完整的調用棧,沒法直觀定位到問題緣由。最終咱們經過定向分析內存數據,定位到這些內存基本都是前端頁面裏緩存的圖片資源,在對該頁面的圖片緩存策略進行優化以後,相關的內存觸頂的異常大幅下降。
案例 4
下圖是 Android 系統上長期存在的一類 Camera 內存泄漏現場。經過分析源碼可知,Camera 在拍攝過程當中會在 native 層持續構造 CameraMetadata 實例,而每一個 CameraMetadata 對象都會指向一塊不小的 native 內存,這塊 native 內存的釋放依賴 Java 層的 CameraMetadataNative 對象執行 finalize 函數。這個邏輯最終致使這部分 native 內存的回收間接依賴 Java 層的 GC。若是一段時間內 Java 層沒有 GC ,這部分 native 內存就會由於沒有及時釋放而堆積,進而在觸頂後引起各類因 native 內存不足而致使的異常。《Android Camera 內存問題剖析》裏有詳細的分析過程,《ART 視角 | 如何讓 GC 同步回收 native 內存》針對此類問題也同步給出了方案,經過溝通 Android 團隊表示會在後續版本里完全修復此問題。
Native 內存泄漏監控的原理相對簡單,但想要作到完美通用卻很困難,最主要的考驗當屬性能和穩定性問題,例如 32 位棧回溯的性能和穩定性、緩存管理的性能等。前期咱們在調研和開發 Raphael 時,基於快速落地和解決緊迫問題的目的,複用了大量第三方代碼,並簡化了不少邏輯。通過長期的治理實踐,工具自身也暴露出一些問題和後續能夠優化的方向。
就代理邏輯而言,Android-Inline-Hook 和 And64InlineHook 雖然都是比較優秀的 inline hook 工具,但實際使用時仍然存在兼容和卡死的問題。雖然 xHook 在兼容性和性能上均可以達到上線標準,但不具備通用性,很難將 native 內存泄漏監控擴展到其餘有上限的資源上(如 JNI Reference Table)。咱們也在調研優化 inline hook,探索更爲穩定高效的 hook 方案。
棧回溯和緩存管理是 native 內存泄漏監控性能和穩定性的瓶頸。相對而言,基於 FP 的 64 位棧回溯方案已經到了極致,但 32 位下目前仍沒有完美理想的方案。在 32 位下,Raphael 經過限制棧回溯深度和控制監控範圍來規避頻繁棧回溯帶來的性能影響,雖然能夠大幅提高性能,但也存在漏報問題。所以,32 位棧回溯性能也是咱們後續的優化方向。此外,Raphael 已開源的版本其緩存管理仍然是經過全局鎖來實現同步的,會有必定的性能損失,這個咱們也會在後續的開源迭代裏同步最新的優化。
衆所周知,物理內存、虛擬內存、Thread、FD、JNI Reference Table 等都是典型的有上限的資源,不合理使用都會形成常規手段難以調查的穩定性問題。顯而易見,內存泄漏的監控邏輯, 一樣適用於其餘這些有上限的資源。甚至於那些雖然沒有明確上限的(如 Binder、流量、耗時等),咱們也能夠構造出相應的上限來實現監控和溯源。基於 Raphael 擴展其餘的監控能力是咱們後續要高優完善的。
Android native 內存泄漏話題由來已久,在此以前業界一直沒有穩定可靠的工具可用,得益於 AOSP 和其餘優秀的開源項目(Android-Inline-Hook、And64InlineHook、xHook、xDL),使得咱們有機會進行相關的嘗試。Raphael 是西瓜視頻基礎技術團隊的初步探索和嘗試,在字節跳動內部衆多 App (如西瓜、抖音、頭條)長期的治理實踐中,不只解決了大量疑難問題,也進一步完善了工具和方法論。
雖然基於 Raphael 的 native 內存泄漏監控方案目前已經足夠成熟和穩定,但其監控過程畢竟滲透到了 App 的運行過程,會有必定程度的性能損失和穩定性風險。咱們倡導的方案是基於此來建設完善線下的內存泄漏防治體系,謹慎帶到線上。因爲內部迭代的 Raphael 版本比較多,且涉及其餘未開源的項目,本次開源咱們只能選擇其中一個穩定可用的版本,其餘優化會在後續逐步開源。
Raphael 只是邁開了其中的一小步,方案還有很大的優化空間。開源不是終點,咱們但願集思廣益、共同探索完善,在 Android 穩定性治理上走的更快更遠。
歡迎加入字節跳動西瓜視頻客戶端團隊,咱們專一於西瓜視頻 App 的開發和基礎技術建設,在客戶端架構、性能、穩定性、編譯構建、研發工具等方向都有投入。若是你也想一塊兒攻克技術難題,迎接更大的技術挑戰,歡迎加入咱們 !
西瓜視頻客戶端團隊正在熱招Android、iOS架構師和研發工程師,最 nice 的工做氛圍和成長機會,各類福利各類機遇,在北京、杭州、上海三地均有職位,歡迎投遞簡歷!聯繫郵箱: tech@bytedance.com ;郵件標題: 姓名-工做年限-西瓜-Android/iOS/基礎技術。
歡迎關注「字節跳動技術團隊」