隨着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]; }; }