ios7 JavaScriptCore.framework

轉:http://justsee.iteye.com/blog/2036713javascript

 

之前想要oc調用js代碼通常是經過uiwebview的stringByEvaluatingJavaScriptFromString方法,或是直接使用JavaScriptCore引擎,例如https://github.com/jfahrenkrug/AddressBookSpy。html

 

 

可是,Apple正式發佈了新的iOS 7系統,引入了JavaScriptCore.framework  ,最大最直觀的改變在於界面變得小清新範了,我也提到《iOS,你真的愈來愈像Android了》。不過對於移動開發者來講,除了要適應Xcode 5,最應該關注的仍是iOS 7在開發接口的上的變化。概覽Apple提供的官方文檔《What’s New in iOS》,最最讓我欣喜的是iOS 7中加入了JavaScriptCore框架。該框架讓Objective-C和JavaScript代碼直接的交互變得更加的簡單方便。

JavaScriptCore

這個框架其實只是基於webkit中以C/C++實現的JavaScriptCore的一個包裝,在舊版本iOS開發中,不少開發者也會自行將webkit的庫引入項目編譯使用。不過雖然iOS7把它當成了標準庫,惋惜目前,我尚未在Apple Developer中找到像以前文章中收集的那樣的官方文檔介紹這個框架的具體使用方法。前端

好在還能夠在Xcode中找到頭文件,並且裏面的註釋對每一個類和方法的功能寫得還算清楚,再結合網上僅有的幾篇文章的介紹,我也在此簡單入門一下JavaScriptCore。更多細節還待開發過程當中發現。java

JavaScriptCore中的類

JavaScriptCore_Head
在項目中引入JavaScriptCore後,鏈到頭文件中,除了大段的Copyright註釋能夠看到裏面只要引入了5個文件,每一個文件裏都定義跟文件名對應的類:android

  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

雖然說代碼中的註釋介紹的也比較詳細了,可是如同一圖頂萬言,對程序員來講代碼更有說服力。本文就先來講說這些類相對比較好理解但又很經常使用的JSContext和JSValue以及它們方法的使用方式和效果。ios

JSContext和JSValue

JSVirtualMachine爲JavaScript的運行提供了底層資源,JSContext就爲其提供着運行環境,經過- (JSValue *)evaluateScript:(NSString *)script;方法就能夠執行一段JavaScript腳本,而且若是其中有方法、變量等信息都會被存儲在其中以便在須要的時候使用。而JSContext的建立都是基於JSVirtualMachine- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;,若是是使用- (id)init;進行初始化,那麼在其內部會自動建立一個新的JSVirtualMachine對象而後調用前邊的初始化方法。git

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    

 

基本類型轉換

先看個簡單的例子:github

  1. JSContext  *context [[JSContext alloc] init];
  2. JSValue  *jsVal [context evaluateScript:@"21+7"];
  3. int iVal [jsVal toInt32];
  4. NSLog (@"JSValue: %@, int: %d", jsVal, iVal);
  5.  
  6. //Output:
  7. // JSValue: 28, int: 28

 

 

