阿里開源 Patrons:大型 32 位 Android 應用穩定性提升50%的“黑科技”

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"0x04 背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"截止到目前,國內的大部分 Android 應用仍然是 32 位架構,特徵是僅提供了 armeabi\/armeabi-v7a 架構的動態庫。Android 系統在啓動此類應用的時候,會使用 32 位的 Zygote 進程孵化應用,讓整個應用運行在 32 位兼容模式。雖然 Android 早在 5.0 版本就已經支持 64 位 CPU,但多年以來,大部分國內應用仍然運行在 32 位兼容模式。早在 2019 年 1 月,Google Play 就開始強制要求開發者上傳包含 64 位架構支持的應用,保證應用運行在 64 位模式。國內各大應用市場也出臺要求,希望國內應用在 2021 年年底之前上架雙架構版本。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大型 32 位 Android 應用存在一個衆所周知的問題,就是虛擬內存的尋址上限只有 "},{"type":"text","marks":[{"type":"strong"}],"text":"2^32=4GB"},{"type":"text","text":",而國內的各個廠商的頭部應用普遍功能繁多,內含各種容器、框架、SDK 以及多媒體能力等等,導致應用啓動後內存水位居高不下,在用戶的高強度使用下,會出現因爲虛擬內存不足而觸發的 Crash(libc:abort)。根據觀察到的線上 Crash 情況,可以發現 "},{"type":"text","marks":[{"type":"strong"}],"text":"Native Crash 的 Top 10 中有大量的 libc abort"},{"type":"text","text":",也就是信號 6,典型的特徵就是在 Crash 堆棧中可以發現地址空間的總和接近 4GB、Console log 中有大量的 (Out of memory)、malloc (xxxx) failed、returning null pointer 等等。隨着業務的發展和時間的推移,該問題逐漸突出,已經成爲穩定性保障中的攔路虎。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想要解決這個問題,"},{"type":"text","marks":[{"type":"strong"}],"text":"關鍵是解決虛擬內存不足的問題"},{"type":"text","text":",而 64 位應用的虛擬內存地址空間上限是 "},{"type":"text","marks":[{"type":"strong"}],"text":"2^39=512GB"},{"type":"text","text":" ,所以目前該問題的唯一解法就是升級到 64 位,因爲 64 位帶來的巨大地址空間除非出現 bug,一般不會觸頂。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常升級 64 位有兩種辦法:合包和拆包。其中合包會帶來巨大的包大小壓力,據 Google Play 調研:包大小每增加 6MB 會減少 1% 的下載。而拆包則需要一定的改造成本和維護成本。誠然終態一定是 64 位單架構,但無論是出於技術探索還是用戶體驗的考慮,都有必要研究一下這個問題,讓 64 位升級可以在不犧牲包大小的前提下平穩過渡。"},{"type":"text","marks":[{"type":"strong"}],"text":"本文將重點分享阿里開源用於緩解虛擬內存地址空間不足的“黑科技”—— Patrons,以及整個解決問題的探索過程,供大家參考。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"0x08 探索過程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虛擬內存地址空間不足的課題在去年雙十一前後被大家重視起來,大家紛紛下場投入了研究,也有很多階段性結論,其中比較典型的觀點有兩種:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Android 10 的內存分配器存在 bug 導致內存泄漏;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Jemalloc5.1(Android10 的內存分配器) 的髒頁釋放條件相比前代版本進行了修改,導致內存延遲釋放,最終使得虛擬內存的水位居高不下。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏簡單科普一下"},{"type":"text","marks":[{"type":"strong"}],"text":"內存分配器"},{"type":"text","text":":大多數情況下,我們寫 Native 代碼的時候,並不會直接調用內核的 API 去申請物理內存,而是使用 malloc 族函數進行內存申請。這時候返回的指針會指向虛擬內存中的地址空間,之後在這部分地址空間真的被使用的時候,纔會發生缺頁中斷觸發真實的物理內存分配,所以通常是兩層分配結構。用戶態代碼申請的內存來自於內存分配器的二次分配,常見的內存分配器有 JeMalloc、TcMalloc、PtMalloc 等等。當然,我們在寫業務的時候並不需要關心內存是哪個內存分配器分配的,Android 9、10 使用的內存分配器均爲 JeMalloc,靜態鏈接在 libc 中。Android 11 使用的是更加高性能、更加安全的 "},{"type":"link","attrs":{"href":"https:\/\/source.android.com\/devices\/tech\/debug\/scudo?hl=zh-cn","title":"xxx","type":null},"content":[{"type":"text","text":"Scudo"}]},{"type":"text","text":" 分配器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對以上觀點,我查閱了一些資料,針對 "},{"type":"text","marks":[{"type":"strong"}],"text":"Jemalloc5.1"},{"type":"text","text":" 延遲釋放的解決方案包括:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"編譯的時候修改編譯參數(不可行,無法決定用戶手機中的 Android 系統的編譯參數);"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"將 "},{"type":"text","marks":[{"type":"strong"}],"text":"dirty_decay_ms"},{"type":"text","text":" 設置爲 0,可以修改 Jemalloc 的髒頁釋放方式。但是經過查閱 Android 10 源碼,發現 Google 在 Android 10 中,已經將 "},{"type":"text","marks":[{"type":"strong"}],"text":"dirty_decay_ms"},{"type":"text","text":" 設置爲 0 了,可知並不存在髒頁釋放延遲的問題。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/7d\/7db7904c9700dc752b65772c67f7400b.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結合目前的資料來看,JeMalloc 可能並沒有什麼問題,但是在崩潰數據上展現出了一個奇怪的特徵,就是 Top 1 的 crash 中,Android 10 和 11 的比例達到了 4:1 左右。難免會讓人覺得這個問題是 Android 10 才突然出現的問題,Android 11 換成 Scudo 就好了。這個結論是否正確暫且按下不表,這裏我做了第一次嘗試:"},{"type":"text","marks":[{"type":"strong"}],"text":"在 Android 10 中使用 Android 11 標配的 Scudo 分配器,而不使用 JeMalloc ,是不是就能解決這個問題了?"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"0x0c 嘗試:移植 Scudo 到 Android 10"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個過程比較艱辛,主要是兩部分工作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"把 Android 11 中的 Scudo 分配器源碼拉出來,單獨編譯成一個動態庫依賴到我們的應用;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"通過一系列 Hook 手段,將應用的 Native 代碼中涉及內存分配的函數,替換成 Scudo 分配器中的分配函數。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現這個方案的時候發現存在一些問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"不只是 malloc 族函數會申請內存,strdup 和 strndup 這兩個函數會在內部自己 malloc ,也要注意 Hook 掉;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"安卓應用的 Native 內存申請不只是我們自己的 Native 代碼,還有相當一部分是安卓自己的系統庫,雖然 Hook 系統庫不是不行,但是會存在下面的一個致命問題,在我們自行提供內存分配器的時候,會出現兩種 case:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a. 使用系統 JeMalloc 申請的內存會嘗試在我們自定義的分配器中釋放:可以判斷一下如果不是我們申請的內存,可以調用 libc 的 free 進行釋放。如何來判斷內存是不是我們的分配器申請的,大家可以學習一下 Scudo 的源碼,寫得非常精緻;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"b. 如果使用我們提供的分配器分配內存,再嘗試使用系統的 JeMalloc 釋放:無解,JeMalloc 不會考慮這種情況,你會得到一個信號 11,當然可以自己去處理 段錯誤,但這麼費勁有點沒必要了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於可能出現 2.b 這種情況,所以這種方案充滿了不確定性,因爲不能預料用戶是怎麼申請內存的,就"},{"type":"text","marks":[{"type":"strong"}],"text":"無法從理論上證明可以 100% 覆蓋所有內存操作"},{"type":"text","text":",達不到上線標準。因此替換掉 JeMalloc 這條路行不通。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"0x10 最終解決方案 Patrons"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然不能把 native 的內存分配到 art 虛擬機中去,那麼幹脆把這部分地址空間讓出來好了。堵不如疏,不需要對 native 的內存分配進行任何修改和 hook,即可讓 libcmalloc 使用這部分地址空間,可以極大緩解虛擬內存不足的問題。我們簡單的回顧一下應用剛啓動的時候,大致的內存佈局:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/b4\/b47f335b3cde00f53c01b1da37dda21c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提升 Android 穩定性的方案 Patrons,目前已經開源到 GitHub 上 : "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/github.com\/alibaba\/Patrons","title":"xxx","type":null},"content":[{"type":"text","text":"https:\/\/github.com\/alibaba\/Patrons"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方案的重點在於"},{"type":"text","marks":[{"type":"strong"}],"text":"如何讓 ART 把 Heap 預分配的地址空間釋放出一部分"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先說一下爲什麼要壓縮這部分地址空間。通常情況下,我們的應用都會開啓 largeHeap 來獲得更大的內存上限,因爲默認配置下只有 192M。這是由參數 "},{"type":"text","marks":[{"type":"strong"}],"text":"dalvik.vm.heapgrowthlimit"},{"type":"text","text":" 決定的,對於大部分阿里系應用來說顯然是不夠的。但是開啓 largeHeap 之後,應用啓動的時候就會申請 1GB 的地址空間。同樣對於大部分應用來說,直到 abort 或者應用被殺死,都不會使用如此多的地址空間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際上我們需要的是可以動態調整堆的地址空間。要想操作這部分地址空間,自然要研究一下 Android 是怎麼管理這部分地址空間的。上文中提到 Android 實際上存在多種地址空間管理辦法,目前的 Android 8(90%+ 以上用戶) 以上版本"},{"type":"text","marks":[{"type":"strong"}],"text":"都是通過 Region Space"},{"type":"text","text":", 具體細節大家可以自行查閱 Android 源碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼問題就明確成了:如何動態調整 Region Space 的大小?這裏需要了解一些前提,Region Space 之所以一經啓動就會佔用地址空間,顯然是通過 mmap 拿到的,那分配結果自然會保存在某個成員變量上,具體怎麼找到它?依然是閱讀源碼找到答案:"},{"type":"text","marks":[{"type":"strong"}],"text":"region_space_ 保存在 Heap 實例中,而 Heap 實例則保存在 Runtime 中的 heap_,而這些字段都最終編譯在 libart.so 中"},{"type":"text","text":",所以要想操作這些,前提是至少可以手動加載 libart.so。最終需要構成下面的一條依賴關係:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"libart ------> runtime_ ------> heap_ ------> region_space_"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏會遇到第一個問題,namespace 隔離機制,用以保證無法跨命名空間加載其他 so,可以通過閱讀源碼瞭解 Android 中 so 是怎麼加載的。最終會使用到 libdl 的 __loader_dlopen,這個 dlopen 和我們常見的 dlopen 似乎有點區別,它多了一個參數,也就是 caller_addr,這是一個函數指針。再往下看就會發現它是通過拿到調用者之後去查表,找到 namespace,並和要加載的目標 so 的 namespace 進行對比,看看是否是互相可見的 namespace,來確定是否允許加載的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣一來想要破解這個隔離機制就易如反掌了,需要兩樣東西:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"拿到包含 3 個參數的方法:__loader_dlopen 的指針;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"拿到一個和目標 so 相同 namespace 的函數指針。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼拿到這個函數以及虛假的 caller_addr 不在這篇文章的討論範圍內,大家可以自行研究,核心思路是解析 ELF 即可(這裏用到了 iqiyi 的開源項目 xhook:生產級的 PLT Hook 方案)。通過同樣的套路,就可以破解 dlsym 了,也就能夠完成 libart.so 的加載。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二個問題就是如何通過找到各個屬性相對於實例的偏移呢?我們以查找 region_space_ 相對於 heap_ 的偏移舉例:首先需要找到一個 Heap 的實例方法,其中需要包含有對 region_space_ 的操作,比方說 art::gc::Heap::TrimSpaces() 這個函數,因爲這是一個實例方法,所以 r0 就是實例本身 (heap_), 那麼反編譯 libart.so 之後,觀察這個 TrimSpace 方法,可以發現在中間連續三個 if 的最後一個 if 的條件中用到了 region_space_,那麼我們就拿到了 region_space_ 相對於 r0 也就是 heap_ 的偏移:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/1c\/1c2ecc237ab31f73b742f9374fe70a93.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到此爲止,調整 Region Space 所需要的前提都已經具備了,接下來就是通過找到 region_space_ 相對 heap_ 的偏移,一環套一環找到所有需要的條件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可能有同學會問,爲什麼沒有提到怎麼調整 region_space_ 呢?其實也非常簡單,可以通過 RegionSpace 的實例方法:RegionSpace::ClampGrowthLimit(size_t new_capacity) 來對 region_space_ 進行調整,大家可以自行查閱源碼,邏輯比較簡單,因爲 Region Space 管理的內存是線性分配的,只要是還沒用到 region 都是可以釋放掉的。所以前面一系列的準備,就是爲了能調用到這個方法。也就是說,在 >= 9 的 Android 版本中,找到 runtime_、heap_、region_space_ 之後,直接調用 ClampGrowthLimit 就可以調整 Region Space 大小了。在 <= 8 的版本中並沒有這個方法,不過這也難不倒大家,自己參照 Android 9 中的源碼實現一下就好了,沒有太大的難度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/e7\/e7ab5d613a923b8acc94fb2827dfae24.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可能還有同學會問:如果依賴偏移查找會不會需要做大量適配工作?就實際情況來看,國內廠商對 ART 內存管理部分的修改是比較小的,參照 aosp 的版本就能做的差不多了。因爲本來也不需要保證 100% 都能對的上,對不上就對不上好了,自己做好校驗和保護,通過 longjump 等方式做一個 try catch 即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏簡單的提一下如何對前述通過偏移量拿到的各個實例進行校驗:因爲 Region Space 是按 Region 管理內存分配的,所以有以下公式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"num_regions_ * kRegionSize = *limit_ - *begin_;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"剩下的就比較簡單了,上層業務做一個定時輪詢,設定一個閾值,每當虛擬內存達到 n% 的時候,就壓縮一下 Region Space ,設置好步長,保證好下限即可。把以上邏輯封裝起來,輔以相關的檢查和調度,就是 Patrons 的全部內容了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"0x14 Patrons 實戰效果"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Patrons 方案在阿里集團內部推出之後,某航旅類應用首先完成了接入,新版本上線後,Native Crash 率下降了 80%。後續其他頭部應用也陸續完成了接入,某電商類應用 Native Crash 下降了 78.6%。經過一段時間的統計,接入 Patrons 的集團頭部應用,Native Crash 平均下降了接近 50%,大幅度提升了應用的穩定性,讓客戶用起來更加穩定放心。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"0x18 附錄 1:使用方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工程中添加 gradle 依賴:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"# build.gradle"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"implementation “com.alibaba:patrons:1.0.6.3”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"# java"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int code = Patrons.init(null); \/\/ 非 0 則初始化失敗,可以埋點上報便於統計"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"默認情況下只需要在內存不足之前進行 Patrons 的初始化即可,兼容 Android 8、8.1、9、10、11,不兼容版本中不會生效,也沒有副作用。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"0x1c 附錄 2:我的應用是否可以接入?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Patrons 方案已經開源到阿里集團的 GitHub Group 下,應用 "},{"type":"link","attrs":{"href":"https:\/\/github.com\/alibaba\/Patrons\/blob\/main\/LICENSE","title":"xxx","type":null},"content":[{"type":"text","text":"MIT"}]},{"type":"text","text":" 授權協議,遵照該協議即可使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是需要注意的是,該方案僅對大型 32 位 Android 應用有效,因爲中小型應用因爲業務規模有限,一般不會遇到虛擬內存不足的問題。當然這是建立在沒有 or 較少內存泄漏的前提下,由於大量內存泄漏導致的虛擬內存不足不在本文討論範圍內。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者介紹:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"劉志龍,花名正緯,阿里巴巴高級無線開發專家,手機天貓端側交易鏈路負責人。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2015 年畢業於哈爾濱理工大學,同年加入阿里雲,負責手機阿里雲從 0 到 1 建設,以及部分 Emas 移動服務相關業務。2019 年來到手機天貓,負責手貓端側詳情、交易鏈路,以及 Android 基礎架構、端側動態化搭建平臺等相關技術創新、服務於千萬天貓用戶。打造並開源了 Android 組件化通信方案 "},{"type":"link","attrs":{"href":"https:\/\/github.com\/alibaba\/ARouter","title":"xxx","type":null},"content":[{"type":"text","text":"ARouter"}]},{"type":"text","text":"、Android Native 穩定性保障方案 Patrons,在國內 Android 領域有着廣泛的應用。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章