上兩天寫了一篇《我只是想要截個(gè)屏》的博文, 來描述了在書寫SwViewCapture中遇到的一些坎坷和解決方案。在《我只是想要截個(gè)屏》中并沒有找到針對(duì)WKWebView的全內(nèi)容截圖的相對(duì)完美的解決方案, 只是用一種滾動(dòng)的暴力的方式去截圖然后組裝臨時(shí)解決。
本文主要在上篇文章中做一些粗略的補(bǔ)充, 來描述SwViewCapture中是怎么更好的解決WKWebView的截屏問題, 還有怎么找到這種取巧的解決方案的~
PS: 如果大家想直接看實(shí)現(xiàn)原理, 請(qǐng)?zhí)^幾次失敗嘗試章節(jié)~
幾次失敗嘗試
閱讀過《我只是想要截個(gè)屏》的童鞋們可能都知道, 對(duì)于WKWebView的截圖, 只能使用View的drawViewHierarchyInRect:afterScreenUpdates:方法去獲取截圖。在此前我曾嘗試用如下幾種方案去截圖, 均以失敗收尾。
- 將WKWebView的frame拉長(zhǎng)和ContentSize的高度保持一致, 然后截圖
- 將WKWebView的frame拉長(zhǎng)和ContentSize的高度一致, 然后通過WKWebView的
snapshotViewAfterScreenUpdates獲取的view進(jìn)行截圖 - 對(duì)WKWebView內(nèi)部的WKContentView直接截圖
- 將WKScrollView對(duì)應(yīng)的Screen進(jìn)行拉伸, 然后對(duì)WKWebView進(jìn)行等價(jià)拉伸, 再截圖
- 使用私有API
_snapshotRect:intoImageOfWidth:completionHandler
上述第一、二、三種方法是筆者自己腦洞嘗試, 可是截圖要么完全是空白, 要么就只能顯示屏幕區(qū)域的圖。
第四種和第五種是對(duì)WKWebView源碼不了解的窺看后, 進(jìn)行一種投機(jī)取巧嘗試。
既然已經(jīng)實(shí)在找不到解決方案了, 筆者就去官網(wǎng)下載源碼, 希望能夠找到突破口。WKWebView是開源的, 其源碼放置在蘋果官方開源網(wǎng)站http://opensource.apple.com中, 項(xiàng)目名字為WebKit2。
筆者以為下載到源碼了, 至少能夠找到一個(gè)突破口, 在打開工程項(xiàng)目后, 筆者就發(fā)現(xiàn)自己錯(cuò)了, 這個(gè)工程太龐大了。。。
WKWebView的組成筆者尚不熟悉, iOS的WKWebView底層更多的是WebKit的底層實(shí)現(xiàn), 如果徹底從理解去閱讀代碼, 估計(jì)半個(gè)月甚至大半年都不一定讀的完~ 有這個(gè)心思去閱讀代碼, 還不如先去閱讀《WebKit技術(shù)內(nèi)幕》這本書~
筆者自知不可能從閱讀理解源碼進(jìn)行著手, 那就只能直奔要點(diǎn): 關(guān)鍵字跟蹤!筆者一開從WKWebView.mm文件進(jìn)行突破, 去尋找遮蓋關(guān)鍵字unobscured, 從這個(gè)關(guān)鍵字中發(fā)現(xiàn)遮蓋區(qū)域和scrollView的window相關(guān), 因此嘗試第四種方法, 修改window的大小~ 失敗的結(jié)果唯一能夠告訴筆者的就是: 沒有找到遮蓋視圖不渲染的根源!
筆者在第一次尋找關(guān)鍵字失敗后嘗試從snapshot這個(gè)關(guān)鍵字去突破, 結(jié)果發(fā)現(xiàn)了私有API_snapshotRect:intoImageOfWidth:completionHandler。這也是第五種方法的嘗試來源。通過snapshot關(guān)鍵字其實(shí)還發(fā)現(xiàn)了隱藏在WKWebView.mm底下的_takeViewSnapshot方法, 可是該方法返回的對(duì)象是C++對(duì)象, 筆者就沒有從Object-C層級(jí)對(duì)方法進(jìn)行調(diào)用嘗試。
結(jié)合snapshot和unobscured兩個(gè)關(guān)鍵字的搜索, 筆者在底層一串跟蹤, 發(fā)現(xiàn)了WebPage、DrawingArea等一系列概念, 筆者偶然間在WebPage的初始化方法中發(fā)現(xiàn)有個(gè)WebPageCreationParameters參數(shù)作為構(gòu)造WebPage的初始參數(shù), 其中包含了如下幾個(gè)參數(shù)
#if PLATFORM(IOS)
WebCore::FloatSize screenSize;
WebCore::FloatSize availableScreenSize;
float textAutosizingWidth;
#endif
通過全局搜索availableScreenSize, 在WebPageProxyIOS.mm源碼中發(fā)現(xiàn), WebPage的屏幕尺寸是根據(jù)WKGetAvailableScreenSize()和WKGetScreenSize()獲取的, 核心代碼如下:
FloatSize WebPageProxy::screenSize()
{
return FloatSize(WKGetScreenSize());
}
FloatSize WebPageProxy::availableScreenSize()
{
return FloatSize(WKGetAvailableScreenSize());
}
終于有些眉目了, 一全局搜索WKGetAvailableScreenSize就崩潰了~ 在WebKit2開源中并沒有這個(gè)方法的定義, 并且無法通過Google和Apple Developer搜索到相關(guān)信息... T_T
最雞血的來了, 跟蹤了幾個(gè)小時(shí), 筆者放棄了... 沒錯(cuò), 筆者直到最后都沒有從源碼中找到解決方案~ =。=
WKWebView截圖方案
雖然沒有通過源碼找到解決方案, 但是通過改變Window的嘗試讓我的腦洞打開, 想到了另外一種和滾動(dòng)截圖很相似的暴力的解決方式。
PS: 滾動(dòng)截圖是筆者在我只想要截個(gè)屏中所描述的暴力解決截圖的方式。實(shí)現(xiàn)方式就是滾動(dòng)一頁截取一頁, 最后組裝成一張長(zhǎng)圖。
筆者想: 既然WKWebView的渲染區(qū)域是屏幕范圍固定的, 那我不滾動(dòng)視圖, 不斷的往上推視圖呢?
不斷往上推視圖的意思就是改變View的origin的y軸, 每截取一張圖片后去上移View的高度(高度等價(jià)于該WKWebView在界面中的顯示范圍)和拉長(zhǎng)WKWebView的總高度, 直到截取到了最后一張圖并組裝。
這個(gè)思路有個(gè)小小的問題, 就是筆者曾經(jīng)嘗試通過放大WKWebView本身去截圖, 但是卻截出一片空白的情況。透過這個(gè)問題可以假設(shè), 我不斷上移y軸并放大高度的最后一張情況和上述有問題的情況完全一致, 可以猜測(cè)這個(gè)方法是無法正確的截取WKWebView的圖的。
筆者用了一種很巧妙的方法去躲避了這個(gè)問題, 就是去截取WKWebView的父視圖, 因?yàn)闊o論WKWebView怎么改變, 通過WKWebView父視圖截圖是可以正確獲取對(duì)應(yīng)的界面的(筆者實(shí)驗(yàn)的)。
通過優(yōu)化后大致的流程如下:
- 基于WKWebView的尺寸偽造一個(gè)UIView, 并拉長(zhǎng)至ContentSize高度
- 將偽造的UIView作為WKWebView的父視圖
- 放置一張大畫布長(zhǎng)度和WKWebView的ContentSize高度一致
- 對(duì)父視圖進(jìn)行普通截圖并放置在大畫布中
- 將WKWebView的高度上移一個(gè)父視圖的高度
- 循環(huán)執(zhí)行步驟3和步驟4直到總高度和WKWebView的ContentSize高度一致
- 讀取畫布中的圖像并返回
大致思路如圖:

