Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 499, comments - 13, trackbacks - 0, articles - 1

          本文由微信客戶(hù)端團(tuán)隊(duì)rhythm分享,原題“視頻號(hào)直播:如何進(jìn)一步降低功耗占用?”,本文有修訂和改動(dòng)。

          1、引言

          功耗優(yōu)化一直是 app 性能優(yōu)化中讓人頭疼的問(wèn)題,尤其是在直播這種用戶(hù)觀(guān)看時(shí)長(zhǎng)特別久的場(chǎng)景。怎樣能在不影響主體驗(yàn)的前提下,進(jìn)一步優(yōu)化微信iOS端視頻號(hào)直播的功耗占用,本文給出了一個(gè)不太一樣的答案。

           
           
          技術(shù)交流:

          - 移動(dòng)端IM開(kāi)發(fā)入門(mén)文章:《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM

          - 開(kāi)源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK備用地址點(diǎn)此

          (本文已同步發(fā)布于:http://www.52im.net/thread-4507-1-1.html

          2、問(wèn)題背景

          問(wèn)題的起因是我們測(cè)試統(tǒng)計(jì)發(fā)現(xiàn)帶有點(diǎn)贊的直播會(huì)比無(wú)點(diǎn)贊動(dòng)畫(huà)的直播 GPU 占用要高將近一倍,同時(shí) FPS 差異也很大。

          高刷屏下,PerfDog 測(cè)試顯示,有點(diǎn)贊情況下的大部分視頻號(hào)直播居然是以60fps在跑,這導(dǎo)致了極高的GPU占用。

          但我們根本沒(méi)有60fps 這么高的直播流,且絕大部分直播流都只有30fps 而已,少部分也就最高60fps,怎么到了設(shè)備上就達(dá)到了60fps?

          而且這還是我們開(kāi)啟了強(qiáng)制低幀率UIViewAnimationOptionPreferredFramesPerSecond30后的效果,沒(méi)開(kāi)之前直接奔120fps 去了。

          如下圖所示 PerfDog 數(shù)據(jù)顯示在 13 pro max上直播點(diǎn)贊期間 FPS 直奔120:

          正常情況下,視頻號(hào)直播里大部分主播開(kāi)播流基本都是30fps 以?xún)?nèi),也就是正常情況下我們只需要維持30fps 渲染,就能保持好流程的用戶(hù)體驗(yàn)。

          那為什么這里降幀后依舊會(huì)出現(xiàn)60fps 呢?

          經(jīng)過(guò)一系列排查我們發(fā)現(xiàn)這是由于直播的點(diǎn)贊動(dòng)畫(huà)導(dǎo)致的高幀率,如果去掉動(dòng)畫(huà)后 FPS 就會(huì)回到正常情況下了,且 GPU 占用也有了明顯下降。

          這到底是怎么回事?我們是否可以降動(dòng)畫(huà)的幀率降低到某個(gè)值來(lái)去優(yōu)化我們整體的 GPU 占用呢?

          3、知識(shí)儲(chǔ)備1:iOS中的動(dòng)畫(huà)分類(lèi)

          在iOS中,大部分動(dòng)畫(huà)的本質(zhì)就是根據(jù)輸入的時(shí)間戳,返回對(duì)應(yīng)屬性的動(dòng)畫(huà)參數(shù),從而移動(dòng)圖像,達(dá)到運(yùn)動(dòng)的效果。根據(jù)動(dòng)畫(huà) api 實(shí)現(xiàn)方式的特點(diǎn)我們可以把動(dòng)畫(huà) api 劃分為如下幾類(lèi)。

          3.1UIView block animation

          基于 「+[UIView animateWithDuration:delay : options:animations:completion:]」 動(dòng)畫(huà)api驅(qū)動(dòng)的動(dòng)畫(huà),特點(diǎn)是所有動(dòng)畫(huà)都在 animations block 里同步觸發(fā),可以方便的設(shè)置任何屬性動(dòng)畫(huà)。

          [UIView animateWithDuration:duration

                                       delay:0

                                     options:option

                                  animations:^{

                                      view.top -= offsetY;

                                      view.left -= offsetX;

                                  }

                                  completion:completion];

          3.2CAAnimation

          基于 「CAAnimation api」 直接觸發(fā)提交的動(dòng)畫(huà),例如:

          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)畫(huà),例如定時(shí)去修改某個(gè) imageView.image,使得它能定期變換的效果。基于 「CADisplayLink」 觸發(fā)的動(dòng)畫(huà),和基于 NSTimer 觸發(fā)類(lèi)似,只不過(guò)這個(gè) timer 源是和渲染保持一致的,能夠做到更流暢更貼合。

          比如我們要實(shí)現(xiàn)自定義的 UIScrollView 動(dòng)畫(huà),就可以基于 CADisplayLink 來(lái)做。

          3.4UIViewPropertyAnimator

          「UIViewPropertyAnimator」是iOS10開(kāi)始蘋(píng)果推動(dòng)的新的動(dòng)畫(huà)api,相比 UIView block animation 可以更靈活的控制動(dòng)畫(huà)的過(guò)程。

          [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:duration

                                               delay:0

                                             options:option

                                          animations:^{

                                              view.top -= offsetY;

                                              view.left -= offsetX;

                                          }

                                          completion:completion];

          4、知識(shí)儲(chǔ)備2:iOS中的動(dòng)畫(huà)渲染

          iOS中的動(dòng)畫(huà)或者 UIView 的修改到底是怎么被渲染到屏幕上去的?

          4.1Core Animation Pipeline

          iOS 的 UI 更新和動(dòng)畫(huà)操作都離不開(kāi) Core Animation 和 UIKit,他們的底層都是 QuartzCore,所有的 UI 刷新和動(dòng)畫(huà)提交都會(huì)打包成對(duì)應(yīng)的 CA::Transaction 和 CAAnimation 對(duì)象并提交給 Render Server 去處理。

          App 本身并不負(fù)責(zé)渲染,渲染是由獨(dú)立的進(jìn)程 Render Server 來(lái)負(fù)責(zé)的,Render Server 最終調(diào)用 -[AGXG14FamilyRenderContext drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:] 等 GPU 接口來(lái)完成 GPU 任務(wù)的提交,最終觸發(fā)屏幕更新操作。

          整體過(guò)程大概如下:

          • 1)App 處理事件,例如 touch 事件或者 displaylink timer 事件;
          • 2)App 完成視圖的 layout、圖像 decode 等操作,并觸發(fā) CA::Transaction 提交;
          • 3)Render Server 接收 App 提交的 Transction 和圖片數(shù)據(jù),Render Server 可直接跨進(jìn)程訪(fǎng)問(wèn) App 進(jìn)程的位圖內(nèi)存資源,并最終觸發(fā) GPU 調(diào)用;
          • 4)GPU 最終完成了圖像的渲染并顯示到屏幕 Display。

          4.2Render Server

          如下圖所示,最終的上屏任務(wù)提交操作是由 Render Server 也即 backboardd 進(jìn)程來(lái)最終觸發(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)畫(huà)和UI更新操作。

          如上圖所示,time profiler 里我們能清晰看到 backboardd 進(jìn)程在處理來(lái)自 app 進(jìn)程的圖像提交操作。

          微信直播之前就遇到過(guò)好幾次 Render Server 命中了 gpu io fence 導(dǎo)致系統(tǒng)全局卡死的問(wèn)題。

          如下圖 ips 文件日志所示:

          4.3Render Loop

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

          4.4動(dòng)畫(huà)渲染

          當(dāng)我們調(diào)用-[UIView animateWithDuration:animations:] api觸發(fā)動(dòng)畫(huà)后,整體動(dòng)畫(huà)渲染過(guò)程如下圖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)致了無(wú)用的功耗但并沒(méi)有帶來(lái)更好的體驗(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)來(lái)達(dá)到降低 GPU 功耗的目的,對(duì)于可變刷新率的屏幕,那只要減少了 GPU 幀率就自然而然也減少了屏幕的刷新率,使得屏幕和 GPU 功耗都下降了。

          在我們遇到的問(wèn)題中,我們的視頻流 FPS 是25,那么我們預(yù)期的最終 GPU FPS 和屏幕 FPS 理應(yīng)同理也是接近25才是,而這里卻達(dá)到了60fps,說(shuō)明了有重復(fù)的內(nèi)容幀一直被 Render Server 重復(fù)的復(fù)制并提交給 GPU,導(dǎo)致了畫(huà)質(zhì)細(xì)節(jié)沒(méi)有增加,但頻繁的拷貝渲染造成了更高的 GPU 占用。這就是我們的問(wèn)題所在。

          5、知識(shí)儲(chǔ)備3:iOS中的動(dòng)畫(huà)降幀

          5.1概述

          結(jié)合上文,我們要解決直播幀率異常升高的問(wèn)題,就需要解決點(diǎn)贊動(dòng)畫(huà)的高幀率問(wèn)題。

          很幸運(yùn),蘋(píng)果在 iOS15提供了一個(gè) CAAnimation 的 api,即-[CAAnimation preferredFrameRateRange],它接受3個(gè)參數(shù)分別指定minimum 幀率,maximum 幀率,以及 preferred 幀率,基于這個(gè)api我們可以對(duì)于 CAAnimation 動(dòng)畫(huà)設(shè)置幀率。

          蘋(píng)果的建議是把動(dòng)畫(huà)分為了如下幾檔:

          5.2CAAnimation 降幀原理

          iOS15開(kāi)始蘋(píng)果引入了 CAFrameRateRange 相關(guān) api 來(lái)供 app 去設(shè)置 CADisplayLink 和 CAAnimation 的 preferredFrameRateRange,以方便調(diào)節(jié)幀率,達(dá)到在高刷機(jī)上能進(jìn)一步降低功耗的目的。

          那它又是如何工作的呢?

          首先需要明確 iOS15后 CAAnimation 和 CADisplayLink 的幀率控制底層都是一致的,也就是都是 CA:: Display: : DisplayLinkItem 來(lái)驅(qū)動(dòng)觸發(fā)的。而動(dòng)畫(huà)的本質(zhì)就是根據(jù)時(shí)間的輸入來(lái)得到對(duì)應(yīng)的動(dòng)畫(huà) fraction 并觸發(fā)對(duì)應(yīng)進(jìn)度的動(dòng)畫(huà)修改,再提交上屏完成修改。

          具體而言,我們以 UIScrollView的 setContentOffset:animated 動(dòng)畫(huà)為例。

          5.3setContentOffset:animated 動(dòng)畫(huà)機(jī)制

          當(dāng)我們觸發(fā)[scrollView setContentOffset:CGPointMake(120,0) animated:YES]后,會(huì)觸發(fā)創(chuàng)建一個(gè) UIScrollViewAnimation 的實(shí)例對(duì)象(UIAnimation的子類(lèi)),接下來(lái)會(huì)調(diào)用 UIUpdateSequenceInsertItem 將這個(gè)動(dòng)畫(huà)實(shí)例注冊(cè)到當(dāng)前的 UIUpdateCycle 循環(huán)中。

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

          如圖:

          每次觸發(fā)觸發(fā) _UIUpdateSequenceRun 時(shí),會(huì)向 UIScrollViewAnimation 請(qǐng)求[UIAnimation fractionForTime:]來(lái)返回對(duì)應(yīng)時(shí)間戳的 contentOffset 和 progress,然后觸發(fā)修改 contentOffset,最終接近目標(biāo) contentOffset 后就完成了完整的動(dòng)畫(huà)。

          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)畫(huà)。

          如下圖:

          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 即可全局打開(kāi)這個(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);

          }

          由于以上能力無(wú)法在非越獄設(shè)備上開(kāi)啟,所以實(shí)際上我們無(wú)法檢測(cè) app 在任意時(shí)刻的 FPS 變化情況。

          不過(guò)經(jīng)過(guò)分析,我們發(fā)現(xiàn)只要觸發(fā)了以下行為就可能代表要幀率要變化了。如下。

          1)在設(shè)置->動(dòng)態(tài)效果里開(kāi)啟或關(guān)閉“限制幀速率”:修改限制幀速率會(huì)觸發(fā)系統(tǒng)拋出 com.apple.CoreAnimation.CAWindowServer.DisplayChanged 的通知,QuartCore 會(huì)在啟動(dòng)時(shí)注冊(cè)這個(gè)通知,并收到通知后通過(guò) mach port 通信獲取當(dāng)前注冊(cè)的幀速率值,以動(dòng)態(tài)修改 displaylink 的回調(diào)頻次。

          2)直接通過(guò) opengl/metal api 提交一幀畫(huà)面給 Render Server。

          3)觸發(fā) CA::Transaction 對(duì)象的提交:除了觸發(fā)動(dòng)畫(huà)提交,觸發(fā) view property 提交變更外,甚至創(chuàng)建 view 也會(huì)導(dǎo)致 source0觸發(fā)一次,如下圖所示。

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

          我們只要修改 UIUpdateSequenceRun 的回調(diào)頻率,也就是 -[UIAnimator _advanceAnimationsOnScreenWithIdentifier:withTimestamp:] 的回調(diào)頻次我們就能控制部分系統(tǒng)動(dòng)畫(huà)的幀率,強(qiáng)制調(diào)節(jié)他的執(zhí)行頻次,或者我們通過(guò)模擬系統(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)畫(huà)刷新率調(diào)節(jié)為 240/8=30hz。

          基于以上研究,理論上我們可以嘗試調(diào)用私有 api 來(lái)全局控制 CADisplay 的刷新率,來(lái)進(jìn)一步降低性能占用,但是由于 Render Server 是在其他進(jìn)程,我們還是無(wú)法控制 Render Server 的刷新率,并且私有 api 會(huì)導(dǎo)致 app 被拒審,所以我們最終依舊只能改造部分系統(tǒng)動(dòng)畫(huà)實(shí)現(xiàn)以繼續(xù)基于 CAAnimation api 去優(yōu)化幀率。

          6、我們的優(yōu)化方案

          6.1概述

          從 iOS15開(kāi)始蘋(píng)果新增加了 preferredFrameRateRange api 可用于設(shè)置相應(yīng)動(dòng)畫(huà)或timer的刷新頻率,我們就可以基于該方案去改造相應(yīng)動(dòng)畫(huà)即可。

          @propertyCAFrameRateRange preferredFrameRateRange

              API_AVAILABLE(macos(12.0), ios(15.0), watchos(8.0), tvos(15.0));

          但新的問(wèn)題又來(lái)了,系統(tǒng)僅給 CAAnimation 和 CADisplayLink 的 api 提供了動(dòng)態(tài)修改幀率的操作。

          但是在我們直播場(chǎng)景中,一共有如下幾種場(chǎng)景的動(dòng)畫(huà)提交:

          • 1)UIView block 動(dòng)畫(huà);
          • 2)UIScrollView scroll 動(dòng)畫(huà);
          • 3)NSTimer 動(dòng)畫(huà);
          • 4)CAAnimation。

          除了4我們可以直接修改為 iOS15支持的 preferredFrameRateRange api 外,其它幾個(gè)我們要怎么解決呢?

          針對(duì)以上1~3點(diǎn)我們分別做如下處理。

          6.2UIView block 動(dòng)畫(huà)

          通過(guò)分析 +[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à)不會(huì)去觸發(fā)對(duì)應(yīng)的 setter 與 getter 去讀取新修改的值,而是被覆蓋為一個(gè)默認(rèn)值,導(dǎo)致無(wú)法降幀。

          再進(jìn)一步調(diào)試發(fā)現(xiàn)與UIViewPropertyAnimator 里是有主動(dòng) setPreferredFrameRateRange:的操作,那是否可以從這里入手?

          經(jīng)過(guò)驗(yàn)證,果然可行,于是我們可以將所有的 UIView block animation 動(dòng)畫(huà)都無(wú)縫替換為新方案后,即可實(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];

              }

          新的接口可以無(wú)縫替換原有的+[UIView animateWithDuration:delay : options:animations:completion:] 調(diào)用,可對(duì)所有系統(tǒng)實(shí)現(xiàn)降幀調(diào)節(jié)優(yōu)化,極大的方便了業(yè)務(wù)開(kāi)發(fā)同學(xué)在不同場(chǎng)景中選擇合適的動(dòng)畫(huà)幀率,以達(dá)到效果和耗電的平衡。

          6.3UIScrollView 動(dòng)畫(huà)

          經(jīng)過(guò)上文的分析我們發(fā)現(xiàn) UIScrollView setContentOffset 的動(dòng)畫(huà)是基于系統(tǒng)_UIUpdateTarget 機(jī)制來(lái)驅(qū)動(dòng)的,由于對(duì)應(yīng)的回調(diào)是私有 api 觸發(fā)的,所以我們無(wú)法直接調(diào)節(jié)它的幀率,于是我們干脆自己實(shí)現(xiàn)一個(gè)基于 CADisplayLink 驅(qū)動(dòng)的 setContentOffset 滑動(dòng)動(dòng)畫(huà)即可解決問(wèn)題。

          即:創(chuàng)建一個(gè)CADisplayLink對(duì)象,指定我們需要的 preferredFrameRateRange 幀率,然后在每一幀回調(diào)時(shí),根據(jù)當(dāng)前的時(shí)間戳計(jì)算出當(dāng)前需要設(shè)置的 contentOffset 值,直到最終達(dá)到了指定的動(dòng)畫(huà) 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)畫(huà)

          根據(jù)蘋(píng)果Explore UI animation hitches and the render loop 的說(shuō)法,為了避免 vsync 信號(hào)和 timer  可能不同步的情形,我們直接用 CADisplayLink 來(lái)替換原先的 NSTimer,并在 CADisplayLink 回調(diào)里再觸發(fā)對(duì)應(yīng)的 UI 提交操作和動(dòng)畫(huà)即可。

          這是因?yàn)椋?/strong>如下圖所示,對(duì)于13 pro max 高刷屏設(shè)備而言,其 UI 動(dòng)畫(huà)的系統(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 來(lái)替換 NSTimer,盡可能避免和 Display 不同步的渲染觸發(fā)操作。

          7、優(yōu)化后的效果

          按照蘋(píng)果的建議 ,app 內(nèi)容在沒(méi)有頻繁更新時(shí),應(yīng)該盡量降低 FPS 以平衡功耗占用,因?yàn)楦咚⒈厝粠?lái)更頻繁的 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)景)。并且由于我們的視頻畫(huà)面依舊是25fps的低幀率,所以此處降幀只是降低了 QuartzCore 的重復(fù)幀,而沒(méi)有減少任何畫(huà)面細(xì)節(jié),最終本質(zhì)上是無(wú)損的畫(huà)面降幀。

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

          如下:

          8、問(wèn)題擴(kuò)展

          我們通過(guò)一些奇怪的繞過(guò)方式間接的實(shí)現(xiàn)了對(duì)所有基于 UIView block animation api 調(diào)用的動(dòng)畫(huà)以及 CAAnimation api 調(diào)用的動(dòng)畫(huà)都實(shí)現(xiàn)了動(dòng)態(tài)降幀,這極大的改善了低幀率直播間的業(yè)務(wù)動(dòng)畫(huà)導(dǎo)致的 GPU 功耗占用問(wèn)題。

          那對(duì)于高幀率直播間我們還能怎么解決呢?

          基于蘋(píng)果的文檔幀率檔位設(shè)置建議和我們的綜合實(shí)踐效果,我們對(duì)高幀率直播間采取了部分用戶(hù)無(wú)明顯感知的有損降級(jí)策略。即當(dāng)檢測(cè)到設(shè)備過(guò)熱后,我們會(huì)將60fps 的直播流,以渲染端均勻丟幀的方式降幀到48fps。

          方案如下:

          最終也一樣取得了GPU 同比下降28%甚至更高的效果,有效減輕了過(guò)熱時(shí)的系統(tǒng)負(fù)載和功耗,并且從肉眼上基本無(wú)法分辨出差異。

          9、本文小結(jié)

          本文在不影響現(xiàn)有用戶(hù)體驗(yàn)和業(yè)務(wù)邏輯的情況下,通過(guò)擴(kuò)展系統(tǒng)接口的能力與實(shí)驗(yàn)調(diào)試分析,最終實(shí)現(xiàn)了一套 UI 動(dòng)畫(huà)的幀率調(diào)節(jié)方案。

          該方案得到的效果是:

          • 1)快速改造既有業(yè)務(wù)的所有動(dòng)畫(huà),動(dòng)態(tài)的控制各自的幀率;
          • 2)最終達(dá)到不影響效果的前提下,盡可能的降低了功耗;
          • 3)同時(shí)極大的減輕了業(yè)務(wù)開(kāi)發(fā)同學(xué)適配多系統(tǒng)和改造動(dòng)畫(huà)的工作量。

          該方案最終在微信視頻號(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] 首次披露:快手是如何做到百萬(wàn)觀(guān)眾同場(chǎng)看直播仍能秒開(kāi)且不卡頓的?

          [5] 淺談實(shí)時(shí)音視頻直播中直接影響用戶(hù)體驗(yàn)的幾項(xiàng)關(guān)鍵技術(shù)指標(biāo)

          [6] 移動(dòng)端實(shí)時(shí)視頻直播技術(shù)實(shí)踐:如何做到實(shí)時(shí)秒開(kāi)、流暢不卡

          [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)帶來(lái)的移動(dòng)端軟件架構(gòu)上的思考

          IM開(kāi)發(fā)寶典:史上最全,微信各種功能參數(shù)和邏輯規(guī)則資料匯總

          微信團(tuán)隊(duì)分享:微信直播聊天室單房間1500萬(wàn)在線(xiàn)的消息架構(gòu)演進(jìn)之路

          企業(yè)微信的IM架構(gòu)設(shè)計(jì)揭秘:消息模型、萬(wàn)人群、已讀回執(zhí)、消息撤回等

          IM全文檢索技術(shù)專(zhuān)題(四):微信iOS端的最新全文檢索技術(shù)優(yōu)化實(shí)踐

          微信團(tuán)隊(duì)分享:微信后臺(tái)在海量并發(fā)請(qǐng)求下是如何做到不崩潰的

          微信Windows端IM消息數(shù)據(jù)庫(kù)的優(yōu)化實(shí)踐:查詢(xún)慢、體積大、文件損壞等

          微信技術(shù)分享:揭秘微信后臺(tái)安全特征數(shù)據(jù)倉(cāng)庫(kù)的架構(gòu)設(shè)計(jì)

          企業(yè)微信針對(duì)百萬(wàn)級(jí)組織架構(gòu)的客戶(hù)端性能優(yōu)化實(shí)踐

          揭秘企業(yè)微信是如何支持超大規(guī)模IM組織架構(gòu)的——技術(shù)解讀四維關(guān)系鏈

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


          (本文已同步發(fā)布于:http://www.52im.net/thread-4507-1-1.html



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開(kāi)發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀(guān)工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


          只有注冊(cè)用戶(hù)登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 沂源县| 永和县| 四平市| 宁国市| 永安市| 鹰潭市| 伊金霍洛旗| 响水县| 广河县| 青田县| 临安市| 安宁市| 贵阳市| 保靖县| 张北县| 二手房| 雷山县| 壤塘县| 崇左市| 枣阳市| 兰考县| 远安县| 克东县| 朝阳区| 万州区| 莱西市| 弥渡县| 辽中县| 新竹市| 九江县| 海城市| 花垣县| 敖汉旗| 南康市| 仁布县| 广宗县| 龙游县| 巢湖市| 徐州市| 长汀县| 东宁县|