開發App的過程中,常常會遇到在App內部加載網頁,通常用UIWebView加載。而這個自iOS2.0開始使用的Web容器一直是開發的心病:加載速度慢,占用內存多,優化困難。如果加載網頁多,還可能因為過量占用內存而給系統kill掉。各種優化的方法效果也不那么明顯iOS8 以后,蘋果推出了新框架 WebKit,提供了替換 UIWebView 的組件 WKWebView。各種 UIWebView 的性能問題沒有了,速度更快了,占用內存少了,體驗更好了,下面列舉一些其它的優勢:
1、在性能、穩定性、功能方面有很大提升(最直觀的體現就是加載網頁是占用的內存,模擬器加載百度與開源中國網站時,WKWebView占用23M,而UIWebView占用85M);
2、允許JavaScript的Nitro庫加載并使用(UIWebView中限制);
3、支持了更多的HTML5特性;
4、高達60fps的滾動刷新率以及內置手勢;
5、將UIWebViewDelegate與UIWebView重構成了14類與3個協議(查看蘋果官方文檔);
14個類
WKBackForwardList: 之前訪問過的 web 頁面的列表,可以通過后退和前進動作來訪問到。
WKBackForwardListItem: webview 中后退列表里的某一個網頁。
WKFrameInfo: 包含一個網頁的布局信息。
WKNavigation: 包含一個網頁的加載進度信息。
WKNavigationAction: 包含可能讓網頁導航變化的信息,用于判斷是否做出導航變化。
WKNavigationResponse: 包含可能讓網頁導航變化的返回內容信息,用于判斷是否做出導航變化。
WKPreferences: 概括一個 webview 的偏好設置。
WKProcessPool: 表示一個 web 內容加載池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含網頁發出的信息。
WKUserScript: 表示可以被網頁接受的用戶腳本。
WKWebViewConfiguration: 初始化 webview 的設置。
WKWindowFeatures: 指定加載新網頁時的窗口屬性。
3個協議
WKNavigationDelegate: 提供了追蹤主窗口網頁加載過程和判斷主窗口和子窗口是否進行頁面加載新頁面的相關方法。
WKUIDelegate: 提供用原生控件顯示網頁的方法回調。
WKScriptMessageHandler: 提供從網頁中收消息的回調方法。
所有相關的類的API
//上文介紹過的偏好配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
// 導航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// 用戶交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 頁面前進、后退列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;
// 默認構造器
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
//加載請求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 加載URL
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0);
// 直接加載HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 直接加載data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);
// 前進或者后退到某一頁面
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;
// 頁面的標題,支持KVO的
@property (nullable, nonatomic, readonly, copy) NSString *title;
// 當前請求的URL,支持KVO的
@property (nullable, nonatomic, readonly, copy) NSURL *URL;
// 標識當前是否正在加載內容中,支持KVO的
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
// 當前加載的進度,范圍為[0, 1]
@property (nonatomic, readonly) double estimatedProgress;
// 標識頁面中的所有資源是否通過安全加密連接來加載,支持KVO的
@property (nonatomic, readonly) BOOL hasOnlySecureContent;
// 當前導航的證書鏈,支持KVO
@property (nonatomic, readonly, copy) NSArray *certificateChain NS_AVAILABLE(10_11, 9_0);
// 是否可以招待goback操作,它是支持KVO的
@property (nonatomic, readonly) BOOL canGoBack;
// 是否可以執行gofarward操作,支持KVO
@property (nonatomic, readonly) BOOL canGoForward;
// 返回上一頁面,如果不能返回,則什么也不干
- (nullable WKNavigation *)goBack;
// 進入下一頁面,如果不能前進,則什么也不干
- (nullable WKNavigation *)goForward;
// 重新載入頁面
- (nullable WKNavigation *)reload;
// 重新從原始URL載入
- (nullable WKNavigation *)reloadFromOrigin;
// 停止加載數據
- (void)stopLoading;
// 執行JS代碼
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
// 標識是否支持左、右swipe手勢是否可以前進、后退
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
// 自定義user agent,如果沒有則為nil
@property (nullable, nonatomic, copy) NSString *customUserAgent NS_AVAILABLE(10_11, 9_0);
// 在iOS上默認為NO,標識不允許鏈接預覽
@property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE(10_11, 9_0);
#if TARGET_OS_IPHONE
/*! @abstract The scroll view associated with the web view.
*/
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
#endif
#if !TARGET_OS_IPHONE
// 標識是否支持放大手勢,默認為NO
@property (nonatomic) BOOL allowsMagnification;
// 放大因子,默認為1
@property (nonatomic) CGFloat magnification;
// 根據設置的縮放因子來縮放頁面,并居中顯示結果在指定的點
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;
使用
1、首先需要先引入WebKit庫
#import <WebKit/WebKit.h>
2、兩種初始化方法
// 默認初始化
- (instancetype)initWithFrame:(CGRect)frame;
// 根據對webview的相關配置,進行初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
3、加載網頁
最基礎的方法和UIWebView一樣
NSURL *url = [NSURL URLWithString:@"www.lxweimin.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
一些其他的加載方法
//加載本地URL文件
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL
allowingReadAccessToURL:(NSURL *)readAccessURL
//加載本地HTML字符串
- (nullable WKNavigation *)loadHTMLString:(NSString *)string
baseURL:(nullable NSURL *)baseURL;
//加載二進制數據
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL
4、代理方法
1、【WKNavigationDelegate協議】
該代理提供的方法,可以用來追蹤加載過程(頁面開始加載、加載完成、加載失敗)、決定是否執行跳轉。
// 頁面開始加載時調用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 當內容開始返回時調用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 頁面加載完成之后調用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// 頁面加載失敗時調用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation;
頁面跳轉的代理方法有三種,分為(收到跳轉與決定是否跳轉兩種)
// 接收到服務器跳轉請求之后調用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 在收到響應后,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 在發送請求之前,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
2、【WKUIDelegate協議】
WKUIDelegate從名稱能看出它是webView在user interface上的代理,共有5個可選類型的代理方法。它為webView提供了原生的彈框,而不是JavaScript里的提示框。雖然JavaScript的提示框可以做的跟原生一樣,但是對于ios開發者來說,如果要更改提示框就不方便了。提供這個代理,可以讓ios端更加靈活的修改提示框的樣式。
// 新建WKWebView
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;
// 關閉WKWebView
- (void)webViewDidClose:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
// 對應js的Alert方法
/**
* web界面中有彈出警告框時調用
*
* @param webView 實現該代理的webview
* @param message 警告框中的內容
* @param frame 主窗口
* @param completionHandler 警告框消失調用
*/
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
// 對應js的confirm方法
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
// 對應js的prompt方法
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler;
3、【WKScriptMessageHandler】
這個協議中包含一個必須實現的方法,這個方法是native與web端交互的關鍵,它可以直接將接收到的JS腳本轉為OC或Swift對象。
// 從web界面中接收到一個腳本時調用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
重點之WKWebVieW和JS交互
HTML代碼
<html>
<!--描述網頁信息-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>title</title>
<style>
*{
font-size: 50px;
}
.btn{height:80px; width:80%; padding: 0px 30px; background-color: #0071E7; border: solid 1px #0071E7; border-radius:5px; font-size: 1em; color: white}
</style>
<script>
//OC調用JS的方法列表
function alertMobile() {
document.getElementById('mobile').innerHTML = '不帶參數'
}
function alertName(msg) {
//有一個參數
document.getElementById('name').innerHTML = '有一個參數 :' + msg
}
function alertSendMsg(num,msg) {
//有兩個參數
document.getElementById('msg').innerHTML = '有兩個參數:' + num + ',' + msg + '!!'
}
//JS響應方法列表
function btnClick1() {
window.webkit.messageHandlers.showMobile.postMessage(null)
}
function btnClick2() {
window.webkit.messageHandlers.showName.postMessage('有一個參數')
}
function btnClick3() {
window.webkit.messageHandlers.showSendMsg.postMessage(['兩個參數One', '兩個參數Two'])
}
</script>
</head>
<!--網頁具體內容-->
<body>
<br/>
<div>
<label>WKWebView&JS交互</label>
</div>
<br/>
<div id="mobile"></div>
<div>
<button class="btn" type="button" onclick="btnClick1()">不帶參數</button>
</div>
<br/>
<div id="name"></div>
<div>
<button class="btn" type="button" onclick="btnClick2()">一個參數</button>
</div>
<br/>
<div id="msg"></div>
<div>
<button class="btn" type="button" onclick="btnClick3()">兩個參數</button>
</div>
</body>
</html>
OC代碼
需要的頭文件
#import <WebKit/WebKit.h>
需要遵守的代理<WKScriptMessageHandler>
1、設置偏好設置,以及JS調用OC 添加處理腳本,這里的內容我寫在了viewDidLoad里面,但是需要注意的是,需要在我們結束的時候釋放WKUserContentController,不然會造成內存泄漏
- (void)viewDidLoad {
[super viewDidLoad];
// 設置偏好設置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 默認為0
config.preferences.minimumFontSize = 10;
//是否支持JavaScript
config.preferences.javaScriptEnabled = YES;
//不通過用戶交互,是否可以打開窗口
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height/2) configuration:config];
[self.view addSubview:self.webView];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSURL *baseURL = [[NSBundle mainBundle] bundleURL];
[self.webView loadHTMLString:[NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil] baseURL:baseURL];
WKUserContentController *userCC = config.userContentController;
//JS調用OC 添加處理腳本
[userCC addScriptMessageHandler:self name:@"showMobile"];
[userCC addScriptMessageHandler:self name:@"showName"];
[userCC addScriptMessageHandler:self name:@"showSendMsg"];
}
2、釋放WKUserContentController代碼
-(void)removeAllScriptMsgHandle{
WKUserContentController *controller = self.webView.configuration.userContentController;
[controller removeScriptMessageHandlerForName:@"showMobile"];
[controller removeScriptMessageHandlerForName:@"showName"];
[controller removeScriptMessageHandlerForName:@"showSendMsg"];
}
在JS調用OC以后會走的代理
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"%@",NSStringFromSelector(_cmd));
NSLog(@"%@",message.body);
if ([message.name isEqualToString:@"showMobile"]) {
[self showMsg:@"沒有參數"];
}
if ([message.name isEqualToString:@"showName"]) {
NSString *info = [NSString stringWithFormat:@"%@",message.body];
[self showMsg:info];
}
if ([message.name isEqualToString:@"showSendMsg"]) {
NSArray *array = message.body;
NSString *info = [NSString stringWithFormat:@"有兩個參數: %@, %@ !!",array.firstObject,array.lastObject];
[self showMsg:info];
}
}
網頁加載完成之后調用JS代碼才會執行,因為這個時候html頁面已經注入到webView中并且可以響應到對應方法。OC調用JS代碼
//不帶參數
- (IBAction)NOParameter:(id)sender {
[self.webView evaluateJavaScript:@"alertMobile()" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//JS 返回結果
NSLog(@"%@ %@",response,error);
}];
}
//一個參數
- (IBAction)oneParameter:(id)sender {
/*
*alertName('奧特們打小怪獸')
*alertName JS方法名
*奧特們打小怪獸 帶的參數
*/
[self.webView evaluateJavaScript:@"alertName('奧特們打小怪獸')" completionHandler:nil];
}
//兩個參數
- (IBAction)twoParameter:(id)sender {
/*
*alertSendMsg('我是參數1','我是參數2')
*alertSendMsg JS方法名
*我是參數1 帶的參數
*我是參數2
*/
[self.webView evaluateJavaScript:@"alertSendMsg('我是參數1','我是參數2')" completionHandler:nil];
}
- (void)showMsg:(NSString *)msg {
[[[UIAlertView alloc] initWithTitle:nil message:msg delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
}
解決POST請求傳參不管用問題
當我們用UIWebView POST請求傳參時一般是這樣寫的
// 創建WebView
UIWebView *webView = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 設置訪問的URL
NSURL *url = [NSURL URLWithString:@"http://www.example.com"];
// 根據URL創建請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設置請求方法為POST
[request setHTTPMethod:@"POST"];
// 設置請求參數
[request setHTTPBody:[@"username=aaa&password=123" dataUsingEncoding:NSUTF8StringEncoding]];
// WebView加載請求
[webView loadRequest:request];
// 將WebView添加到視圖
[self.view addSubview:webView];
但是當我們用WKWebView這樣加載的時候并沒有什么卵用。
解決方法
參考文章:
http://stackoverflow.com/questions/26253133/cant-set-headers-on-my-wkwebview-post-request
http://www.lxweimin.com/p/403853b63537
1、將一個包含JavaScript的POST請求的HTML代碼放到工程目錄中
2、加載這個包含JavaScript的POST請求的代碼到WKWebView
3、加載完成之后,用Native調用JavaScript的POST方法并傳入參數來完成請求
1、創建包含JavaScript的POST請求的HTML代碼
<html>
<head>
<script>
//調用格式: 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>
將這段代碼拷貝下來,然后粘貼到文本編輯器中,名字可以隨意起,比方說保存為:JSPOST.html,然后拷貝到工程目錄中,記得選擇對應的Target和勾選Copy items if needed(默認應該是勾選的)。這時候,就可以用這段JavaScript代碼來發送帶參數的POST請求了。
2、將對應的JavaScript代碼通過加載本地網頁的形式加載到WKWebView
// JS發送POST的Flag,為真的時候會調用JS的POST方法(僅當第一次的時候加載本地JS)
self.needLoadJSPOST = YES;
// 創建WKWebView
self.webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
//設置代理
self.webView.navigationDelegate = self;
// 獲取JS所在的路徑
NSString *path = [[NSBundle mainBundle] pathForResource:@"JSPOST" ofType:@"html"];
// 獲得html內容
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
// 加載js
[self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
// 將WKWebView添加到當前View
[self.view addSubview:self.webView];
3、Native調用JavaScript腳本并傳入參數來完成POST請求
這里需要用到WKWebView和JS的交互
// 加載完成的代理方法
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
// 判斷是否需要加載(僅在第一次加載)
if (self.needLoadJSPOST) {
// 調用使用JS發送POST請求的方法
[self postRequestWithJS];
// 將Flag置為NO(后面就不需要加載了)
self.needLoadJSPOST = NO;
}
}
// 調用JS發送POST請求
- (void)postRequestWithJS {
// 發送POST的參數
NSString *postData = @"\"username\":\"aaa\",\"password\":\"123\"";
// 請求的頁面地址
NSString *urlStr = @"http://www.postexample.com";
// 拼裝成調用JavaScript的字符串
NSString *jscript = [NSString stringWithFormat:@"post('%@', {%@});", urlStr, postData];
// NSLog(@"Javascript: %@", jscript);
// 調用JS代碼
[self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
}];
}