经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » iOS » 查看文章
iOS使用WebView生成长截图的第3种解决方案
来源:jb51  时间:2018/9/26 17:38:09  对本文有异议

前言

WebView就是一个内嵌浏览器控件,在iOS中主要有两种WebView:UIWebView和WKWebView,UIWebView是iOS2之后开始使用,WKWebView是在iOS8开始使用,WKWebView将逐步取代笨重的UIWebView。

由于项目需要,新近实现了一个长截图库 SnapshotKit。其中,需要支持 UIWebView、WKWebView 组件生成长截图。为了实现这个特性,查阅了很多资料,同时也做了不同的新奇思路尝试,最终实现了一个新的、取巧的技术方案。

以下主要总结了在“WebView生成长截图”需求方面,“网上已有方案”和“我的全新方案”的各自实现要点和优缺点。

WebView生成长截图的已有方案

根据 Google 所搜索到的资料,目前iOS WebView生成长截图的方案主要有2种:

  • 方案一:修改Frame,截图组件

  • 方案二:分页截图组件内容,合成长图

下面将会简述方案一和方案二的具体实现。

方案一:修改Frame,截图组件

方案一的实现要点在于:修改 webView.scrollView 的 frameSize  为 contentSize,然后对整个 webView.scrollView 进行截图。

不过,这个方案只适用 UIWebView 组件,因为其是一次性加载网页所有的内容。而 WKWebView 组件,为了节省内存,加载网页内容时,只加载可视部分——这一点类似 UITableView 组件。在修改webView.scrollView 的 frameSize 后,立即执行了截图操作, 这时候,WKWebView由于还没把网页的内容加载出来,导致生成的长截图是空白的。

方案一核心代码如下:

  1. extension UIScrollView {
  2.  public func takeSnapshotOfFullContent() -> UIImage? {
  3.   let originalFrame = self.frame
  4.   let originalOffset = self.contentOffset
  5.  
  6.   self.frame = CGRect.init(origin: originalFrame.origin, size: self.contentSize)
  7.   self.contentOffset = .zero
  8.  
  9.   let backgroundColor = self.backgroundColor ?? UIColor.white
  10.  
  11.   UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0)
  12.  
  13.   guard let context = UIGraphicsGetCurrentContext() else {
  14.    return nil
  15.   }
  16.   context.setFillColor(backgroundColor.cgColor)
  17.   context.setStrokeColor(backgroundColor.cgColor)
  18.  
  19.   self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
  20.   let image = UIGraphicsGetImageFromCurrentImageContext()
  21.   UIGraphicsEndImageContext()
  22.  
  23.   self.frame = originalFrame
  24.   self.contentOffset = originalOffset
  25.  
  26.   return image
  27.  }
  28. }

测试代码:

  1. // example code
  2.  private func takeSnapshotOfUIWebView() {
  3.  let image = self.webView.scrollView.takeSnapshotOfFullContent()
  4.  // 处理image
  5. }

方案二:分页截图组件内容,合成长图

方案二的实现要点在于:分页滚动WebView组件的内容,然后生成分页截图,最后把所有分页截图合成一张长图。

这个方案适用于 UIWebView 组件和 WKWebView 组件。

