在iOS開發中, 蘋果api味咱們提供了多種javascript和Objective-C交互的方法, 使用仍是比較簡單的.javascript
經過webView的stringByEvaluatingJavaScriptFromString:方法調用js代碼. 此方法能夠無限制的執行任何的js代碼.html
/** 原生調用js, 普通的方法: * 經過webView的stringByEvaluatingJavaScriptFromString:方法, 能夠無限制的執行任意的js代碼,能夠經過js代碼操控webView上面的任意元素, 也能夠直接經過js調用webView中的js代碼. 實現從原生代碼到javascript的聯繫. * */ [self.webView stringByEvaluatingJavaScriptFromString:@"showAlert('javascript message')"]; [self.webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('abshdfnb').style.backgroundColor = 'blue';"];
普通的方法是實現代理協議, 在shouldStartLoadWithRequest:方法中攔截髮生在webView上的請求,判斷這個請求前綴,根據前綴和參數不一樣,能夠作不一樣的處理和操做.這種方式在iOS7如下也是通用的.前端
/** * 是否要加載連接 */ - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { /********************************************************************************** * js調用原生代碼, 普通的方式: * 在將要開始請求網址時候, 攔截請求, 判斷請求信息並作相應的處理. */ NSURL *url = [request URL]; NSString *scheme = [url scheme]; if ([scheme isEqualToString:@"firstclick"]) { NSArray *args = [url.query componentsSeparatedByString:@"&"]; for (NSString *arg in args) { NSLog(@"arg : %@", arg); } return NO; } return YES; }
(1), 獲取js上下文, 經過evaluateScript:方法調用js代碼.方法一:java
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; NSString *script = @"showAlert('JSContext message');document.getElementById('abshdfnb').style.backgroundColor = 'blue';"; [context evaluateScript:script];
(2), 原生代碼調用js代碼方法二:ios
JSValue *jsFunc = context[@"showAlert"]; JSValue *result = [jsFunc callWithArguments:@[@"message from JSValue"]];
//使用javascriptCore框架, 實現js調用原生代碼. JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; //定義好JS要調用的方法, share就是調用的share方法名 context[@"share"] = ^() { NSLog(@"+++++++Begin Log+++++++"); NSArray *args = [JSContext currentArguments]; //回到主線程更新UI dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"方式二" message:@"這是OC原生的彈出窗" delegate:self cancelButtonTitle:@"收到" otherButtonTitles:nil]; [alertView show]; }); for (JSValue *jsVal in args) { NSLog(@"%@", jsVal.toString); } NSLog(@"-------End Log-------"); };
NSURLRequest
類獲取NSURL對象[request URL]
.// 執行一段script代碼, 返回值是javascript代碼最後的返回值 - (JSValue *)evaluateScript:(NSString *)script; //從一個javascript源文件中執行一個javascript方法. 第一個參數是要執行的js代碼, 第二個參數是js源文件位置的URL. - (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL NS_AVAILABLE(10_10, 8_0); //經過類方法,獲取js代碼的上下文對象. + (JSContext *)currentContext; //類方法獲取js的t方法的his對象 + (JSValue *)currentThis; //獲取當前的參數, 好比js調用原生代碼時候,穿了幾個參數,經過此類方法能夠獲取參數數組, + (NSArray *)currentArguments; //異常處理的block,當執行js遇到異常以後會執行這裏的代碼. @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception); // 打印異常,因爲JS的異常信息是不會在OC中被直接打印的,因此咱們在這裏添加打印異常信息, self.context.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"%@", exceptionValue); };
JSVirtualMachine爲JavaScript的運行提供了底層資源,JSContext就爲其提供着運行環境,經過- (JSValue *)evaluateScript:(NSString *)script;方法就能夠執行一段JavaScript腳本,而且若是其中有方法、變量等信息都會被存儲在其中以便在須要的時候使用。而JSContext的建立都是基於JSVirtualMachine:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;,若是是使用- (id)init;進行初始化,那麼在其內部會自動建立一個新的JSVirtualMachine對象而後調用前邊的初始化方法。web
JSValue則能夠說是JavaScript和Object-C之間互換的橋樑,它提供了多種方法能夠方便地把JavaScript數據類型轉換成Objective-C,或者是轉換過去。其一一對應方式可見下表:編程
Objective-C | JavaScript | JSValue Convert | JSValue Constructor |
---|---|---|---|
nil | undefined | valueWithUndefinedInContext | |
NSNull | null | valueWithNullInContext: | |
NSString | string | toString | |
NSNumber | number, boolean | toNumber toBool toDouble toInt32 toUInt32 | valueWithBool:inContext: valueWithDouble:inContext: valueWithInt32:inContext: valueWithUInt32:inContext: |
NSDictionary | Object object | toDictionary | valueWithNewObjectInContext: |
NSArray | Array object | toArray | valueWithNewArrayInContext: |
NSDate | Date object | toDate | |
NSBlock | Function object | ||
id | Wrapper object | toObject toObjectOfClass: | valueWithObject:inContext: |
Class | Constructor object |
// 基本數據類型轉換 JSContext *context1 = [[JSContext alloc] init]; JSValue *jsVal = [context1 evaluateScript:@"21+7"]; int iVal = [jsVal toInt32]; NSLog(@"JSValue: %@, int: %d", jsVal, iVal); // 輸出: JSValue: 28, int: 28
還能夠存一個JavaScript變量在JSContext中,而後經過下標來獲取出來。而對於Array或者Object類型,JSValue也能夠經過下標直接取值和賦值。api
//能夠存一個JavaScript變量在JSContext中,而後經過下標來獲取出來。而對於Array或者Object類型,JSValue也能夠經過下標直接取值和賦值。 JSContext *ctx = [[JSContext alloc] init]; [ctx evaluateScript:@"var arr = [21, 4, 'Move', 'HAHA']"]; // JSValue *jsArr = ctx[@"arr"]; JSValue *jsArr = [ctx objectForKeyedSubscript:@"arr"]; NSLog(@"jsArr = %@ length = %@", jsArr, jsArr[@"length"]); //能夠像操做js數據同樣操做JSValue對象 jsArr[3] = @"blog"; jsArr[5] = @5; NSLog(@"jsArr = %@ length = %@", jsArr, jsArr[@"length"]); //將js數組轉換成NSArray數組 NSArray *nsArray = [jsArr toArray]; NSLog(@"%@", nsArray); /** 輸出: jsArr = 21,4,Move,HAHA length = 4 2016-05-05 11:04:52.887 TestJSOC[1286:1121702] jsArr = 21,4,Move,blog,,5 length = 6 2016-05-05 11:04:52.887 TestJSOC[1286:1121702] ( 21, 4, Move, blog, "<null>", 5 ) */
各類數據類型能夠轉換,Objective-C的Block也能夠傳入JSContext中當作JavaScript的方法使用。好比在前端開發中經常使用的log方法,雖然JavaScritpCore沒有自帶(畢竟不是在網頁上運行的,天然不會有window、document、console這些類了),仍然能夠定義一個Block方法來調用NSLog來模擬:數組
JSContext *context = [[JSContext alloc] init]; context[@"log"] = ^() { NSLog(@"+++++++Begin Log+++++++"); NSArray *args = [JSContext currentArguments]; for (JSValue *jsVal in args) { NSLog(@"%@", jsVal); } JSValue *this = [JSContext currentThis]; NSLog(@"this: %@",this); NSLog(@"-------End Log-------"); }; [context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
經過Block成功的在JavaScript調用方法回到了Objective-C,並且依然遵循JavaScript方法的各類特色,好比方法參數不固定。也由於這樣,JSContext提供了類方法來獲取參數列表(+ (NSArray *)currentArguments)和當前調用該方法的對象(+ (JSValue *)currentThis)。上邊的例子中對於"this"輸出的內容是GlobalObject,這也是JSContext對象方法- (JSValue *)globalObject;所返回的內容。由於咱們知道在JavaScript裏,全部全局變量和方法其實都是一個全局變量的屬性,在瀏覽器中是window,在JavaScriptCore是什麼就不得而知了。瀏覽器
Block能夠傳入JSContext做方法,可是JSValue沒有toBlock方法來把JavaScript方法變成Block在Objetive-C中使用。畢竟Block的參數個數和類型已經返回類型都是固定的。雖然不能把方法提取出來,可是JSValue提供了- (JSValue *)callWithArguments:(NSArray *)arguments;方法能夠反過來將參數傳進去來調用方法。
// 獲取javascript上下文中的方法或者值的兩種方法: // 方法一. JSValue *function = [self.context objectForKeyedSubscript:@"factorial"]; // 方法二. JSValue * function = self.context[@"factorial"]; JSValue *result = [function callWithArguments:@[inputNumber]]; self.showLable.text = [NSString stringWithFormat:@"%@", [result toNumber]];
從以前的例子和介紹應該有體會到Block在JavaScriptCore中起到的強大做用,它在JavaScript和Objective-C之間的轉換 創建起更多的橋樑,讓互通更方便。可是要注意的是不管是把Block傳給JSContext對象讓其變成JavaScript方法,仍是把它賦給exceptionHandler屬性,在Block內都不要直接使用其外部定義的JSContext對象或者JSValue,應該將其當作參數傳入到Block中,或者經過JSContext的類方法+ (JSContext *)currentContext;來得到。不然會形成循環引用使得內存沒法被正確釋放。
Objective-C的異常會在運行時被Xcode捕獲,而在JSContext中執行的JavaScript若是出現異常,只會被JSContext捕獲並存儲在exception屬性上,而不會向外拋出。時時刻刻檢查JSContext對象的exception是否不爲nil顯然是不合適,更合理的方式是給JSContext對象設置exceptionHandler,它接受的是^(JSContext *context, JSValue *exceptionValue)形式的Block。其默認值就是將傳入的exceptionValue賦給傳入的context的exception屬性
JSContext並不能讓Objective-C和JavaScript的對象直接轉換,畢竟二者的面向對象的設計方式是不一樣的:前者基於class,後者基於prototype。但全部的對象其實能夠視爲一組鍵值對的集合,因此JavaScript中的對象能夠返回到Objective-C中當作NSDictionary類型進行訪問。
JSValue *obj =[context evaluateScript:@"var jsObj = { number:7, name:'Ider' }; jsObj"]; NSLog(@"%@, %@", obj[@"name"], obj[@"number"]); NSDictionary *dic = [obj toDictionary]; NSLog(@"%@, %@", dic[@"name"], dic[@"number"]); //Output: // Ider, 7 // Ider, 7
一樣的,NSDicionary和NSMutableDictionary傳入到JSContext以後也能夠直接當對象來調用:
NSDictionary *dic = @{@"name": @"Ider", @"#":@(21)}; context[@"dic"] = dic; [context evaluateScript:@"log(dic.name, dic['#'])"]; //OutPut: // Ider // 21
JavaScript能夠脫離prototype繼承徹底用JSON來定義對象,可是Objective-C編程裏可不能脫離類和繼承了寫代碼。因此JavaScriptCore就提供了JSExport做爲兩種語言的互通協議。JSExport中沒有約定任何的方法,連可選的(@optional)都沒有,可是全部繼承了該協議(@protocol)的協議(注意不是Objective-C的類(@interface))中定義的方法,均可以在JSContext中被使用。語言表述起來有點繞,仍是用例子來講明會更明確一點。
@protocol PersonProtocol <JSExport> @property (nonatomic, retain) NSDictionary *urls; - (NSString *)fullName; @end @interface Person :NSObject <PersonProtocol> @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @end; @implementation Person @synthesize firstName, lastName, urls; - (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName]; } @end
在上邊的代碼中,定義了一個PersonProtocol,並讓它繼承了神祕的JSExport協議,在新定義的協議中約定urls屬性和fullName方法。以後又定義了Person類,除了讓它實現PersonProtocol外,還定義了firstName和lastName屬性。而fullName方法返回的則是兩部分名字的結合。
下邊就來建立一個Person對象,而後傳入到JSContext中並嘗試使用JavaScript來訪問和修改該對象。
// initialize person object Person *person = [[Person alloc] init]; context[@"p"] = person; person.firstName = @"Ider"; person.lastName = @"Zheng"; person.urls = @{@"site": @"http://www.iderzheng.com"}; // ok to get fullName [context evaluateScript:@"log(p.fullName());"]; // cannot access firstName [context evaluateScript:@"log(p.firstName);"]; // ok to access dictionary as object [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"]; // ok to change urls property [context evaluateScript:@"p.urls = {blog:'http://blog.iderzheng.com'}"]; [context evaluateScript:@"log('-------AFTER CHANGE URLS-------')"]; [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"]; // affect on Objective-C side as well NSLog(@"%@", person.urls); //Output: // Ider Zheng // undefined // undefined // site: // http://www.iderzheng.com // blog: // undefined // -------AFTER CHANGE URLS------- // site: // undefined // blog: // http://blog.iderzheng.com // { // blog = "http://blog.iderzheng.com"; // }
從輸出結果不難看出,當訪問firstName和lastName的時候給出的結果是undefined,由於它們跟JavaScript沒有JSExport的聯繫。但這並不影響從fullName()中正確獲得兩個屬性的值。和以前說過的同樣,對於NSDictionary類型的urls,能夠在JSContext中當作對象使用,並且還能夠正確地給urls賦予新的值,並反映到實際的Objective-C的Person對象上。
JSExport不只能夠正確反映屬性到JavaScript中,並且對屬性的特性也會保證其正確,好比一個屬性在協議中被聲明成readonly,那麼在JavaScript中也就只能讀取屬性值而不能賦予新的值。
對於多參數的方法,JavaScriptCore的轉換方式將Objective-C的方法每一個部分都合併在一塊兒,冒號後的字母變爲大寫並移除冒號。好比下邊協議中的方法,在JavaScript調用就是:doFooWithBar(foo, bar);
@protocol MultiArgs <JSExport> - (void)doFoo:(id)foo withBar:(id)bar; @end
若是但願方法在JavaScript中有一個比較短的名字,就須要用的JSExport.h中提供的宏:JSExportAs(PropertyName, Selector)。
@protocol LongArgs <JSExport> JSExportAs(testArgumentTypes, - (NSString *)testArgumentTypesWithInt:(int)i double:(double)d boolean:(BOOL)b string:(NSString *)s number:(NSNumber *)n array:(NSArray *)a dictionary:(NSDictionary *)o ); @end
好比上邊定義的協議中的方法,在JavaScript就只要用testArgumentTypes(i, d, b, s, n, a, dic);來調用就能夠了。
雖然JavaScriptCore框架尚未官方編程指南,可是在JSExport.h文件中對神祕協議的表述仍是比較詳細的,其中有一條是這樣描述的:
By default no methods or properties of the Objective-C class will be exposed to JavaScript, however methods and properties may explicitly be exported. For each protocol that a class conforms to, if the protocol incorporates the protocol JSExport, then the protocol will be interpreted as a list of methods and properties to be exported to JavaScript.
這裏面有個incorporate一詞值得推敲,通過驗證只有直接繼承了JSExport的自定義協議(@protocol)才能在JSContext中訪問到。也就是說好比有其它的協議繼承了上邊的PersonProtocol,其中的定義的方法並不會被引入到JSContext中。從源碼中也能看出JavaScriptCore框架會經過class_copyProtocolList方法找到類所遵循的協議,而後再對每一個協議經過protocol_copyProtocolList檢查它是否遵循JSExport協議進而將方法反映到JavaScript之中。
對於自定義的Objective-C類,能夠經過以前的方式自定義繼承了JSExport的協議來實現與JavaScript的交互。對於已經定義好的系統類或者從外部引入的庫類,她們都不會預先定義協議提供與JavaScript的交互的。好在Objective-C是能夠在運行時實行對類性質的修改的。
好比下邊的例子,就是爲UITextField添加了協議,讓其能在JavaScript中能夠直接訪問text屬性。該接口以下:
@protocol JSUITextFieldExport <JSExport> @property(nonatomic,copy) NSString *text; @end
以後在經過class_addProtocol爲其添加上該協議:
- (void)viewDidLoad { [super viewDidLoad]; textField.text = @"7"; class_addProtocol([UITextField class], @protocol(JSUITextFieldExport)); }
爲一個UIButton添加以下的事件,其方法只要是將textField傳入到JSContext中而後讀取其text值,自增1後從新賦值:
- (IBAction)pressed:(id)sender { JSContext *context = [[JSContext alloc] init]; context[@"textField"] = textField; NSString *script = @"var num = parseInt(textField.text, 10);" "++num;" "textField.text = num;"; [context evaluateScript:script]; }
一、建立環境 JSContext *context=[[JSContext alloc] init]; 二、調用evaluateScript建立JavaScript代碼 [context evaluateScript:@"var num=5+5"]; [context evaluateScript:@"var names=['Lusieke','Lemon','Honey']"]; [context evaluateScript:@"var triple=function(value){return value * 3}"]; [context evaluateScript:@"var sum=function(num1,num2){return num1+num2}"]; 三、任何出自JSContext的值都被包裹在一個JSValue對象中。像JavaScript這樣的動態語言須要一個動態類型(var),因此JSValue包裝了每個可能的JavaScript值:字符、數字、數組、對象和方法。JSValue包括一系列方法用於訪問其可能的值以保證有正確的Foundation類型 JSValue *tripNum=[context evaluateScript:@"num"]; //輸出10,即定義的JavaScript腳本中var num=5+5的值 NSLog(@"triple :%d",[tripNum toInt32]); JSValue *names=[context evaluateScript:@"names"]; //輸出names數組的值 NSLog(@"names :%@",[names toArray]); 四、JSContext和JSValue實例使用下標的方法,咱們能夠很容易的訪問到咱們以前建立的context的任何值。JSContext須要一個字符串下標,而JSValue容許字符串或整數下標來獲得裏面的對象和數組 JSValue *names=context[@"names"]; JSValue *initialName=names[0]; NSLog(@"The first name :%@",[initialName toString]); 五、如何調用定義的JavaScript函數?JSValue包裝了一個JavaScript函數,咱們能夠在Object-c代碼中使用Foundation類型做爲參數來直接調用該函數 JSContext *context=[[JSContext alloc] init]; [context evaluateScript:@"var sum = function sum(num1,num2){return num1+num2;}"]; JSValue *sumFunc=[context[@"sum"] callWithArguments:@[@2,@3]]; NSLog(@"2+3的和=%d",[sumFunc toInt32]); 輸出內容: 2015-03-12 11:23:32.787 javaScriptCore[742:140647] 2+3的和=5 程序中JSValue類型的變量context[@"sum"]經過callWithArguments:傳遞Foundation類型的變量@2,@3來調用JavaScript函數sum,並輸出計算後的值
#References:
http://www.2cto.com/kf/201503/381906.html
http://blog.iderzheng.com/introduction-to-ios7-javascriptcore-framework/
http://blog.iderzheng.com/ios7-objects-management-in-javascriptcore-framework/