思路核心代碼如下:
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;的標(biāo)簽重復(fù)的問題。
<font color='orange'>上述代碼起示意作用, 實(shí)際循環(huán)部分需要等待延遲, 因?yàn)樾枰却齏KWebView在改變frame之后準(zhǔn)備完畢執(zhí)行下一次循環(huán)。</font>
總結(jié)
因?yàn)閃KWebView只能渲染屏幕范圍大小左右的視圖范圍, 因此筆者就利用這個(gè)點(diǎn), 不斷的去改變WKWebView的frame去截圖, 然后組裝成為一張內(nèi)容截圖。通過這種方式可以巧妙的躲避過因?yàn)闈L動(dòng)視圖產(chǎn)生的部分頁面元素重復(fù)的問題。
其實(shí)WKWebView現(xiàn)在在iOS開發(fā)應(yīng)用中并沒有UIWebView廣泛, 做截圖相關(guān)功能的開發(fā)者也可能會(huì)優(yōu)先采用UIWebView最為搭載容器, 但是多多少少本篇文章應(yīng)該還是會(huì)幫助到一些使用WKWebView的先驅(qū)者的~
另: 本文提供的解決方案可能只是眾多解決方案的其中一種, 并且相當(dāng)?shù)暮臅r(shí)也消耗內(nèi)存, 希望大家可以一起想想能否有更優(yōu)的解決方案~ 希望多多交流~ 如果有更好的方案, 跪求Pull Request或者提交issue到SwViewCapture。
PS: 鑒于個(gè)人水平有限, 有錯(cuò)誤之處, 請(qǐng)大家及時(shí)指出~ 謝謝~