方案二核心代码如下:

  1. extension UIScrollView {
  2.  public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
  3.   // 分页绘制内容到ImageContext
  4.   let originalOffset = self.contentOffset
  5.  
  6.   // 当contentSize.height<bounds.height时,保证至少有1页的内容绘制
  7.   var pageNum = 1
  8.   if self.contentSize.height > self.bounds.height {
  9.    pageNum = Int(floorf(Float(self.contentSize.height / self.bounds.height)))
  10.   }
  11.  
  12.   let backgroundColor = self.backgroundColor ?? UIColor.white
  13.  
  14.   UIGraphicsBeginImageContextWithOptions(self.contentSize, true, 0)
  15.  
  16.   guard let context = UIGraphicsGetCurrentContext() else {
  17.    completion(nil)
  18.    return
  19.   }
  20.   context.setFillColor(backgroundColor.cgColor)
  21.   context.setStrokeColor(backgroundColor.cgColor)
  22.  
  23.   self.drawScreenshotOfPageContent(0, maxIndex: pageNum) {
  24.    let image = UIGraphicsGetImageFromCurrentImageContext()
  25.    UIGraphicsEndImageContext()
  26.    self.contentOffset = originalOffset
  27.    completion(image)
  28.   }
  29.  }
  30.  
  31.  fileprivate func drawScreenshotOfPageContent(_ index: Int, maxIndex: Int, completion: @escaping () -> Void) {
  32.  
  33.   self.setContentOffset(CGPoint(x: 0, y: CGFloat(index) * self.frame.size.height), animated: false)
  34.   let pageFrame = CGRect(x: 0, y: CGFloat(index) * self.frame.size.height, width: self.bounds.size.width, height: self.bounds.size.height)
  35.  
  36.   DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
  37.    self.drawHierarchy(in: pageFrame, afterScreenUpdates: true)
  38.  
  39.    if index < maxIndex {
  40.     self.drawScreenshotOfPageContent(index + 1, maxIndex: maxIndex, completion: completion)
  41.    }else{
  42.     completion()
  43.    }
  44.   }
  45.  }
  46. }

测试代码:

  1. // example code
  2. private func takeSnapshotOfUIWebView() {
  3.  self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
  4.   // 处理image
  5.  }
  6. }
  7.  
  8. private func takeSnapshotOfWKWebView() {
  9.  self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
  10.   // 处理image
  11.  }
  12. }

WebView生成长截图的新方案

除了方案一和方案二,还有新方案吗?

答案是肯定加确定以及一定的。

这个新方案的要点在于:iOS系统的WebView打印功能。

iOS系统支持把WebView的内容打印到PDF文件上,借助这个特性,新方案的设计如下:

  • 把 WebView组件的内容全部打印到一页PDF上

  • 把PDF转换成图片

