作者 NewPan 关注

2017.07.15 14:24* 字数 3110 阅读 749评论 8喜欢 17

声明:我为这个框架写了四篇文章:
第一篇:[iOS]UINavigationController全屏pop之为每个控制器自定义UINavigationBar
第二篇:[iOS]UINavigationController全屏pop之为每个控制器添加底部联动视图
第三篇:[iOS]UINavigationController全屏pop之为控制器添加左滑push
第四篇:[iOS]调和 pop 手势导致 AVPlayer 播放卡顿


框架特性

✅ 全屏 pop 手势支持
✅ 全屏 push 到绑定的控制器支持
✅ 为每个控制器定制 UINavigationBar 支持(包括设置颜色和透明度等)
✅ 为每个控制器添加底部联动视图支持
✅ 自定义 pop 手势范围支持(从屏幕最左侧开始计算宽度)
✅ 为单个控制器关闭 pop 手势支持
✅ 为所有控制器关闭 pop 手势支持
❤️ 当当前控制器使用 AVPlayer 播放视频的时候, 使用自定义的 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.
  • 动画元素:自己添加的.
  • 百分比驱动:系统的.
  • 动画容器:系统的.

从上面的分析可以知道,我们只是自己定义了手势动画元素,但是百分比手势驱动动画容器都是系统的,所以问题只有可能出在百分比手势驱动动画容器上面。我想找到问题所在,所以逐个排除。

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 轴平移,来看下效果。

接下来我们把 speed 设置为 0 timeOffset 设为 0,再开始动画。

没有做动画,对吧?因为我们已经把 speed 设置为 0 了,那么 t = (tp - beginTime) * speed + timeOffset 这个方法的结果恒等于 0,所以不会有任何动画。接下来我们移动一下 offsetTime 滑条,更改一下上面公式的 timeOffset 的值,再看一下效果:

是不是和系统的 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>>

系统有一个私有类 UIViewControllerWrapperView,每个控制器(UIViewController 或者其子类,但是UINavigationController 和 UITabbarController 除外)在渲染到屏幕上的时候都被一个 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 手势的时候,里面究竟发生了什么,还是未解之谜。

看到这里,诸君各位可能要骂我没找到原因也敢写文章,而且还起了这么一个浮夸的标题。是的,这个骂名我担了,确实没有找到问题所在,但是我的标题也算比较谨慎,我用了一个调和,并不敢在标题里用解决这个字眼。而且诸君不要担心,虽然没找出原因,但是我已经找到了一个可实践的应对这个问题的方法。

我们来分析一下这个 view 的层次和结构,从 UIViewControllerWrapperView 开始,下面有三个等级相当的 view,依次是 UIImageViewJPTransitionShadowView、当前控制的 view。可以看到使用 JPNavigationController 来应对这个有视频播放的控制器的 pop 的时候,我会创建这三层 view 用来做动画。

  • 最下面的 UIImageView 里装的是上个界面的截屏。

  • 中间的 JPTransitionShadowView装的是一个模拟系统的阴影图片,事实上系统的动画还会更加细腻,在上一幅 3d 图中你可以找到一个 _UIParallaxDimmingView,顾名思义,这个 view 是用来模拟渐变色的。但是我还没有把这一点做进去。

  • 最上面一层是当前控制器的 view。

有了这三层以后,我会用手势来驱动这三层进行动画,以模拟系统的 pop 手势效果。代码太长了,我已经放在 GitHub 上了,这里就不贴了。

07.最后

至此,向诸君交了一份 60 分的考卷,GitHub 地址在这里 JPNavigationController。谢谢大家。

我的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。如果某篇文章刚好在你的实际开发中帮到你,又或者提供一种不同的实现思路,让你觉得有用,那就看看这句话 “坚持每天点赞的人,99%都是帅哥美女,再也不用单身了”。

我的文章集合索引

你还可以关注我自己维护的简书专题 iOS开发心得。这个专题的文章都是实打实的干货。
如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy上给我留言,以及访问我的 Github。

