我只是想要截个屏(续)

目录
x
  1. 几次失败尝试
  2. WKWebView截图方案
  3. 总结
    1. 参考文献

上两天写了一篇《我只是想要截个屏》的博文, 来描述了在书写SwViewCapture中遇到的一些坎坷和解决方案。在《我只是想要截个屏》中并没有找到针对WKWebView的全内容截图的相对完美的解决方案, 只是用一种滚动的暴力的方式去截图然后组装临时解决。

本文主要在上篇文章中做一些粗略的补充, 来描述SwViewCapture中是怎么更好的解决WKWebView的截屏问题, 还有怎么找到这种取巧的解决方案的~

PS: 如果大家想直接看实现原理, 请跳过几次失败尝试章节~

几次失败尝试

阅读过《我只是想要截个屏》的童鞋们可能都知道, 对于WKWebView的截图, 只能使用View的drawViewHierarchyInRect:afterScreenUpdates:方法去获取截图。在此前我曾尝试用如下几种方案去截图, 均以失败收尾。

  1. 将WKWebView的frame拉长和ContentSize的高度保持一致, 然后截图
  2. 将WKWebView的frame拉长和ContentSize的高度一致, 然后通过WKWebView的snapshotViewAfterScreenUpdates获取的view进行截图
  3. 对WKWebView内部的WKContentView直接截图
  4. 将WKScrollView对应的Screen进行拉伸, 然后对WKWebView进行等价拉伸, 再截图
  5. 使用私有API_snapshotRect:intoImageOfWidth:completionHandler

上述第一、二、三种方法是笔者自己脑洞尝试, 可是截图要么完全是空白, 要么就只能显示屏幕区域的图。

第四种和第五种是对WKWebView源码不了解的窥看后, 进行一种投机取巧尝试。

既然已经实在找不到解决方案了, 笔者就去官网下载源码, 希望能够找到突破口。WKWebView是开源的, 其源码放置在苹果官方开源网站http://opensource.apple.com中, 项目名字为WebKit2。

笔者以为下载到源码了, 至少能够找到一个突破口, 在打开工程项目后, 笔者就发现自己错了, 这个工程太庞大了。。。

WKWebView的组成笔者尚不熟悉, iOS的WKWebView底层更多的是WebKit的底层实现, 如果彻底从理解去阅读代码, 估计半个月甚至大半年都不一定读的完~ 有这个心思去阅读代码, 还不如先去阅读《WebKit技术内幕》这本书~

笔者自知不可能从阅读理解源码进行着手, 那就只能直奔要点: 关键字跟踪!笔者一开从WKWebView.mm文件进行突破, 去寻找遮盖关键字unobscured, 从这个关键字中发现遮盖区域和scrollView的window相关, 因此尝试第四种方法, 修改window的大小~ 失败的结果唯一能够告诉笔者的就是: 没有找到遮盖视图不渲染的根源!

笔者在第一次寻找关键字失败后尝试从snapshot这个关键字去突破, 结果发现了私有API_snapshotRect:intoImageOfWidth:completionHandler。这也是第五种方法的尝试来源。通过snapshot关键字其实还发现了隐藏在WKWebView.mm底下的_takeViewSnapshot方法, 可是该方法返回的对象是C++对象, 笔者就没有从Object-C层级对方法进行调用尝试。

结合snapshotunobscured两个关键字的搜索, 笔者在底层一串跟踪, 发现了WebPage、DrawingArea等一系列概念, 笔者偶然间在WebPage的初始化方法中发现有个WebPageCreationParameters参数作为构造WebPage的初始参数, 其中包含了如下几个参数

1
2
3
4
5
#if PLATFORM(IOS)
WebCore::FloatSize screenSize;
WebCore::FloatSize availableScreenSize;
float textAutosizingWidth;
#endif

通过全局搜索availableScreenSize, 在WebPageProxyIOS.mm源码中发现, WebPage的屏幕尺寸是根据WKGetAvailableScreenSize()WKGetScreenSize()获取的, 核心代码如下:

1
2
3
4
5
6
7
8
9
FloatSize WebPageProxy::screenSize()
{
return FloatSize(WKGetScreenSize());
}

FloatSize WebPageProxy::availableScreenSize()
{
return FloatSize(WKGetAvailableScreenSize());
}

