OC與JS混合開發


    隨着iOS開發的成本增大,越來越多的公司開始使用html5混合開發軟件了,因為使用原生的開發花費的成本跟時間都很大,而使用html5來搭建界面會方便很多,效率相對而言也提高了。雖然使用UIWebView實現的交互效果與原生效果相比還是會大打折扣,這類界面通常沒有復雜的交互效果,所以現在主流應用大多采用混合開發。花了幾天時間,把JS的基礎全部看了一遍,又研究了一下巧神的書,寫了一個iOS7以前的JS與OC混合開發的demo。

    既然是html5頁面搭建的布局,那么肯定是得有html5頁面的,所以首先我們得先寫一個html5的頁面。既然我們做的是App,所以我們針對的是手機頁面,需要加入針對移動端頁面優化的viewport,然后在body里面加入一個按鈕,綁定一個點擊事件,在JS里面實現這個方法,通過location.href = ""; 方法進行跳轉網頁,里面的地址是自己自定義的,然后在OC里面解析。

<html>
    <head>
        <meta charset = "UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi"/>
        <title>測試網頁</title>
        <script type="text/javascript">
            function sum(){
                location.href = 'jsoc://call_?100'; // 自己寫的跳轉網址
            }
        
            function ocCallJsNoParamsFunction(){
                alert("OC調用JS中的無參方法");
            }
        
            function ocCallJsHasParamsFunction(name, food){
                alert(name+"喜歡吃:"+url);
            }
        </script>
    </head>
    <body>
        <button style = "backgroud:red; width:100px; height:30px;" onclick = "click();">點我一下試試</button>
        <br>
        <a href = "http://www.baidu.com">百度一下,你就知道</a>
    </body>
</html>

  寫完了一個簡單的html5頁面,接下來就要寫JS調用OC以及OC調用JS了。首先,使用storyboard來搭建布局,當然你得拖約束。然后把刷新的Item的設置為Refresh。

  把webView拖為屬性,把Refresh拖成方法,當點擊這個按鈕的時候刷新webView。

  - (IBAction)refresh:(id)sender{
      [self.webView reload];
  }

 

  接下來就可以設置webView的屬性了。 使用webView加載網頁,可以加載網絡上的,也可以加載本地的,如果你需要加載的是網頁上面的,那么你就需要在 info.plist 里面添加一個配置。

 

  加載本地的,你只需要得到本地html5頁面的路徑,就能在webView上面展示了。

    // 系統可以自動檢測電話、鏈接、地址、日歷、郵箱
    self.webView.dataDetectorTypes = UIDataDetectorTypeAll;
    self.webView.delegate = self;
    // 根據資源名,擴展名獲取該資源對應的 URL
    NSURL *htmlUrl = [[NSBundle mainBundle] URLForResource:@"info.html" withExtension:nil];
    [self.webView loadRequest:[NSURLRequest requestWithURL:htmlUrl]];

    設置好了webView,現在運行起來,你應該是能看到效果的,如下圖所示。

                                   

  先來講一個OC調用JS方法,通過  - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;  方法,我們就可以調用JS里面我們寫好的方法了。這里呢,我把調用的方法放在     - (void)webViewDidFinishLoad:(UIWebView *)webView;    當webView加載完頁面后調用JS里面的方法。實現OC調用JS。我寫了兩個方法,第一種是不帶任何參數的,所以直接調用方法就可以,而第二種是帶參數的,所以第二種方法寫法會不一樣,stringByEvaluatingJavaScriptFromString 只能帶字符串,所以需要先通過stringWithFormat: 拼接字符串,然后把字符串傳進去。

- (void)webViewDidFinishLoad:(UIWebView *)webView{
//   NSString *js = [webView stringByEvaluatingJavaScriptFromString:@"ocCallJsNoParamsFunction();"];
    NSString *js = [NSString stringWithFormat:@"ocCallJsHasParamsFunction('%@','%@')",@"哈哈",@"蘋果"];
    // webView調用JS代碼,等webView全部加載html界面之后調用
    [webView stringByEvaluatingJavaScriptFromString:js];
}

  OC調用JS的代碼很簡單,但是JS調用OC就有點困難了,因為這是iOS7以前的,那個時候沒有出JavaScriptCore,所以開發起來有難度。下面來講講JS如何調用OC的代碼。JS調用OC,並沒有現成的API,可以使用"曲線救國"的方法,間接達到。在UIWebView內發起的所有網絡請求,都可以通過delegate函數在原生界面得到通知。所以在html5頁面發起一個特殊的網絡請求,請求加載的網址內容通常不是真實的地址,OC中判斷這個特殊的網絡請求來實現不同的功能。

