替换WKWebView的原因有:
内存占用少
项目h5较多,发现当有高清图或gif时在4s上crash较多,几乎都是内存原因。WKWebView占用内存较少,立马简单的测试下,原本必挂的网页运行正常,决定换。
#import <WebKit/WebKit.h>
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
[self.view addSubview:_webView];
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlPath]];
[_webView loadRequest:request];
...
}
进度容易获取,无需引用第三方框架
首先看下,WKWebView的estimatedProgress属性注解
/*! @abstract An estimate of what fraction of the current navigation has been completed.
@discussion This value ranges from 0.0 to 1.0 based on the total number of
bytes expected to be received, including the main document and all of its
potential subresources. After a navigation completes, the value remains at 1.0
until a new navigation starts, at which point it is reset to 0.0.
@link WKWebView @/link is key-value observing (KVO) compliant for this
property.
*/
注册通知:
- (void)viewDidLoad
[super viewDidLoad];
...
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
}
响应,progressView用的是开源的NJKWebViewProgressView:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == self.webView && [keyPath isEqualToString:@"estimatedProgress"]) {
CGFloat newprogress = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
GTRWeakSelf
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.webViewProgressView setProgress:newprogress animated:YES];
});
}
}
移除:
- (void)dealloc {
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
}
js与native通讯方式多样化,和android统一,但不支持javascript core
1、js使用alert、prompt和confirm等方式,wkwebview用
native:
#pragma mark - WKUIDelegate
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
2、URL方式,如果地址是网络上,可把html放到本地可解决
js:
<script language="javascript">
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 发起请求后这个iFrame就没用了,所以把它从dom上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
function getVersion() {
loadURL("gtrAction://getVersion");
}
</script>
native:
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSURL *URL = navigationAction.request.URL;
NSString *scheme = [URL scheme];
if ([scheme isEqualToString:@"haleyaction"]) {
[self handleCustomAction:URL];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
3、用MessageHandler方式,我们选择这种方式实现。
js实现:
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
native实现祥见集成问题实现。
集成问题
WKWebView使用有循环引用,原因是UIViewController->WKWebView->WKWebViewConfiguration->WKUserContentController,最后WKUserContentController在addScriptMessageHandler:name:又引用UIViewController。
有2个解决方案:
1、这种方式简单,只需在viewWillAppear和viewWillDisappear相应的添加和删除messageHandler。不过部分系统上再次addScriptMessageHandler无效。
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"getVersion"];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[_webView.configuration.userContentController removeScriptMessageHandlerForName:@"getVersion"];
}
- (void)dealloc {
DebugLog(@"GTRViewController dealloc");
}
2、打破addScriptMessageHandler这个循环引用,用中间代理方式实现。
GTRWeakScriptMessageDelegate.h
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
@protocol GTRWeakScriptMessageDelegate <NSObject>
- (void)getVersion;
@end
@interface GTRWeakScriptMessageDelegate : NSObject <WKScriptMessageHandler>
@property (nonatomic, weak) id <GTRWeakScriptMessageDelegate> gDelegate;
- (instancetype)initWithDelegate:(id <GTRWeakScriptMessageDelegate>)delegate;
@end
GTRWeakScriptMessageDelegate.m
#import "GTRWeakScriptMessageDelegate.h"
@interface GTRWeakScriptMessageDelegate ()
@end
@implementation GTRWeakScriptMessageDelegate
- (instancetype)initWithDelegate:(id <GTRWeakScriptMessageDelegate>)delegate {
self = [super init];
if (self) {
self.gDelegate = delegate;
}
return self;
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if (![self.gDelegate conformsToProtocol:@protocol(GTRWeakScriptMessageDelegate)]) {
return;
}
if ([message.name isEqualToString:@"getVersion"]) {
//异步回掉
[self.gDelegate getVersion];
}
}
- (void)dealloc {
DebugLog(@"GTRWeakScriptMessageDelegate dealloc");
}
@end
GTRViewController.m
@property (nonatomic, strong) GTRWeakScriptMessageDelegate *weakScriptDelegate;
- (void)viewDidLoad {
[super viewDidload];
_weakScriptDelegate = [[GTRWeakScriptMessageDelegate alloc] initWithDelegate:self];
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
//添加messageHandler
[_webView.configuration.userContentController addScriptMessageHandler:self.weakScriptDelegate name:@"getVersion"];
_webView.scrollView.delegate = self;
_webView.navigationDelegate = self;
_webView.UIDelegate = self;
[self.view addSubview:_webView];
}
- (void)dealloc {
[_webView.configuration.userContentController removeScriptMessageHandlerForName:@"getVersion"];
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
_webView.UIDelegate = nil;
_webView.navigationDelegate = nil;
_webView.scrollView.delegate = nil;
DebugLog(@"GTRViewController dealloc");
}
附送wkwebview修改Agent,用来标示是应用内web,ios9后用setCustomUserAgent:方法.
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSString *newUserAgent = [userAgent stringByAppendingString:@" ua gtr_demo"];//自定义需要拼接的字符串
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newUserAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
[[NSUserDefaults standardUserDefaults] synchronize];
}
GTRViewController.m
- (void)viewDidLoad {
...
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
DebugLog(@"Webview UserAgent:%@", result);
}];
...
}