[iOS]调和 pop 手势导致 AVPlayer 播放卡顿
声明:我为这个框架写了四篇文章:
第一篇:[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,依次是 UIImageView
、JPTransitionShadowView
、当前控制的 view。可以看到使用 JPNavigationController
来应对这个有视频播放的控制器的 pop 的时候,我会创建这三层 view 用来做动画。
最下面的
UIImageView
里装的是上个界面的截屏。中间的
JPTransitionShadowView
装的是一个模拟系统的阴影图片,事实上系统的动画还会更加细腻,在上一幅 3d 图中你可以找到一个_UIParallaxDimmingView
,顾名思义,这个 view 是用来模拟渐变色的。但是我还没有把这一点做进去。- 最上面一层是当前控制器的 view。
有了这三层以后,我会用手势来驱动这三层进行动画,以模拟系统的 pop 手势效果。代码太长了,我已经放在 GitHub 上了,这里就不贴了。
07.最后
至此,向诸君交了一份 60 分的考卷,GitHub 地址在这里 JPNavigationController。谢谢大家。
我的文章集合
下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。如果某篇文章刚好在你的实际开发中帮到你,又或者提供一种不同的实现思路,让你觉得有用,那就看看这句话 “坚持每天点赞的人,99%都是帅哥美女,再也不用单身了”。
我的文章集合索引
你还可以关注我自己维护的简书专题 iOS开发心得。这个专题的文章都是实打实的干货。
如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy上给我留言,以及访问我的 Github。
相关文章:

Cocos2d-x学习笔记(三十)之 游戏存档
游戏中的存档功能可以保证玩家在游戏过程中有足够的延续性,这点在单机游戏开发中尤为重要。Cocos2D-x中支持的游戏存档类CCUserDefault可以作为一个轻量化的数据库来使用。它支持存储的数据类型包括bool(布尔型)、int(整型&#x…

github删除文件夹
git rm -rf dirgit add .git commit -m remove dirgit push origin master //dir是要删除的文件夹路径转载于:https://www.cnblogs.com/xulei1992/p/5650399.html

Web漏洞扫描(三:Burp Suite的基本操作)
任务二、Burp Suite基础Proxy功能; 2.1、在Kali虚拟机中打开Burp Suite工具并设置,打开“Proxy”选项卡,选中“Options”子选项卡,单机“Add”按钮,增加一个监听代理,设置为127.0.0.1:8080; 2.…
UITableView嵌套WKWebView的那些坑
最近项目中遇到了一个需求,TableView中需要嵌套Web页面,我的解决办法是在系统的UITableViewCell中添加WKWebView。开发的过程中,遇到了些坑,写出来分享一下。 1.首先说一下WKWebView的代理方法中,页面加载完成后会走的…

深入了解line-height
1.定义 行高:两行文字baseline(基线)之间的距离 示意图: 2.为何line-height可以让单行文本垂直居中 其实并没有垂直居中,除非将font-size:0; 3.line-height的高度原理(可以先看看行内盒子的原理) * 行内元素的高度是lin…

实现一个简单的投票功能
实现一个简单的投票功能 最近项目中需要用到一个投票功能,当时觉得简单,向都没想就动手开始做,没想到走了不少弯路。 后来才发现,是想的太过简单了。来看看改进后的功能。 第一步:数据库设计 两个表:一个主…

Web漏洞扫描(四:知识点及错误总结)
WVS软件: WVS(Web Vulnerability Scanner)是一个自动化的Web应用程序安全测试工具,它可以扫描任何可通过Web浏览器访问的和遵循HTTP/HTTPS规则的Web站点和Web应用程序。适用于任何中小型和大型企业的内联网、外延网和面向客户、雇员、厂商和其它人员的W…
【VS开发】【智能语音处理】Windows下麦克风语音采集
简介 这是我很早以前的大学毕业设计,忽然间找到贴出来以纪念自己的纯真年代...但是因为CSDN不给面子所以导致短短的一篇文章贴了足足7次..他老提时说文章超过了64K,老大,拜托,那是算上了里面的图片大小吧...:-( 本文简单介绍了声卡的工作原理 , 录音的原理以及数字音频的基本知…

iOS音频——AudioToolbox
一、前言 二、音频文件Audio File Services 三、音频文件转换Extended Audio File Services 四、音频流Audio File Stream Services 五、音频队列Audio Queue Services 一、前言 AudioToolbox提供的API主要是C 使用起来相对晦涩,针对本文提供了简单的代码示例减小学…

【WA】九度OJ题目1435:迷瘴
题目描述: 通过悬崖的yifenfei,又面临着幽谷的考验——幽谷周围瘴气弥漫,静的可怕,隐约可见地上堆满了骷髅。由于此处长年不见天日,导致空气中布满了毒素,一旦吸入体内,便会全身溃烂而死。幸好y…

云端应用SQL注入攻击
实验目的及要求: 完成VMware Workstations14平台安装,会应用相关操作;完成Windows server2008R2操作系统及Kali Linux操作系统的安装;掌握SQLmap攻击工具的使用;使用SQLmap对目标站点进行渗透攻击; 实验环…

SPOJ375(树链剖分)
题目:Query on a tree 题意:给定一棵树,告诉了每条边的权值,然后给出两种操作: (1)把第i条边的权值改为val (2)询问a,b路径上权值最大的边 分析:本题与HDU3966差不多,区别就是&#…

简单的python服务器程序
一个接受telnet输入的服务器端小程序 #!/usr/local/bin/python3.5 #coding:utf-8 import sockethost port 51423s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen(1)print(S…

iOS:一句代码实现文本输入的限制
前言 实际开发中,往往需要处理UITextView、UITextField输入的限制。比如输入必须是价格格式(一个小数点、小数点后面最多两位);输入最大长度限制;对输入内容的实时回调。处理这些的时候,我们通常需要做一些…

Linux操作系统(一:基本操作)
1、创建一个以自己姓名拼音简写一致的用户名( useradd -d /home/abc abc) 2、在linux中使用打印命令,在命令行中输入“当前用户名Hello world !” 3、显示现在天年月日,显示后一天的日期,显示上一月日期,显…

Core Text 学习笔记-基础
前言 最近在学习YYKit框架,看到关于CoreText相关的知识的时候感到非常吃力,于是乎就恶补了一下Core Text相关的基础知识。 Glyphs(字形) 字符的图形形式, 则是文字中字母 (character) 的视觉表现。(字形&am…

虚拟机下CentOS 6.5配置IP地址的三种方法
实验软件环境:虚拟机Vmware Workstation10.0 、CentOS 6.5 32位 1、自动获取IP地址 虚拟机使用桥接模式,相当于连接到物理机的网络里,物理机网络有DHCP服务器自动分配IP地址。 dhclient 自动获取ip地址命令 ifconfig 查询系统里网卡信息&…

GIS 相关知识扫盲
1、什么是GIS GIS:地理信息系统,它是一种特定的十分重要的空间信息系统。它是在计算机硬、软件系统支持下,对整个或部分地球表层(包括大气层)空间中的有关地理分布数据进行采集、储存、管理、运算、分析、显示和描述的技术系统。2…

Linux操作系统(二:shell脚本)
练习一:编写shell脚本,计算1-100的和; 练习二:将一目录下所有的文件的扩展名改为bak 练习三:写一个脚本,统计。/etc/ 目录下共有多少个目录文件 练习四:写一个脚本,依次向/etc/passw…

用OpenGLES实现yuv420p视频播放界面
背景 例子TFLive这个项目里,是我按着ijkPlayer写的直播播放器,要运行需要编译ffmpeg的库,网盘里存了一份, 提取码:vjce。OpenGL ES播放相关的在在OpenGLES的文件夹里。 learnOpenGL学到会使用纹理就可以了。 播放视频,就是把画面一…

C++类的静态成员详细讲解
在C中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。 静态成员的定义或声明要…

jenkins2 multibranch
通过multibranch类型的pipeline job使得对于多个branch的支持更加简单。只需要创建一个multibranch job,jenkins将自动地为所有的branch创建job。 文章来自:http://www.ciandcd.com文中的代码来自可以从github下载: https://github.com/ciand…

Nagios的安装和基本配置(一:知识点总结及环境准备)
实验目的及要求 掌握Nagios监控的基本使用;掌握Nagios监控服务的搭建和配置; 实验环境: 1、满足实验要求的PC端; Host-name OS IP sofaware Nagios-server Centos7 192.168.1.119 Apache,php,Nagios,Nagios-plguins Nag…

浅谈Android四大组件之Service
一:Service简介 Android开发中,当需要创建在后台运行的程序的时候,就要使用到Service。 1:Service(服务)是一个没有用户界面的在后台运行执行耗时操作的应用组件。其他应用组件能够启动Service,并且当用户切…

使用 fastlane 实现 iOS 持续集成(二)
本文接上篇文章主要说下怎样使用 fastlane 上传到fir和蒲公英,下面先介绍下 plugin 命令。 plugin命令介绍: 列出所有可用插件 fastlane search_plugins 搜索指定名称的插件: fastlane search_plugins [query] 添加插件: fastlane add_plugin [name] 安装插件: fast…

Nagios的安装和基本配置(二:Nagios-Server的安装)
任务二、Nagios-server的安装 2.1、创建Nagios用户和组 注: #useradd Nagios -s /bin/nologin #groundadd nagcmd #usermod -a -G nagcmd Nagios #usermod -a G nagcmd apache 2.2、安装Nagios 2.2.1、上传软件包至操作系统中; 2.2.2、解压软件并…

shell编程-正则表达式
1.正则表达式是什么 它主要用于字符串的模式分割,匹配,查找及替换操作。 2、正则表达式与通配符 正则表达式用来在文件中匹配符合条件的字符串,正则包含匹配。grep,awk,sed等命令可以支持正则表达式。 通配符用来匹配符合条件的文件名&#x…
使用 CocoaPods 给微信集成 SDK 打印收发消息
推荐序 本文介绍的是一套逆向工具,可以在非越狱手机上给任意应用增加插件。在文末的示例中,作者拿微信举例,展示出在微信中打印收发消息的功能。 这套工具可以加快逆向开发的速度,其重签名思想也可以用于二次分发别人的应用。 其实…

数据库之子查询四(多重,表复制)
一、多重子查询 select teaID,teaName,age,sex,dept,professionfrom tteacherwhere dept(select dept from teaIDt103265)and profession(select professionfrom tteacherwhere teaIDt103265)这里的子查询就是为了从表中提取出有效信息参与外部查询二、create table 语句中子查…

Nagios的安装和基本配置(三:Nagios-Client的安装)
任务三、Nagios-Client的安装 3.1、关闭防火墙和selinux 注: #systemctl stop firewalld.service #systemctl disable firewalld.service #vi /etc/selinux/config 3.2、配置环境 #yum install gcc glibc-common -y #yum install gd gd-devel openssl openssl…