/**
 *  每當webview發送請求之前就會調用這個方法(js調用oc)
 */
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    //獲得url全路徑
    NSString *url = request.URL.absoluteString;
    NSString *protocol = @"jsoc://";
    
    // 判斷url是否以protocol開頭
    if([url hasPrefix:protocol]){
        //獲得協議后面的路徑,substringFromIndex:表示從指定位置開始截取字符串到最后,所截取位置包含該指定位置
        NSString *path = [url substringFromIndex:protocol.length];
        //利用占位符進行切割
        NSArray *subpaths =[path componentsSeparatedByString:@"?"];
        //獲得方法名 jsoc://call_?100
        NSString *methodName = [[subpaths firstObject] stringByReplacingOccurrencesOfString:@"_" withString:@":"];
        NSArray *params = nil;
        if (subpaths.count == 2) {
            params = [[subpaths lastObject] componentsSeparatedByString:@"&"];
        }
        
        //調用方法
        SEL sel = sel_registerName([methodName UTF8String]);
        [self jsoc_performSelector:sel withObjects:params];
      
        return NO;
    }
    
    return YES;
    
}

  sel_registerName([xxx UTF8String]);  和 jsoc_performSelector:withObjects: 用到了runtime,所以還需要具備一些runtime的知識,sel只是一個指向方法的指針,一個根據方法名hash化了的KEY值,能唯一代表一個方法,它的存在只是為了加快方法的查詢速度。然后通過自己定義的 jsoc_performSelector:withObjects: 來調用通過sel找到的這個方法。這里我們用了自定義的performSelector,是因為系統給的只能帶一個或者兩個參數,如果我想要帶多個參數,那就只能通過自定義了。通過 sel 指向了一個方法,所以我們得把這個方法實現,這里我就簡單的實現。

- (void)call:(NSString *)number{
    NSLog(@"參數:%@",number);
    NSLog(@"調用了oc的%s方法",__func__);
}

  下面就來講講自定義的 performSelector怎么實現。給NSObject添加一個Category,然后把jsoc_performSelecor:withObjects:實現以下。這里我們會用到 NSMethodSignature 和 NSInvocation,廢話不多說,直接上代碼。

- (id)jsoc_performSelector:(SEL)aSelector withObjects:(NSArray *)objects
{
    //NSInvocation 利用一個NSInvocation對象包裝一次方法調用(方法調用者,方法名,方法參數,方法返回值)
    // 通過選擇器獲取方法簽名
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
    if(signature == nil){
       NSString *reason = [NSString stringWithFormat:@"** The method[%@] is not find **",NSStringFromSelector(aSelector)];
       @throw [NSException exceptionWithName:@"錯誤!" reason:reason userInfo:nil];
        
    }
    // iOS中可以直接調用某個對象的消息方式有兩種,其中一種就是NSInvocation(對於>2個的參數或者有返回值的處理) 另一種performSelector:withObject
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = aSelector;
    NSInteger paras = signature.numberOfArguments - 2;
    paras = MIN(paras, objects.count);
    
    for(NSInteger i=0;i<paras;i++){
        id object = objects[i];
        if([object isKindOfClass:[NSNull class]]) continue;
        // index從2開始 ,原因為:0 1 兩個參數已經被target 和selector占用
        [invocation setArgument:&object atIndex:i+2];
    }
    //調用方法
    [invocation invoke];

    id returnValue = nil;
    if(signature.methodReturnLength){
        [invocation getReturnValue:&returnValue];
    }
    return returnValue;
}

  NSMethodSignature 是方法簽名,官方定義該類為對方法的參數、返回類似進行封裝,協同NSInvocation實現消息轉發。signature.numberOfArguments - 2 是因為底層系統本身就帶了2個參數,所以我們統計數量的時候需要把系統的減去,下面 setArgument:atIndex:   i + 2 也是一樣。

  iOS7 以前的JS OC混合開發就寫好了。這里是一個傳送門。推薦幾個開源庫,WebViewJavascriptBridge、EasyJSWebView等。

 

  上面講了iOS 7 以前如何實現混合開發,下面來講講iOS7 之后如何實現混合開發,iOS7開始,出現了一個JavaScriptCore 這么一框架,通過它來實現JS調用OC代碼,會簡單很多,不過代碼就有點不一樣。JavaScriptCore是webkit的一個重要組成部分,主要是對JS進行解析和提供執行環境。代碼是開源的,可以下下來看看源碼

  JSContext

  JS執行的環境,同時也通過JSVirtualMachine管理着所有對象的聲明周期,每個JSValue都和JSContext相關聯並且強引用context。

  JSValue

  JS對象在JSVirtualMachine中的一個強引用,其實就是Hybird對象。我們對JS的操作都是通過它。並且每個JSValue都是強引用一個context。同時,OC和JS對象之間的轉換也是通過它。

  JSManageValue

  JS和OC對象的內存管理輔助對象。由於JS內存管理是垃圾回收,並且JS中的對象都是強引用,而OC是引用計數。如果雙方相互引用,勢必會造成循環引用,而導致內存泄露。我們可以用JSManagedValue保存JSValue來避免。

  JSVirtualMachine

  JS運行的虛擬機,有獨立的堆空間和垃圾回收機制。

  JSExport

  一個協議,如果JS對象想直接調用OC對象里面的方法和屬性,那么這個OC對象只要實現這個JSExport協議就可以了。

   

  好了,下面直接上代碼吧。先來看一下html5頁面。這里面JS調用OC需要在onclick中加入調用的方法,前面是跟OC規定好的一個名稱,點后面的是方法的名稱,這個名稱在OC代碼中必須得一致。

