微信團(tuán)隊(duì)分享:詳解iOS版微信視頻號(hào)直播中因幀率異常導(dǎo)致的功耗問題

本文由微信客戶端團(tuán)隊(duì)rhythm分享,原題“視頻號(hào)直播:如何進(jìn)一步降低功耗占用?”,本文有修訂和改動(dòng)。
1、引言
功耗優(yōu)化一直是 app 性能優(yōu)化中讓人頭疼的問題,尤其是在直播這種用戶觀看時(shí)長特別久的場(chǎng)景。怎樣能在不影響主體驗(yàn)的前提下,進(jìn)一步優(yōu)化微信iOS端視頻號(hào)直播的功耗占用,本文給出了一個(gè)不太一樣的答案。

技術(shù)交流:
- 移動(dòng)端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)
(本文已同步發(fā)布于:http://www.52im.net/thread-4507-1-1.html)
2、問題背景
問題的起因是我們測(cè)試統(tǒng)計(jì)發(fā)現(xiàn)帶有點(diǎn)贊的直播會(huì)比無點(diǎn)贊動(dòng)畫的直播 GPU 占用要高將近一倍,同時(shí) FPS 差異也很大。
高刷屏下,PerfDog 測(cè)試顯示,有點(diǎn)贊情況下的大部分視頻號(hào)直播居然是以60fps在跑,這導(dǎo)致了極高的GPU占用。
但我們根本沒有60fps 這么高的直播流,且絕大部分直播流都只有30fps 而已,少部分也就最高60fps,怎么到了設(shè)備上就達(dá)到了60fps?
而且這還是我們開啟了強(qiáng)制低幀率UIViewAnimationOptionPreferredFramesPerSecond30后的效果,沒開之前直接奔120fps 去了。
如下圖所示 PerfDog 數(shù)據(jù)顯示在 13 pro max上直播點(diǎn)贊期間 FPS 直奔120:

正常情況下,視頻號(hào)直播里大部分主播開播流基本都是30fps 以內(nèi),也就是正常情況下我們只需要維持30fps 渲染,就能保持好流程的用戶體驗(yàn)。
那為什么這里降幀后依舊會(huì)出現(xiàn)60fps 呢?
經(jīng)過一系列排查我們發(fā)現(xiàn)這是由于直播的點(diǎn)贊動(dòng)畫導(dǎo)致的高幀率,如果去掉動(dòng)畫后 FPS 就會(huì)回到正常情況下了,且 GPU 占用也有了明顯下降。
這到底是怎么回事?我們是否可以降動(dòng)畫的幀率降低到某個(gè)值來去優(yōu)化我們整體的 GPU 占用呢?
3、知識(shí)儲(chǔ)備1:iOS中的動(dòng)畫分類
在iOS中,大部分動(dòng)畫的本質(zhì)就是根據(jù)輸入的時(shí)間戳,返回對(duì)應(yīng)屬性的動(dòng)畫參數(shù),從而移動(dòng)圖像,達(dá)到運(yùn)動(dòng)的效果。根據(jù)動(dòng)畫 api 實(shí)現(xiàn)方式的特點(diǎn)我們可以把動(dòng)畫 api 劃分為如下幾類。
3.1UIView block animation
基于 「+[UIView animateWithDuration:delay : options:animations:completion:]」 動(dòng)畫api驅(qū)動(dòng)的動(dòng)畫,特點(diǎn)是所有動(dòng)畫都在 animations block 里同步觸發(fā),可以方便的設(shè)置任何屬性動(dòng)畫。
[UIView animateWithDuration:duration
?????????????????????????????delay:0
???????????????????????????options:option
????????????????????????animations:^{
????????????????????????????view.top -= offsetY;
????????????????????????????view.left -= offsetX;
????????????????????????}
????????????????????????completion:completion];
3.2CAAnimation
基于 「CAAnimation api」 直接觸發(fā)提交的動(dòng)畫,例如:
CABasicAnimation *ani_position = [CABasicAnimation animationWithKeyPath:@"position"];
ani_position.fromValue = @(val.position.from);
ani_position.toValue = @(val.position.to);
[view.layer addAnimation:group forKey:key];
3.3Timer
基于 「NSTimer/GCD」 觸發(fā)的動(dòng)畫,例如定時(shí)去修改某個(gè)?imageView.image,使得它能定期變換的效果。基于 「CADisplayLink」 觸發(fā)的動(dòng)畫,和基于 NSTimer 觸發(fā)類似,只不過這個(gè) timer 源是和渲染保持一致的,能夠做到更流暢更貼合。
比如我們要實(shí)現(xiàn)自定義的 UIScrollView 動(dòng)畫,就可以基于 CADisplayLink 來做。
3.4UIViewPropertyAnimator
「UIViewPropertyAnimator」是iOS10開始蘋果推動(dòng)的新的動(dòng)畫api,相比 UIView block animation 可以更靈活的控制動(dòng)畫的過程。
[UIViewPropertyAnimator runningPropertyAnimatorWithDuration:duration
?????????????????????????????????????delay:0
???????????????????????????????????options:option
????????????????????????????????animations:^{
????????????????????????????????????view.top -= offsetY;
????????????????????????????????????view.left -= offsetX;
????????????????????????????????}
????????????????????????????????completion:completion];
4、知識(shí)儲(chǔ)備2:iOS中的動(dòng)畫渲染
iOS中的動(dòng)畫或者 UIView 的修改到底是怎么被渲染到屏幕上去的?
4.1Core Animation Pipeline
iOS 的 UI 更新和動(dòng)畫操作都離不開 Core Animation 和 UIKit,他們的底層都是 QuartzCore,所有的 UI 刷新和動(dòng)畫提交都會(huì)打包成對(duì)應(yīng)的?CA::Transaction?和?CAAnimation?對(duì)象并提交給 Render Server 去處理。
App 本身并不負(fù)責(zé)渲染,渲染是由獨(dú)立的進(jìn)程 Render Server 來負(fù)責(zé)的,Render Server 最終調(diào)用?-[AGXG14FamilyRenderContext drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:]?等 GPU 接口來完成 GPU 任務(wù)的提交,最終觸發(fā)屏幕更新操作。
整體過程大概如下:
1)App 處理事件,例如 touch 事件或者 displaylink timer 事件;
2)App 完成視圖的 layout、圖像 decode 等操作,并觸發(fā) CA::Transaction 提交;
3)Render Server 接收 App 提交的 Transction 和圖片數(shù)據(jù),Render Server 可直接跨進(jìn)程訪問 App 進(jìn)程的位圖內(nèi)存資源,并最終觸發(fā) GPU 調(diào)用;
4)GPU 最終完成了圖像的渲染并顯示到屏幕 Display。

