直播應用送禮大動畫實現(xiàn)

送禮物作為觀眾打賞支持主播的一種方式, 也是直播應用的一大收入來源, 每個直播平臺都包含送禮這一功能, 并且都把禮物動畫效果做的特別炫酷. 如此的動畫效果再搭配美女或帥哥主播的一句"謝謝某某某送的大飛機~", 是不是想想都有點小激動, 感覺瞬間成為了全場的焦點?

本文主要敘述的就是大禮物動效的實現(xiàn). 全文共3000字左右, 大概閱讀時間為5~12分鐘.

先放上按序列幀播放方案實現(xiàn)的動畫引擎FXAnimationEngine, Demo中實現(xiàn)了直播間禮物隊列蝌箍、禮物配置折联、禮物列表, 另外還分別用動畫引擎與原生Core Animation去播放序列幀動畫以做比較.

然后國際慣例, 上兩張圖

夢幻城堡
天使

一裸卫、直播應用禮物動畫的常見方案

僅個人了解, 實現(xiàn)iOS側(cè)動畫配置化常見方案有如下幾種:

iOS方案 優(yōu)點 缺點
Core Animation(此處不計CAKeyFrameAnimation) 效果流暢逼真 安卓需重新實現(xiàn); 配置化成本高, 需自定義模型、協(xié)議前标、轉(zhuǎn)換方法等(iOS側(cè)已有現(xiàn)成工具, 某幾家直播公司想必也有自己的動畫配置化工具); 不解決動態(tài)配置問題, 則只能隨包更新.
序列幀播放(CAKeyframeAnimation坠韩、CADisplaylink、ImageView等) 設(shè)計哥工具可直接導出動畫序列幀圖片, 簡單易用; 多平臺兼容 效果略差; 圖片幀數(shù)多易導致資源大
Cocos2d-x 效果好; 多平臺兼容 學習成本; 相應動畫制作工具; 必須引入Cocos2d庫;
Lottie 橫跨三端, iOS, Android, React Native. 設(shè)計師可以完全按照自己的想法設(shè)計. 無需考慮實現(xiàn)這一塊. 內(nèi)存占用? 作者本人尚未使用過, 不敢妄自評論

可以看出, 序列幀播放方案是其中最簡單易行的一個. 在我看來, 花椒直播用的即是這套方案, 他們每一個動畫, 都會對應一個配置文件config.ini及對應該動畫的所有序列幀圖片.

感興趣的朋友可以移至最后一部分禮物資源的下載策略炼列、資源目錄結(jié)構(gòu)等相關(guān)內(nèi)容, 更建議嘗試去探索一下花椒只搁、映客等主流直播應用的bundle目錄以及document中的資源.

二、序列幀播放方案實踐

2.1 實現(xiàn)方式

序列幀播放動畫一方案的具體實現(xiàn)必須能夠滿足以下需求:

  1. 圖片展示: CALayer俭尖、UIImageView
  2. 按時間間隔逐幀播放: CAKeyframeAnimation氢惋、UIImageView、定時器類(CADisplayLink稽犁、NSTimer焰望、dispatch_source_t)+切換關(guān)鍵幀邏輯
  3. 提供所有序列幀播放完的事件: CAAnimationDelegate、CATransaction CompletionBlock已亥、定時器類+回調(diào)觸發(fā)邏輯

組合方式很多, 比如: CALayer+CAKeyframeAnimation+delegate, UIImageView+定時器, CALayer+定時器類等等.

我們先選定這一套組合進行實踐: CALayer+CAKeyframeAnimation+delegate

// 偽代碼
- (void)startAnimation {
    UIImage *frame = [UIImage imageWithContentsOfFile:...];
    NSArray<UIImage *> *frames = @[(id)frame.CGImage, ...];
    CAKeyframeAnimation *keyframeAnim = ...;
    keyframeAnim.contents = frames;
    ...
    keyframeAnim.delegate = self;
    [xxx.layer addAnimation:keyframeAnim forKey:@"xxx"];
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    // 觸發(fā)動畫播放結(jié)束(全部播放完熊赖、中途結(jié)束)回調(diào)
    ...
}

