最近因Apple的要求:App需要將UIWebview換成WKWebview,經(jīng)過(guò)一個(gè)月的開(kāi)發(fā)和測(cè)試,重要上線了! 下文重點(diǎn)記錄下遇到一些問(wèn)題.
一 UserAgent的設(shè)置
項(xiàng)目中UIWebview設(shè)置UA是全局設(shè)置的,還要防止UA被篡改,調(diào)研很多關(guān)于WK設(shè)置UA的文章,大多數(shù)都是webveiw loadRequest時(shí)設(shè)置UA,沒(méi)有達(dá)到預(yù)期,考慮到用WK設(shè)置UA風(fēng)險(xiǎn)高,一旦UA出了問(wèn)題就徹底晾涼了! UA繼續(xù)用UIWebview來(lái)設(shè)置;小心駛得萬(wàn)年船!
二 Cookie的寫入
本次改造大部分時(shí)間花在了Cookie的問(wèn)題上,WK作為多進(jìn)程組件其實(shí)是對(duì)UIWebveiw的拆分和組裝,新增了一些個(gè)人比較喜歡的功能:
1 estimatedProgress:這個(gè)屬性幫我們實(shí)現(xiàn)界面加載進(jìn)度條,告別了仿真進(jìn)度條的尷尬,WK上的一些原生特殊元素添加也變得游刃有余了.
2 title: 獲取界面title,好多界面title是異步請(qǐng)求,在UIWebview無(wú)能為力,只好拿著定時(shí)器碰運(yùn)氣,WK
3 backForwardList:在某些特定場(chǎng)景下需要返回H5界面的首頁(yè),在UIWebview中無(wú)法通過(guò)API實(shí)現(xiàn)的,可以通過(guò)JS實(shí)現(xiàn):
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"if( window.history.length > 1 ) { window.history.go( -( window.history.length - 1 ) ) };"]];
這年頭不會(huì)點(diǎn)JavaScript寸步難行,不好意思說(shuō)開(kāi)發(fā)過(guò)hybrid App.
有喜有憂,WKWebView實(shí)例不會(huì)把Cookie存入到App標(biāo)準(zhǔn)的Cookie容器(NSHTTPCookieStorage
)中,需要開(kāi)發(fā)者手動(dòng)存入,雖然在iOS11Apple在Webkit框架中新增了WKHTTPCookieStore
,可以實(shí)現(xiàn)WK Cookie同步;但App需要支持iOS9以上版本,采用了head中寫入Cookie,document.cookie
中注入Cookie方式實(shí)現(xiàn).
記錄下遇到的問(wèn)題
(1) 跨域情況.
1.1 服務(wù)端不設(shè)置Domian
的情況
1.2 本地寫Cookie的情況
NSMutableDictionary *cookiePropertiesZtappcliver = [NSMutableDictionary dictionary];
[cookiePropertiesZtappcliver setObject:@"ztappcliver" forKey:NSHTTPCookieName];
NSString *version = [UserLoginHelper sharedInstance].systemVersion;
version = [NSString stringWithFormat:@"ios@%@",version];
[cookiePropertiesZtappcliver setObject:SAFE_STRING(version) forKey:NSHTTPCookieValue];
//為空字符串所有的域名都可以訪問(wèn) 不寫NSHTTPCookieDomain 域名沒(méi)法訪問(wèn).
[cookiePropertiesZtappcliver setObject:@"" forKey:NSHTTPCookieDomain];
[cookiePropertiesZtappcliver setObject:@"/" forKey:NSHTTPCookiePath];
[cookieArray addObject:cookiePropertiesZtappcliver];
(2) cookie會(huì)出現(xiàn)重復(fù)的情況
登錄時(shí)存入WKHTTPCookieStore
容器中cookie和服務(wù)端重新寫入cookie名稱相同時(shí)就會(huì)出現(xiàn)重復(fù)的情況,解決辦法是覆蓋WKHTTPCookieStore
容器中cookie,具體在 - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
中去重.
方法為:
if ([GlobalMembers IsArraySafe:cookies]) {
for (NSHTTPCookie *cookie in cookies) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (NSHTTPCookie *ex_cookie in [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies) {
// 響應(yīng)cookie 如果和客戶端請(qǐng)求cookie重復(fù) 則 這個(gè)判斷是有風(fēng)險(xiǎn)的 name值相同 不代表其他屬性是相同的
if ([ex_cookie.name isEqualToString:cookie.name]) {
DLog(@"%@ cookie========",cookie.name);
[[NSHTTPCookieStorage sharedHTTPCookieStorage]deleteCookie:ex_cookie];
}
}
});
[[NSHTTPCookieStorage sharedHTTPCookieStorage]setCookie:cookie];
}
}
WK 真的不能同步WKHTTPCookieStore
中的cookie嗎Σ(⊙⊙"a ?
可以同步,通過(guò)這個(gè)例子可以看出,WKWebView 實(shí)例其實(shí)也會(huì)將 Cookie 存儲(chǔ)于 NSHTTPCookieStorage 中,但存儲(chǔ)時(shí)機(jī)有延遲,因?yàn)閃KWebView內(nèi)也有cookie的容器,而且這個(gè)同步是進(jìn)程級(jí)別的同步铛嘱,而且這個(gè)同步是單向.
(3) 刪除cookie和WK緩存清理問(wèn)題
客戶端退出登錄時(shí)會(huì)清空Cookie,連帶將WK的緩存清空,測(cè)試階段并沒(méi)有發(fā)現(xiàn)問(wèn)題,后期測(cè)試發(fā)現(xiàn)用戶首次登錄某些4g網(wǎng)絡(luò)下業(yè)務(wù)無(wú)法單點(diǎn)登錄,通過(guò)和早期支持UIWebiew版本對(duì)比得出結(jié)論:WK緩存清空導(dǎo)致的,關(guān)于WK緩存問(wèn)題,可以拜讀https://blog.csdn.net/u012413955/article/details/79783282
(4) cookie因某種原因丟失的情況
一般是重定向或者iOS9以下系統(tǒng)會(huì)出現(xiàn).
(5) iOS13上無(wú)法搖一搖的情況
在iOS13上Apple對(duì)于瀏覽器訪問(wèn)設(shè)備運(yùn)動(dòng)和方向事件在默認(rèn)情況下處于關(guān)閉狀態(tài).
前端解決方案如下:
警告:Call to requestPermission() failed, reason: Browsing context is not secure 訪問(wèn)鏈接改成https即可霎俩。
(5) evaluateJavaScript
:WK調(diào)用JS方法,completionHandler
返回值的id類型。
JS方法:
completionHandler
:返回的值是個(gè)NSCFBoolean
,切記不能當(dāng)作BOOL值強(qiáng)轉(zhuǎn)處理,因?yàn)镹SCFBoolean本身是個(gè)NSNumber對(duì)象仁堪。
(6)Webkit:JScore深入理解
1 從iOS7之后,JScore作為系統(tǒng)的Framework被蘋果提供個(gè)開(kāi)發(fā)者,Webkit
作為iOS的頁(yè)面渲染及邏輯處理引擎,HTML5通過(guò)WebKit處理后就可以顯示界面,WebKit
框架中主要包括:WebCore
,JScore
,WebKit Embedding API
,WebKit Ports
哮洽。
2 WebKit Embedding API
:負(fù)責(zé)瀏覽器和UI交互。
3 WebKit Ports
:讓WebKit
方便移植到各個(gè)操作系統(tǒng)上弦聂。
4 WebCore
:是WebKit
中最核心的渲染引擎鸟辅。
5 JScore
: 是WebKit
的JS默認(rèn)內(nèi)嵌引擎。
5.1 JSCore
采用基于寄存器的指令結(jié)構(gòu),寄存器指令 執(zhí)行效率更高莺葫。但是由于這樣的架構(gòu)也造成內(nèi)存開(kāi)銷更大的問(wèn)題,還存在移植性弱的問(wèn)題匪凉,因?yàn)樘摂M機(jī)中的虛擬寄存器需要去匹配到真實(shí)機(jī)器中CPU的寄存器,可能會(huì)存在真實(shí)CPU寄存器不足的問(wèn)題捺檬。
5.2 JSCore
單線程機(jī)制,整個(gè)js在一條線程中執(zhí)行再层。
5.3 JSCore
事件驅(qū)動(dòng)機(jī)制。
5.4 WKWebview
中,也封裝了JSCore
,但無(wú)法使用系統(tǒng)的JSCore Framework
;下面簡(jiǎn)單學(xué)習(xí)記錄下JSCore Framework
的幾個(gè)重要概念。
5.4.1 JSVM
一個(gè)JSVirtualMachine(以下簡(jiǎn)稱JSVM)實(shí)例代表了一個(gè)自包含的JS運(yùn)行環(huán)境聂受,或者是一系列JS運(yùn)行所需的資源蒿秦。該類有兩個(gè)主要的使用用途:一是支持并發(fā)的JS調(diào)用,二是管理JS和Native之間橋?qū)ο蟮膬?nèi)存.
JSCore
被認(rèn)為是一個(gè)虛擬機(jī)蛋济,那JSVM又是什么渤早?實(shí)際上,JSVM就是一個(gè)抽象的JS虛擬機(jī)瘫俊,讓開(kāi)發(fā)者可以直接操作鹊杖。在App中,我們可以運(yùn)行多個(gè)JSVM來(lái)執(zhí)行不同的任務(wù)扛芽。而且每一個(gè)JSContext都從屬于一個(gè)JSVM骂蓖。但是需要注意的是每個(gè)JSVM都有自己獨(dú)立的堆空間,GC
也只能處理JSVM內(nèi)部的對(duì)象川尖。所以說(shuō)登下,不同的JSVM之間是無(wú)法傳遞值的。
5.4.2 JSContext
__weak typeof (UIWebView *)weakWebView = webView;
JSContext *Context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//weview 持有JSContext 這個(gè)會(huì)使得引用系數(shù)+1
Context[BridgeObject] = ^(){};
在UIWebview中通過(guò)KVC獲取JSContext
,然后調(diào)用JS,訪問(wèn)JS的值和函數(shù),提供JS訪問(wèn)Native的接口叮喳。
Objective-C type | JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock | Function object
id | Wrapper object
Class | Constructor object
5.4.3 JSExport
(7) WK下鍵盤彈起,無(wú)法輸入問(wèn)題,UIWebview正常 如圖:
前端代碼
App端的原因:
通過(guò)排查,發(fā)現(xiàn)客戶端WK協(xié)議中實(shí)現(xiàn)了[self evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil]; [self evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:nil];
這個(gè)兩個(gè)限制是為了防止用戶長(zhǎng)按彈出一些系統(tǒng)彈窗,強(qiáng)迫癥把它去掉了,沒(méi)想到影響這個(gè)鍵盤彈起輸入,按理說(shuō)長(zhǎng)按和鍵盤輸入八竿子打不著啊 ,怎么這樣呢,繼續(xù)入坑查.....
-webkit-touch-callout
:當(dāng)觸摸并按住觸摸目標(biāo)時(shí)候是否彈出系統(tǒng)默認(rèn)彈窗,默認(rèn)值為inherit
不彈出, 當(dāng)值為none
靜止展示系統(tǒng)默認(rèn)彈窗.
document.documentElement.style.webkitUserSelect='none';
:在WK中屏蔽長(zhǎng)按彈出默認(rèn)的UIMenuController
真機(jī)debug調(diào)試界面得出結(jié)論:document.documentElement.style.webkitUserSelect='none';
影響了html的contenteditable
屬性,導(dǎo)致鍵盤無(wú)法輸入.
(8) WK下鍵盤彈起,報(bào)錯(cuò)日志
當(dāng)鍵盤彈出時(shí)被芳,Xcode提示(WKWebView constrains issue when keyboard pops up),未解決.
(9) didFailNavigation
和 didFailProvisionalNavigation
的區(qū)別
這兩個(gè)方法容易被混淆,本人居然一直使用didFailNavigation
監(jiān)聽(tīng)webveiw請(qǐng)求失敗的操作,而且上線運(yùn)行了2個(gè)版本,啊啊啊!!!
/*! @abstract Invoked when an error occurs during a committed main frame(在提交的主框架期間發(fā)生錯(cuò)誤時(shí)調(diào)用)
navigation.
@param webView The web view invoking the delegate method.
@param navigation The navigation.
@param error The error that occurred.
*/
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
/*! @abstract Invoked when an error occurs while starting to load data for
the main frame(在開(kāi)始為加載數(shù)據(jù)時(shí)發(fā)生錯(cuò)誤時(shí)調(diào)用).
@param webView The web view invoking the delegate method.
@param navigation The navigation.
@param error The error that occurred.
*/
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
(9) WK error: Error Domain=NSURLErrorDomain Code=-1007 "too many HTTP redirects":
抓包分析:地址302重定向,客戶端似乎沒(méi)有發(fā)起請(qǐng)求,導(dǎo)致一直重定向地址. 302跳轉(zhuǎn)異常.
客戶端原因:
在302跳轉(zhuǎn)中,重寫請(qǐng)求head
信息,導(dǎo)致跳轉(zhuǎn)下一個(gè)界面時(shí),上一個(gè)界面設(shè)置的cookie丟失了. 再次體會(huì)下大神的總結(jié).
(10) Error Domain=WKErrorDomain Code=3 "WKWebView已失效" UserInfo={NSLocalizedDescription=WKWebView已失效}
出現(xiàn)這種情況,在AppDelegate中獲取ua時(shí),代碼如下
@objc class func wksetUserAgent(){
let webView = WKWebView.init()
webView?.evaluateJavaScript("window.navigator.userAgent;") { (result:Any?, error:Error?) in
if(error != nil){
debugPrint(error ?? "userAgent獲取失敗");
}else{
debugPrint(result ?? "userAgent獲取成功")
}
}
}
swift類提前釋放造成的,可以將當(dāng)前類改成單利(不建議使用單利),定義一個(gè)全局局部存儲(chǔ)屬性.
//WK 設(shè)置UA
var webView:WKWebView?
class BaseHelper: NSObject
@objc func wksetUserAgent(){
webView = WKWebView.init()
webView?.evaluateJavaScript("window.navigator.userAgent;") { [weak self](result:Any?, error:Error?) in
let strongSelf = self
if(error != nil){
debugPrint(error ?? "userAgent獲取失敗");
}else{
debugPrint(result ?? "userAgent獲取成功")
}
//strongSelf?.webView = nil;
}
}
deinit {
debugPrint("釋放了");
}
如果是OC的話,屬性強(qiáng)引用下,在block置空下
@property(nonatomic, strong) WKWebView * webView; // 強(qiáng)引用一下
(10) WK截取整個(gè)Html界面
+(UIImage *)wk_saveWebviewImage:(WKWebView *)webview
{
UIGraphicsBeginImageContextWithOptions(webview.bounds.size, webview.opaque, 0.0);
// [webview.layer renderInContext:UIGraphicsGetCurrentContext()];
// UIImage *senderimage = UIGraphicsGetImageFromCurrentImageContext();
// UIGraphicsEndImageContext();
//
// return senderimage;
CGFloat scale = [UIScreen mainScreen].scale;
CGSize boundsSize = webview.bounds.size;
CGFloat boundsWidth = boundsSize.width;
CGFloat boundsHeight = boundsSize.height;
CGSize contentSize = webview.scrollView.contentSize;
CGFloat contentHeight = contentSize.height;
// CGFloat contentWidth = contentSize.width;
CGPoint offset = webview.scrollView.contentOffset;
[webview.scrollView setContentOffset:CGPointMake(0, 0)];
NSMutableArray *images = [NSMutableArray array];
while (contentHeight > 0) {
UIGraphicsBeginImageContextWithOptions(boundsSize, NO, [UIScreen mainScreen].scale);
[webview.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[images addObject:image];
CGFloat offsetY = webview.scrollView.contentOffset.y;
[webview.scrollView setContentOffset:CGPointMake(0, offsetY + boundsHeight)];
contentHeight -= boundsHeight;
}
[webview.scrollView setContentOffset:offset];
CGSize imageSize = CGSizeMake(contentSize.width * scale,
contentSize.height * scale);
UIGraphicsBeginImageContext(imageSize);
[images enumerateObjectsUsingBlock:^(UIImage *image, NSUInteger idx, BOOL *stop) {
[image drawInRect:CGRectMake(0,
scale * boundsHeight * idx,
scale * boundsWidth,
scale * boundsHeight)];
}];
UIImage *fullImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// UIImageView * snapshotView = [[UIImageView alloc]initWithFrame:CGRectMake(0,0, webview.scrollView.contentSize.width, webview.scrollView.contentSize.height)];
// 拉伸圖片
//snapshotView.image = [fullImage resizableImageWithCapInsets:UIEdgeInsetsZero];
return fullImage;
}
本人還是沿用以前UIWebveiw的方法,代碼如上;功能藏得比較深,也沒(méi)有做測(cè)試,只是替換,通過(guò)幾個(gè)版本迭代,發(fā)現(xiàn)截取界面有空白;參考https://www.cnblogs.com/breezemist/p/7569798.html,文章提供的github項(xiàng)目使用偶然會(huì)也會(huì)出現(xiàn)界面空白的情況,還有一些非必須的截取bug,比如界面加載動(dòng)畫View也被截取上了,不符合要求.
最后將這個(gè)操作由前端實(shí)現(xiàn);前端繪制界面,以base64的形式傳給客戶端解析,這種做法的缺陷也很明顯,有卡頓,有時(shí)需要好幾秒;甚至有些設(shè)備會(huì)出現(xiàn)圖片空白的情況;應(yīng)該是某些舊的設(shè)備因?yàn)橘Y源開(kāi)銷問(wèn)題(繪制圖片耗時(shí)等),WKwebview崩潰了,同時(shí)觸發(fā)調(diào)用了webViewWebContentProcessDidTerminate
方法
客戶端主要實(shí)現(xiàn)
NSArray *imageArray = [base64Str componentsSeparatedByString:@","];
screenImages = [UIImage imageWithData:[[imageArray objectAtIndex:1] base64DecodedData]];
有好的做法,歡迎交流.
(11) webview ur包含&provId=##provId##&secCode=##secCode##&sign=##sign##&sysId 這種##的地址界面展示空白,無(wú)法響應(yīng)webvew協(xié)議方法的問(wèn)題
有人說(shuō)是##被編碼引起,但客戶端只是對(duì)漢字編碼,所以應(yīng)該不是這個(gè)原因. ?????
未完待續(xù)
排查工程中UIWebView grep -r UIWebView .
JSCore參考文獻(xiàn)
https://www.cnblogs.com/meituantech/p/9528285.html