[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

NewPan發表於2018-01-09

宣告:我為這個框架寫了四篇文章:

第一篇:[iOS]UINavigationController全屏pop之為每個控制器自定義UINavigationBar

第二篇:[iOS]UINavigationController全屏pop之為每個控制器新增底部聯動檢視

第三篇:[iOS]UINavigationController全屏pop之為控制器新增左滑push

第四篇:[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

框架特性

✅ 全屏 pop 手勢支援

✅ 全屏 push 到繫結的控制器支援

✅ 為每個控制器定製 UINavigationBar 支援(包括設定顏色和透明度等)

✅ 為每個控制器新增底部聯動檢視支援

✅ 自定義 pop 手勢範圍支援(從螢幕最左側開始計算寬度)

✅ 為單個控制器關閉 pop 手勢支援

✅ 為所有控制器關閉 pop 手勢支援

❤️ 噹噹前控制器使用 AVPlayer 播放視訊的時候, 使用自定義的 pop 動畫以保證 AVPlayer 流暢播放.

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

01.真有這麼回事?

做過視訊的朋友都知道系統的 pop 手勢會導致視訊畫面卡頓,沒做過的朋友都不敢相信,這絕對不是蘋果的風格,居然留了這麼一個坑。如果碰到這個問題,嘗試去網上搜關鍵詞pop 手勢 AVPlayer 卡頓,你搜不到太多有價值的解決方案。

因為我之前寫了一個導航控制器的輪子,還寫了一個視訊播放器的輪子,所以理所當然,我必須趟平這個坑。下面我們花幾分鐘一起來看一下我是怎麼做的。

02.思路分析

pop 手勢就是為了在大屏下能獲得更好的使用者體驗設計的。有了 pop 手勢,返回的時候不用非要點一下返回按鈕,只需優雅的右滑就能返回。但是系統的播放器會和 pop 手勢衝突,對於有追求的程式設計師來說,這樣做太影響使用者體驗了。

如果不做任何處理,系統在執行 pop 動畫的時候,視訊聲音仍然播放正常,但是畫面會阻塞會卡頓,等你取消 pop 手勢仍然回到當前頁面的時候,你會驚喜的發現,系統也知道畫面出問題了,所以飛快的向後查詢當前需要播放的那幀畫面,但是很遺憾,系統也找不到了,所以最後播放的時候,聲音和畫面對不上,或者畫面根本就不更新了,就卡在那裡,然後聲音一直在播放。

為了應對這個系統的 bug,開發者心裡一般是默唸一句...(此處略去三個字),然後在 -viewWillDisappear:裡寫下一行:

[self.player pause];
複製程式碼

可是別人的 APP 都沒這個問題啊,你看看騰訊視訊、嗶哩嗶哩、愛奇藝...

為了說明這個問題,我前段時間在公司內部分享上講了這個事情,這裡我簡單說一下。如果你自己對比一下這些實現了 pop 手勢不導致畫面卡頓的 APP,你會發現他們的 pop 動畫和系統預設的似乎有些不一樣,至於究竟有哪些不一樣,請諸君各位自己去自己觀察。

有了這樣的觀察以後,我們的思路似乎變得清晰起來,沒錯,就是自己實現 pop 手勢。

03.動手實現

思路有了,趕緊來驗證一下我們的思路吧。

我在 [iOS]UINavigationController全屏pop之為控制器新增左滑push 這篇文章裡詳細的說了如何實現 push 動畫,雖然現在 JPNavigationController 2.0 的具體實現已經全部重新寫過了,但是大致思路還是一樣的。為了保證內容不重複,我這裡就不再講一遍一樣的知識點了,如果你不知道怎麼實現,你去看那篇文章就好了。

我們的動畫結構仍然是在動畫容器上面新增我們當前要 pop 的 view 以及要 pop 到的元素的 view,然後用一個 UIPercentDrivenInteractiveTransition 百分比手勢來驅動整個動畫過程。按照這個思路實現以後,然後在要 pop 的頁面上新增了一個 AVPlayer 播放視訊,日了狗了,發現和系統的居然是一樣的卡頓。

這樣就比較鬱悶了,瞬間感覺自己方向錯了,有一種柯潔面對 AlphaGo 的趕腳。

但是從別的 APP 分析得到的啟發就是要自己實現這個 pop 動畫,這一點肯定沒錯。仔細想一下,pop 動畫整個過程有以下幾個部分:

  • 手勢:自己定義的 UIPanGestureRecognizer.
  • 動畫元素:自己新增的.
  • 百分比驅動:系統的.
  • 動畫容器:系統的.

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

從上面的分析可以知道,我們只是自己定義了手勢動畫元素,但是百分比手勢驅動動畫容器都是系統的,所以問題只有可能出在百分比手勢驅動動畫容器上面。我想找到問題所在,所以逐個排除。

04.如何實現自己的百分比手勢驅動類?

我們先來看 CAMediaTiming 協議下的一個屬性

/* Additional offset in active local time. i.e. to convert from parent
 * time tp to active local time t: t = (tp - begin) * speed + offset.
 * One use of this is to "pause" a layer by setting `speed' to zero and
 * `offset' to a suitable value. Defaults to 0. */

@property CFTimeInterval timeOffset;
複製程式碼

這裡說了動畫時間的計算方法 t = (tp - beginTime) * speed + timeOffset,比方說我們約定一個動畫在 0.25 秒內執行完成,系統預設 beginTime = 0,speed = 1 ,timeOffset = 0,這樣處理以後,這個計算式就變成了 t = tp。當動畫開始,tp 開始從 0 增長到 0.25,那麼動畫執行的進度 t = tp,也是從 0 增長到 0.25。

這裡還有一句 **"One use of this is to "pause" a layer by setting speed to zero and offset to a suitable value."**也就是說可以通過設定 speed = 0的方式來實現動畫的技術性暫停。

@interface CALayer : NSObject <NSCoding, CAMediaTiming>
複製程式碼

CALayer 的標頭檔案,我們可以看到 CALayer 是遵守了 CAMediaTiming 協議的。所以我們可以寫一個 demo 來模仿一下。

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UISlider *speedSlider;
@property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
@property (weak, nonatomic) IBOutlet UILabel *speedLabel;
@property (weak, nonatomic) IBOutlet UILabel *timeOffsetLabel;

@property(nonatomic, strong) UIView *animateView;

@end

@implementation ViewController

- (void)viewDidLoad{
    [super viewDidLoad];

    self.animateView = ({
        UIView *view = [UIView new];
        view.frame = self.containerView.bounds;
        view.backgroundColor = [UIColor redColor];
        [self.containerView addSubview:view];
    
        view;
    });
}

- (IBAction)updateSliders{
    self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", self.speedSlider.value];
    self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", self.timeOffsetSlider.value];

    CFTimeInterval timeOffset = self.timeOffsetSlider.value;
    self.animateView.layer.timeOffset = timeOffset * 0.25;
}

- (IBAction)play{
    CGRect rect = self.animateView.frame;
    rect.origin.x = rect.size.width;
  
    [UIView animateWithDuration:0.25 animations:^{
    
        self.animateView.frame = rect;
    
    }];
    self.animateView.layer.speed = self.speedSlider.value;
}

@end
複製程式碼

我們先把動畫速度 speed 設定為 1,timeOffset 設為 0,很簡單的動畫,就是一個 x 軸平移,來看下效果。

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

接下來我們把 speed 設定為 0 timeOffset 設為 0,再開始動畫。

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

沒有做動畫,對吧?因為我們已經把 speed 設定為 0 了,那麼 t = (tp - beginTime) * speed + timeOffset 這個方法的結果恆等於 0,所以不會有任何動畫。接下來我們移動一下 offsetTime 滑條,更改一下上面公式的 timeOffset 的值,再看一下效果:

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

是不是和系統的 pop 手勢有點像,這裡是用滑條的值(0 到 1)來驅動動畫的進度,系統是用手勢的位置的百分比來驅動 pop 動畫的進度,為此,系統專門抽出一個 UIPercentDrivenInteractiveTransition 來負責這個用手勢來驅動動畫的功能,叫做百分比手勢驅動。我們瞭解了這個知識點以後,就可以動手實現一個自己的 PercentDrivenInteractiveTransition 了。但是由於篇幅原因我不帶大家實現了,這裡只負責授人以漁。如果你感興趣,想要一探究竟,可以去看一下這篇文章 Interactive Custom Container View Controller Transitions

我自己實現了這個類,然後把這個類用到我們的 JPNavigationController 專案中來,但是並沒有能夠解決我們播放視訊卡頓的問題。至此 pop 動畫四個組成部分,我們排除了三個。

05.凶手真的是動畫容器?

雖然沒有成功實現我們的目標,但是我們知道了問題可能就出在系統提供的動畫容器上,事實上,當我們自己代理系統的 transition 動畫的時候,遵守 UIViewControllerContextTransitioning 協議的動畫上下文都會有一個 containerView 的屬性:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    _containerView = [transitionContext containerView];
}
複製程式碼

通過斷點攔截,我們可以看一下這個 containerView 是個什麼東西。

Printing description of self->_containerView:
<UIViewControllerWrapperView: 0x7fc40fe17810; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60800002f6c0>>
複製程式碼

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

系統有一個私有類 UIViewControllerWrapperView,每個控制器(UIViewController 或者其子類,但是UINavigationControllerUITabbarController 除外)在渲染到螢幕上的時候都被一個 UIViewControllerWrapperView 包裹。

通過我的測試,我使用 UIView 寫了一個進度條,通過更新 frame.size.width 方式,在執行 pop 手勢的時候新增定時器來更新這個寬度,進而達到進度條的效果,這裡進度條更新沒有問題。同樣的我把這個進度條的更新放到 AVPlayer 的播放進度回撥中,再執行 pop 手勢,這個時候進度條就不更新了。

[player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 10.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time){
   float current = CMTimeGetSeconds(time);
   float total = CMTimeGetSeconds(sItem.currentPlayerItem.duration);
    if (current && progress) {
         progress(current / total);
    }
}];
複製程式碼

