关于ReplayKit,现今苹果开放的能力还十分有限,所以充斥着各种各样的”奇技淫巧“
屏幕录制组件
近年来,由于娱乐方式的移动化和手游化,手机屏幕录制乃至录屏直播一直都是一个呼声很高的需求。但是由于众所周知苹果的封闭保守,直到iOS10,苹果才陆续开放了
直到iOS12,才十分”吝啬“地提供了RPSystemBroadcastPickerView
这个应用内发起系统级录屏的功能组件。并且,RPSystemBroadcastPickerView
其实只能算是一个Button,需要添加到UIView上并让用户手动点击,然后才会触发真正的录屏控制器。
RPSystemBroadcastPickerView
本身只提供了设置preferredExtension和showsMicrophoneButton这俩功能,而且这个按钮本身并不是很美观。因此大多数情况下,我们会用自己的开始按钮,然后隐藏RPSystemBroadcastPickerView,通过给里面的按钮sendAction的方式触发。
1 | for (UIView *v in self.sysBroadcastPicker.subviews) { |
用这种方式,我们可以仿佛是自己的Button调起了系统的录屏广播页面,同时配置好preferredExtension,让可用列表里面显示我们自己的APP。
(其实直接去系统组件面板开始录屏也是一样的,但是要确保勾选到自己的APP。
发起录屏后自动隐藏
在启动了系统的录屏页面后,我们选中指定的APP,会有一个3秒倒计时,倒计时结束后,我们的BroadcastExtension会收到开始录屏的回调,但是这个时候系统录屏页面并不会自动关闭,也没有提供关闭的方法,需要用户手动点击关闭,但是这样对用户体验不太友好(说好的傻瓜式操作)。那有没有什么另辟蹊径的方法,自然是有的,虽然这其中有一个大坑,这个等下再说。
上图这个页面在弹出的时候,可以看到这应该是一个系统级的页面,并不依附在我们的APP中,但是更重要的是,我注意到, 这个页面在调起的时候,我们的APP会触发- (void)applicationDidEnterBackground:(UIApplication *)application
退到后台的方法。而在点击投屏页面后,会自动关闭并且APP进入前台。
然后我有一个大胆的想法,那我们能不能强行激活我们的APP,让我们的APP进入前台呢?
首先想到的,自然是最简单的SCHEME方式,我们配置好自己APP的的SCHEME,然后直接用openURL的方式唤醒APP。
1 | // 因为系统开播控件会使本APP进入后台,可以用SCHEME唤醒自身 同时可以关闭系统界面 |
试一下,发现It work!
看起来非常顺利,然而事情并不简单。
iOS13.x以下系统中奇怪的调起
功能开发完成后,在进行多机型的测试中,发现有部分系统会出现无法自动关闭的问题。然后在实际测试中,发现并不是所有系统都是全屏覆盖的方式,在iOS13及以下的系统,在某些机型中会出现另外一种样式的系统录屏页面。(并不准确,因为测试机有限)
从表现上看,这个页面很像是调用了presentModalViewController的方式进行展示的,而且发现APP也没有退到后台和进入前台,因此我们openURL的方式肯定是不会生效的。
连续触发调用两次RPSystemBroadcastPickerView
的button,然后出现了以下错误 Application tried to present modally an active controller
,确认了的确是使用了presentModalViewController
的方法。
接着通过试验,发现并非是在原本的ViewController上进行的,猜测是使用了一个新的UIWindow。
但是在打印[UIApplication sharedApplication].windows
以及使用Xcode的UI运行时调试,都没有发现这个Window的出现。
这window还能隐身不成?
最后感谢万能的Reveal,终于发现了它的庐山真面目 —— RPModalPresentationWindow
。
1 | /* Generated by RuntimeBrowser |
RPModalPresentationWindow
会直接调用presentViewController,而这个controller就是
1 | /* Generated by RuntimeBrowser |
另外,由此可见,UIApplication的getWindows方法里面是会过滤掉到一些系统级的window的。
言归正传,虽然我们知道了系统是创建了一个RPModalPresentationWindow
然后presentRPBroadcastPickerStandaloneViewController
,但是我们拿不到这个window,又要如何调用dismiss呢。
这就要用到Objective-C著名的黑科技,方法Swizzle了。
其实即使我们不知道上面两个具体的window和controller,我们也可以替换掉全局的present,然后观察所有的vc,也能发现端倪。
1 | @implementation UIViewController (hack) |
重新启动,顺利发现可以被捕获到,然后自然成功地Dismiss关闭了页面,大功告成。
还有个小完善
上面的代码其实有个小坑,在无意中发现如果在同一时间连续调用presentViewController方法, 会触发一个系统异常Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active controller
。
其实这也是通用的一个注意点,我们正好可以在这里加上一个判断,可以在全局范围内避免这个问题:
1 | - (void)swiz_presentViewController:(UIViewController *)vc animated:(BOOL)animated completion:(void(^)(void))completion |