4.2Render Server
如下圖所示,最終的上屏任務(wù)提交操作是由 Render Server 也即 backboardd 進(jìn)程來最終觸發(fā)的。

在 iOS 中 Render Server 通常指的是 backboardd 進(jìn)程,backboardd 進(jìn)程是一個(gè)與 SpringBoard 守護(hù)進(jìn)程一起運(yùn)行的守護(hù)進(jìn)程。
它在 iOS 6 中引入,旨在減輕 Springboard 的一些職責(zé),主要是事件處理的職責(zé)。它主要負(fù)責(zé)把 touch 事件分發(fā)到 app 進(jìn)程以及處理 app 進(jìn)程觸發(fā)的動(dòng)畫和UI更新操作。
如上圖所示,time profiler 里我們能清晰看到 backboardd 進(jìn)程在處理來自 app 進(jìn)程的圖像提交操作。
微信直播之前就遇到過好幾次 Render Server 命中了 gpu io fence 導(dǎo)致系統(tǒng)全局卡死的問題。
如下圖 ips 文件日志所示:

4.3Render Loop
Render Loop 是包括了從 app 到 Render Server 再最終到屏幕的一系列任務(wù)觸發(fā),刷新,更新與提交,直到上屏的一系列過程,是對(duì)渲染管道的進(jìn)一步封裝,類似于一套 runloop 循環(huán)機(jī)制,能隨時(shí)的處理輸入和輸出。

