前言
一直想系統(tǒng)的總結(jié)下UIWebView和WKWebView,這里整理了一個
Demo可供參考
分為兩部分:
UIWebView & WKWebView 上
UIWebView & WKWebView 下
簡介
WKWebView是Apple于iOS 8.0推出的WebKit中的核心控件,用來替代UIWebView。WKWebView比UIWebView的優(yōu)勢在于:
- 更多的支持HTML5的特性
- 高達60fps的滾動刷新率以及內(nèi)置手勢
- 與Safari相同的JavaScript引擎
- 將UIWebViewDelegate與UIWebView拆分成了14類與3個協(xié)議(官方文檔說明)
- 可以獲取加載進度:
estimatedProgress(UIWebView需要調(diào)用私有Api)
POST請求
WKWebView相關(guān)的post請求實現(xiàn)
Html實現(xiàn)
<html>
<head>
<script>
//調(diào)用格式: post('URL', {"key": "value"});
function post(path, params) {
var method = "post";
var form = document.createElement("form");
form.setAttribute("method",method);
form.setAttribute("action",path);
for(var key in params) {
if(params.hasOwnProperty(key)) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type","hidden");
hiddenField.setAttribute("name",key);
hiddenField.setAttribute("value",params[key]);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
</script>
</head>
<body>
</body>
</html>
OC中代碼實現(xiàn):
思路:
1 、將一個包含JavaScript的POST請求的HTML代碼放到工程目錄中
2 、加載這個包含JavaScript的POST請求的代碼到WKWebView
3 、加載完成之后,用Native調(diào)用JavaScript的POST方法并傳入?yún)?shù)來完成請求
//僅當(dāng)?shù)谝淮蔚臅r候加載本地JS
// @property(nonatomic,assign) BOOL needLoadJSPOST;
- (void)viewDidLoad
{
// JS發(fā)送POST的Flag,為真的時候會調(diào)用JS的POST方法
self.needLoadJSPOST = YES;
//POST使用預(yù)先加載本地JS方法的html實現(xiàn),請確認(rèn)WKJSPOST存在
[self loadHostPathURL:@"WKJSPOST"];
}
- (void)loadHostPathURL:(NSString *)url
{
//獲取JS所在的路徑
NSString *path = [[NSBundle mainBundle] pathForResource:url ofType:@"html"];
//獲得html內(nèi)容
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//加載js
[self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
}
//加載成功,對應(yīng)UIWebView的- (void)webViewDidFinishLoad:(UIWebView *)webView; 網(wǎng)頁加載完成,導(dǎo)航的變化
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
/*
主意:這個方法是當(dāng)網(wǎng)頁的內(nèi)容全部顯示(網(wǎng)頁內(nèi)的所有圖片必須都正常顯示)的時候調(diào)用(不是出現(xiàn)的時候就調(diào)用),,否則不顯示,或則部分顯示時這個方法就不調(diào)用。
*/
// 判斷是否需要加載(僅在第一次加載)
if (self.needLoadJSPOST) {
// 調(diào)用使用JS發(fā)送POST請求的方法
[self postRequestWithJS];
// 將Flag置為NO(后面就不需要加載了)
self.needLoadJSPOST = NO;
}
}
#pragma mark - JSPOST
// 調(diào)用JS發(fā)送POST請求
- (void)postRequestWithJS
{
// 拼裝成調(diào)用JavaScript的字符串
NSString *jscript = [NSString stringWithFormat:@"post('%@',{%@})",self.URLString,self.postData];
NSLog(@"Javascript: %@", jscript);
//post('http://www.postexample.com',{"username":"aaa","password":"123"})
// 調(diào)用JS代碼
[self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
NSLog(@"%@",error);
}];
}
UIWebView的post請求實現(xiàn)
- (void)viewDidLoad {
[super viewDidLoad];
NSURL*url=[NSURL URLWithString:@"http://your_url.com"];
NSString*body=[NSString stringWithFormat:@"arg1=%@&arg2=%@",@"val1",@"val2"];
NSMutableURLRequest*request=[[NSMutableURLRequest alloc]initWithURL:url];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
[self.webView loadRequest:request];
}
Cookie相關(guān)
WKWebView的cookie管理
比起UIWebView的自動管理,WKWebView的Cookie管理坑還是比較多的,注意事項如下:
1、WKWebView加載網(wǎng)頁得到的Cookie會同步到NSHTTPCookieStorage中
2、WKWebView加載請求時,不會同步NSHTTPCookieStorage中已有的Cookie
3、通過共用一個WKProcessPool并不能解決2中Cookie同步問題,且可能會造成Cookie丟失。
添加cookie
動態(tài)注入js
WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//添加自定義的cookie
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"document.cookie = 'SyhCookie=Syh;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加腳本
[UserContentController addUserScript:newCookieScript];

