from: http://my.oschina.net/fwj/blog/471035?p={{page}}
過去3、4年都在進行跨平臺的混合應用開發,但一直沒有系統梳理跨平臺技術的底層原理,趁新工作未正式入職,這裏整理一下。跨平臺的一種實現是基於webview。所謂webview,實質是在原生app中打開一個內嵌瀏覽器,具體到iOS平臺就是使用UIWebView這個控件。然後就很容易理解了,我們相當於開發一個webapp(網頁應用),然後通過原生應用作爲用戶入口(而非原生瀏覽器),用戶會訪問到遠程服務器的網頁內容。從用戶感知上,似乎是在使用一個App,但實際上是在訪問一個網頁。
以上,只是跨平臺基於webview實現的工作原理,而更重要的是如何橋接webview的js和app的objc,使得webapp也可用使用原生的功能api,如調用攝像頭等,而app又可以調用webview裏的js,即向雙通信。
Apple開放了一個叫做JavascriptCore的框架,此框架最早在OSX10.2就存在,但到了2013年在OSX10.9上才發佈其調用的API,而後又在iOS7上公開,由此我們可用名正言順地使用了。
JavascriptCore提供了以下幾個API,實現跨平臺通信:
- JavascriptCore/API/JSContext.h
- JavascriptCore/API/JSExport.h
- JavascriptCore/API/JSValue.h
JSContext是JavascriptCore的主入口,它代表了JS的運行時環境,在其中可以定義對象、方法等,這些實體(對象、方法)的生命週期在JSContext被釋放的時候才結束。而且可用指定的JSVirtualMachine來創建JSContext,每個JSVM都會獨立運行在一個線程上。
我們可用通過JSContext的evaluateScript方法來定義我們的JS方法,而且是通過字符串定義代碼,當然可以通過讀取外邊js文件來實現。看下面的例子:
// getting a JSContext
JSContext *context = [JSContext new];
// defining a JavaScript function
NSString *jsFunctionText =
@"var isValidNumber = function(phone) {"
" var phonePattern = /^[0-9]{3}[ ][0-9]{3}[-][0-9]{4}$/;"
" return phone.match(phonePattern) ? true : false;"
"}";
[context evaluateScript:jsFunctionText];
這裏,相當於在objc層,向JSContext注入了一個isValidNumber的js方法。
正如上文所述,JSContext代表了一個JS運行環境,而我們的示例代碼都是單獨創建這個JSContext運行環境的,實際上UIWebView實例也有它自己的JSContext運行環境。爲了修改web上的內容,我們需要訪問UIWebView的JSContext。
但Apple就是一個悶騷男,雖然已經公開了JavascriptCore的API,但又不提供直接訪問UIWebView’s JSContext的方法。
幸好“key-value”把我們救了回來:
// get JSContext from UIWebView instance
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
然後,我們可以通過JSValue來獲取JSContext中js方法的引用和執行的結果:
// 獲取isValidNumber方法的引用
JSValue *jsFunction = context[@"isValidNumber"];
// 通過callWithArguments方法調用js方法
JSValue *value = [jsFunction callWithArguments:@[ phone ]];
JavascriptCore會自動轉換JSValue的對象類型,比如這裏isValidNumber返回的boolean,同時還支持NSString, NSDate, NSDictionary, NSArray等。
另外,我們還可以增加異常捕捉
[context setExceptionHandler:^(JSContext *context, JSValue *value) {
NSLog(@"%@", value);
}];
再有,通過JSExport可以將objc的方法暴露給JS。
@protocol BNRContactAppJS <JSExport>
- (void)addContact:(BNRContact *)contact;
@end
@interface BNRContactApp : NSObject <BNRContactAppJS>
...
@end
addContact這個方法是在BNRContactAppJS協議中聲明的,BNRContactAppJS又源自於JSExport,所以addContact方法將會暴露給JS環境,而其他方法則對JS環境而言是隱藏的。
最後,我們看一個完整的例子
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
// get JSContext from UIWebView instance
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// enable error logging
[context setExceptionHandler:^(JSContext *context, JSValue *value) {
NSLog(@"WEB JS: %@", value);
}];
// give JS a handle to our BNRContactApp instance
context[@"myApp"] = self.app;
// register BNRContact class
context[@"BNRContact"] = [BNRContact class];
// add function for processing form submission
NSString *addContactText =
@"var contactForm = document.forms[0];"
"var addContact = function() {"
" var name = contactForm.name.value;"
" var phone = contactForm.phone.value;"
" var address = contactForm.address.value;"
" var contact = BNRContact.contactWithNamePhoneAddress(name, phone, address);"
" myApp.addContact(contact);"
"};"
"contactForm.addEventListener('submit', addContact);";
[context evaluateScript:addContactText];
}
最終跨平臺調用就在這一句:
myApp.addContact(contact);
在JS環境調用了objc的方法。
總結一下:
- ==從objc調用js:JSContext的evaluateScriptf方法和JSValue的callWithArguments方法;==
- ==捕捉JS執行的異常;==
- ==從WebView實例獲取JSContext;==
- ==通過JSExport將objc方法暴露給js調用。==
最後囉嗦一下,iOS7以前,並沒有JavascriptCore,所以多使用 stringByEvaluatingJavaScriptFromString。
Titanium 就是使用了JavascriptCore的方式。