如果此處你已經(jīng)下載了Demo, 可以打開Debug Navigator(cmd+6)簡單查看內(nèi)存增長或者留意Xcode Instrument-Allocations中VM:ImageIO_PNG_Data一項, 就會看到有內(nèi)存增長波峰. 而且序列幀圖片越多, 波峰越明顯.

那么其他方案是否出現(xiàn)了相同的問題呢? 是的, 其他方案一樣會如此, 換成UIImageView自帶的animationImages來做序列幀播放或是其他組合方式, 也出現(xiàn)內(nèi)存激增的情況.

2.2 了解圖片加載

在我們搞清楚是什么導致內(nèi)存激增前, 我們先了解一下圖片從磁盤加載, 到寫入內(nèi)存, 最后顯示到屏幕上分別都發(fā)生了什么. 大致分為如下步驟:

  1. 為磁盤中的圖片創(chuàng)建映射
  2. IO操作讀取圖片數(shù)據(jù)流
  3. 圖片解碼位圖拷貝, 寫入內(nèi)存
  4. 硬件繪制渲染到屏幕

2.2.1 映射文件

當我們通過[UIImage imageWithContentsOfFile:]從磁盤加載圖片數(shù)據(jù)流, 實際上只是為此圖片創(chuàng)建了一個文件映射數(shù)據(jù), 圖片文件既沒有真正被加載到內(nèi)存, 更沒有被解碼成位圖的形式可供Core Animation傳遞給底層硬件進行渲染, 故此時內(nèi)存并不會明顯增加, 也不會出現(xiàn)因為解碼操作導致CPU使用增加的情形. 但從網(wǎng)絡下載圖片數(shù)據(jù)不包含在內(nèi).

簡單提及一下映射文件:

A mapped file uses virtual memory techniques to avoid copying
pages of the file into memory until they are actually needed.

直譯就是一個映射文件借助虛擬內(nèi)存技術(shù)來避免當他們還沒有真正使用到時就被拷貝到內(nèi)存中.

下面來一組對照驗證一下:

對照組一

- (void)test1 {
    UIImage *frame = [UIImage imageWithContentsOfFile:filePath];
    // 確保超出局部作用域后, 依舊保持對這個Image對象的強引用
    self.frame = frame;
}
// 待上方函數(shù)執(zhí)行完后, 再查看內(nèi)存使用情況

對照組二

- (void)test2 {
    UIImage *frame = [UIImage imageWithContentsOfFile:filePath];
    self.imageview.image = frame;
}

我們可以發(fā)現(xiàn)對照組二的內(nèi)存占用明顯比對照組一要多. 即通過imageWithContentsOfFile:創(chuàng)建的UIImage對象后, 內(nèi)存并沒有明顯增長, 等我們將該UIImage對象賦值給UIImageView的image屬性后的某個時刻, 內(nèi)存才出現(xiàn)明顯增長.

此處再留幾個問題:

  1. 我們都知道imageWithName:方法加載的圖片, 會被系統(tǒng)緩存, 那么第一次通過該方法進行如上兩個對照組的實驗, 結(jié)果如何呢?
  2. 通過imageWithName:方法第2、3..n次加載同名圖片時, 加載的圖片數(shù)據(jù)流會不會再次被解碼? 期間CPU占用有沒有增加?
  3. 嘗試把創(chuàng)建的UIImage對象橋接賦值給CALayer的contents屬性, 結(jié)果如何?

2.2.2 淺談CALayer的隱式動畫及事務