解決后續(xù)Ajax請求Cookie丟失問題
添加WKUserScript,需保證sharedHTTPCookieStorage中你的Cookie存在。
/*!
* 更新webView的cookie
*/
- (void)updateWebViewCookie
{
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加Cookie
[self.configuration.userContentController addUserScript:cookieScript];
}
- (NSString *)cookieString
{
NSMutableString *script = [NSMutableString string];
[script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
// Skip cookies that will break our script
if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
continue;
}
// Create a line that appends this cookie to the web view's document's cookies
[script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.da_javascriptString];
}
return script;
}
@interface NSHTTPCookie (Utils)
- (NSString *)da_javascriptString;
@end
@implementation NSHTTPCookie (Utils)
- (NSString *)da_javascriptString
{
NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
self.name,
self.value,
self.domain,
self.path ?: @"/"];
if (self.secure) {
string = [string stringByAppendingString:@";secure=true"];
}
return string;
}
@end
解決跳轉(zhuǎn)新頁面時Cookie帶不過去問題
當(dāng)你點擊頁面上的某個鏈接,跳轉(zhuǎn)到新的頁面,Cookie又丟了,需保證sharedHTTPCookieStorage中你的Cookie存在。
//核心方法:
/**
修復(fù)打開鏈接Cookie丟失問題
@param request 請求
@return 一個fixedRequest
*/
- (NSURLRequest *)fixRequest:(NSURLRequest *)request
{
NSMutableURLRequest *fixedRequest;
if ([request isKindOfClass:[NSMutableURLRequest class]]) {
fixedRequest = (NSMutableURLRequest *)request;
} else {
fixedRequest = request.mutableCopy;
}
//防止Cookie丟失
NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
if (dict.count) {
NSMutableDictionary *mDict = request.allHTTPHeaderFields.mutableCopy;
[mDict setValuesForKeysWithDictionary:dict];
fixedRequest.allHTTPHeaderFields = mDict;
}
return fixedRequest;
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
#warning important 這里很重要
//解決Cookie丟失問題
NSURLRequest *originalRequest = navigationAction.request;
[self fixRequest:originalRequest];
//如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳轉(zhuǎn)
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationActionPolicyAllow);
//可能有小伙伴,會說如果originalRequest是NSURLRequest,不可變,那不就添加不了Cookie了,是的,我們不能因為這個問題,不允許跳轉(zhuǎn),也不能在不允許跳轉(zhuǎn)之后用loadRequest加載fixedRequest,否則會出現(xiàn)死循環(huán),具體的,小伙伴們可以用本地的html測試下。
NSLog(@"%@", NSStringFromSelector(_cmd));
}
#pragma mark - WKUIDelegate
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
#warning important 這里也很重要
//這里不打開新窗口
[self.webView loadRequest:[self fixRequest:navigationAction.request]];
return nil;
}
Cookie依然丟失
什么的方法需保證sharedHTTPCookieStorage中你的Cookie存在。怎么保證呢?由于WKWebView加載網(wǎng)頁得到的Cookie會同步到NSHTTPCookieStorage中的特點,有時候你強行添加的Cookie會在同步過程中丟失。抓包(Mac推薦Charles)你就會發(fā)現(xiàn),點擊一個鏈接時,Request的header中多了Set-Cookie字段,其實Cookie已經(jīng)丟了。下面推薦筆者的解決方案,那就是把自己需要的Cookie主動保存起來,每次調(diào)用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies方法時,保證返回的數(shù)組中有自己需要的Cookie。下面上代碼,用了runtime的Method Swizzling
首先是在適當(dāng)?shù)臅r候,保存
//比如登錄成功,保存Cookie
NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
for (NSHTTPCookie *cookie in allCookies) {
if ([cookie.name isEqualToString:DAServerSessionCookieName]) {
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];
if (dict) {
NSHTTPCookie *localCookie = [NSHTTPCookie cookieWithProperties:dict];
if (![cookie.value isEqual:localCookie.value]) {
NSLog(@"本地Cookie有更新");
}
}
[[NSUserDefaults standardUserDefaults] setObject:cookie.properties forKey:DAUserDefaultsCookieStorageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
break;
}
}
在讀取時,如果沒有則添加
@implementation NSHTTPCookieStorage (Utils)
+ (void)load
{
class_methodSwizzling(self, @selector(cookies), @selector(da_cookies));
}
- (NSArray<NSHTTPCookie *> *)da_cookies
{
NSArray *cookies = [self da_cookies];
BOOL isExist = NO;
for (NSHTTPCookie *cookie in cookies) {
if ([cookie.name isEqualToString:DAServerSessionCookieName]) {
isExist = YES;
break;
}
}
if (!isExist) {
//CookieStroage中添加
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];
if (dict) {
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
NSMutableArray *mCookies = cookies.mutableCopy;
[mCookies addObject:cookie];
cookies = mCookies.copy;
}
}
return cookies;
}
@end
UIWebView的cookie管理
UIWebView的Cookie管理很簡單,一般不需要我們手動操作Cookie,因為所有Cookie都會被[NSHTTPCookieStorage sharedHTTPCookieStorage]這個單例管理,而且UIWebView會自動同步CookieStorage中的Cookie,所以只要我們在Native端,正常登陸退出,h5在適當(dāng)時候刷新,就可以正確的維持登錄狀態(tài),不需要做多余的操作。
可能有一些情況下,我們需要在訪問某個鏈接時,添加一個固定Cookie用來做區(qū)分,那么就可以通過header來實現(xiàn)
思路:
1、主動操作NSHTTPCookieStorage,添加一個自定義Cookie
2、讀取所有Cookie
3、Cookie轉(zhuǎn)換成HTTPHeaderFields,并添加到request的header
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.URLString]];
//主動操作NSHTTPCookieStorage,添加一個自定義Cookie
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookieName: @"customCookieName",
NSHTTPCookieValue: @"heiheihei",
NSHTTPCookieDomain: @".baidu.com",
NSHTTPCookiePath: @"/"
}];
//Cookie存在則覆蓋,不存在添加
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
//讀取所有Cookie
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies數(shù)組轉(zhuǎn)換為requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//設(shè)置請求頭
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];