4.4動(dòng)畫渲染
當(dāng)我們調(diào)用-[UIView animateWithDuration:animations:]?api觸發(fā)動(dòng)畫后,整體動(dòng)畫渲染過程如下圖3步所示。

4.5幀率
幀率即 FPS(frames per second),每秒渲染了多少幀,正常情況下只要我們定期提交一次 opengl 上屏?[curContext presentRenderbuffer:GL_RENDERBUFFER]?就會(huì)觸發(fā)一幀的上屏操作,這就回導(dǎo)致 FPS 發(fā)生變化,也最終影響了 app 的性能占用。
FPS 越高對(duì)于游戲等高清視頻效果就更細(xì)膩更好,但是并不是所有情況都需要高 FPS,部分情況下高 FPS 反而導(dǎo)致了無用的功耗但并沒有帶來更好的體驗(yàn)。
4.6屏幕刷新率
對(duì)于 iOS15/iPhone 13以前的設(shè)備,屏幕是固定的刷新率,在這之后 iPhone 13和 iPad Pro 后引入了高刷屏,并且支持了動(dòng)態(tài)刷新率。
對(duì)于直播場(chǎng)景 FPS 有3個(gè):
1)視頻流 FPS;
2)Render Server FPS;
3)屏幕 FPS。
對(duì)于非可變刷新率的屏幕,我們可以盡可能減少 GPU 的幀率(即 Render Server 提交的 FPS)來達(dá)到降低 GPU 功耗的目的,對(duì)于可變刷新率的屏幕,那只要減少了 GPU 幀率就自然而然也減少了屏幕的刷新率,使得屏幕和 GPU 功耗都下降了。
在我們遇到的問題中,我們的視頻流 FPS 是25,那么我們預(yù)期的最終 GPU FPS 和屏幕 FPS 理應(yīng)同理也是接近25才是,而這里卻達(dá)到了60fps,說明了有重復(fù)的內(nèi)容幀一直被 Render Server 重復(fù)的復(fù)制并提交給 GPU,導(dǎo)致了畫質(zhì)細(xì)節(jié)沒有增加,但頻繁的拷貝渲染造成了更高的 GPU 占用。這就是我們的問題所在。
5、知識(shí)儲(chǔ)備3:iOS中的動(dòng)畫降幀
5.1概述
結(jié)合上文,我們要解決直播幀率異常升高的問題,就需要解決點(diǎn)贊動(dòng)畫的高幀率問題。
很幸運(yùn),蘋果在 iOS15提供了一個(gè) CAAnimation 的 api,即-[CAAnimation preferredFrameRateRange],它接受3個(gè)參數(shù)分別指定minimum 幀率,maximum 幀率,以及 preferred 幀率,基于這個(gè)api我們可以對(duì)于 CAAnimation 動(dòng)畫設(shè)置幀率。
蘋果的建議是把動(dòng)畫分為了如下幾檔:

5.2CAAnimation 降幀原理
iOS15開始蘋果引入了 CAFrameRateRange 相關(guān) api 來供 app 去設(shè)置 CADisplayLink 和 CAAnimation 的?preferredFrameRateRange,以方便調(diào)節(jié)幀率,達(dá)到在高刷機(jī)上能進(jìn)一步降低功耗的目的。
那它又是如何工作的呢?
首先需要明確 iOS15后 CAAnimation 和 CADisplayLink 的幀率控制底層都是一致的,也就是都是?CA:: Display: : DisplayLinkItem?來驅(qū)動(dòng)觸發(fā)的。而動(dòng)畫的本質(zhì)就是根據(jù)時(shí)間的輸入來得到對(duì)應(yīng)的動(dòng)畫 fraction 并觸發(fā)對(duì)應(yīng)進(jìn)度的動(dòng)畫修改,再提交上屏完成修改。
具體而言,我們以 UIScrollView的?setContentOffset:animated?動(dòng)畫為例。
5.3setContentOffset:animated 動(dòng)畫機(jī)制
當(dāng)我們觸發(fā)[scrollView setContentOffset:CGPointMake(120,0) animated:YES]后,會(huì)觸發(fā)創(chuàng)建一個(gè) UIScrollViewAnimation 的實(shí)例對(duì)象(UIAnimation的子類),接下來會(huì)調(diào)用?UIUpdateSequenceInsertItem?將這個(gè)動(dòng)畫實(shí)例注冊(cè)到當(dāng)前的 UIUpdateCycle 循環(huán)中。

