从截图到投影的探索

截图

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


常规方法: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的特点

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

笔迹绘制探索

线段基本组成结构

  • 点上的压感
  • 点上的高度与方向角

Alt Text
图1(通过手指采样,频率120次/秒)
> 我们可以看到充满棱角的线段,这是点的数目不够密集,无法做到最大程度的还原情况。在iOS上采样的情况是120次/秒。我们再看下面图2

右侧图2相对更加平滑,这条线段是通过Apple Pencil进行采样绘制,其频率达到240次/秒。能够更大程度还原点的情况

ApplePencil

通过以上图,我们看到如果需要绘制顺滑的线段,从实际中最简单的方式是通过采样频率更高来完成。但是这样就存在两个问题。1、采样数据变大,2、需要设备支持更大的采样率,或者需要外设(比如Apple Pencil)去支持。但是否我们可以通过算法去做更多的事情???


点上的压感

在我们常规的设备情况下,是无法获取到压感,如果要获取到真实的压感,必须通过传感器来完成。

Force Image

上图的横坐标表示第几个点,纵坐标表示点的压感值(从官方提供的接口直接获取,并无处理)。总的数据进行了排序,通过压感值的大小进行过排序,越排在后面的点,压感值越大。

通过 Apple Pencil 压感方面的采样,从图形上来看,都是比较渐变顺滑。没有出现断崖式的压感变化,可以看出Apple Pencil在压感方面做得挺细腻的。个人从数据上看,渐变的精度能达到小数后9位。实际上,我们在使用上肯定没那么精细,毕竟绘制上最小精度只能达到一像素。

这里插个小插曲,压感级别目前分1024,2048未来可能有更多(1024级适合于初学者及非专业人士,2048适合高级用户)。有些专业人士猜测Apple Pencil已经达到2048压感级别,但是官方并没有对此做出说法。这里我也想埋个问题:是否只有外设才能实现压感,是否有别的数据可以转化成压感(比如速度??)。有机会弄篇关于压感判断的文章吧,哈哈,继续下文


点上的高度与方向角

这两个属性,我想会比较陌生它的概念。可以这么理解,如果有了这两个数据,你就可以知道用户怎么拿着笔去触碰设备。来个图看看

Altitude-Azimuth Image

altitude(高度),azimuth(方位角)

有了以上的两个数据,其实就可以做到模拟画笔渐变过程。我们在拿铅笔进行画画的时候,总会遇到打阴影的过程,而这种画法画出来的线并不是均匀颜色深度,越靠近笔头的地方,颜色越深。如果没有以上两个数据,我们难以判断,哪里该深,哪里该浅。

# 通过绘制方法-绘制-平滑线段

是否有想过这么一个问题,当采样率不够的时候,我们如何继续画出平滑的线段

此处我用的是画出一个最接近原有轨迹的线段(信息实在不够,无法还原真实的轨迹)。二阶贝塞尔曲线

Bezier Image

一个起点,一个控制点,一个终点。形成最美好的弧线

绘制无粗细变化的线段

1
2
3
4
5
6
7
8
9
// iOS Code
CGPathMoveToPoint(path, NULL, pointFrom.x, pointFrom.y);
CGPathAddQuadCurveToPoint(path, NULL, controlPoint.x, controlPoint.y, pointTo.x, pointTo.y);

CGContextAddPath(ctx, path);
CGContextSetStrokeColorWithColor(ctx,penColor.CGColor);
CGContextSetLineWidth(ctx, lineWidth);
CGContextDrawPath(ctx, kCGPathStroke);

此处你可能会遇到一个问题,何以开始点,控制点,终止点。如果以采样的点,那岂不是要手指移动一个遥远的位置才能开始画,对于用户来说,会觉得很突兀。怎么线段突然就出现了。那第四个点出现,我又该怎么处理,不画???等第五个点出现吗??岂不是出现的线可能有时紧贴手指,有时又远离手指好怪

sign optimization Image

原设备采样点(红点),通过采样点得到的中点(蓝点),个人策略真正绘制的线段(绿色线段)

由于原来的采样点不足,贝塞尔曲线需要起点、控制点、终点。所以采取了计算出两个采样点的中点,来衍生出一个新的点。这样除了第一个采样点外,每增加一个采样点,就会增加一个中点。通过使用中点作为起点、终点,采样点作为控制点,就可以绘制出接近原轨迹的线段。如此一来,我们就可以绘制一条平滑的线,看下下面的效果图

signInk Image{: class=“bigger-image” }

# 粗细变化的笔迹

对于粗细变化的笔迹,我们将不能只是stoke轨迹,而需要的是fill这个轨迹,问题我们怎么去形成不规则的轨迹path,先看看效果图

strokeInk Image{: class=“bigger-image” }


以下只是个人策略,每个人都有其一套算法,我来讲讲我的

strokeInk Image

原设备采样点(红点),通过采样点得到的中点(蓝点)
黄线G、H上的蓝点,是线上绿点的中点。线上两个绿点的距离代表笔迹经过蓝点时的笔宽
A、B、C与H线垂直,D、E、F与G线垂直

我们可以通过线段的垂直关系,得出四个二元一次方程组计算橘色点的位置坐标。使用绿点-橘色点-绿点这样的关系可以在上方绘制一个贝塞尔曲线,下方一个贝塞尔曲线,再结合两个半圆,就可以弄出一个填充路径(Fill Path)

可以结合矩阵求解,更快哟

那个时候,我就在想,我的确是得到了两个橘色点的坐标,但是前提我还要知道绿点的坐标,我只知道绿点间的距离(笔迹经过蓝点的形成的笔宽我们是知道的,它等同于两个绿点间距离)。当时我就在想,我要求又要去求绿点的坐标,好累!!!再上图

perpendicularBisector Image

原设备采样点(红点F),通过采样点得到的中点(蓝点B)
蓝点B为绿点A、C的中点
线段BE、EF相互垂直,线段BD、AD相互垂直

从图关系可看出,三角形ABD与三角形BEF是相似三角形。我们已知点B、点F坐标,从而可以推算出BE,EF的长度。AC的长度我们也是知道的(笔迹经过蓝点的形成的笔宽我们是知道的,它等同于两个绿点间距离)。

根据相似三角形公式,我们能得到以下关系从而计算出AD、BD,再推算出A、C点的坐标位置

$$
\frac {AB} {BF} = \frac {AD} {BE} = \frac {BD} {EF}
$$

未完待续

关于如果加入高度、方位角目前还没做出什么实现,也不好说。谢谢大家用宝贵的时间看完此文章,如果有什么好想法,一起交流交流,邮箱你懂的