在ANDROID中,WebView控件有setJavaScriptEnable接口,這裏大概的意思就是讓客戶端可以響應來自WebView的回調,還有一個接口是addJavaScriptInterface(obj, "external"),這個接口的大概意思是給obj開一個叫"external"的口子,這樣前端經過window.external.func(param1,param2...)這樣的方式就能夠直接調用obj中名叫"func"的方法了。html
在IOS中,要想實現這樣的WebView須要通過一段周章,下面開始簡要說明一下前端可以調用到客戶端的代碼的基本原理:客戶端無論是根據本地的html加載網頁仍是url動態加載網頁,實際上都已經接管了網頁上的源碼,然而這個源碼是用JavaScript寫的,這種源碼是不能直接對IOS的OC代碼進行調用的,咱們要作的就是這樣的一個轉換,讓JS經過一個bridge間接調用OC。
前端
;(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(); })();
咱們一般使用IOS的WebView控件都是經過實現shouldStartLoadWithRequest等相關代理來截獲網頁url變化這個通知,在url中一般就隱含了咱們須要的參數,然而這種方式並不夠人性化,前端要是可以直接經過函數調用的方法來call OC的native是比較合理的方式。web
shouldStartLoadWithRequest何時會被調用?是否必定要url變化纔會調用?數組
shouldStartLoadWithRequest不只在url變化的時候調用,並且只要網頁內容變化的時候也能調用app
上面的JS代碼函數
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ':' + arguments.callee.name + ':' + encodeURIComponent(JSON.stringify(arguments));
就是對網頁內容進行改變,經過在webview中植入這樣的代碼,就能夠調到shouldStartLoadWithRequest,shouldStartLoadWithRequest是OC的代碼,這樣就實現了從JS到OC的調用。和Java的反射有點相似。lua
接下來解決如何在webview中植入這樣的代碼url
- (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]; //防止隱藏的系統方法名包含「.」致使js報錯 if ([methodName rangeOfString:@"."].location!=NSNotFound) { continue; } [methodList appendString:@"\""]; [methodList appendString:[methodName stringByReplacingOccurrencesOfString:@":" withString:@""]]; [methodList appendString:@"\","]; } if (methodList.length>0) { [methodList deleteCharactersInRange:NSMakeRange(methodList.length-1, 1)]; } free(methods); 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]]; } }
webViewDidFinishLoad這個代理在webview加載完成後調用。spa
stringByEvaluatingJavaScriptFromString
至關於在webview的尾部追加一段代碼,這裏不只追加進去了js代碼,還有本地的函數列表,也就是OC暴露給前端能夠調用的函數列表,當咱們點擊webview中的某個按鈕觸發前端執行了window.external.func(param1, param2)這樣的代碼,而這個代碼由於咱們注入了上面那段JS代碼,不只觸發了shouldStartLoadWithRequest的執行,還把前端調用的函數名和參數傳了回來,接下來就是在shouldStartLoadWithRequest中對這些參數進行整合,變成OC能夠識別的代碼,就可以正確調用到OC的native方法了代理
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if (webView != _webView) { return YES; } NSURL *url = [request URL]; 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 { return YES; }
這裏的request和真實的url改變帶回來的參數組成不太同樣,這個值是在JS代碼中拼接的,因此這裏解析也要按照那個規則逆向解析,後面用到了selector,將函數名function轉換成selector,在run-time時就會調到了那個OC中的同名函數了
- (void)writeTopic:(NSArray *)params { NSLog(@"writeTopic called"); }
這裏整合成一個參數,params數組,能夠經過objectAtIndex來取出每一個參數,進行後面的相關操做。
總結:
1 經過注入JS代碼到webview
2 注入的JS代碼在能改變webview的內容,實現網頁的跳轉(這裏用的是一個空白的什麼都沒有的不可見的網頁)
3 根據注入的JS中的規則在shouldStartLoadWithRequest中反向解析,並經過SEL動態調用。