在之前的一篇文章《iOS開發——從UIWebView說起》中,介紹了UIWebView的常規用法以及注意事項,這篇文章將深入介紹一些工作中可能需要的高級用法。
UIWebView與native交互
工作中,我們經常遇到二者交互的需求,例如native根據webview內容的高度自適應處理布局或者事件響應,webview調用native實現h5無法完成的功能等。下面就從兩個方面介紹一下UIWebView與native相互交互的方法。
native獲取webview信息
通常的做法是,native執行h5頁面中提供的js方法獲得相關信息。這就涉及到native執行js語句的操作,既然介紹UIWebView,這里主要從它的角度去說如何調用js吧。
上一篇文章中介紹了UIWebView的實例方法:
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
script為要執行的js語句,返回值即得到的結果,這個結果是string類型的,一般還要經過相關的轉換處理。
下面列舉一些常用的webview信息獲取方法。
- 取消長按webView上的鏈接彈出actionSheet
[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout = 'none';"];
- 根據內容獲取webview高度
NSString *fitHeight = [webview stringByEvaluatingJavaScriptFromString:@"document.body.scrollHeight;"];
- 獲取webview頁面內容
NSString *docStr=[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.textContent"];
- 去除某個標簽內容
[webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('some-tag-id’).remove();"];
- 獲取當前頁面的title:
NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];
- 獲取當前頁面的url
NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];
- 獲取環境變量
[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
- 禁止彈出菜單
[webView valueForKeyPath:@" document.documentElement.style.webkitTouchCallout = "none";"];
- 禁止選中
[webView valueForKeyPath:@"document.documentElement.style.webkitUserSelect = "none";"];
webview調用native方法
UIWebView中的處理要想調用native方法,就需要約定一些偽協議接口,常稱做bridge接口,然后攔截請求去判斷是否符合協議,主要判斷schema,滿足則調用相關的接口。上篇文章中提到了通過UIWebViewDelegate的方法去處理,這里不再贅述。
iOS7.0開始引入了JavaScriptCore.framework,這個庫為我們帶來了很多驚喜。這個庫提供了訪問webkit的JavaScript引擎,是objective-c的封裝。有了這個框架,objective-c或swift都可以很方便的與JavaScript進行互通。
打開JavaScriptCore.h文件,你會發現,其實你需要掌握的只有這幾個類:
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
####### JSVirtualMachine
先說 JSVirtualMachine 類,原因是所有 JavaScript 代碼的執行都依賴虛擬機對象資源,JSVirtualMachine 代表一個對象空間,擁有自己的堆結構和垃圾回收機制,為 JavaScript 的運行提供了底層資源。一般情況下,開發者不需要與該類的實例交互,除非要處理設計多線程或內存管理等問題。
####### JSContext
JSContext 對象為 JavaScript 代碼的執行提供了上下文環境,在 web 開發中,相當于 window 對象。通過JSContext對象可以執行 js 代碼,因此,經常通過它給 JavaScript 注冊接口,或者拿到 JavaScript 的某些信息。JSContext 對象的初始化需要指定一個 JSVirtualMachine ,你可以理解為,一個 JSContext 的存在必然有一個指定的 JSVirtualMachine 去承接,
####### JSValue
JSValue 是 JavaScript 對象的實體,包括數字、字符串、自定義對象、函數等,只不過經過 objective-c 的包裝,因此也就有轉換成 objective-c 對象的方法。
####### JSManagedValue
JSManagedValue 的本質其實就是一個JSValue,但是這個類可以處理內存管理相關的問題,輔助更高效地管理 JavaScript 對象的內存。一般用的也比較少。
####### JSExport
JSExport 是一個 JavaScriptCore 中很關鍵的協議,是 objective-c 與 JavaScript 交互的核心方法。通過該協議,開發者可以給 JavaScript 暴露接口,也可以通過 callback 參數去完成 JavaScript 的調用。
請求攔截
說到請求攔截,大家立刻會想到UIWebView的代理方法:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
是的,沒錯!在上篇文章中也有提到,該方法用于判斷是否要處理收到的URL請求,return YES;表示開始加載請求,return NO;則攔截當前的請求并按照自己的方式在return之前去處理。
NSURLProtocol攔截
其實對于UIWebView還有更專業地操作方式,那就是使用NSURLProtocol類。在蘋果的url loading系統中,NSURLProtocol是非常重要的一個類,它獨立存在但又能攔截整個URL loading系統中所有的網絡請求,同時該類提供了豐富的接口輔助開發者根據自己需求去處理攔截到的請求。(更詳細介紹見這里)
下面從應用的角度,介紹一下如何將NSURLProtocol于UIWebView結合起來進行請求攔截。首先看一下URLProtocol常用的屬性和方法:
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
@property (nullable, readonly, copy) NSCachedURLResponse *cachedResponse;
// 該子類化的Protocol能否處理給定的request (2)
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 根據給定request,返回一個規范的request (3)
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
// 啟動加載請求(規范后的),在這里實現本地請求處理
- (void)startLoading; (4)
// 停止請求加載
- (void)stopLoading; (5)
// 存儲屬性信息,用于傳遞request相關信息
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
// 獲取存儲在request中的屬性信息
+ (nullable id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
// 注冊子類化的NSURLProtocol (1)
+ (BOOL)registerClass:(Class)protocolClass;
// 注銷子類化的NSURLProtocol (6)
+ (void)unregisterClass:(Class)protocolClass;
上面標記的數字表示請求加載時抽象接口的調用順序,因此在子類中,需要按照相應的順序進行處理。
在類中有一個屬性 client,該屬性正是開發者用于與 loading 系統交互的橋梁,當我們攔截住請求并成功處理之后,需要告訴loading系統,你請求的數據我們已經準備好了,這個時候就是通過這個 client 去代理完成。相關回調參考協議<NSURLProtocolClient>
一般我們這么處理:
if(!error){
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
} else {
[self.client URLProtocol:self didFailWithError:error];
[self.client URLProtocolDidFinishLoading:self];
}
其中,response、data需要我們提前準備好。
使用注意
NSURLProtocol是一個抽象類,要想使用必須子類化它,并且,最好在用完之后能 unregister,避免無休止的攔截判斷,影響app性能。
Protocols 的遍歷是反向的,如果客戶端同時注冊了多個 NSURLProtocol,那么首先會走到最后一個注冊的 Protocol 里,直到第一個。
NSURLCache 攔截
為提高 webview 加載體驗,緩存是必不可少的部分,除了上面的方法,其實蘋果專門提供了一個強大卻經常被開發者忽略的類來處理 url 請求的緩存——NSURLCache。觀察蘋果的URL loading系統,Cache Management 部分其實很值得注意。通常情況下,當loading系統加載一個請求時,首先會從NSURLCache 里查找有沒有緩存,有緩存就拿來用,沒有則根據url去網絡加載。
因此,無論如何,系統都會帶著 request 走到 NSURLCache 中,然后判斷。因此,與 NSURLProtocol 思路相同,我們也可以子類化 NSURLCache 然后判斷是否要對request進行攔截。
看一下 NSURLCache 提供的常用屬性和方法:
- (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
@property NSUInteger memoryCapacity;
@property NSUInteger diskCapacity;
@property (readonly) NSUInteger currentMemoryUsage;
@property (readonly) NSUInteger currentDiskUsage;
之所以很多開發者忽略NSURLCache,是因為雖然 NSURLCache 內存緩存和磁盤緩存,支持設置最大緩存大小,但是實際上,NSURLCache 只能使用內存緩存,并且最大容量為4M。但是,作為攔截只用,NSURLCache 還是可以支持的。