開發與維運

播放內核“輕量化”實戰

作者| 阿里文娛無線開發專家 城泉

一、概述

優酷播放內核是優酷自主開發的一個基於 pipeline 結構的 SDK。它對上承接了優酷豐富靈 活的業務邏輯,對下屏蔽了各端系統的差異,是一個高可靠、可擴展、跨平臺的優秀播放 SDK。
但是,跨團隊協作及長時間的迭代,也使得當前播放內核顯得有些“臃腫”。佔用內存過高、 使用線程太多等這些問題除了會影響用戶的體驗之外,也在一定程度上制約了一些業務的實現, 例如針對短視頻的多實例方案。所以,急需對內核各模塊進行一次“輕量化”的改造。目標是:
1)更少的線程
2)更小的內存
3)更低的功耗

二、改造前的摸底

優酷播放內核實現了一套基於 pipelie 的框架,結構如下:

image.png

包含了接口層,處理命令和消息上報的 engine,透傳消息的 filter 層,主體幹活的 module層,數據下載模塊以及渲染和後處理模塊。
經過梳理跟測試,確認我們的播放內核使用的線程會比一些開源的播放內核(比如 ijkplayer) 多很多,內存使用量以及視頻耗電量等數據相比競品也處於劣勢。所以我們亟需對我們的播放 內核進行一輪改造。

三、改造的詳細過程

我們改造的方向包含:線程、內存、功耗這三個方面。希望用最少的線程實現整個播放流 程,用最小的內存使得播放依然流暢,佔用最少的 cpu 資源使得播放更持久。
採用的策略是做“加法”。根據播放流程,保留必要的線程,去除冗餘的線程,重用可複用 的線程。然後 review 每一個保留下來的線程,測試使用內存及 cpu 佔用率是否符合預期,如果 異常再進行逐一排查。

1.線程精簡

優化前內核使用的線程數有近 30 個,相比其他開源播放器多了很多。其中有些是必不可少, 有些是可被其他線程複用,還有些是邏輯冗餘,可以直接去除。在梳理要留下哪些線程的時候, 我們考慮了一個播放過程所需要的線程“最小集”,應該會包括如下一些線程模塊:
 engine:用於接收接口命令,以及上報內核消息;
 source:用於數據讀取並驅動 pipeline 數據向後流動;
 decoder:音視頻各一個,用於音視頻數據解碼;
 consumer:音視頻各一個,用於同步及渲染;
 hal buffer:用於解複用及緩存狀態監控;
 ykstream:用於控制下載模塊並和切片解析模塊交互;
 render:用於渲染管理。
可以看到,播放流程必須用的線程其實就 9 個。而其他的線程除了預加載管理、播放質量 監控以及字幕相關等在需要的時候會被啟用之外,其餘都可以去除。
精簡步驟如下:
1)去除多餘的 filter 線程
filter 只有在創建 module 的時候用到,後面都是消息透傳,顯得有些多餘,所以可以直接 去除。將創建module 的邏輯移到 engine 的 prepare 流程,打通 engine 與 module 之間的消息通道,上面下達的命令以及下面上報的消息不再經過 filter。
2)去除消息傳遞器和時鐘管理器
優化前消息上報通道比較混亂,有些直接上報給engine,有些上報給消息傳遞器進行一次中轉,然後再上報給 engine。消息傳遞器這層邏輯有些多餘,所以去除了這個線程,所有消息上報都通過 engine。
時鐘管理器作為同步時間來用,這個不需要線程,線程的存在是用作一個定時器。目前內 核使用到定時器的就一兩個點,通過其他線程邏輯複用,去除了對定時器的依賴,這個線程也可以去除。
3)去除接口命令線程和消息上報線程 接口層加了一個線程中轉一個下發的命令,目的是為了接口超時的時候內核有 forcestop 的機制。在經過多輪優化後,內核觸發 forcestop 的情況大大減少,所以這個線程顯得有些多餘,就算還會出現卡住的情況,也會有 anr 來替代原先的 crash,這個線程可以去除。 消息上報線程是為了內核層多實例上報消息加上的,實際上經過代碼複用,這個線程也不是必須的,可以去除。
4)去除解複用線程和二級緩存線程
內核獲取數據一直是邏輯最臃腫的地方,優化前有 5 個線程來實現這部分功能。優化後保 留 3 個即可,解複用線程和二級緩存線程可以去除。
5)去除預加載管理器和字幕解碼模塊 預加載管理器不管有沒有開啟預加載都會運行,需要加上開關控制,只有在預加載開啟情況下才會運行。
字幕的實現主要是數據讀取、解析和 render,其中不同於音視頻,文本信息在讀取後就可 以直接去解析,所以字幕解碼模塊可以去除。
優化後,線程有 9 個必須的,加上播放質量監控,總共保留 12 個線程。沒有字幕的視頻只 剩下 10 個。

2.內存裁剪

消耗內存地方主要有四處:緩存下載數據的 buffer、pipe 管線中的 buffer、存 msg 信息的結 構體、以及各 class 對象的內存。class 對象除非不用,否則沒有太多裁剪的空間,所以內存裁 剪就從緩存、pipe 管線及信息存儲結構體三個角度去實行。
1)排查內存使用不符合預期的地方
掃描線程內存數據發現,讀 buffer 的線程內存消耗高出設置值很多。分析每個 es sample 的 數據,發現除了數據部分之外,還存了一個 codec 的 context,每個packet 都要存一個。各 packet 的 codec context 都應該是一樣的,只需存一份即可。內核針對這部分不合理的邏輯進行了修復,內存使用降低了近 1/3。
2)減少緩存 buffer
緩存 buffer 相比競品設置的有些大,考慮到下載模塊也有一塊不小的 buffer,所以內核的 buffer 可以裁剪,平衡卡頓數據,可將 buffer 設置在較低的水位。
3)減少 pipe 管線內存使用
pipe 管線內存加上內核二級緩存使用量達到 3.5M,source 重構後去除了二級緩存,加上對pipe buffer pool 的優化,這部分內存可減小到 0.5M。
4)優化部分數據結構
比如存放信息的 AMessage 結構,每一個 AMessage 會消耗 4k bytes。針對 hls 智能檔的場 景,每一條記錄都會創建一個 AMessage,所以的記錄加起來會超過6MB,這還不包括其他使 用 AMessage 的地方。所以我們重寫一個功能類似的結構體進行替換,接口上與 AMessage 保持 一致,減少了內部不必要的內存開消。
優化後,播放內核峰值內存已經降到原來的 1/3,大大減少了單個實例使用的內存數。

3.功耗優化

功耗的主要影響因素有:cpu 佔用率、網絡請求時長、屏幕及 audio 等設備的耗電。屏幕亮 度音量等這些因素是固定的,所以降低功耗主要從 cpu 佔用率和網絡請求時長這兩個方面去考慮。
1)減少不必要的流程,裁剪多餘線程
這部分在線程裁剪中已經完成,這裡不再詳述。
2)控制網絡請求時長,避免過長的網絡連接
移動設備在請求網絡的時候,網絡設備 wifi/4G 會及時通電,這部分耗電很大。所以大塊 的讀取一段數據然後 wait 要好過頻繁小段的請求數據。考慮卡頓等其他因素,內核默認設置在緩存消耗到低於 2/3 之後才重新啟動下載。
3)替換數據存儲結構,去除冗餘存取邏輯
排查發現,每次數據寫入 buffer,cpu 都會異常的繁忙,這與預期不符。review 代碼找到異 常點:我們存儲數據用的是 vector 數據結構,每次來數據都是 push 到 front,當 vector 的 size 達到數萬的量級之後,這個 push_front 的操作會非常的消耗 cpu。修改的辦法是將 vector 改成 list, 數據寫入到 tail,從 header 讀取,該問題不再復現。
4)omx 同步調用改成異步,減少解碼 cpu 耗時
android 平臺上,硬解 omx 模塊默認用的是同步調用模式。android9.0 以下 native 層只提供 了這種模式,會循環的進行 queue/dequeue 操作,cpu 消耗較大。android9.0 及以上,native 層提供了 omx 的異步調用模式,會只在 queue/dequeue 完畢之後 callback 調用解碼模塊幹活,所以 cpu 消耗比同步要小。如下圖所示,異步比同步要明顯稀疏一些。

image.png

5)減少倍速算法冗餘計算
review 發現 audio consumer 線程 cpu 消耗比 audio decoder 多很多,不符合預期,檢查發現 當沒有開啟倍速情況下,也會走倍速相關的運算邏輯,導致 cpu 異常消耗,修復前後對比如下 圖:

image.png

6)內核層實現彈幕邏輯
彈幕的實現原先是應用層通過 view 來實現,在彈幕數據多的情況下,非常影響功耗,甚至 會出現彈幕模糊的情況。所以考慮將彈幕的實現移到內核層,由內核接收彈幕數據實現 render。 經過驗證,優化後彈幕的功耗降低了 2/3.
優化後,播放運行時平均 cpu 佔用率已經低於 7%(android 中端機測試),1080p/90 分鐘的 視頻耗電量降到 12%,相比優化前有了 30%的提升。

四、小結

至此,播放內核相比優化前已經大大的“瘦身”了。瘦身後內核的代碼邏輯變得更加的清晰, 數據傳遞也更加簡潔高效,這讓參與內核開發的同學可以更多的關注到自己的業務本身。內存 使用量大幅降低,只從內存的角度講,優化前兩個實例的內核,現在可以創建 6 個,極大的拓 寬了上層業務邏輯的邊界。功耗也變得更低,大大提升了用戶的播放體驗。
需要注意的是:我們的業務複雜多變,參與開發的團隊也有很多,版本迭代一段時間之後, 難免會讓內核變得越來越臃腫。所以我們需要對每個正式的版本進行內存、功耗等多個緯度的 監測,發現問題立即修改,這樣便不會將這些問題積累下去。內核也要定期進行小規模的重構, 去除不合理的代碼,統一通用的邏輯處理單元,這樣才能讓高質量的內核持續保持下去。

Leave a Reply

Your email address will not be published. Required fields are marked *