Flipboard 在 iOS 上怎樣播放 GIF 動畫

jopen 8年前發布 | 14K 次閱讀 Flipboard iOS開發 移動開發

本文翻譯自:http://engineering.flipboard.com/2014/05/animated-gif/

原作者:Raphael Schaad

譯者:@nixzhu

Flipboard 一直謀求的是“烹飪原始Web”并將其轉化為如雜志般優雅的東西。我們考慮到許多細節——從文章的排版到相片的布局——以盡可能忠實地展現內容的本質。

而對于 GIF 來說,我們想讓它們在我們的應用里自動播放。自動播放是動態 GIF 的主要訴求。然而,iOS 上的許多應用僅僅只是渲染 GIF 的靜態幀——一個由在真機上實時地流暢播放多個 GIF 所導致的復雜性而帶來的不幸結果。

某些人可能會想,這么古老的圖片格式對于在現代的 iOS 設備上工作的開發者來說應該是開箱即用的吧。但甚至是 Apple 自家的一些應用都不支持播放它們。當用移動瀏覽器查看過它們后,系統經常會變慢到像在爬行。在定時重放并保證保真度的同時,保持較小的內存占用和 CPU 使用率實在是一個挑戰。

我們關于支持動態 GIF 的需求如下:

  • 以堪比桌面瀏覽器的播放速度同時播放多個 GIF
  • 保證可變幀延遲
  • 在內存壓力下依然表現優雅
  • 在第一次播放時消除延遲或卡頓
  • 如同現代瀏覽器那樣解譯 fast GIF 的幀延遲(注解1)

因為沒有內置的方法或開源庫能夠滿足所有這些需求,所以我們創建了一個引擎并從去年發布開始起就一直磨練它。我們認為與社區分享它是我們目前最好的選擇。

如果你想增加對動態 GIF 的支持以增強你的 iOS 應用,請前往 GitHub 按照簡單的指示來使用我們的開源組件

iOS 為動態圖像提供了什么

iOS 上顯示圖片的典型方式是用圖像數據創建一個 UIImage 并將其展現在屏幕上的某個 UIImageView 中。然而,雖然有超過一打的初始化方法,但沒有一個可以從單個的多幀圖像(注解2)里創建一個動態圖像。Image View 僅僅顯示第一個靜態幀,所以屏幕上沒有動畫。程序員必須用 Apple 的 ImageIO 框架將每一幀加載為一個單獨的圖像并使用UIImageView的 API:animatedImages、animationDuration以及animationRepeatCount來完成動畫。

這種方法的缺點是無法保證 GIF 的可變幀延遲。

具有可變幀延遲的五個幀。

讓我們假設幀和元數據的加載代碼在UIImage+animatedImage類別里并考慮如下代碼:

imageView.animationDuration = image.delayTimes[0] * animatedImage.frameCount;

imageView.animationDuration = image.totalDelayTimes;

很明顯代碼使用第一幀的延遲作為每一幀的延遲或預先假設全部幀延遲的長度,但這兩種方式都不會帶來很好的效果。

另一種選擇是使用UIWebView,這樣 GIF 就由 WebKit 負責解碼和顯示,就如同桌面瀏覽器那樣。

然而,Web View 沒有為在移動設備上播放 GIF 而做優化,經常會降低播放速度。同時也不能很好地控制回放過程或內存占用。

實現你的自定義播放

一個用UIImageView顯示并支持可變幀延遲的方式是找到所有幀延遲的最大公約數并將延遲稍長的幀多放入幾個到對應的animatedImages數組里。

譯者注:就是說,盡管每一幀的延遲都一樣,但因為原本延遲長的那些幀因為相應的多放了幾個進去,這樣播放時,整個動畫的感覺就不會變。


槽的持續時間(Slot Duration)由 GCD 決定,是 1 秒。

某個幀第一次顯示在屏幕上時,壓縮的圖像數據會被解碼為未壓縮的位圖形式。這是一個較為消耗 CPU 的操作,因此第一次播放時會變慢。

更棘手的是內存占用;一旦圖像被解碼,位圖就附加到圖像對象上并會一直在它的整個生命周期里存在。這將使得我們的 Image View 緩存著所有這些巨大的(注解3)位圖數據——這幾乎是最糟糕的消耗內存的方式(注解4)。

當 Apple 給UIImage和UIImageView添加動畫屬性時,他們實際上是將其設計為用于小型的 UI 動畫,例如轉動的加載指示器,而不是用于大型的動態圖像。也許某些應用可以擺脫這種方法,但在我們的情況里,這樣做會使得我們要為每一個 GIF 添加一個播放按鈕而且一次只允許播放一個。這就將 GIF 的樂趣剝除了,對于我們的用戶體驗來說完全不可接受。