自定義瀏覽器UserAgen
這個其實在App開發(fā)中,比較重要。比如常見的微信、支付寶App等,都有自己的UserAgent,而UA最常用來判斷在哪個App內(nèi),一般App的下載頁中只有一個按鈕"點擊下載",當(dāng)用戶點擊該按鈕時,在微信中則跳轉(zhuǎn)到應(yīng)用寶,否則跳轉(zhuǎn)到AppStore。那么如何區(qū)分在哪個App中呢?就是js判斷UA
//js中判斷
if (navigator.userAgent.indexOf("MicroMessenger") !== -1) {
//在微信中
}
關(guān)于自定義UA,這個UIWebView不提供Api,而WKWebView提供Api,前文中也說明過,就是調(diào)用customUserAgent屬性。
self.webView.customUserAgent = @"WebViewDemo/1.1.0"; //自定義UA,只支持WKWebView
而有沒有其他的方法實現(xiàn)自定義瀏覽器UserAgent呢?有
//最好在AppDelegate中就提前設(shè)置
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
//設(shè)置自定義UserAgent
[self setCustomUserAgent];
return YES;
}
- (void)setCustomUserAgent
{
//get the original user-agent of webview
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
//add my info to the new agent
NSString *newAgent = [oldAgent stringByAppendingFormat:@" %@", @"WebViewDemo/1.0.0"];
//regist the new agent
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", newAgent, @"User-Agent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}
@end
說明:
1、通過NSUserDefaults設(shè)置自定義UserAgent,可以同時作用于UIWebView和WKWebView。
2、WKWebView的customUserAgent屬性,優(yōu)先級高于NSUserDefaults,當(dāng)同時設(shè)置時,顯示customUserAgent的值。
WKWebView自定義返回/關(guān)閉按鈕
//返回按鈕
@property (nonatomic)UIBarButtonItem* customBackBarItem;
//關(guān)閉按鈕
@property (nonatomic)UIBarButtonItem* closeButtonItem;
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
[self updateNavigationItems];
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
[self updateNavigationItems];
}
- (void)updateNavigationItems
{
if (self.webView.canGoBack) {
UIBarButtonItem *spaceButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spaceButtonItem.width = -6.5;
[self.navigationItem setLeftBarButtonItems:@[spaceButtonItem,self.customBackBarItem,self.closeButtonItem] animated:NO];
}else {
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
[self.navigationItem setLeftBarButtonItems:@[self.customBackBarItem]];
}
}
-(UIBarButtonItem*)customBackBarItem{
if (!_customBackBarItem) {
UIImage* backItemImage = [[UIImage imageNamed:@"backItemImage"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImage* backItemHlImage = [[UIImage imageNamed:@"backItemImage-hl"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIButton* backButton = [[UIButton alloc] init];
[backButton setTitle:@"返回" forState:UIControlStateNormal];
[backButton setTitleColor:self.navigationController.navigationBar.tintColor forState:UIControlStateNormal];
[backButton setTitleColor:[self.navigationController.navigationBar.tintColor colorWithAlphaComponent:0.5] forState:UIControlStateHighlighted];
[backButton.titleLabel setFont:[UIFont systemFontOfSize:17]];
[backButton setImage:backItemImage forState:UIControlStateNormal];
[backButton setImage:backItemHlImage forState:UIControlStateHighlighted];
[backButton sizeToFit];
[backButton addTarget:self action:@selector(customBackItemClicked) forControlEvents:UIControlEventTouchUpInside];
_customBackBarItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
}
return _customBackBarItem;
}
-(void)customBackItemClicked{
if (self.webView.goBack) {
[self.webView goBack];
}else{
[self.navigationController popViewControllerAnimated:YES];
}
}
-(UIBarButtonItem*)closeButtonItem{
if (!_closeButtonItem) {
_closeButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"關(guān)閉" style:UIBarButtonItemStylePlain target:self action:@selector(closeItemClicked)];
}
return _closeButtonItem;
}
-(void)closeItemClicked{
[self.navigationController popViewControllerAnimated:YES];
}
WKWebView添加進度條
- (void)viewDidLoad
{
//設(shè)置加載進度條
//@property (nonatomic,strong) UIProgressView *progressView;
//static void *WkwebBrowserContext = &WkwebBrowserContext;
//添加進度條
[self.view addSubview:self.progressView];
[self.webView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:0 context:WkwebBrowserContext];
}
//開始加載,對應(yīng)UIWebView的- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{
//開始加載的時候,讓加載進度條顯示
self.progressView.hidden = NO;
}
#pragma mark - 進度條
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == self.webView) {
self.progressView.alpha = 1.0f;
BOOL animated = self.webView.estimatedProgress > self.progressView.progress;
[self.progressView setProgress:self.webView.estimatedProgress animated:animated];
// Once complete, fade out UIProgressView
if (self.webView.estimatedProgress >= 1.0f) {
[UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
self.progressView.alpha = 0.0f;
} completion:^(BOOL finished) {
[self.progressView setProgress:0.0f animated:NO];
}];
}
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (UIProgressView *)progressView{
if (!_progressView) {
_progressView = [[UIProgressView alloc]initWithProgressViewStyle:UIProgressViewStyleDefault];
if (_isNavHidden == YES) {
_progressView.frame = CGRectMake(0, 20, self.view.bounds.size.width, 3);
}else{
_progressView.frame = CGRectMake(0, 64, self.view.bounds.size.width, 3);
}
// 設(shè)置進度條的色彩
[_progressView setTrackTintColor:[UIColor colorWithRed:240.0/255 green:240.0/255 blue:240.0/255 alpha:1.0]];
_progressView.progressTintColor = [UIColor greenColor];
}
return _progressView;
}
WKWebView填坑
js alert方法不彈窗
實現(xiàn)- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;方法,如果不實現(xiàn),就什么都不發(fā)生,好吧,乖乖實現(xiàn)吧,實現(xiàn)了就能彈窗了。
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(nonnull void (^)(void))completionHandler
{
//js 里面的alert實現(xiàn),如果不實現(xiàn),網(wǎng)頁的alert函數(shù)無效
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alertController animated:YES completion:^{}];
}
白屏問題
當(dāng)WKWebView加載的網(wǎng)頁占用內(nèi)存過大時,會出現(xiàn)白屏現(xiàn)象。解決方案是
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
[webView reload]; //刷新就好了
}
有時白屏,不會調(diào)用該方法,具體的解決方案是
比如,最近遇到在一個高內(nèi)存消耗的H5頁面上 present 系統(tǒng)相機,拍照完畢后返回原來頁面的時候出現(xiàn)白屏現(xiàn)象(拍照過程消耗了大量內(nèi)存,導(dǎo)致內(nèi)存緊張,WebContent Process 被系統(tǒng)掛起),但上面的回調(diào)函數(shù)并沒有被調(diào)用。在WKWebView白屏的時候,另一種現(xiàn)象是 webView.titile 會被置空, 因此,可以在 viewWillAppear 的時候檢測 webView.title 是否為空來 reload 頁面。(出自WKWebView 那些坑)
自定義contentInset刷新時頁面跳動的bug
self.webView.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 49, 0);
//史詩級神坑,為何如此寫呢?參考https://opensource.apple.com/source/WebKit2/WebKit2-7600.1.4.11.10/ChangeLog
[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"]; //kvc給WKWebView的私有變量_obscuredInsets設(shè)置值