很簡單吧,還能夠存一個JavaScript變量在JSContext中,而後經過下標來獲取出來。而對於Array或者Object類型,JSValue也能夠經過下標直接取值和賦值。web

  1. JSContext  *context [[JSContext alloc] init];
  2. [context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"];
  3. JSValue  *jsArr = context[@"arr"]; // Get array from JSContext
  4.  
  5. NSLog (@"JS Array: %@; Length: %@", jsArr, jsArr[@"length"]);
  6. jsArr [1@"blog"; // Use JSValue as array
  7. jsArr [7= @7;
  8.  
  9. NSLog (@"JS Array: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]);
  10.  
  11. NSArray  *nsArr [jsArr toArray];
  12. NSLog (@"NSArray: %@", nsArr);
  13.  
  14. //Output:
  15. // JS Array: 21,7,iderzheng.com Length: 3
  16. // JS Array: 21,blog,iderzheng.com,,,,,7 Length: 8
  17. // NSArray: (
  18. // 21,
  19. // blog,
  20. // "iderzheng.com",
  21. // "<null>",
  22. // "<null>",
  23. // "<null>",
  24. // "<null>",
  25. // 7
  26. // )

 

經過輸出結果很容易看出代碼成功把數據從Objective-C賦到了JavaScript數組上,並且JSValue是遵循JavaScript的數組特性:無下標越位,自動延展數組大小。而且經過JSValue還能夠獲取JavaScript對象上的屬性,好比例子中經過"length"就獲取到了JavaScript數組的長度。在轉成NSArray的時候,全部的信息也都正確轉換了過去。

方法的轉換

各類數據類型能夠轉換,Objective-C的Block也能夠傳入JSContext中當作JavaScript的方法使用。好比在前端開發中經常使用的log方法,雖然JavaScritpCore沒有自帶(畢竟不是在網頁上運行的,天然不會有window、document、console這些類了),仍然能夠定義一個Block方法來調用NSLog來模擬:

  1. JSContext  *context [[JSContext alloc] init];
  2. context [@"log"^({
  3. NSLog (@"+++++++Begin Log+++++++");
  4.  
  5. NSArray  *args [JSContext currentArguments];
  6. for (JSValue *jsVal in args{
  7. NSLog (@"%@", jsVal);
  8. }
  9.  
  10. JSValue  *this [JSContext currentThis];
  11. NSLog (@"this: %@",this);
  12. NSLog (@"-------End Log-------");
  13. };
  14.  
  15. [context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
  16.  
  17. //Output:
  18. // +++++++Begin Log+++++++
  19. // ider
  20. // 7,21
  21. // [object Object]
  22. // this: [object GlobalObject]
  23. // -------End Log-------

 

 

經過Block成功的在JavaScript調用方法回到了Objective-C,並且依然遵循JavaScript方法的各類特色,好比方法參數不固定。也由於這樣,JSContext提供了類方法來獲取參數列表(+ (JSContext *) 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;方法能夠反過來將參數傳進去來調用方法。

  1. JSContext  *context [[JSContext alloc] init];
  2. [context evaluateScript:@"function add(a, b) { return a + b; }"];
  3. JSValue  *add = context[@"add"];
  4. NSLog (@"Func: %@", add);
  5.  
  6. JSValue  *sum [add callWithArguments:@[@(7), @(21)]];
  7. NSLog (@"Sum: %d",[sum toInt32]);
  8. //OutPut:
  9. // Func: function add(a, b) { return a + b; }
  10. // Sum: 28

JSValue還提供 - (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;讓咱們能夠直接簡單地調用對象上的方法。只是若是定義的方法是全局函數,那麼很顯然應該在 JSContextglobalObject對象上調用該方法;若是是某JavaScript對象上的方法,就應該用相應的 JSValue對象調用。

 

異常處理

Objective-C的異常會在運行時被Xcode捕獲,而在JSContext中執行的JavaScript若是出現異常,只會被JSContext捕獲並存儲在exception屬性上,而不會向外拋出。時時刻刻檢查JSContext對象的exception是否不爲nil顯然是不合適,更合理的方式是給JSContext對象設置exceptionHandler,它接受的是^(JSContext *context, JSValue *exceptionValue)形式的Block。其默認值就是將傳入的exceptionValue賦給傳入的contextexception屬性:

  1. ^(JSContext *context, JSValue *exceptionValue{
  2. context.exception  = exceptionValue;
  3. };

 

咱們也能夠給exceptionHandler賦予新的Block以便在JavaScript運行發生異常的時候咱們能夠當即知道:

  1. JSContext  *context [[JSContext alloc] init];
  2. context.exceptionHandler  ^(JSContext *con, JSValue *exception{
  3. NSLog (@"%@", exception);
  4. con.exception  = exception;
  5. };
  6.  
  7. [context evaluateScript:@"ider.zheng = 21"];
  8.  
  9. //Output:
  10. // ReferenceError: Can't find variable: ider

 

使用Block的注意事項

從以前的例子和介紹應該有體會到Block在JavaScriptCore中起到的強大做用,它在JavaScript和Objective-C之間的轉換 創建起更多的橋樑,讓互通更方便。可是要注意的是不管是把Block傳給JSContext對象讓其變成JavaScript方法,仍是把它賦給exceptionHandler屬性,在Block內都不要直接使用其外部定義的JSContext對象或者JSValue,應該將其當作參數傳入到Block中,或者經過JSContext的類方法+ (JSContext *)currentContext;來得到。不然會形成循環引用使得內存沒法被正確釋放。

好比上邊自定義異常處理方法,就是賦給傳入JSContext對象con,而不是其外建立的context對象,雖然它們實際上是同一個對象。這是由於Block會對內部使用的在外部定義建立的對象作強引用,而JSContext也會對被賦予的Block作強引用,這樣它們之間就造成了循環引用(Circular Reference)使得內存沒法正常釋放。
對於JSValue也不能直接從外部引用到Block中,由於每一個JSValue上都有JSContext的引用 (@property(readonly, retain) JSContext *context;),JSContext再引用Block一樣也會造成引用循環。

 

 

前面十分的簡單方便並且高效,不過也僅限於數值型、布爾型、字符串、數組等這些基礎類型。本文將擴展到更復雜的類型,介紹一下該強大的框架是如何讓Objective-C對象和JavaScript對象進行直接互通的。

爲了方便起見,如下全部代碼中的JSContext對象都會添加以下的log方法和eventHandler

  1. JSContext  *context [[JSContext alloc] init];
  2. context.exceptionHandler  ^(JSContext *con, JSValue *exception{
  3. NSLog (@"%@", exception);
  4. con.exception  = exception;
  5. };
  6.  
  7. context [@"log"^({
  8. NSArray  *args [JSContext currentArguments];
  9. for (id obj in args{
  10. NSLog (@"%@",obj);
  11. }
  12. };



 

 

鍵值對編程—Dictionary

JSContext並不能讓Objective-C和JavaScript的對象直接轉換,畢竟二者的面向對象的設計方式是不一樣的:前者基於class,後者基於prototype。但全部的對象其實能夠視爲一組鍵值對的集合,因此JavaScript中的對象能夠返回到Objective-C中當作NSDictionary類型進行訪問。

  1. JSValue  *obj =[context evaluateScript:@"var jsObj = { number:7, name:'Ider' }; jsObj"];
  2. NSLog (@"%@, %@", obj[@"name"], obj[@"number"]);
  3. NSDictionary  *dic [obj toDictionary];
  4. NSLog (@"%@, %@", dic[@"name"], dic[@"number"]);
  5. //Output:
  6. // Ider, 7
  7. // Ider, 7

一樣的,NSDicionaryNSMutableDictionary傳入到JSContext以後也能夠直接當對象來調用:

  1. NSDictionary  *dic = @{@"name"@"Ider", @"#":@(21)};
  2. context [@"dic"= dic;
  3. [context evaluateScript:@"log(dic.name, dic['#'])"];
  4. //OutPut:
  5. // Ider
  6. // 21

 

語言穿梭機—JSExport協議

JavaScript能夠脫離prototype繼承徹底用JSON來定義對象,可是Objective-C編程裏可不能脫離類和繼承了寫代碼。因此JavaScriptCore就提供了JSExport做爲兩種語言的互通協議。JSExport中沒有約定任何的方法,連可選的(@optional)都沒有,可是全部繼承了該協議(@protocol)的協議(注意不是Objective-C的類(@interface))中定義的方法,均可以在JSContext中被使用。語言表述起來有點繞,仍是用例子來講明會更明確一點。

  1. @protocol PersonProtocol <JSExport>
  2.  
  3. @property (nonatomic, retainNSDictionary *urls;
  4. (NSString *)fullName;
  5.  
  6. @end
  7.  
  8. @interface Person :NSObject <PersonProtocol>
  9.  
  10. @property (nonatomic, copyNSString *firstName;
  11. @property (nonatomic, copyNSString *lastName;
  12.  
  13. @end;
  14.  
  15. @implementation Person
  16.  
  17. @synthesize firstName, lastName, urls;
  18.  
  19. (NSString *)fullName {
  20. return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
  21. }
  22.  
  23. @end

在上邊的代碼中,定義了一個PersonProtocol,並讓它繼承了神祕的JSExport協議,在新定義的協議中約定urls屬性和fullName方法。以後又定義了Person類,除了讓它實現PersonProtocol外,還定義了firstName和lastName屬性。而fullName方法返回的則是兩部分名字的結合。

下邊就來建立一個Person對象,而後傳入到JSContext中並嘗試使用JavaScript來訪問和修改該對象。

  1. // initialize person object
  2. Person  *person [[Person alloc] init];
  3. context [@"p"= person;
  4. person.firstName  @"Ider";
  5. person.lastName  @"Zheng";
  6. person.urls  = @{@"site"@"http://www.iderzheng.com"};
  7.  
  8. // ok to get fullName
  9. [context evaluateScript:@"log(p.fullName());"];
  10. // cannot access firstName
  11. [context evaluateScript:@"log(p.firstName);"];
  12. // ok to access dictionary as object
  13. [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"];
  14. // ok to change urls property
  15. [context evaluateScript:@"p.urls = {blog:'http://blog.iderzheng.com'}"];
  16. [context evaluateScript:@"log('-------AFTER CHANGE URLS-------')"];
  17. [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"];
  18.  
  19. // affect on Objective-C side as well
  20. NSLog (@"%@", person.urls);
  21.  
  22. //Output:
  23. // Ider Zheng
  24. // undefined
  25. // undefined
  26. // site:
  27. // http://www.iderzheng.com
  28. // blog:
  29. // undefined
  30. // -------AFTER CHANGE URLS-------
  31. // site:
  32. // undefined
  33. // blog:
  34. // http://blog.iderzheng.com
  35. // {
  36. // blog = "http://blog.iderzheng.com";
  37. // }

從輸出結果不難看出,當訪問firstNamelastName的時候給出的結果是undefined,由於它們跟JavaScript沒有JSExport的聯繫。但這並不影響從fullName()中正確獲得兩個屬性的值。和以前說過的同樣,對於NSDictionary類型的urls,能夠在JSContext中當作對象使用,並且還能夠正確地給urls賦予新的值,並反映到實際的Objective-C的Person對象上。

JSExport不只能夠正確反映屬性到JavaScript中,並且對屬性的特性也會保證其正確,好比一個屬性在協議中被聲明成readonly,那麼在JavaScript中也就只能讀取屬性值而不能賦予新的值。

對於多參數的方法,JavaScriptCore的轉換方式將Objective-C的方法每一個部分都合併在一塊兒,冒號後的字母變爲大寫並移除冒號。好比下邊協議中的方法,在JavaScript調用就是:doFooWithBar(foo, bar);

  1. @protocol MultiArgs <JSExport>
  2. (void)doFoo:(id)foo withBar:(id)bar;
  3. @end

若是但願方法在JavaScript中有一個比較短的名字,就須要用的JSExport.h中提供的宏:JSExportAs(PropertyName, Selector)

  1. @protocol LongArgs <JSExport>
  2.  
  3. JSExportAs (testArgumentTypes,
  4. (NSString *)testArgumentTypesWithInt:(int)i double:(double)d
  5. boolean :(BOOL)b string:(NSString *)s number:(NSNumber *)n
  6. array :(NSArray *)a dictionary:(NSDictionary *)o
  7. );
  8.  
  9. @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之中。

 

對已定義類擴展協議— class_addProtocol

對於自定義的Objective-C類,能夠經過以前的方式自定義繼承了JSExport的協議來實現與JavaScript的交互。對於已經定義好的系統類或者從外部引入的庫類,她們都不會預先定義協議提供與JavaScript的交互的。好在Objective-C是能夠在運行時實行對類性質的修改的

好比下邊的例子,就是爲UITextField添加了協議,讓其能在JavaScript中能夠直接訪問text屬性。該接口以下:

  1. @protocol JSUITextFieldExport <JSExport>
  2.  
  3. @property(nonatomic,copyNSString *text;
  4.  
  5. @end

以後在經過class_addProtocol爲其添加上該協議:

  1. (void)viewDidLoad {
  2. [super viewDidLoad];
  3.  
  4. textField.text  @"7";
  5. class_addProtocol ([UITextField class], @protocol(JSUITextFieldExport));
  6. }

爲一個UIButton添加以下的事件,其方法只要是將textField傳入到JSContext中而後讀取其text值,自增1後從新賦值:

  1. (IBAction)pressed:(id)sender {
  2. JSContext  *context [[JSContext alloc] init];
  3.  
  4. context [@"textField"= textField;
  5.  
  6. NSString  *script @"var num = parseInt(textField.text, 10);"
  7. "++num;"
  8. "textField.text = num;";
  9. [context evaluateScript:script];
  10. }

當運行點擊UIButton時就會看到UITextField的值在不斷增長,也證實了對於已定義的類,也能夠在運行時添加神奇的JSExport協議讓它們能夠在Objective-C和JavaScript直接實現友好互通。

iOS Simulator Screen shot Nov 3, 2013 2.57.19 PM iOS Simulator Screen shot Nov 3, 2013 2.57.29 PM

 

不一樣內存管理機制—Reference Counting vs. Garbage Collection

雖然Objetive-C和JavaScript都是面向對象的語言,並且它們均可以讓程序員專心於業務邏輯,不用擔憂內存回收的問題。可是二者的內存回首機制全是不一樣的,Objective-C是基於引用計數,以後Xcode編譯器又支持了自動引用計數(ARC, Automatic Reference Counting);JavaScript則如同Java/C#那樣用的是垃圾回收機制(GC, Garbage Collection)。當兩種不一樣的內存回收機制在同一個程序中被使用時就不免會產生衝突。

好比,在一個方法中建立了一個臨時的Objective-C對象,而後將其加入到JSContext放在JavaScript中的變量中被使用。由於JavaScript中的變量有引用因此不會被釋放回收,可是Objective-C上的對象可能在方法調用結束後,引用計數變0而被回收內存,所以JavaScript層面也會形成錯誤訪問。

一樣的,若是用JSContext建立了對象或者數組,返回JSValue到Objective-C,即便把JSValue變量retain下,但可能由於JavaScript中由於變量沒有了引用而被釋放內存,那麼對應的JSValue也沒有用了。

怎麼在兩種內存回收機制中處理好對象內存就成了問題。JavaScriptCore提供了JSManagedValue類型幫助開發人員更好地管理對象內存。

  1. @interface JSManagedValue NSObject
  2.  
  3. // Convenience method for creating JSManagedValues from JSValues.
  4. (JSManagedValue *)managedValueWithValue:(JSValue *)value;
  5.  
  6. // Create a JSManagedValue.
  7. (id)initWithValue:(JSValue *)value;
  8.  
  9. // Get the JSValue to which this JSManagedValue refers. If the JavaScript value has been collected,
  10. // this method returns nil.
  11. (JSValue *)value;
  12.  
  13. @end

在《iOS7新JavaScriptCore框架入門介紹》有提到JSVirtualMachine爲整個JavaScriptCore的執行提供資源,因此當將一個JSValue轉成JSManagedValue後,就能夠添加到JSVirtualMachine中,這樣在運行期間就能夠保證在Objective-C和JavaScript兩側均可以正確訪問對象而不會形成沒必要要的麻煩。

  1. @interface JSVirtualMachine NSObject
  2.  
  3. // Create a new JSVirtualMachine.
  4. (id)init;
  5.  
  6. // addManagedReference:withOwner and removeManagedReference:withOwner allow
  7. // clients of JSVirtualMachine to make the JavaScript runtime aware of
  8. // arbitrary external Objective-C object graphs. The runtime can then use
  9. // this information to retain any JavaScript values that are referenced
  10. // from somewhere in said object graph.
  11. //
  12. // For correct behavior clients must make their external object graphs
  13. // reachable from within the JavaScript runtime. If an Objective-C object is
  14. // reachable from within the JavaScript runtime, all managed references
  15. // transitively reachable from it as recorded with
  16. // addManagedReference:withOwner: will be scanned by the garbage collector.
  17. //
  18. (void)addManagedReference:(id)object withOwner:(id)owner;
  19. (void)removeManagedReference:(id)object withOwner:(id)owner;
  20.  
  21. @end

 

瞭解更多更多—Source Code

對於iOS7提供JavaScriptCore已經介紹的差很少了,以前也提到這實際上是一個開源的框架,因此若是想要在低版本的iOS上使用,也能夠很容易地自行添加源碼進行編譯和使用。

閱讀源碼也能夠更加了解JavaScriptCore是怎麼實現的,在開發時候也能夠注意到更多的細節避免錯誤的發生,想要閱讀框架的源碼能夠在這裏(源碼1源碼2源碼3)。

文章中的代碼和例子都比較簡單,若是想了解更多JavaScriptCore的使用方法,在這裏有詳細的測試案例能夠提供一些線索。不過經驗證並非全部的測試案例在iOS7中都會經過,這大概是測試案例所用的JavaScriptCore是爲chromium實現的而iOS7是webkit吧。

References:
  1. Steamclock Software – Apple’s new Objective-C to Javascript Bridge
  2. JavaScriptCore and iOS 7 » Big Nerd Ranch BlogBig Nerd Ranch Blog
  3. IOS7開發~JavaScriptCore (二) – 阿福的專欄 – 博客頻道 – CSDN.NET
  4. API in trunk/Source/JavaScriptCore – WebKit
  5. Objective-C Runtime Reference
  6. Automatic Reference Counting vs. Garbage Collection – The Oxygene Language Wiki
  7. http://www.oschina.net/translate/how-to-choose-a-javascript-engine-for-ios-and-android-apps?cmp

感謝:http://blog.iderzheng.com/introduction-to-ios7-javascriptcore-framework/

相關文章
相關標籤/搜索