由此我們排除了動畫容器的嫌疑,同時引出了凶手的另一個人選 AVPlayer

06.萬流歸宗

從一開始懷疑是動畫的過程有問題,到用排除法排除了所有的已知選項,最後一路順藤摸瓜找到 AVPlayer,這一切並不容易,而且似乎有一種禪宗的為萬流,皆歸宗的感覺。

到現在為止,我們所做的只是把矛頭指向 AVPlayer,但是這個類在系統執行 pop 手勢的時候,裡面究竟發生了什麼,還是未解之謎。

看到這裡,諸君各位可能要罵我沒找到原因也敢寫文章,而且還起了這麼一個浮誇的標題。是的,這個罵名我擔了,確實沒有找到問題所在,但是我的標題也算比較謹慎,我用了一個調和,並不敢在標題裡用解決這個字眼。而且諸君不要擔心,雖然沒找出原因,但是我已經找到了一個可實踐的應對這個問題的方法。

[iOS]調和 pop 手勢導致 AVPlayer 播放卡頓

我們來分析一下這個 view 的層次和結構,從 UIViewControllerWrapperView 開始,下面有三個等級相當的 view,依次是 UIImageViewJPTransitionShadowView、當前控制的 view。可以看到使用 JPNavigationController 來應對這個有視訊播放的控制器的 pop 的時候,我會建立這三層 view 用來做動畫。

  • 最下面的 UIImageView 裡裝的是上個介面的截圖。

  • 中間的 JPTransitionShadowView裝的是一個模擬系統的陰影圖片,事實上系統的動畫還會更加細膩,在上一幅 3d 圖中你可以找到一個 _UIParallaxDimmingView,顧名思義,這個 view 是用來模擬漸變色的。但是我還沒有把這一點做進去。

  • 最上面一層是當前控制器的 view。

有了這三層以後,我會用手勢來驅動這三層進行動畫,以模擬系統的 pop 手勢效果。程式碼太長了,我已經放在 GitHub 上了,這裡就不貼了。

07.最後

至此,向諸君交了一份 60 分的考卷,GitHub 地址在這裡 JPNavigationController。謝謝大家。

08.注意

注意: tabBar 的 translucent 預設為 YES, 使用 JPNavigationCotroller 不能修改 tabBar 的透明屬性. 這是因為 Xcode 9 以後, 蘋果對導航控制器內部做了一些修改, 一旦將 tabBar 設為不透明, 當前架構下的 UI 就會錯亂, 設定 tabBar 的 backgroundImage 為不透明圖片, 或者設定 backgroundColor 為不透明的顏色值也是一樣的會出錯.

NewPan 的文章集合

下面這個連結是我所有文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有原始碼。

NewPan 的文章集合索引

如果你有問題,除了在文章最後留言,還可以在微博 @盼盼_HKbuy 上給我留言,以及訪問我的 Github

相關文章