UIUpdateCycle 負(fù)責(zé)根據(jù)設(shè)備的 CADisplay 屏幕刷新率和設(shè)置動(dòng)態(tài)效果里設(shè)置的是否限制幀速率來抉擇出到底是以120hz還是60hz來驅(qū)動(dòng) UIUpdateCycle 循環(huán)的觸發(fā),當(dāng)以120hz觸發(fā)動(dòng)畫循環(huán)時(shí),接著會(huì)在每8ms間觸發(fā)一次_UIUpdateSequenceRun,來執(zhí)行 UIScrollViewAnimation 的動(dòng)畫 progress 計(jì)算操作。
如圖:

每次觸發(fā)觸發(fā)?_UIUpdateSequenceRun?時(shí),會(huì)向 UIScrollViewAnimation 請(qǐng)求[UIAnimation fractionForTime:]來返回對(duì)應(yīng)時(shí)間戳的 contentOffset 和 progress,然后觸發(fā)修改 contentOffset,最終接近目標(biāo) contentOffset 后就完成了完整的動(dòng)畫。
5.4CAFrameRateRange
當(dāng)我們?cè)O(shè)置 CAAnimation 的?preferredFrameRateRange?后,QuartzCore 會(huì)將 CAFrameRateRange 轉(zhuǎn)為CAFrameIntervalRange 結(jié)構(gòu),并最終嘗試觸發(fā) Render Server 在指定幀間隔內(nèi)渲染每一幀動(dòng)畫。
如下圖:

5.5幀率變化探索
所有的幀提交操作最終都是在 Render Server 觸發(fā)的,也就是只有從 Render Server 統(tǒng)計(jì)FPS才是最終的實(shí)際 FPS,那我們要怎么統(tǒng)計(jì)呢?
QuartzCore 提供了一個(gè)系統(tǒng)級(jí)的面板工具,它可以很方便的顯示當(dāng)前的 QuartzCore 渲染信息,包括fps,frame duration等一應(yīng)俱全。

我們可以在越獄后給 app 自簽名?com.apple.QuartzCore.debug?這個(gè) entitlement 后,再調(diào)用如下代碼所示的私有 api 即可全局打開這個(gè)面板,可以方便的在手機(jī)端查看 Render Server 上的實(shí)際 FPS。
extern"C"{
intCARenderServerGetDebugOption(mach_port_t port, intkey);
intCARenderServerGetDebugValue(mach_port_t port, intkey);
voidCARenderServerSetDebugOption(mach_port_t port, intkey, intvalue);
voidCARenderServerSetDebugValue(mach_port_t port, intkey, intvalue);
}
由于以上能力無法在非越獄設(shè)備上開啟,所以實(shí)際上我們無法檢測(cè) app 在任意時(shí)刻的 FPS 變化情況。
不過經(jīng)過分析,我們發(fā)現(xiàn)只要觸發(fā)了以下行為就可能代表要幀率要變化了。如下。
1)在設(shè)置->動(dòng)態(tài)效果里開啟或關(guān)閉“限制幀速率”:修改限制幀速率會(huì)觸發(fā)系統(tǒng)拋出 com.apple.CoreAnimation.CAWindowServer.DisplayChanged 的通知,QuartCore 會(huì)在啟動(dòng)時(shí)注冊(cè)這個(gè)通知,并收到通知后通過 mach port 通信獲取當(dāng)前注冊(cè)的幀速率值,以動(dòng)態(tài)修改 displaylink 的回調(diào)頻次。
2)直接通過 opengl/metal api 提交一幀畫面給 Render Server。
3)觸發(fā) CA::Transaction 對(duì)象的提交:除了觸發(fā)動(dòng)畫提交,觸發(fā) view property 提交變更外,甚至創(chuàng)建 view 也會(huì)導(dǎo)致 source0觸發(fā)一次,如下圖所示。

