iOS开发:使用webview给网页截图
前段时间国乒出事,网上有很多曝黑幕的帖子,不过没多久帖子不是找不到,就是被删掉了。那个时候,就在想,有没有一种方法把网页保存下来,比如保存为图片呢?作为程序员好处之一就是想到就可以自己去搞一下。
1. SwViewCapture
本着不重复造轮子的原则,还是先去github上找了一下,结果发现还真的有人在做这件事情:SwViewCapture。简单的看了下他的实现原理,发现他是将图片一张一张的截取,最后用来拼接成一张图片。这样会带来一些性能上的问题在绘图的绘图的过程中会出现内存过大甚至会出现闪退的情况。另外最为直接的一个情况就是如果网页中出现了一些浮层,那么在截取图片的过程中,每一张拼接的图片中都会有多个的浮层,如下图:
![](https://img.haomeiwen.com/i112975/28c8ff9f062bb259.jpg)
2. UIWebView改造版本
我们知道webView
本身是继承于UIScrollView
的,那么网页的展示同样也是基于UIScrollView
的contentSize
,那么是否可以用这个特性来做一些事情呢?
- (void)screenShotCompletion:(void(^)(UIImage *image))completion {
CGRect oldFrame = self.frame;
CGPoint oldOffset = self.scrollView.contentOffset;
CGRect newFrame = oldFrame;
newFrame.size.height = self.scrollView.contentSize.height;
[self.scrollView setContentOffset:CGPointZero];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.frame = newFrame;
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIImage *image = [self screenShot:self rect:self.bounds];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.scrollView setContentOffset:oldOffset];
self.frame = oldFrame;
completion(image);
});
});
}
在上面代码中,先缓存下webView的frame,然后拿到当前网页加载的高度,也就是UIScrollView
的contentSize.height
。由此,就拿到了一个新的frame,这个frame也就是要截取的网页的frame了,由此我们也就拿到了网页所有内容,并且也能够避免掉网页中的某些JS的效果导致的多个浮层的问题。
3. WKWebView改造
上面的方案较为完美的解决了UIWebView
,那么拿到WKWebView
上可行么?简单的尝试了下,发现效果理想。WKWebView
和UIWebView
的实现机制较为不同,它只能渲染屏幕范围大小左右的视图范围,上面的使用这种方式截取的话,就会出现大面具的白图的情况。所以针对这种情况,也就借鉴了SwViewCapture
的做法:不断的去改变WKWebView
的frame
去截图,然后组装成为一张内容截图。代码如下:
- (void)screenShotCompletion:(void(^)(UIImage *image))completion {
UIView *oldSuperView = nil;
if ([self.superview isKindOfClass:[UIScrollView class]]) {
oldSuperView = self.superview;
[self.superview.superview addSubview:self];
}
CGRect oldFrame = self.frame;
CGPoint oldOffset = self.scrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
CGRect newFrame = oldFrame;
newFrame.size.height = self.scrollView.contentSize.height;
if (newFrame.size.height > WKMaxHeight) {
newFrame.size.height = WKMaxHeight;
}
self.frame = newFrame;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, NO, [UIScreen mainScreen].scale);
CGContextRef contextRef = UIGraphicsGetCurrentContext();
CGRect rect = self.bounds;
[self drawInView:self totalHeight:self.scrollView.contentSize.height rect:rect context:contextRef completion:^{
UIImage *shotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (oldSuperView) {
[oldSuperView addSubview:self];
}
self.scrollView.contentOffset = oldOffset;
self.frame = oldFrame;
completion(shotImage);
});
}];
});
}
- (void)drawInView:(WKWebView *)webView totalHeight:(CGFloat)totalHeight rect:(CGRect)rect context:(CGContextRef)contextRef completion:(void(^)())completion
{
[webView drawViewHierarchyInRect:CGRectMake(0, rect.origin.y, webView.bounds.size.width, webView.bounds.size.height) afterScreenUpdates:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
CGFloat tailingHeight = totalHeight - rect.origin.y - rect.size.height;
if (tailingHeight > 0) {
CGFloat offsetY = totalHeight - tailingHeight;
CGFloat offsetYY = totalHeight - webView.bounds.size.height;
offsetY = MIN(offsetY, offsetYY);
webView.scrollView.contentOffset = CGPointMake(0, offsetY);
CGRect tailingRect = webView.bounds;
CGFloat h = MIN(webView.bounds.size.height, tailingHeight);
tailingRect.origin.y = totalHeight-tailingHeight;
tailingRect.size.height = h;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self drawInView:webView totalHeight:totalHeight rect:tailingRect context:contextRef completion:completion];
});
}
else {
completion();
}
});
}
不过基于这种方式同样也会存在SwViewCapture
中存在的内存损耗以及多个浮层的问题,针对这个问题,想了很久,还没想到太好的方式来处理。
4. 截图滚动的处理
从上面代码,可以看到。在执行截图操作的时候将contentOffset
设置为了CGPointZero
。这样做就会直接带来一个问题:在截图的过程中,用户会直接看到网页会向上滚动。
针对这个问题,解决起来到也简单:在截图之前先将用户看到的当前页面截取下来,作为一张图片挡住接下来所执行的截取操作,并且在执行完截图操作后,将截取的遮盖图片销毁。代码如下:
UIImageView *coverImageView = [[UIImageView alloc] initWithFrame:self.webView.frame];
coverImageView.image = [self.webView coverImage];
[self.view addSubview:coverImageView];
//截图
[self.webView screenShotCompletion:^(UIImage *image) {
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), NULL);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[coverImageView removeFromSuperview];
});
}];
5. 总结
因为UIWebView
和WKWebView
的实现机制的不同,也针对其特性使用了不同的技术方案。不过每种方案都有其优缺点,比如WKWebView
中多个浮层目前还属于待解决问题(如果有方案,还请多多交流)。
另:代码已经上传到github,因本人水平有限,有不恰或错误之处,还请及时指出,谢谢。