從上一節(jié)中, 我們發(fā)現(xiàn)當給UIImageView的image屬性或CALayer的contents屬性賦值Image對象后的某一刻, 內(nèi)存和CPU占用才會出現(xiàn)明顯變化. 那是因為每一次Runloop循環(huán), Core Animation都會在其開始創(chuàng)建一個動畫事務, 在本次Runloop結(jié)束時才去執(zhí)行所有添加到該事務里的所有動畫操作. 此刻圖片才被解碼加載入內(nèi)存, 圖片數(shù)據(jù)會被解碼為渲染可用的bitmap數(shù)據(jù). 一些相關(guān)細節(jié)可看我另一篇分享.

淺談CALayer隱式動畫及事務

2.3 解決內(nèi)存激增問題

當前我們面臨的問題是無論采用何種實現(xiàn)方案, 在執(zhí)行序列幀動畫時, 所有圖片都會被解碼成為位圖并載入內(nèi)存中.

2.3.1 解壓后的圖片所占內(nèi)存大小

圖片解碼后的格式為位圖形式. 位圖是由一組像素(pixel)組成的, 每一個像素就代表圖片中的一個點. 比如常見的JPEG, 以及PNG格式的圖片文件都是位圖圖片.

我們還需要知道, JPEG和PNG圖片實際上都是一種編碼/壓縮后的位圖格式, 它們是不能直接用來圖片渲染的, 所以得先對其壓縮的數(shù)據(jù)進行解碼/解壓縮操作.

那么一張解壓后的位圖其所占內(nèi)存大小怎么計算呢?

此處假設(shè)我們有一張32位的PNG格式圖片, 其像素格式為RGBA四部分組成, 每部分占8位, 該圖片尺寸為160px * 320px.

32位的圖片意味著其每個像素占32位, 即4個字節(jié).
又根據(jù)圖片尺寸計算出總像素數(shù)量為 160*320 個像素.
所以該圖片解碼后所占內(nèi)存大小就為 像素總數(shù) * 單位像素的字節(jié)數(shù)
即 (160*320) * 4 / 1024 = 200 KB.

所以可想而知, 假設(shè)一個序列幀動畫有80張圖片, 200 * 80 / 1024 = 15.625 mb, 就會占用15mb的內(nèi)存. 序列幀圖片越多, 占用內(nèi)存越大!

2.3.2 解決方案

那么有什么方法可以避免呢? 可否每次播放到哪一幀時就去加載那一幀的圖片, 即每次僅加載一張圖片到內(nèi)存中. 這樣當播放到下一張圖片時, 上一張圖片已無任何引用, 系統(tǒng)自然會對其進行釋放.

這就是最簡單可行的一套方案. 但是我們無法靠CAAnimation及其派生類CAKeyframeAnimation來實現(xiàn)這一方案, 因為所有的圖片都會解碼導致占用大量的內(nèi)存.

但我們可以通過CADisplayLink來實現(xiàn)該方案, 選CADisplayLink的原因是它比NSTimer精度要高很多, 正常情況下CADisplayLink的回調(diào)會在屏幕每次刷新時觸發(fā), 即一般1/60秒觸發(fā)一次, 適合用于做UI的重繪, 因此可以通過它來周期性的替換關(guān)鍵幀圖片, 從而達到播放動畫的效果. 那么具體怎么做呢?

在CADisplayLink的回調(diào)中獲取兩次屏幕刷新的間隔時間, 通過不斷的累加間隔時間來判斷總的時間是否已經(jīng)滿足下一幀的播放時刻, 如果大于下一幀的播放時刻就可以替換為下一幀圖片了, 直至最后一張關(guān)鍵幀也播放完成.

舉個例子, 我們要在1秒內(nèi)播放完一個含有5張關(guān)鍵幀圖片的動畫, 每張圖片的停留時間虑椎、切換時間如下圖2.3.2.a所示. 所以第0秒的時候就開始展示第一張關(guān)鍵幀, 直到1.0秒這一刻時, 動畫播放結(jié)束.

圖 2.3.2.a