通過調(diào)試分析,我們大概清楚了 iOS15引入的 CAAnimation 的?preferredFrameRateRange?工作機(jī)制,如下圖所示。

我們只要修改 UIUpdateSequenceRun 的回調(diào)頻率,也就是?-[UIAnimator _advanceAnimationsOnScreenWithIdentifier:withTimestamp:]?的回調(diào)頻次我們就能控制部分系統(tǒng)動(dòng)畫的幀率,強(qiáng)制調(diào)節(jié)他的執(zhí)行頻次,或者我們通過模擬系統(tǒng)設(shè)置->動(dòng)態(tài)效果->限制幀速率的實(shí)現(xiàn)方式,主動(dòng)調(diào)用?-[CADisplay overrideMinimumFrameDuration:]?傳入4便可將 UIAnimator 的刷新率調(diào)節(jié)為?240/4=60hz,或者傳入8即可將系統(tǒng)動(dòng)畫刷新率調(diào)節(jié)為?240/8=30hz。
基于以上研究,理論上我們可以嘗試調(diào)用私有 api 來全局控制 CADisplay 的刷新率,來進(jìn)一步降低性能占用,但是由于 Render Server 是在其他進(jìn)程,我們還是無法控制 Render Server 的刷新率,并且私有 api 會(huì)導(dǎo)致 app 被拒審,所以我們最終依舊只能改造部分系統(tǒng)動(dòng)畫實(shí)現(xiàn)以繼續(xù)基于 CAAnimation api 去優(yōu)化幀率。
6、我們的優(yōu)化方案
6.1概述
從 iOS15開始蘋果新增加了 preferredFrameRateRange api 可用于設(shè)置相應(yīng)動(dòng)畫或timer的刷新頻率,我們就可以基于該方案去改造相應(yīng)動(dòng)畫即可。
@propertyCAFrameRateRange preferredFrameRateRange
????API_AVAILABLE(macos(12.0), ios(15.0), watchos(8.0), tvos(15.0));
但新的問題又來了,系統(tǒng)僅給 CAAnimation 和 CADisplayLink 的 api 提供了動(dòng)態(tài)修改幀率的操作。
但是在我們直播場(chǎng)景中,一共有如下幾種場(chǎng)景的動(dòng)畫提交:
1)UIView block 動(dòng)畫;
2)UIScrollView scroll 動(dòng)畫;
3)NSTimer 動(dòng)畫;
4)CAAnimation。
除了4我們可以直接修改為 iOS15支持的?preferredFrameRateRange?api 外,其它幾個(gè)我們要怎么解決呢?
針對(duì)以上1~3點(diǎn)我們分別做如下處理。
6.2UIView block 動(dòng)畫
通過分析?+[UIView animateWithDuration:delay : options:animations:completion:]?調(diào)用,我們發(fā)現(xiàn) animations block 里的 property animation 會(huì)被同步的創(chuàng)建為 CAAnimation 對(duì)象。
如圖所示:

那我們是否可以 hook CAAnimation 然后尋找時(shí)機(jī)設(shè)置它的?preferredFrameRateRange?以達(dá)到降幀的目的?
很遺憾,不行,因?yàn)檫@個(gè) api 觸發(fā)的動(dòng)畫不會(huì)去觸發(fā)對(duì)應(yīng)的 setter 與 getter 去讀取新修改的值,而是被覆蓋為一個(gè)默認(rèn)值,導(dǎo)致無法降幀。
再進(jìn)一步調(diào)試發(fā)現(xiàn)與UIViewPropertyAnimator?里是有主動(dòng)?setPreferredFrameRateRange:的操作,那是否可以從這里入手?
經(jīng)過驗(yàn)證,果然可行,于是我們可以將所有的 UIView block animation 動(dòng)畫都無縫替換為新方案后,即可實(shí)現(xiàn)自動(dòng)降幀隨意靈活控制的目的了。
部分代碼如下:
????if(@available(iOS 15.0, *)) {
????????setFrameRateLevel(level);
????????[UIViewPropertyAnimator runningPropertyAnimatorWithDuration:duration
??????????????????????????????????????????????????????????????delay:delay
????????????????????????????????????????????????????????????options:options
?????????????????????????????????????????????????????????animations:animations
completion:^(UIViewAnimatingPosition finalPosition) {
?????????????????????????????????????????????????????????????if(completion) {
?????????????????????????????????????????????????????????????????completion(YES);
?????????????????????????????????????????????????????????????}
?????????????????????????????????????????????????????????}];
????????clearFrameRateLevel();
????} else{
????????if(level != MMAnimationFrameRateLevelNone) {
????????????if(level <= MMAnimationFrameRateLevelMedium)
????????????????options |= UIViewAnimationOptionPreferredFramesPerSecond30;
????????}
????????[selfanimateWithDuration:duration delay:delay options:options animations:animations completion:completion];
????}
新的接口可以無縫替換原有的+[UIView animateWithDuration:delay : options:animations:completion:]?調(diào)用,可對(duì)所有系統(tǒng)實(shí)現(xiàn)降幀調(diào)節(jié)優(yōu)化,極大的方便了業(yè)務(wù)開發(fā)同學(xué)在不同場(chǎng)景中選擇合適的動(dòng)畫幀率,以達(dá)到效果和耗電的平衡。
6.3UIScrollView 動(dòng)畫
經(jīng)過上文的分析我們發(fā)現(xiàn) UIScrollView setContentOffset 的動(dòng)畫是基于系統(tǒng)_UIUpdateTarget?機(jī)制來驅(qū)動(dòng)的,由于對(duì)應(yīng)的回調(diào)是私有 api 觸發(fā)的,所以我們無法直接調(diào)節(jié)它的幀率,于是我們干脆自己實(shí)現(xiàn)一個(gè)基于 CADisplayLink 驅(qū)動(dòng)的?setContentOffset?滑動(dòng)動(dòng)畫即可解決問題。
即:創(chuàng)建一個(gè)CADisplayLink對(duì)象,指定我們需要的?preferredFrameRateRange?幀率,然后在每一幀回調(diào)時(shí),根據(jù)當(dāng)前的時(shí)間戳計(jì)算出當(dāng)前需要設(shè)置的 contentOffset 值,直到最終達(dá)到了指定的動(dòng)畫 duration 時(shí)間后,我們?cè)侔?contentOffset 調(diào)整為目標(biāo)值,即可。
主體代碼大致如下:
- (void)tt_contentOffset:(CGPoint)contentOffset duration:(CFTimeInterval)duration {
????self.duration = duration;
????self.deltaContentOffset = CGPointMinus(contentOffset, self.scrollView.contentOffset);
????if(!self.displayLink) {
????????self.displayLink = [CADisplayLink displayLinkWithTarget:selfselector:@selector(updateContentOffset:)];
????????if(@available(iOS 15.0, *)) {
????????????self.displayLink.preferredFrameRateRange = CAFrameRateRangeMake(15, 24, 0);
????????} else{
????????????self.displayLink.preferredFramesPerSecond = 30;
????????}
????????[self.displayLink addToRunLoop:[NSRunLoopcurrentRunLoop] forMode:NSDefaultRunLoopMode];
????} else{
????????self.displayLink.paused = NO;
????}
}
- (void)tt_onDisplayLink:(CADisplayLink *)displayLink {
????if(self.beginTime == 0.0) {
????????self.beginTime = self.displayLink.timestamp;
????????self.beginContentOffset = self.scrollView.contentOffset;
????} else{
????????CFTimeInterval duration = displayLink.timestamp - self.beginTime;
????????CGFloat percent = (CGFloat)(duration / self.duration);
????????if(percent < 1.0) {
????????????CGFloat progress = (CGFloat)timingFunctionValue(self.timingFunction, percent);
????????????if(1 - progress < 0.001) {
????????????????[selftt_stopAnimation];
????????????} else{
????????????????[selftt_updateProgress:progress];
????????????}
????????} else{
????????????[selftt_stopAnimation];
????????}
????}
}
6.4NSTimer 動(dòng)畫
根據(jù)蘋果Explore UI animation hitches and the render loop 的說法,為了避免 vsync 信號(hào)和 timer??可能不同步的情形,我們直接用 CADisplayLink 來替換原先的 NSTimer,并在 CADisplayLink 回調(diào)里再觸發(fā)對(duì)應(yīng)的 UI 提交操作和動(dòng)畫即可。
這是因?yàn)椋?/strong>如下圖所示,對(duì)于13 pro max 高刷屏設(shè)備而言,其 UI 動(dòng)畫的系統(tǒng)回調(diào)頻次是240hz,渲染幀率是可變的可為0~120fps 之間,而常規(guī)的 NSTimer 是基于 RunLoop 觸發(fā)回調(diào)的,RunLoop 的回調(diào)間隔可能只有幾十 us,那么 Timer 的靈敏度遠(yuǎn)高于 DisplayLink,所以完全是有可能在2幀渲染之間,回調(diào)了一次 Timer,而最終導(dǎo)致可能會(huì)多觸發(fā)了一幀的提交或一次渲染事件。所以我們采取 CADisplayLink 來替換 NSTimer,盡可能避免和 Display 不同步的渲染觸發(fā)操作。