幀的按需產出和消費

在內存有限的情況下,若不能存儲問題的結果那就只能每次都重新計算。在我們的情況里,我需要在幀被顯示之前加載并解碼它,并將不在屏幕上的那些幀清除。這就是所謂的生產者-消費者問題;一個線程生產數據,另一個線程消耗它。我們需要一個生產者產出一個用于視圖的幀流,以按需消費它們。生產者在可用內存變少時會節流,而消費者將相應改變幀定時。譯者注:大概是說,內存緊張時,動畫就會變慢。


重要組件的概述已在 UML 中高亮顯示

FLAnimatedImage

FLAnimatedImage 用 GIF 數據初始化,之后它的工作就是在被通過- (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index請求時盡可能快地遞發那一幀,而且僅使用小部分內存。

它試著智能地根據圖像尺寸和內存情況選擇幀緩存的大小:如果是小的 GIF ,我們會試著將所有幀都放在內存里,減輕 CPU 的負擔。如果是很大的 GIF,我們會試著通過只緩存足夠用于實時回放的幀來降低內存占用。

當系統發出內存警告時,所有的動態圖像實例都丟棄所有位于離屏緩存(off-screen buffer)內的幀并倒回到按需解碼。過一段時間,它們會再次建立起緩存。如果發生了多次內存警告,它們將會保持在每一幀都按需解碼。設計這種行為時,避免最糟糕的用戶體驗(即應用崩潰)是非常重要的,為此我們寧愿降低播放速度。

FLAnimatedImageView

FLAnimatedImageView 可接受一個FLAnimatedImage并在內部使用一個CADisplayLink實時播放它。

為了避免處理復雜的鎖獲取(Lock Acquisition),我們將在沒有準備好消費者所需數據的情況下讓生產者返回nil,并保持顯示前一幀。這簡化了多線程代碼并稍慢一點得到所期望的結果(譯者注:大概指返回nil的那一幀),而且能正確地播放動畫。

設計一個封裝良好的插入式組件

FLAnimatedImage直接繼承自NSObject,因為它若繼承于UIImage能獲得收益其實很少。另一方面說來,FLAnimatedImageView完全兼容UIImageView子類并可立即投入使用以取代其位置;設置一個UIImage或一個FLAnimatedImage在其上都可以以我們所期望的方式正常工作。所以無論何處我們要顯示一個圖像,我們就使用FLAnimatedImageView,然后它就自動正確地處理剩下的事情。

當考慮架構時,創建一個自包含、可重用的組件是非常重要的。多個FLAnimatedImage-FLAnimatedImageView對依然可在沒有中央緩存時被使用。它們都尊重系統并且獨立地嘗試做到最好以成為里面的偉大公民(They're all aware of the system and independently try to do the best to be great citizens.)。

這個模塊有大約1000行的代碼并試圖堅持“做一件事并做好”的Unix哲學。目前,它已經經過良好測試,可用于生產環境。它依然還有改進空間,我們歡迎社區的貢獻者。

.gif

Flipboard 的讀者已創造了許多動態雜志,例如“GIF Me A Break”、“Goals goals goals!” 或 “Cat GIFs” 。GIF 不只是一個文件格式,它是一種文化,可以說是 Internet 的原生藝術形式。通過分享此工程挑戰以及源代碼,我們希望能貢獻它更長久的生命。

  1. 這里有個很長的歷史介紹,關于如何限制太快的 GIF。
  2. iOS 5 為UIImage添加了新的初始化方法以便創建一個“動態圖像”(由私有類_UIAnimatedImage支持),但它依然期望其圖像還是獨立的圖像。
  3. 一個 1MB 的 GIF 可變成 55MB 的未壓縮數據!(800x600 像素 * RGBA 4 字節每像素 * 30 幀)
  4. WWDC 2011 session “iOS Performance In Depth” 描述了堆對象只是冰山的一角。這可以用虛擬機跟蹤儀進行測量。“臟內存” 是需要監視的數據,它的內存沒有映射到文件,因此無法被清除。我們預繪制的圖像將在此類別里。注意到“Memory Tag 70”同樣來自圖像(ImageIO)。

特別感謝 Ryan、Evan、Troy、Eugene、Josh、Charles 以及 Chris 提供想法和改進建議。

有許多事情我們都沒有親眼所見,但并不能表示我們可以有意或無意地忽略它們。請耐心了解一段歷史:xx事件

譯者注:歡迎非商業轉載,但請一定注明出處:https://github.com/nixzhu/dev-blog

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!