终于有些眉目了, 一全局搜索WKGetAvailableScreenSize崩溃了~ 在WebKit2开源中并没有这个方法的定义, 并且无法通过GoogleApple Developer搜索到相关信息… T_T

最鸡血的来了, 跟踪了几个小时, 笔者放弃了… 没错, 笔者直到最后都没有从源码中找到解决方案~ =。=

WKWebView截图方案

虽然没有通过源码找到解决方案, 但是通过改变Window的尝试让我的脑洞打开, 想到了另外一种和滚动截图很相似的暴力的解决方式。

PS: 滚动截图是笔者在我只想要截个屏中所描述的暴力解决截图的方式。实现方式就是滚动一页截取一页, 最后组装成一张长图。

笔者想: 既然WKWebView的渲染区域是屏幕范围固定的, 那我不滚动视图, 不断的往上推视图呢?

不断往上推视图的意思就是改变View的origin的y轴, 每截取一张图片后去上移View的高度(高度等价于该WKWebView在界面中的显示范围)和拉长WKWebView的总高度, 直到截取到了最后一张图并组装。

这个思路有个小小的问题, 就是笔者曾经尝试通过放大WKWebView本身去截图, 但是却截出一片空白的情况。透过这个问题可以假设, 我不断上移y轴并放大高度的最后一张情况和上述有问题的情况完全一致, 可以猜测这个方法是无法正确的截取WKWebView的图的。

笔者用了一种很巧妙的方法去躲避了这个问题, 就是去截取WKWebView的父视图, 因为无论WKWebView怎么改变, 通过WKWebView父视图截图是可以正确获取对应的界面的(笔者实验的)。

通过优化后大致的流程如下:

  1. 基于WKWebView的尺寸伪造一个UIView, 并拉长至ContentSize高度
  2. 将伪造的UIView作为WKWebView的父视图
  3. 放置一张大画布长度和WKWebView的ContentSize高度一致
  4. 对父视图进行普通截图并放置在大画布中
  5. 将WKWebView的高度上移一个父视图的高度
  6. 循环执行步骤3和步骤4直到总高度和WKWebView的ContentSize高度一致
  7. 读取画布中的图像并返回

大致思路如图:

WKWebView合成示意

思路核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let containerView  = UIView(frame: self.bounds)

self.removeFromSuperview()
containerView.addSubview(self)


let totalSize = self.scrollView.contentSize
let page = floorf(Float( totalSize.height / containerView.bounds.height))

UIGraphicsBeginImageContextWithOptions(totalSize, false, UIScreen.mainScreen().scale)

for index in 0...Int(page) {
// async for, action need package a method

var splitFrame = CGRectMake(0, CGFloat(index) * containerView.frame.size.height, containerView.bounds.size.width, containerView.frame.size.height)
var myFrame = self.frame
myFrame.origin.y = -(CGFloat(index) * containerView.frame.size.height)
self.frame = myFrame
containerView.drawViewHierarchyInRect(splitFrame, afterScreenUpdates: true)
}

let capturedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

通过这种方式果然可以截取完整的WKWebView, 并且不存在position: fixed;的标签重复的问题。

上述代码起示意作用, 实际循环部分需要等待延迟, 因为需要等待WKWebView在改变frame之后准备完毕执行下一次循环。

总结

因为WKWebView只能渲染屏幕范围大小左右的视图范围, 因此笔者就利用这个点, 不断的去改变WKWebView的frame去截图, 然后组装成为一张内容截图。通过这种方式可以巧妙的躲避过因为滚动视图产生的部分页面元素重复的问题。

其实WKWebView现在在iOS开发应用中并没有UIWebView广泛, 做截图相关功能的开发者也可能会优先采用UIWebView最为搭载容器, 但是多多少少本篇文章应该还是会帮助到一些使用WKWebView的先驱者的~

另: 本文提供的解决方案可能只是众多解决方案的其中一种, 并且相当的耗时也消耗内存, 希望大家可以一起想想能否有更优的解决方案~ 希望多多交流~ 如果有更好的方案, 跪求Pull Request或者提交issueSwViewCapture

PS: 鉴于个人水平有限, 有错误之处, 请大家及时指出~ 谢谢~

参考文献

  1. Apple Open Source - WebKit2
  2. iOS Developer library - WKWebView
  3. 我只是想要截个屏
如果您觉得文章有用, 打赏一个呗~~ ( ⊙ o ⊙ )/
本站总访问量 本站访客数人次