[iOS]调和 pop 手势导致 AVPlayer 播放卡顿相关推荐

  1. iOS 视屏播放卡顿

    问题:因为录制的原始视频帧数太大,导致的播放卡顿 解决办法:对视频数据做处理,对其进行压缩,设置合适的渲染帧的频率,每秒 10 帧无明显的反应.设置 AVMutableVideoComposition ...

  2. 《直播疑难杂症排查》之二:播放卡顿

    ##播放卡顿的表现 播放卡顿的表现总结下来包括但不限于以下这些: 频繁出现缓冲 播放不够流畅,画面一卡一卡的 ##常见播放卡顿问题排查 从代码层面来看,什么是卡顿?其实是指播放器渲染的帧率太低,比如: ...

  3. 直播疑难杂症排查(2) — 播放卡顿

    本文是 <直播疑难杂症排查>系列的第二篇文章,我们主要分析下如何排查播放卡顿问题. 1. 播放卡顿的表现 播放卡顿的表现总结下来包括但不限于以下这些: 频繁出现缓冲 播放不够流畅,画面一卡 ...

  4. 《直播疑难杂症排查》:播放卡顿

    原文来自七牛云,感谢原作者. 1.播放卡顿的表现 播放卡顿的表现总结下来包括但不限于以下这些: 频繁出现缓冲 播放不够流畅,画面一卡一卡的 2.常见播放卡顿问题排查 从代码层面来看,什么是卡顿?其实是 ...

  5. kodi android 卡顿,解决KODI v17/16在电视上不能打开4K播放卡顿的问题

    本帖最后由 sziboy 于 2018-4-5 16:16 编辑 接下来,解决4K片源播放卡顿的问题. 以为就可以开开心心看片了是吧?非也,1080P的还好,4K,特别是高码率4K的问题就来了,那个卡 ...

  6. 解决 js 长任务导致的页面卡顿(时间分片技术)

    时间分片技术 解决 js 长任务导致的页面卡顿 界面操作使用transform动画会使用GPU,不会让界面卡死 解决 js 长任务导致的页面卡顿 在web界面运行长时间代码时,会造成界面卡死,最新看了 ...

  7. foobar2000播放卡顿优化总结

    foobar2000播放卡顿优化总结 参考文档https://www.sohu.com/a/292643634_283167 第一类方法的第2中可以,可以改为524300kb,即500MB,或者按照自 ...

  8. PS占用CPU太高,导致电脑异常卡顿

    Adobe Photoshop(PS):占用CPU太高,导致电脑异常卡顿 1.软件环境 2.问题描述 3.解决方案 3.1.获取后台服务关闭工具 3.2.永久禁止`Adobe`后台服务 3.2.1.快 ...

  9. unity如何解决协程开启频繁导致的程序卡顿

    unity如何解决协程开启频繁导致的程序卡顿 一.协程 协程并不会在Unity中开辟新的线程来执行,其执行仍然发生在主线程中.当我们有较为耗时的操作时,可以将该操作分散到几帧或者几秒内完成,而不用在一 ...

最新文章

  1. python用Levenshtein计算文本相似度
  2. 《程序员修炼之道》读感
  3. HDU1247Hat’s Words(字典树)
  4. oracle将字段nullable设为Y,从DB模型中消除NULLable列的选项(为了避免SQL的三值逻辑)?...
  5. aws s3 python_Python 操作amazon s3 | 学步园
  6. java线程基础(一些常见多线程用法)
  7. python兼职程序员工资-Python 程序员的工资能有多高?
  8. 在matlab中产生dsp程序学习
  9. 通过WORD精灵在Word中批量添加页码,批量添加或者删除页眉页脚
  10. ping通服务器和telnet通端口
  11. 如何在Android状态栏中隐藏图标
  12. win2003服务器360修复漏洞打不开网页,360浏览器打不开网页,教您怎样解决360浏览器打不开网页...
  13. Android7.0(Android N)适配教程,心得
  14. 【Android】模拟返回键、菜单键、Home键
  15. 阅读 | 皮囊与灵魂
  16. 微信小程序:设置字体跟随手机系统
  17. 华为又走在美国芯片企业前面,将率先发布5nm工艺芯片
  18. Null pointer access: The variable xxx can only be null at this location 解决方案
  19. 企业如何进行融资(企业融资的6大正确方式)
  20. javascript实用例子

热门文章

  1. MySQL更新命令_UPDATE
  2. RAP Mock.js语法规范
  3. Laravel Predis Error while reading line from the server.
  4. [转载] linux、Solaris下xdmcp远程桌面服务
  5. 如何破解压缩文件密码-省时省力的方法
  6. css中绝对定位中的left和top属性
  7. HDU2673-shǎ崽(水题)
  8. IHttpHandler 概述
  9. NDKJNI Android 相关资料整理(四)
  10. NDK JNI方式读写Android系统的demo(二)