最近在iOS項目中需要使用到oc與js之間的相互調用,而且要求是實現方式必須與Android中的相同,方便js中統一處理。於是在對第三方庫WebViewJavascriptBridge進行研究之後,仿照Android中的WebView與JS的交互機制,實現了一個,在這裏分享給大家。
首先要說明的是,在iOS中js調用Objective-C的代碼只能通過重定向的形式進行,即js中通過修改iframe的src,或者直接跳轉到一個url,在Objective-C中通過UIWebView的
webView:shouldStartLoadWithRequest:navigationType:方法攔截這個跳轉,然後通過解析跳轉的url獲取js需要調用的方法名和參數。而在Android中,只需要調用WebView的addJavascriptInterface方法,將一個js對象綁定到一個java類,在類中實現相應的函數,當js需要調用java的方法時,只需要直接在js中通過綁定的對象調用相應的函數即可。
顯然Android中js交互的方式要比iOS上方便得多,因此,我們可以在iOS上實現一套與Android相類似的機制。下面先說明一下實現的原理,要在js中直接通過綁定的對象調用相應的函數,那麼就需要在js中添加相應的代碼,但是爲了確保與Android的一致性,js代碼應該在客戶端以注入的形式加入。所以,我們先實現一下需要注入的代碼:
- ;(function() {
- var messagingIframe,
- bridge = 'external',
- CUSTOM_PROTOCOL_SCHEME = 'jscall';
- if (window[bridge]) { return }
- function _createQueueReadyIframe(doc) {
- messagingIframe = doc.createElement('iframe');
- messagingIframe.style.display = 'none';
- doc.documentElement.appendChild(messagingIframe);
- }
- window[bridge] = {};
- var methods = [%@];
- for (var i=0;i<methods.length;i++){
- var method = methods[i];
- var code = "(window[bridge])[method] = function " + method + "() {messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ':' + arguments.callee.name + ':' + encodeURIComponent(JSON.stringify(arguments));}";
- eval(code);
- }
- //創建iframe,必須在創建external之後,否則會出現死循環
- _createQueueReadyIframe(document);
- //通知js開始初始化
- //initReady();
- })();
- - (void)webViewDidFinishLoad:(UIWebView *)webView {
- if (webView != _webView) { return; }
- //is js insert
- if (![[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"typeof window.%@ == 'object'", kBridgeName]] isEqualToString:@"true"]) {
- //get class method dynamically
- unsigned int methodCount = 0;
- Method *methods = class_copyMethodList([self class], &methodCount);
- NSMutableString *methodList = [NSMutableString string];
- for (int i=0; i<methodCount; i++) {
- NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(methods[i])) encoding:NSUTF8StringEncoding];
- [methodList appendString:@"\""];
- [methodList appendString:[methodName stringByReplacingOccurrencesOfString:@":" withString:@""]];
- [methodList appendString:@"\","];
- }
- if (methodList.length>0) {
- [methodList deleteCharactersInRange:NSMakeRange(methodList.length-1, 1)];
- }
- NSBundle *bundle = _resourceBundle ? _resourceBundle : [NSBundle mainBundle];
- NSString *filePath = [bundle pathForResource:@"WebViewJsBridge" ofType:@"js"];
- NSString *js = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
- [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:js, methodList]];
- }
- __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
- if (strongDelegate && [strongDelegate respondsToSelector:@selector(webViewDidFinishLoad:)]) {
- [strongDelegate webViewDidFinishLoad:webView];
- }
- }
該委託對加載完成的網頁進行了js注入,將類中實現的方法添加到了js中。注意,以上代碼在注入前需要判斷是否已經注入,避免重複注入。接下來,我們可以再實現一個webview的委託來攔截重定向事件:
- - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
- if (webView != _webView) { return YES; }
- NSURL *url = [request URL];
- __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
- NSString *requestString = [[request URL] absoluteString];
- if ([requestString hasPrefix:kCustomProtocolScheme]) {
- NSArray *components = [[url absoluteString] componentsSeparatedByString:@":"];
- NSString *function = (NSString*)[components objectAtIndex:1];
- NSString *argsAsString = [(NSString*)[components objectAtIndex:2]
- stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
- NSData *argsData = [argsAsString dataUsingEncoding:NSUTF8StringEncoding];
- NSDictionary *argsDic = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:argsData options:kNilOptions error:NULL];
- //convert js array to objc array
- NSMutableArray *args = [NSMutableArray array];
- for (int i=0; i<[argsDic count]; i++) {
- [args addObject:[argsDic objectForKey:[NSString stringWithFormat:@"%d", i]]];
- }
- //ignore warning
- #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
- SEL selector = NSSelectorFromString([args count]>0?[function stringByAppendingString:@":"]:function);
- if ([self respondsToSelector:selector]) {
- [self performSelector:selector withObject:args];
- }
- return NO;
- } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
- return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
- } else {
- return YES;
- }
- }
以上代碼中對重定向的url進行了判斷,如果符合我們事先定義的協議,就進行解析,否則就進行跳轉。解析的時候以冒號分隔,取出函數名和參數列表,並調用相應的方法。至此,我們就在iOS中實現了與Android相同的調用機制。
爲了方便大家使用,我將以上代碼封裝成了一個類,具體的使用方法見Demo。
需要注意的是:
1. 在實際應用中可能出現js執行順序的問題,如果網頁中的js在注入前先獲取綁定的對象進行保存,是無法獲取到的,因爲這時候待綁定的對象爲空。這就需要網頁中js的初始化在注入之後,因此在上文代碼中有一個initReady方法。
2. 由於performSelector最多隻能包含兩個參數,因此例子中是通過數組來傳遞參數列表的。