从截图到投影的探索

截图

这是一个老生常谈的话题,但是可否想过高频截图下,速度不够而导致各种主线程卡顿问题。先来看看各种截图的方法吧,然后再来个总结表


常规方法:renderInContext

此方案的好处在于历史悠久,稳定,保证一定能截出内容。比如我们目前去截取View A的截图,renderInContent会去寻找View A根Layer的contents内容是否可以被渲染到CGContext上,再逐层渲染View A上的子视图。可以理解成从缓存中进行渲染。但是关键是这里渲染的时候,并不单单获取缓存的contents。如果View A及其子视图中存在CATiledLayer,并且drawRect方法被实现的话,那将会引发一连串的drawRect方法被调用,可想而知,为啥这种方法截图存在着风险。在不会触发的情况下,频繁截半屏幕iPad图,能达到55fps。但是一旦使用类似CATiledLayer的情况下,就下降到16fps。下降之速何其恐怖。

对了,可能存在使用CATiledLayer比较复杂的情况下,直接用根视图的Layer是无法正确截图,这个时候,需要用layer.presentationLayer来调用renderInContent才能截取到正确的图片。


iOS7 新出的截图API:drawViewHierarchyInRect

此方法呀,速度还是可以滴。无论是使用CALayer,还是CATiledLayer,其帧频依然能达到24fps。它是完全利用view内已经渲染好的contents进行截图。并不会走drawRect这个会弹性变动的方法。

注意,此方法当使用CATiledLayer时,你会发现真机上截图什么内容都没有,坑爹呀。我的猜测是,源于其layer的contents是CAImageProvider,而不同于普通的layer的contents(CABackingStore)。我想苹果官方只是实现了基本视图的截图(截止至iOS9)。提醒下哟,此方法只能获取到设备可视区域


iOS7 新出的截屏视图API:snapshotViewAfterScreenUpdates

这个方法截取的并非图片,而是与原视图一摸一样的copy View(_UIReplicantView)。在官方的文档中讲述,此方法用于做动画,因为如果对原视图进行一些操作,可能存在刺激里面的重绘等损耗性能的事情。而这个UIReplicantView只是和原视图一致的图像信息,但是各种操作并不会引起什么事情。它的速度非常快,能达到60fps。

注意:此方法只能获取到设备可视区域


截图性能表

ScreenShot Speed

从上图:1、我们可以看出,renderInContext在渲染出一张图时,内存增长相较于其他两个方法还好。但是是否想过一个问题,如果大量快速截图时,内存又没及时释放,是否这是一个恐怖的增长。2、如果当我们不需要一张图,使用一个视图也可以解决问题的时候,snapshotViewAfterScreenUpdates会是一个最好的选择

# 投影的世界

投影是什么,把小屏幕的内容投影到一个大屏幕(世人都知道的事情T-T)。可能会有人问苹果官方一插线,或者AirPlay都可以做到无线投影,为什么还要如此麻烦另外做投影功能?官方的投影虽好,是否有想过一个问题是,我们的投影是否达到最大利用化了?当前很多屏幕是4:3,16:9,16:10等等比例,而苹果官方投影,是把当前小屏幕的比例投影到大屏幕上。这样就存在大屏幕可能存在黑边。这也是官方的无奈,毕竟它不知道如果压缩了小屏幕的内容,是否开发本身兼容自动适配。那如果自行控制,也就是要有高效图像信息流。


有线投影

有线投影相对简单,它就是多了一个UIScreen,这个就是拓展屏幕的视图。我们只需要在上面创建viewcontroller、window。然后剩下的类似平时写代码操作界面一样。这里顺便提下,为什么上面我提到snapshotViewAfterScreenUpdates这个截取到视图,而不是图片的方法。就是为了这里埋下伏笔,因此有线投影的天然优势,让我们有了环境可以使用此方法。它也成为了截图最快,内存使用最少的最佳方案

有线投影:明显的优点传输不存在问题,但是就存在演讲者操作并不舒适。如果无线投影的性能跟得上,或许无线投影是一个更好的选择


无线投影:目前未深入探究,待有成效必会完善,请稍等


投影与截图的坑

我相信使用截图方式去投影的团队,很有可能遇到使用renderInContext会存在非常卡顿的效果。而使用iOS7 新截图方法也存在移动时,截图会出现黑影的问题。官方文档讲述过新方法建议在调用完layoutSubView之后才调用,不然可能存在图层未渲染完就开始截图了。官方也提供了一个参数(afterUpdates),虽然这个参数可以确保截出来的没问题,但也存在效率大大下降。下面来罗列一下我想的两个解决方案:

方案一:多线程截图(失败)

使用多线程推送?我默默使用了renderInContext/新的截图方式加入afterUpdates为YES。但是默默地忧伤起来,居然卡着不动了。原来内容在主线程刷新,而此时的我想要通过别的线程进行渲染,这可谓死锁。好,那我就想用快速的方法snapshotViewAfterScreenUpdates:NO的方法拿到UIReplicantView,再用UIReplicantView获取截图。最后的结果,又被系统的UIReplicantView坑爹了,其layer的contents(CASlotProxy)也是无法再次获取到图片信息,类似上面的CATiledLayer(CAImageProvider)。在网上和一位小哥聊过,他在webView上也曾经有过这个想过,但只是iOS7有效,后来就没效了(Github项目:AKWebRenderer)。

方案二:CFRunloop抓渲染时机(成功)

既然新的截图方法,我们无论使用afterUpdates为YES/NO都有其存在的问题。但是我要确保速度能够保证,我毅然地选择了NO(afterUpdates)。获取一个可能存在不完整的截图/视图。此时我就在想,是否我可以抓住渲染完成的一瞬间,然后在那一瞬间截图。这个时候我们需要使用到较为底层的CFRunloop。

CFRunloop

CFRunloop逻辑结构

从上图,我们可以看到会调用代码块Source0,Timer,然后就会进入到线程休眠,等待下一个Source0,Timer的唤醒。是否我们可以监听这个时刻,进行截图就会获取到完成时机下的截图/视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数指针
static void multiDiplayRunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
if (activity == kCFRunLoopAfterWaiting)
{
// do something
}
}

// 添加Runloop观察者
CFRunLoopObserverContext context = {0, (__bridge void*)self};
CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAfterWaiting, NO, 0, &multiDiplayRunloopObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, kCFRunLoopCommonModes);

# 题外话

是否有团队遇到过就是想要有个截图,而不是一个视图。drawViewHierarchyInRect在理论上优于传统的renderInContext。因为如果子Layer中存在CATiledLayer,或者WebView等可能都存在真机drawViewHierarchyInRect无法截取实际屏幕显示的内容。欢迎使用项目LSTiledLayer点击这里会介绍此Layer的特点

如果对以上内容觉得有问题、或者有别的想法,快快邮件我,我等得好着急!!!就这么多先啦!!!