新方案的核心代码如下:

  1. import UIKit
  2. import WebKit
  3.  
  4. /// WebViewPrintPageRenderer: use to print the full content of webview into one image
  5. internal final class WebViewPrintPageRenderer: UIPrintPageRenderer {
  6.  
  7.  private var formatter: UIPrintFormatter
  8.  
  9.  private var contentSize: CGSize
  10.  
  11.  /// 生成PrintPageRenderer实例
  12.  ///
  13.  /// - Parameters:
  14.  /// - formatter: WebView的viewPrintFormatter
  15.  /// - contentSize: WebView的ContentSize
  16.  required init(formatter: UIPrintFormatter, contentSize: CGSize) {
  17.   self.formatter = formatter
  18.   self.contentSize = contentSize
  19.   super.init()
  20.   self.addPrintFormatter(formatter, startingAtPageAt: 0)
  21.  }
  22.  
  23.  override var paperRect: CGRect {
  24.   return CGRect.init(origin: .zero, size: contentSize)
  25.  }
  26.  
  27.  override var printableRect: CGRect {
  28.   return CGRect.init(origin: .zero, size: contentSize)
  29.  }
  30.  
  31.  private func printContentToPDFPage() -> CGPDFPage? {
  32.   let data = NSMutableData()
  33.   UIGraphicsBeginPDFContextToData(data, self.paperRect, nil)
  34.   self.prepare(forDrawingPages: NSMakeRange(0, 1))
  35.   let bounds = UIGraphicsGetPDFContextBounds()
  36.   UIGraphicsBeginPDFPage()
  37.   self.drawPage(at: 0, in: bounds)
  38.   UIGraphicsEndPDFContext()
  39.  
  40.   let cfData = data as CFData
  41.   guard let provider = CGDataProvider.init(data: cfData) else {
  42.    return nil
  43.   }
  44.   let pdfDocument = CGPDFDocument.init(provider)
  45.   let pdfPage = pdfDocument?.page(at: 1)
  46.  
  47.   return pdfPage
  48.  }
  49.  
  50.  private func covertPDFPageToImage(_ pdfPage: CGPDFPage) -> UIImage? {
  51.   let pageRect = pdfPage.getBoxRect(.trimBox)
  52.   let contentSize = CGSize.init(width: floor(pageRect.size.width), height: floor(pageRect.size.height))
  53.  
  54.   // usually you want UIGraphicsBeginImageContextWithOptions last parameter to be 0.0 as this will us the device's scale
  55.   UIGraphicsBeginImageContextWithOptions(contentSize, true, 2.0)
  56.   guard let context = UIGraphicsGetCurrentContext() else {
  57.    return nil
  58.   }
  59.  
  60.   context.setFillColor(UIColor.white.cgColor)
  61.   context.setStrokeColor(UIColor.white.cgColor)
  62.   context.fill(pageRect)
  63.  
  64.   context.saveGState()
  65.   context.translateBy(x: 0, y: contentSize.height)
  66.   context.scaleBy(x: 1.0, y: -1.0)
  67.  
  68.   context.interpolationQuality = .low
  69.   context.setRenderingIntent(.defaultIntent)
  70.   context.drawPDFPage(pdfPage)
  71.   context.restoreGState()
  72.  
  73.   let image = UIGraphicsGetImageFromCurrentImageContext()
  74.   UIGraphicsEndImageContext()
  75.  
  76.   return image
  77.  }
  78.  
  79.  /// print the full content of webview into one image
  80.  ///
  81.  /// - Important: if the size of content is very large, then the size of image will be also very large
  82.  /// - Returns: UIImage?
  83.  internal func printContentToImage() -> UIImage? {
  84.   guard let pdfPage = self.printContentToPDFPage() else {
  85.    return nil
  86.   }
  87.  
  88.   let image = self.covertPDFPageToImage(pdfPage)
  89.   return image
  90.  }
  91. }
  92.  
  93. extension UIWebView {
  94.  public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
  95.   self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
  96.   DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
  97.    let renderer = WebViewPrintPageRenderer.init(formatter: self.viewPrintFormatter(), contentSize: self.scrollView.contentSize)
  98.    let image = renderer.printContentToImage()
  99.    completion(image)
  100.   }
  101.  }
  102. }
  103.  
  104. extension WKWebView {
  105.  public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
  106.   self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
  107.   DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
  108.    let renderer = WebViewPrintPageRenderer.init(formatter: self.viewPrintFormatter(), contentSize: self.scrollView.contentSize)
  109.    let image = renderer.printContentToImage()
  110.    completion(image)
  111.   }
  112.  }
  113. }

WebViewPrintPageRenderer 是该方案的核心类,负责把 WebView组件内容打印到PDF,然后把PDF转换为图片。
UIWebView 和 WKWebView 则实现对应的扩展。

测试代码:

  1. // example code
  2. private func takeSnapshotOfUIWebView() {
  3.  self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
  4.   // 处理image
  5.  }
  6. }
  7.  
  8. private func takeSnapshotOfWKWebView() {
  9.  self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
  10.   // 处理image
  11.  }
  12. }

三种技术方案优劣对比

那么,这三种技术方案各自存在什么优缺点呢,适用什么场景呢?

方案一:只适用 UIWebView;若网页内容很多,生成长截图时,会占用过多内存。 所以,该方案只适合不需要支持 WKWebView, 且网页内容不会太多的场景。

方案二:适用 UIWebView 和 WKWebView,且特别适合 WKWebView。由于采用分页生成截图机制,有效减少内存占用。不过,这个方案存在一个问题:若网页存在 position: fixed 的元素(如网页头部固定的导航栏),该元素会重复出现在生成的长图上。

方案三:适用 UIWebView 和 WKWebView。其中最重要的一步——“把WebView内容打印到PDF” 是由iOS系统实现,所以该方案的性能在理论上是可以得到保障的。不过,这个方案存在一个问题:在把网页内容打印到PDF时,iOS系统获取的 contentSize 比WebView的实际contentSize 要大,从而导致生成的图片在靠近底部的内容部分和实际存在一点差异。具体可以下载运行我的长截图库 SnapshotKit 的 Demo,通过其中的 UIWebView 和 WKWebView 截图示例查看具体截图效果。

以上三个方案,总的来说,解决了部分场景的需求,但都不够完美,仍需做进一步的优化。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对w3xue的支持。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号