<div style="width:300px; margin: 0 auto;background:red;">
        <input type="button" value="帶兩個參數的方法" onclick="Native.testMethodWithParam1Param2('param1_value', 'param2_value')" />
        <br />
        <input type="button" value="帶兩個參數的方法2" onclick="Native.testMethod(1111, '2222')" />
        <br /> 
        <input type="button" value="帶一個參數的方法" onclick="Native.testLog('帶一個參數')" />
        <br />
        <input type="button" value="參數為數組的方式" onclick="Native.testArray([1111, '2222'])" />
        <br />
        <input type="button" value="測試log" onclick="log('測試')" />
        <br />
        <input type="button" value="JS調用OC原生提示框" onclick="alert('alert')" />
        <br />
        <input type="button" value="添加視圖" onclick="addSubView('view')" />
        <br />
        <div>
            <span>輸入一個整數:</span>
            <input id="input" type="text" />
            <br />
            結果為:<p id="result"></p>
        </div>
        <input type="button" value="計算階乘" onclick="Native.calculateForJS(input.value)" />
    </div>

  當然還有JS中的代碼,下面便是JS中的代碼。

<script type="text/javascript">
    function showResult(resultNumber) {
        document.getElementById("result").innerText = resultNumber;
    }
</script>

  然后回到Xcode中,我們需要自己寫代理方法,通過代理方法,當點擊按鈕的時候就可以直接調用相對應的方法。

@protocol JSOCExport <JSExport>

/** 帶兩個參數的方法 */
- (void)testMethodWithParam1:(NSString *)param1 Param2:(NSString *)param2;
/** 帶兩個參數的方法(2) */
- (void)test:(NSNumber *)param1 method:(NSString *)param2;
/** 帶一個參數的方法 */
- (void)testLog:(NSString  *)logText;
/** 參數以數組的方式 */
- (void)testArray:(NSArray *)dataArray;
- (void)calculateForJS:(NSNumber *)number;

@end

  既然是JavaScriptCore框架,需要用到JSContext,聲明一個成員變量,然后再webViewDidFinishLoad:中初始化。

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    // 設置導航欄title
    self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
    // 設置頁面元素
//    [webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.display = 'none'"];
    context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    // 打印異常
    context.exceptionHandler = ^(JSContext *context, JSValue *exceptions) {
        context.exception = exceptions;
        NSLog(@"%@", exceptions);
    };
    
    // 以 JSExport 協議    關聯 Native
    context[@"Native"] = self;
    
    // 以block 形式關聯 JS中的func
    context[@"log"] = ^(NSString *str) {
        NSLog(@"log = %@", str);
    };
    
    UIViewController *vc = self;
    context[@"alert"] = ^(NSString *str) {
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"msg from js" message:str preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
            [alert addAction:action];
            [vc presentViewController:alert animated:YES completion:nil];
        });
    };
    
    context[@"addSubView"] = ^(NSString *str) {
        UIView *v = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
        v.backgroundColor = [UIColor redColor];
        [v addSubview:[[UISwitch alloc] init]];
        [vc.view addSubview:v];
    };
}

  最后把剩下的方法也給實現下,這里OC調用JS的方法會有點不一樣,用到了 - (JSValue *)evaluateScript:(NSString *)script; 方法,直接調用JS的代碼。

  這里提供了一個傳送門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM