扣丁書屋

大麥 Android 選座場景性能優化全解析

通常情況下移動端APP由于受到設備性能所限一般較少有場景會處理超量數據,更多的是將復雜數據處理交付給服務端。本質上降低終端強數據處理是很有必要的,降低CPU使用率、減少內存抖動可以大幅提升APP使用體驗。但是有時移動端也不得不處理超量數據,大麥選座就是這樣一個強數據處理場景。

那么選座場景具體面對的是怎樣的超量數據呢? 上圖是測試過程中使用的“奧運體育場”在大麥APP選座場景下繪制的UI,這個體育場包含了140+個看臺,6萬+個可售座位。我們需要建立數據模型來描述一個座位,它的基本要素會包含座位id、價格id、坐標、座位角度、座位位置、座位狀態 等等。結合業務場景,在購票流程中的變與不變,我們可以將座位拆分成2套數據:

  1. 座位靜態數據,它包含座位相對不變的部分,比如:座位id、價格id、坐標、座位角度、座位位置。
  2. 座位動態數據,即座位狀態。它是變化最為頻繁的,比如:用戶下單時座位從可售狀態變更為已售狀態,用戶取消時座位會從已售狀態回歸到可售狀態。

假設我們使用XML來描述座位靜態數據,使用JSON來描述座位動態數據,那么上述體育場6萬+個座位對應的座位靜態數據的文件大小為9.6M,座位動態數據的文件大小為1.8M。

想象一個場景:擁有6萬+座位的超大體育場、偶像級歌手、售前幾十萬人關注、實時在線選座分鐘級別售罄,靜態數據的下載、動態數據的刷新、數據的解析與合成,不管對于服務端還是客戶端都會是一場考驗。當然選座場景自然是不會直接傳輸這樣未經過壓縮的數據,但這也從側面說明了選座場景的數據量級,也就是說選座移動端必須要有能力快速、高效處理數以萬計座位的解析、合成、渲染,以保障超級場館實時在線搶票。

下面我總結了一些核心的提升選座場景用戶體驗和支撐超大場館實時在線選座的方案和策略。

接口預加載策略

選座場景相對于其他大麥業務使用到了更多的網絡接口及CDN下載,從SKU頁進入一個選座頁最少要使用5次網絡請求才能最終合并成選座UI。

  • 渲染時:

  • Areainfo 網絡接口 ,選座引導接口主要描述靜態看臺信息、靜態文件cdn信息、開關信息等;

  • StaticSeat CDN下載,靜態座位文件網絡下載;

  • StaticSvg CDN下載,靜態SVG文件網絡下載;

  • DynamicInfo 網絡接口,場次信息、票檔實時信息;

  • Seatstatus 網絡接口,座位實時狀態信息。

  • 刷新時:

  • DynamicInfo 網絡接口,場次信息、票檔實時信息;

  • Seatstatus 網絡接口,座位實時狀態信息。

  • 選座中:

  • Precheck 網絡接口,預鎖座功能;

  • CalcPrice 網絡接口,實時算價功能。

已知渲染需要至少使用到”渲染時“標出的5個請求,正如木桶效應一樣,選座場景的頁面加載時長取決于最后一個接口的返回,而通常接口RT取決于網絡繁忙程度、網絡狀況、服務端處理能力等,客戶端要做的是盡可能將這些網絡請求分類、前置化處理,降低網絡請求并發對CPU瞬時負載。

靜態數據預加載

這里的靜態主要主要是指"Areainfo"引導數據、”靜態座位文件“、”靜態SVG文件“的預加載策略,特點:他們通常在一個項目上線后很少發生變化,存儲于CDN服務器中。靜態數據預加載策略 就是在進入選座場景之前的鏈路如SKU(場次與票檔選擇頁)進行閑時下載。這樣可以前置 ”Areainfo“、”StaticSeat“、”StaticSvg“ 請求。

網絡接口預加載

一般在Android App開發過程中我們會在Activity.onCreate()方法中觸發網絡請求,完成頁面渲染。在測試的過程中我們發現像選座Activity這樣的頁面,從startActivity()方法調用到目標activity.onCreate()之間存在50-60ms的調度、創建時間。所以我們將”DynamicInfo“、”Seatstatus“前置到startActivity(),即啟動選座場景時立即發出以上2個請求。