此外, 如果還需要進一步優(yōu)化, 我們可以加入圖片異步解碼震鹉、圖片預加載邏輯等方案.

  • 異步圖片解碼, 圖片解碼是一項比較耗時、比較占CPU的操作, 對于未解碼的圖片, 系統(tǒng)一般會在主線程對其進行解碼, 所以可以通過在異步線程進行圖片強制解壓縮, 從而不占用UI線程. 關(guān)于圖片解碼的詳情, 強烈推薦談談 iOS 中圖片的解壓縮.

  • 圖片預加載, 這個就是為了進一步節(jié)省上下文切換時間, 即前后兩張圖片切換的時間. 就是要做到當上一幀圖片播放完時, 我們不用等下一張圖片解碼完成后再進行圖片的切換, 而是可以直接從已解碼圖片的緩存隊列中取出直接進行切換. 預加載我個人覺得其實主要就是閾值的最優(yōu)選擇, 可參考預加載與智能預加載一文.

  • 字節(jié)對齊(byte alignment)對Core Animation性能的影響

三捆姜、序列幀動畫引擎源代碼及Demo

FXAnimationEngine - Github跳轉(zhuǎn)

針對該Demo近期會另起一文特別介紹, 此處占坑, 等待跳轉(zhuǎn)鏈接

四足陨、禮物資源下載策略及資源目錄結(jié)構(gòu)

4.1 禮物資源下載策略

4.1.1 兩種方式比較

方式 基本思路 優(yōu)點 缺點
整包更新 所有的動畫資源按目錄結(jié)構(gòu)進行壓縮, 客戶端通過比較資源包版本號發(fā)現(xiàn)有更新后, 僅需下載一個資源包壓縮文件, 并進行解壓替換即可 簡單易實現(xiàn), 客戶端每次僅需下載一個資源包 隨著資源包逐漸增大, 下載及解壓時間也會延長, 從而直接影響用戶體驗; 即使是僅是資源中的某個圖片發(fā)生改變, 客戶端都要重新下載整個資源包, 容錯率低且浪費流量
增量更新 每個動畫資源單獨壓縮并上傳CDN, 若客戶端發(fā)現(xiàn)資源版本號有變化, 再對服務器下發(fā)的資源列表跟本地資源列表求差集運算從而得出增量, 單個動畫資源的下載地址或者md5可作為唯一標識進行比較. 得出增量后, 客戶端再對每個增量資源包進行下載, 每下載完一個即可"投入使用" 不怕資源變更頻繁; 僅需下載有新增或有變更的資源包, 更節(jié)省時間以及流量; 邏輯略復雜于整包更新, 比如下載中途用戶把應殺掉, 下次需要找出未更新完的增量資源并繼續(xù)下載

4.1.2 資源更新流程

因?qū)ι霞夜镜拇a保密, 此處不上具體代碼

我們在上一小節(jié)中提及的兩種更新方式, 它們主要的不同的就在于"資源更新"這一步驟

圖 4.1.2.a 整包更新的流程圖

整包更新流程圖.png

圖 4.1.2.b 增量更新的流程圖

增量更新流程圖.png

不知道各位發(fā)現(xiàn)兩個流程共同之處沒? 它們都需要檢測資源版本號大小, 包括游戲補丁、熱更補丁這一步驟都必不可少. 相比于補丁類的, 資源更新不用太考慮灰度發(fā)布娇未、回滾機制等問題, 但還是依舊需要注意資源核對, 內(nèi)部測試, 以及日志監(jiān)控等保障, 我記得在前任公司就遇到了有的地區(qū)下載下來的資源包有問題, 所以不管是CDN的問題或資源本身有問題, 前端都需要為最壞的情況做好打算, 這才是萬全之策.

引用我上家公司, 我老大兼mentor, 達文哥, 告誡的一句箴言

不要相信后臺下發(fā)的數(shù)據(jù)都是正確的

大概意思如此, 原句沒背下來??, 這句話絕非不是指后臺同學不行, 或者甩鍋給后臺, 而是要prepare for the worst.

前后端測試都是一家人, 遇到問題我們先看看是不是自己問題, 不要相互甩鍋..本是同根生相煎何太急, 如果有問題就一塊搓一頓, 一頓不行就再來一頓

