來(lái)自何方
根據(jù)類的前綴可以得知,UIWebView是來(lái)自于UIKit框架,WKWebView來(lái)自于WebKit。所以,UIWebVieW和WKWebView的性能差距可想而知,后來(lái)我們會(huì)一一道來(lái)。
UIWebView 常用 API分析
-(void)loadRequest:(NSURLRequest *)request;
// 以NSURLRequest的方式加載(一般是NSURL的方式初始化request)。
-(void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 把HTML轉(zhuǎn)換成string文本方式加載。
-(void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;
// 以二進(jìn)制流data的方式加載。
-(void)reload; //重新加載當(dāng)前的界面
-(void)stopLoading; // 停止加載
-(void)goBack; // 導(dǎo)航條回退
-(void)goForward; // 導(dǎo)航條前進(jìn)
// goBack 和 goForward 類似于UINavagationController 管理的控制器類型(UIViewController)的棧結(jié)構(gòu)執(zhí)行的Push和Pop操作。
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
//這個(gè)方法經(jīng)常會(huì)用到,script是js腳本,通過(guò)script來(lái)調(diào)起js的方法。
以下是webView常用的屬性:
- @property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack; //是否能回退
- @property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward; // 是否可以前進(jìn)
- @property (nonatomic, readonly, getter=isLoading) BOOL loading; // 是否在加載中
- @property (nonatomic) BOOL scalesPageToFit; // 是否界面是自適應(yīng)的
- @property (nonatomic) UIDataDetectorTypes dataDetectorTypes NS_AVAILABLE_IOS(3_0); // 設(shè)置界面點(diǎn)擊后的類型檢測(cè)。
UIWebViewDelegate 代理
@protocol UIWebViewDelegate
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
//是否接受這個(gè)request的加載請(qǐng)求,返回布爾值。在此代理中可以通過(guò)攔截URL請(qǐng)求,截取request的url的相關(guān)屬性值來(lái)調(diào)起native方法,native和js協(xié)定好字段的相關(guān)邏輯,如果需要調(diào)起native方法,則需要返回NO。
-(void)webViewDidStartLoad:(UIWebView *)webView;
//webView開(kāi)始加載
-(void)webViewDidFinishLoad:(UIWebView *)webView;
//webView加載完畢
-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
//webView加載失敗
UIWebView native 和 js 的交互策略
- 最基本的交互方式
native 調(diào)起 js (有返回值):
-(nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
js調(diào)用native:
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在這個(gè)代理方法中進(jìn)行處理相關(guān)邏輯。
- iOS7 蘋果對(duì)外開(kāi)放的JavaScriptCore提供的交互方式
JavaScriptCore是webkit的一個(gè)重要組成部分,極大的方便了我們對(duì)js的操作。iOS7以前我們對(duì)JS的操作只有webview里面一個(gè)函數(shù)stringByEvaluatingJavaScriptFromString,JS對(duì)OC的回調(diào)都是基于URL的攔截進(jìn)行的操作。
我們要熟悉一下幾個(gè)概念:
JSContext
JS執(zhí)行的上下文環(huán)境,通過(guò)JSVirtualMachine管理著所有對(duì)象的生命周期,每個(gè)JSValue都和JSContext相關(guān)聯(lián)并且強(qiáng)引用context。
JSValue
每個(gè)JSValue都是強(qiáng)引用一個(gè)context,OC和JS對(duì)象之間的轉(zhuǎn)換也是通過(guò)它,相應(yīng)的類型轉(zhuǎn)換如下:
<pre>
@textblock
Objective-C type | JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock (1) | Function object (1)
id (2) | Wrapper object (2)
Class (3) | Constructor object (3)
@/textblock
</pre>
JSManagedValue
由于JS內(nèi)存管理是垃圾回收,并且JS中的對(duì)象都是強(qiáng)引用,而OC是通過(guò)引用計(jì)數(shù),如果相互強(qiáng)引用的話,會(huì)造成內(nèi)存泄漏問(wèn)題,所以用JSManagedValue保存JSValue來(lái)避免這種問(wèn)題的發(fā)生。
JSVirtualMachine
JS運(yùn)行的虛擬機(jī),有獨(dú)立的堆空間和垃圾回收機(jī)制。
JSExport
一個(gè)協(xié)議,如果JS對(duì)象想調(diào)起native方法,那么native需要自定義一個(gè)類來(lái)實(shí)現(xiàn)這個(gè)JSExport協(xié)議就可以了。
介紹過(guò)相關(guān)基礎(chǔ)類后,來(lái)看看調(diào)用的例子。
@protocol WebViewNativeBridgeDelegete <JSExport>
JSExportAs(add, -(int)add:(int)a other:(int)b);
@end
自定義一個(gè) helper class 實(shí)現(xiàn)這個(gè)WebViewNativeBridgeDelegete 協(xié)議就可以了。
native 實(shí)現(xiàn)這個(gè)代理方法
- (int) add: (int)a other: (int)b{
return a + b;
}
// js端在調(diào)起這個(gè)native方法的同時(shí),可以同步拿到返回值。
Note: 在webView加載完畢的時(shí)候,要拿到j(luò)s的上下文。
-(void)webViewDidFinishLoad:(UIWebView *)webView{
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.context[@"native"] = helper;
}
JSContext 通過(guò)evaluateScript 方法實(shí)現(xiàn)動(dòng)態(tài)注入
self.context[@"add"] = ^(NSInteger a, NSInteger b) {
NSLog(@"sum = %@", @(a + b));
};
[self.context evaluateScript:@"add(2,3);"];
OC call JS
JSValue *value = [self.context[@"add"] callWithArguments:@[@1, @2]];
NSLog(@"value = %@", @([value toInt32]));
UIWebView內(nèi)存暴增問(wèn)題的探究
我簡(jiǎn)單的寫了一個(gè)小界面,UIWebView加載百度的首頁(yè),點(diǎn)擊幾個(gè)界面后,內(nèi)存的增長(zhǎng)圖如下:

如圖可以看出內(nèi)存增長(zhǎng)的很急,曲線很陡峭,最大達(dá)到117.4MB,并且當(dāng)頁(yè)面切換后,內(nèi)存居高不下,內(nèi)存無(wú)法釋放。特別如果在設(shè)備性能比較差的設(shè)備,比如iPhone4,iPhone4s等,很容易觸發(fā)看門狗機(jī)制。
為了防止一個(gè)應(yīng)用占用過(guò)多的系統(tǒng)資源,開(kāi)發(fā)iOS的蘋果工程師門設(shè)計(jì)了一個(gè)“看門狗”的機(jī)制。在不同的場(chǎng)景下,“看門狗”會(huì)監(jiān)測(cè)應(yīng)用的性能。如果超出了該場(chǎng)景所規(guī)定的運(yùn)行時(shí)間,“看門狗”就會(huì)強(qiáng)制終結(jié)這個(gè)應(yīng)用的進(jìn)程。
初學(xué)者可能會(huì)想著在dealloc中,設(shè)置webView = nil, 可惜根本沒(méi)用。
我也google了一下,很多的結(jié)果是清除cache,嘗試過(guò),有效果不過(guò)不是讓人滿意的答案,能清除部分的內(nèi)存,可以參考 這篇文章 。
工欲善其事必先利其器,我們就用Instrument的allocations來(lái)檢測(cè)一下內(nèi)存暴增的原因,很明顯這是系統(tǒng)庫(kù)的問(wèn)題,以圖為證。


下圖截取時(shí)間跟上圖不一致,并且在模擬器下操作的,僅作參考
很明顯50%以上的內(nèi)存是開(kāi)辟了一個(gè)WebThread(pthread),在這個(gè)線程中bitmap的位圖渲染。由于UIWebView基于UIKit機(jī)制,文本圖片等資源都是在QuartzCore下的context棧中管理,渲染,繪制,以及runloop的保持的常駐線程。
UIWebView的內(nèi)存問(wèn)題我也無(wú)能為力,可以優(yōu)化的是保持一個(gè)UIWebViewController,持有一個(gè)UIWebView屬性,不要在不同的控制器中頻繁的創(chuàng)建。同時(shí)在適當(dāng)?shù)臅r(shí)候進(jìn)行cache的清空,也可以實(shí)現(xiàn)NSURLProtocol,進(jìn)行URL的攔截,使用緩存的圖片資源,避免重復(fù)請(qǐng)求。
不過(guò),我個(gè)人推薦使用iOS8引入的WKWebView,不過(guò)你要想兼容iOS7的話,可以做一個(gè)兼容性版本,F(xiàn)acebook也引入了,基于WebKit內(nèi)核的WKWebView,性能會(huì)有很大的提升。UIWebView的內(nèi)容就寫到這里,后續(xù)我會(huì)寫下WKWebView。
如果大家對(duì)我的理解有什么異議或者有更深的看法,歡迎一起交流,謝謝大家!