這樣就可以將需要并發的請求打散、按其特點進行分類、分階段執行。即可利用靜態數據不易變更的特性進行前置下載,也兼顧到動態數據的實時性獲取。

靜態數據緩存

正如上述提及的”奧運體育場“,其包含了數以萬計的座位信息、復雜的SVG文件。解析合成都是較為耗時的操作,如果只是使用一次隨即丟棄肯定是非常浪費資源的。在選座場景搶票過程中,用戶可能會在”售前“、”售中“反復進入”商品詳情“、”SKU“、”選座“頁面,為這類靜態數據做緩存策略是非常有必要的,這可以大幅降低CDN下載壓力,提升SVG、座位結構對象的復用率,降低CPU內存抖動程度。

選座場景提供了BaseLoader、策略性Cache對全場景進行數據預加載、內存緩存管理,通過對軟引用的使用,即保持了靜態數據高效可復用,也避免由于內存緊張而出現強未使用態的強引用靜態數據無法及時釋放的情況。

  1. ImageLoader,支持SVG、JPG加載、緩存
  2. Seat3DVrImageDataLoader,支持VR圖像下載、解密等
  3. Seat3DVrDataLoader,支持VR結構化數據下載、解密等
  4. SeatLoader,支持多種靜態座位數據的下載、解密、緩存等

靜態數據壓縮

靜態座位數據壓縮

靜態座位數據壓縮方案,內部代號“Quantum”是大麥自研的一套針對選座靜態座位數據進行壓縮解壓的一套解決方案,其主要組成部分包含了核心解壓縮算法、加密解密方案、數據完整性校驗等。

觀察靜態座位數據的結構,可以總結出一些特點:包含大量的數值型字段如Long型的座位id、Int型的座位編號、Int型的座位角度、Long型的價格id、Int型的坐標 以及批量重復的看臺號、排號等。數值型字段非常適合使用差值法組合Ziazag整數算法進行壓縮,批量重復性的看臺、排號等信息非常適合字典方式簡化存儲。Zigzag的核心思想就是通過位運算、原碼及補碼的轉換移除數值型內存二進制中“無意義的0”,僅存儲從1開始的“有效數據”,從而達成對數據的壓縮,數值絕對值越小壓縮比率越高。

以下是關于“Quantum”方案與原大麥選座場景靜態數據方案的對比。

壓縮效率:相比于”XML+GZIP",“Quantum+GZIP”壓縮后的靜態文件縮減了70%以上。

解析速度:相比于"XML Pull"解析方案,“Quantum”座位解析時長總體縮減了80%以上。

靜態VR數據壓縮

靜態VR數據指的是一套描述看臺-座位-關聯的VR數據信息的一套數據,大麥選座場景使用了Google Protocol Buffers 來存儲VR結構化數據,以”北京某劇院“ 的VR數據為例 使用JSON文件來描述 VR結構數據的文件大約為 640KB,當使用Protocol Buffers時 文件的大小約為250KB,而經過GZIP壓縮后僅有8KB大小。

壓縮效率:相比于使用JSON描述VR結構化數據,使用Protocol Buffers文件大小縮減了35%以上。實際上Protocol Buffers內部使用了Ziazag整數壓縮算法,VR結構化數據中存在座位id屬于Long型,Protocol 雖然可以進行一定程度的壓縮,但由于其算法對整型絕對值越小壓縮比率越高的特點,我們仍然可以通過差值法進一步縮減Protocol 壓縮比率,但從GZIP壓縮后的結果來看已經足夠小了。暫時未啟動進一步優化。

相關鏈接:Google Developers | Protocol Buffers[1]

座位動態數據壓縮

座位的動態數據,這里指的就是座位的狀態。假設使用 2代表可售、8代表已售,早期大麥選座場景使用JSON來描述”座位id“與”狀態“的映射關系。假定我們使用JSON格式以文本文件來存儲“奧運體育場”座位狀態,6萬+個座位對應的狀態文件的大小為1.8M。

1.8M看起來并不算很大,但對于先前提到的超級火爆的項目來說,開搶瞬間會出現龐大的目標用戶在秒級區間涌入選座場景,服務器要想在極短時間處理如此高并發和數據分發所面臨的壓力是可想而知的,即便傳輸過程中使用了壓縮方案。