4.2 資源目錄結(jié)構(gòu)設(shè)計

不管哪個直播平臺, 每個禮物都會對應一個邏輯id, 我們可以通過禮物的id作為該禮物的資源目錄名, 然后在該目錄內(nèi)在去劃分不同類型的圖片子目錄, 如下所示

- 10000             // 一級目錄, 禮物id
    - - gift        // 二級目錄, 小禮物序列幀圖片
    - - giftlist    // 二級目錄, 禮物列表序列幀圖片 
    - - giftanim    // 二級目錄, 大動畫序列幀圖片

這只是其中的一種設(shè)計, 也有的平臺會采用如下形式, 所以主要還是看需求而定

- gift
    - - 10000
- giftlist
    - - 10000
- giftanim
    - - 10000   

此外, 有的平臺還會采用id_version, 即禮物id+禮物版本的形式來命名, 這樣可以方便配置使后臺可以靈活下發(fā)給前端具體要去播放哪個動畫的某個版本了

- 10000_11  // id為10000, 版本為11的禮物資源目錄
- 10000_12
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末墨缘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子零抬,更是在濱河造成了極大的恐慌镊讼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件平夜,死亡現(xiàn)場離奇詭異蝶棋,居然都是意外死亡,警方通過查閱死者的電腦和手機忽妒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門玩裙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人段直,你說我怎么就攤上這事吃溅。” “怎么了鸯檬?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵决侈,是天一觀的道長。 經(jīng)常有香客問我喧务,道長赖歌,這世上最難降的妖魔是什么枉圃? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮庐冯,結(jié)果婚禮上孽亲,老公的妹妹穿的比我還像新娘。我一直安慰自己展父,他們只是感情好墨林,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著犯祠,像睡著了一般旭等。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衡载,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天搔耕,我揣著相機與錄音,去河邊找鬼痰娱。 笑死弃榨,一個胖子當著我的面吹牛嘱吗,可吹牛的內(nèi)容都是我干的苗踪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼关划,長吁一口氣:“原來是場噩夢啊……” “哼坡贺!你這毒婦竟也來了官辈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤遍坟,失蹤者是張志新(化名)和其女友劉穎拳亿,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愿伴,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡肺魁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了隔节。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹅经。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖怎诫,靈堂內(nèi)的尸體忽然破棺而出瘾晃,到底是詐尸還是另有隱情,我是刑警寧澤刽虹,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布酗捌,位于F島的核電站呢诬,受9級特大地震影響涌哲,放射性物質(zhì)發(fā)生泄漏胖缤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一阀圾、第九天 我趴在偏房一處隱蔽的房頂上張望哪廓。 院中可真熱鬧,春花似錦初烘、人聲如沸涡真。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哆料。三九已至,卻和暖如春吗铐,著一層夾襖步出監(jiān)牢的瞬間东亦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工唬渗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留典阵,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓镊逝,卻偏偏與公主長得像壮啊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子撑蒜,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫歹啼、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,033評論 4 62
  • 書寫的很好座菠,翻譯的也棒染突!感謝譯者,感謝感謝辈灼! iOS-Core-Animation-Advanced-Techni...
    錢噓噓閱讀 2,292評論 0 6
  • 在iOS中隨處都可以看到絢麗的動畫效果份企,實現(xiàn)這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌巡莹。在這里你可以看...
    每天刷兩次牙閱讀 8,465評論 6 30
  • 那一眼望不穿秋水司志,望穿的只有高高的大殿。 身著龍袍的唐吃宗坐在純金打造的龍椅上降宅,一邊流著口水吃著食部新做的燒餅骂远,一...
    雨涼城閱讀 503評論 33 15
  • 又是一天,繁忙的工作帶給我許多壓力腰根。離自己的目標還差的很遠激才,明天要杜絕身邊人的影響,靜下心來學習。學習使我進步瘸恼,哈...
    胡胡胡胡說閱讀 212評論 0 0