7、優(yōu)化后的效果
按照蘋果的建議 ,app 內(nèi)容在沒有頻繁更新時(shí),應(yīng)該盡量降低 FPS 以平衡功耗占用,因?yàn)楦咚⒈厝粠砀l繁的 GPU 任務(wù)提交,使得 GPU 占用提升。
并且 app 的刷新率是由所有內(nèi)容的最高刷新率決定的,也就是高 FPS 的界面元素會(huì)導(dǎo)致整個(gè)屏幕全局 FPS 提升,只有適當(dāng)平衡全局界面元素的 FPS 后,才能進(jìn)一步降低不必要的性能消耗。

基于上述指導(dǎo)思想和優(yōu)化方案,我們最終在視頻號(hào)直播上驗(yàn)證測(cè)試如下:
先基于 「UIViewAnimationOptionPreferredFramesPerSecond30」 將直播點(diǎn)贊場(chǎng)景下的fps從高刷屏的120fps 降低到60fps,再基于 「UIViewPropertyAnimator」 將任意UIView block animation的幀率降低到30~48fps(最終全局穩(wěn)定在40~50fps),幀率同比下降16%而 GPU 同比下降了26%~38%(在主場(chǎng)景和其他場(chǎng)景)。并且由于我們的視頻畫面依舊是25fps的低幀率,所以此處降幀只是降低了 QuartzCore 的重復(fù)幀,而沒有減少任何畫面細(xì)節(jié),最終本質(zhì)上是無損的畫面降幀。

另外,「在實(shí)驗(yàn)過程中調(diào)試」,進(jìn)一步發(fā)現(xiàn)了一些很有用的環(huán)境變量,可以幫助我們更好的調(diào)試UI問題。
如下:

8、問題擴(kuò)展
我們通過一些奇怪的繞過方式間接的實(shí)現(xiàn)了對(duì)所有基于 UIView block animation api 調(diào)用的動(dòng)畫以及 CAAnimation api 調(diào)用的動(dòng)畫都實(shí)現(xiàn)了動(dòng)態(tài)降幀,這極大的改善了低幀率直播間的業(yè)務(wù)動(dòng)畫導(dǎo)致的 GPU 功耗占用問題。
那對(duì)于高幀率直播間我們還能怎么解決呢?
基于蘋果的文檔幀率檔位設(shè)置建議和我們的綜合實(shí)踐效果,我們對(duì)高幀率直播間采取了部分用戶無明顯感知的有損降級(jí)策略。即當(dāng)檢測(cè)到設(shè)備過熱后,我們會(huì)將60fps 的直播流,以渲染端均勻丟幀的方式降幀到48fps。
方案如下:

最終也一樣取得了GPU 同比下降28%甚至更高的效果,有效減輕了過熱時(shí)的系統(tǒng)負(fù)載和功耗,并且從肉眼上基本無法分辨出差異。
9、本文小結(jié)
本文在不影響現(xiàn)有用戶體驗(yàn)和業(yè)務(wù)邏輯的情況下,通過擴(kuò)展系統(tǒng)接口的能力與實(shí)驗(yàn)調(diào)試分析,最終實(shí)現(xiàn)了一套 UI 動(dòng)畫的幀率調(diào)節(jié)方案。
該方案得到的效果是:
1)快速改造既有業(yè)務(wù)的所有動(dòng)畫,動(dòng)態(tài)的控制各自的幀率;
2)最終達(dá)到不影響效果的前提下,盡可能的降低了功耗;
3)同時(shí)極大的減輕了業(yè)務(wù)開發(fā)同學(xué)適配多系統(tǒng)和改造動(dòng)畫的工作量。
該方案最終在微信視頻號(hào)直播上得到廣泛應(yīng)用,取得了較大的性能提升。
至此,關(guān)于iOS視頻號(hào)直播的功耗優(yōu)化方案講解完畢。
10、相關(guān)文章
[1]?淘寶直播技術(shù)干貨:高清、低延時(shí)的實(shí)時(shí)視頻直播技術(shù)解密
[2]?技術(shù)干貨:實(shí)時(shí)視頻直播首屏耗時(shí)400ms內(nèi)的優(yōu)化實(shí)踐
[3]?七牛云技術(shù)分享:使用QUIC協(xié)議實(shí)現(xiàn)實(shí)時(shí)視頻直播0卡頓!
[4]?首次披露:快手是如何做到百萬觀眾同場(chǎng)看直播仍能秒開且不卡頓的?
[5]?淺談實(shí)時(shí)音視頻直播中直接影響用戶體驗(yàn)的幾項(xiàng)關(guān)鍵技術(shù)指標(biāo)
[6]?移動(dòng)端實(shí)時(shí)視頻直播技術(shù)實(shí)踐:如何做到實(shí)時(shí)秒開、流暢不卡
[7]?實(shí)現(xiàn)延遲低于500毫秒的1080P實(shí)時(shí)音視頻直播的實(shí)踐分享
[8]?直播系統(tǒng)聊天技術(shù)(五):微信小游戲直播在Android端的跨進(jìn)程渲染推流實(shí)踐
附錄:微信團(tuán)隊(duì)分享的其它文章
《微信團(tuán)隊(duì)分享:極致優(yōu)化,iOS版微信編譯速度3倍提升的實(shí)踐總結(jié)》
《IM“掃一掃”功能很好做?看看微信“掃一掃識(shí)物”的完整技術(shù)實(shí)現(xiàn)》
《微信團(tuán)隊(duì)分享:微信支付代碼重構(gòu)帶來的移動(dòng)端軟件架構(gòu)上的思考》
《IM開發(fā)寶典:史上最全,微信各種功能參數(shù)和邏輯規(guī)則資料匯總》
《微信團(tuán)隊(duì)分享:微信直播聊天室單房間1500萬在線的消息架構(gòu)演進(jìn)之路》
《企業(yè)微信的IM架構(gòu)設(shè)計(jì)揭秘:消息模型、萬人群、已讀回執(zhí)、消息撤回等》
《IM全文檢索技術(shù)專題(四):微信iOS端的最新全文檢索技術(shù)優(yōu)化實(shí)踐》
《微信團(tuán)隊(duì)分享:微信后臺(tái)在海量并發(fā)請(qǐng)求下是如何做到不崩潰的》
《微信Windows端IM消息數(shù)據(jù)庫的優(yōu)化實(shí)踐:查詢慢、體積大、文件損壞等》
《微信技術(shù)分享:揭秘微信后臺(tái)安全特征數(shù)據(jù)倉庫的架構(gòu)設(shè)計(jì)》
《企業(yè)微信針對(duì)百萬級(jí)組織架構(gòu)的客戶端性能優(yōu)化實(shí)踐》
《揭秘企業(yè)微信是如何支持超大規(guī)模IM組織架構(gòu)的——技術(shù)解讀四維關(guān)系鏈》
《微信團(tuán)隊(duì)分享:詳解iOS版微信視頻號(hào)直播中因幀率異常導(dǎo)致的功耗問題》
(本文已同步發(fā)布于:http://www.52im.net/thread-4507-1-1.html)