因此早期使用JSON數據來表達座位狀態的選座場景不得不從緩解服務端壓力的角度改變請求策略:即“座位狀態分組請求”,進入選座頁按服務端給定的看臺分組依次、延時進行請求,比如:把6萬+個座位 按5000左右一組,每隔50ms請求一批,直到所有座位更新完畢。這意味著一個6萬+座位的場館僅其狀態的請求就可能長達600ms以上,加之其他請求、視圖渲染,這類超大型場館的選座場景很難在1s以內完整加載并渲染完畢。

因此我們必須找到新的方案,它應該具備以下特征:一次請求可以獲得全部座位狀態、請求的數據量要足夠的小、客戶端設備解析的性能要足夠的快。

座位動態壓縮策略

所以選座場景推出了動態壓縮方案,它本質上并不復雜,觀察原始1.8M JSON文件,它的"大"主要是因為大量sid(座位id)的存在,JSON本身包含了一下冗余結構。如果移除這些sid呢? 假定座位如果在“靜態座位文件”中是有序的,那么服務端就可以按照座位順序堆積2、8狀態(2代表可售、8代表已售)。比如一個看臺編號為“3538263” 它有7個座位,座位狀態依次是 “2222228”,那么它就可以由“繁復”的JSON的一部分轉變為 :“3538263”:“2222228”

而一旦某個狀態連續出現6個及以上時就可以縮寫為 (x,s),上述case即可以縮寫為 “3538263”:“(6,2)8”。字符的數量是浮動的,因此我們叫它動態壓縮方案。

對比原JSON方案,我們考慮一個最為復雜的場景“奧運體育場”場館座位,相鄰的座位總是一個可售一個不可售,動態壓縮無法進行縮寫優化,那么使用JSON來返回6萬+座位的文本文件大小約0.9M (即1.8M的二分之一,因為僅需要返回可售的即可)。如果使用動態壓縮方案文本的大小約67KB,我們可以簡化的認為這個文件中存儲了6萬+個連續"2"和"8",額外附加一下看臺id信息。不難看出即便是最復雜的場景動態壓縮方案描述狀態的數據量也是非常小的。

那什么場景會觸發極簡壓縮呢? 想象如下一個選座場景,剛剛開售,所有座位均未被售出,那么動態壓縮方案就會處于極簡壓縮狀態,舉個例子: {"3538263":"5000,2"} 即代表"3538263"看臺下的5000個座位均為可售~ ?以測試使用的“奧運體育場”最復雜場景下估算

壓縮效率:座位狀態動態壓縮文件相比于JSON文本縮減了90%以上數據傳輸量。

解析速度:在該場景下使用一加7Pro:FastJson還原6萬+座位耗時200ms,使用動態解壓縮方案僅耗時5ms,解析時長縮短了95%以上。

視圖層級優化

ViewStub

從上圖"布局文件"中,選座場景是大麥中使用ViewStub最多的地方,一般可以被懶加載的視圖均被轉換成ViewStub,相比于復雜的”暫不可見“視圖 ,ViewStub及其簡潔,減少初始化渲染時View對象的創建和排版。

視圖層級

從"Layout Inspector"可以看到選座場景的布局是相對簡潔的,自上而下?!遍_啟過度繪制“全屏處于淺綠色。為了減少視圖層級,選座場景把App共用基類Activity多出用于Title、錯誤頁等工具性布局都移除掉了,自身提供了非通用,但更簡潔的布局,以減少視圖層級結構。

繪制性能優化

座位位圖復用

圖1 為北京某場館,座位近3000個。我們可以看到一個選座場景一般會包含多個票檔,座位的顏色與其票檔的顏色一致,一個座位最終如何展示是由:color (座位顏色)、angle(座位角度)、type(座位類型:已售、可售、鎖定)、addAlpha(是否已選中)共同決定的,座位的位圖是在繪制時根據上面4個要素實時通過SVG繪制出來的,相同4要素的座位會復用已經創建的同一位圖。

原始方案把座位4要素組成一個字符串作為Key,通過HashMap進行座位位圖復用,偽代碼如下:

private final HashMap<String, Bitmap> mSeatPool = new HashMap();

public Bitmap get(int color, float angel, int type, boolean addAlpha) {
    int angelInt = (int) angel;
    String key = "key_" + color + "_" + angelInt + "_" + addAlpha + "_" + type;
    Bitmap seat = mSeatPool.get(key);
    if (seat == null) {
        seat = newBitmap(color, angel, type, addAlpha);
        mSeatPool.put(key, seat);
    }
    return seat;
}

位圖復用的思路是正確的,但是原方案使用的字符串作為Key帶來2個問題,1是字符串拼接的本質是StringBuilder對象的創建;2是字符串拼接的字符數量>16個會觸發StringBuilder的ensureCapacityInternal方法;以上述場館為例每次touch事件導致的繪制都會創建近3000個StringBuilder對象、并觸發其擴充API。反復滑動時必然會導致內存快速增長、GC的頻繁發生。

我首先想到類似的場景就是Android 視圖測量中使用到的View.MeasureSpec,MeasureSpec將int值高2位存儲為Mode,低30位存儲為size。利用相同的思路作用于位圖復用是非常適用的,優化后選座使用Long值作為Key存儲座位4要素,高8位存儲位圖addAlpha(是否選中),9-16位存儲type(座位類型:已售、可售、鎖定),17-32位存儲angle(座位角度),最后32位存儲color(座位顏色),并且使用Android系統優化過的LongSparseArray替代HashMap。這有幾個好處,Key值的計算全程使用位運算,運算速度足夠的快,反復滾動時不會再觸發StringBuilder對象的創建和擴充,內存不會出現激增現象,視圖滑動平均幀率提升了4幀以上。

利用Android Studio Profiler性能分析工具,對位圖復用前后進行對比:

  1. 優化前的方案在選座視圖滑動時存在了大量的StringBuilder對象創建,導致Profiler Method視圖存在大量鋸齒。優化后不會在出現大量鋸齒,onDraw()中不會在出現對象創建場景。
  2. 從內存的角度上看,優化前滑動時會導致內存快速增長,從圖中可以看到很快即有270M增長到328M,并觸發一輪GC,而這樣的事情會在反復滑動、縮放動畫中一直出現,這種內存的抖動勢必會影響到繪制性能,雖然GC幾經Google優化,但仍然避免不了”stop the world“。優化后我們可以看到不管如何滑動內存始終保持在270M左右。

繪制提效優化

如果只是一張座位的位圖,其繪制成本是非常低的。但是當量級足夠大時,繪制的性能將受到影響,以圖1北京某場館為例,一次onDraw()就需要繪制近3000次座位位圖,這對于客戶端設備是有一定挑戰的。因此選座場景針對大場館,初始化展示時僅展示看臺圖,當用戶選中某個看臺時會自動放大適配屏幕,將處于屏幕中的看臺下的座位及其周邊可見的座位實時繪制出來。針對小場館,僅出現在屏幕中的座位會被繪制。這種小技巧對提升繪制效率非常有效。

角度計算優化

圖3所示,可以看到座位可能存在角度,其一般指向舞臺中心。端設備在實時繪制座位時,需要實時計算放大縮小比率結合座位偏轉角度才能最終計算出座位的大小。

早期選座場景使用Matrix來計算角度問題,平時的繪制我們也可能使用Matrix進行相關計算,但無疑在這個地方Matrix矩陣計算是浪費性能的。相反優化后使用Math三角函數進行計算大幅度提升了帶角度的座位大小計算性能。

浮點數據計算優化

當座位大小實時計算完畢,我們發現在使用Canvas進行繪制時,如果將float轉換成int時,會帶來非常好的性能提升。但這也會帶來一定的體驗問題,為什么呢?如果不做任何處理即將浮點轉成整型,實時滑動時就可能會出現座位由于精度問題而產生抖動。因此選座場景在這里做了近似處理,在合理閾值內進行int轉換,即利用整型值計算優勢同時將抖動降低到不明顯可接受程度。

硬件加速

早期的選座場景并未開啟硬件加速,你可能會驚訝于視圖為什么沒有開啟硬件加速,因為絕大多數場景我們并沒有主動關心過硬件加速,因為系統默認的View和一般的自定義View默認就是開啟的。為什么早期的選座場景要主動關閉硬件加速呢?

從選座場景上看我們使用了大量的SVG,無論是場館底圖、場館圖、座位位圖都使用了SVG進行繪制,我們可以簡單的把一個SVG看成一個有序的、可繪制的指令集合,在Android中我們需要一個簡單的可被復用的繪制“容器”,而這個“容器”就是android.graphics.Picture。查看Picture的類描述我們會發現它真的很適合用來記錄可繪制指令,但它仍然存在一些問題,以下是類官方描述:

A Picture records drawing calls (via the canvas returned by beginRecording) and can then play them back into Canvas (via draw(Canvas) or Canvas.drawPicture(Picture)).For most content (e.g. text, lines, rectangles), drawing a sequence from a picture can be faster than the equivalent API calls, since the picture performs its playback without incurring any method-call overhead.

Note: Prior to API level 23 a picture cannot be replayed on a hardware accelerated canvas.

從類描述中的“Note”可以看到在API Level 23以下的設備無法使用硬件加速,查閱Google開發者文檔也可以看到,下方圖片是Google對可支持硬件加速繪制指令、以及第一個支持的Api Level進行了一段描述??梢园l現在API 28是一個分水嶺,絕大多數指令在28及以上得到了支持。

硬件加速方案:

優化方案對API Level進行區分即可,28以下的設備仍然保持早期的使用軟件層進行繪制的方案,對始終不支持硬件加速的繪制指令進行了評估確保我們的生產SVG工具中不會出現這些指令,并添加了遠程開關以確保遇到問題可實時關閉硬件加速功能,當設備API LEVEL >=28時開啟硬件加速。此后選座Android場景的滑動平均幀率終于在這個階段突破了50FPS。

相關鏈接:Android開發者 | 硬件加速[2]

線程任務處理

在接口的預加載策略中有提到選座場景對選座”渲染時“使用到的請求根據其請求的數據特點、時效要求進行了分階段、策略性前置。使得渲染時不會在選座頁面的onCreate()同時并發5個以上的網絡請求。從性能分析工具Perfetto我們可以看到主線程、靜態座位數據下載解析線程、靜態SVG下載解析線程等的時間軸即任務處理情況。

  1. 從Perfetto中我可以觀察到分散的策略是生效的,靜態座位、SVG在SKU頁閑時觸發了下載并完成了解析、內存緩存。
  2. 從Perfetto中關鍵的渲染任務相關線程無明顯的阻塞現象發生。
  3. 在進入選座頁的同時DynamicInfo 請求、SeatStatus 請求很快被發起。
  4. 座位動態解壓縮方案在座位合成過程中非??焖俚耐瓿闪俗粻顟B合成。

相關工具鏈接:Perfetto工具[3]

總結

下圖是21年選座場景在性能監控工具上頁面加載時長、滑動平均幀率??梢钥吹巾撁婕虞d時長呈現明顯的下降趨勢,而滑動平均幀率呈現明顯的上升趨勢。實際上大麥選座場景的頁面加載速度超過了APP自身 80%以上的核心頁面,而選座場景同時也是這些核心頁面中網絡使用、數據處理最為復雜的頁面之一。

截止2022年6月份 ,頁面平均加載時長由20年的850ms下降到當前的320ms,頁面滑動平均幀率由20年的38幀上升到54幀。選座場景的優化并不是一蹴而就的,而是經歷了長期的、持續的性能優化~

以此篇文章,整理記錄一下選座場景在持續提升用戶體驗過程中做過的一些努力 ~

參考資料

[1]Google Developers | Protocol Buffers: https://developers.google.com/protocol-buffers/

[2]Android開發者 | 硬件加速: https://developer.android.google.cn/guide/topics/graphics/hardware-accel

[3]Perfetto工具: https://ui.perfetto.dev/#


https://mp.weixin.qq.com/s/9Kq2YJqYbH_6cBkMRwW4mQ

最多閱讀

簡化Android的UI開發 3年以前  |  515859次閱讀
Android 深色模式適配原理分析 2年以前  |  27474次閱讀
Android 樣式系統 | 主題背景覆蓋 2年以前  |  8727次閱讀
Android Studio 生成so文件 及調用 2年以前  |  6741次閱讀
30分鐘搭建一個android的私有Maven倉庫 4年以前  |  5508次閱讀
Android設計與開發工作流 3年以前  |  5225次閱讀
移動端常見崩潰指標 2年以前  |  5081次閱讀
Android陰影實現的幾種方案 5月以前  |  5057次閱讀
Google Enjarify:可代替dex2jar的dex反編譯 4年以前  |  5018次閱讀
Android內存異常機制(用戶空間)_NE 2年以前  |  4766次閱讀
Android-模塊化-面向接口編程 2年以前  |  4667次閱讀
Android多渠道打包工具:apptools 4年以前  |  4570次閱讀
Google Java編程風格規范(中文版) 4年以前  |  4423次閱讀
Android死鎖初探 2年以前  |  4391次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>