前端直接調用OC的native方法

    在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動態調用。

相關文章
相關標籤/搜索