JavaScriptCore
JavaScriptCore是webkit的一個重要組成部分,主要是對JS進行解析和提供執行環境。程式碼是開源的,可以下下來看看(原始碼)。iOS7後蘋果在iPhone平臺推出,極大的方便了我們對js的操作。我們可以脫離webview直接執行我們的js。iOS7以前我們對JS的操作只有webview裡面一個函式 stringByEvaluatingJavaScriptFromString
,JS對OC的回撥都是基於URL的攔截進行的操作。大家用得比較多的是WebViewJavascriptBridge和EasyJSWebView這兩個開源庫,很多混合都採用的這種方式。
JavaScriptCore和我們相關的類不是很多,使用起來也非常簡單。
1 2 3 4 5 |
#import "JSContext.h" #import "JSValue.h" #import "JSManagedValue.h" #import "JSVirtualMachine.h" #import "JSExport.h" |
JSContext
JS執行的環境,同時也通過JSVirtualMachine管理著所有物件的生命週期,每個JSValue都和JSContext相關聯並且強引用context。
JSValue
JS物件在JSVirtualMachine中的一個強引用,其實就是Hybird物件。我們對JS的操作都是通過它。並且每個JSValue都是強引用一個context。同時,OC和JS物件之間的轉換也是通過它,相應的型別轉換如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
Objective-C type | JavaScript type --------------------+--------------------- nil | undefined NSNull | null NSString | string NSNumber | number, boolean NSDictionary | Object object NSArray | Array object NSDate | Date object NSBlock (1) | Function object (1) id (2) | Wrapper object (2) Class (3) | Constructor object (3) |
JSManagedValue
JS和OC物件的記憶體管理輔助物件。由於JS記憶體管理是垃圾回收,並且JS中的物件都是強引用,而OC是引用計數。如果雙方相互引用,勢必會造成迴圈引用,而導致記憶體洩露。我們可以用JSManagedValue儲存JSValue來避免。
JSVirtualMachine
JS執行的虛擬機器,有獨立的堆空間和垃圾回收機制。
JSExport
一個協議,如果JS物件想直接呼叫OC物件裡面的方法和屬性,那麼這個OC物件只要實現這個JSExport協議就可以了。
OC和JS之間的通訊
兩者之間的通訊還是很簡單的,直接看簡單程式碼示例吧。
Objective-C -> JavaScript
1 2 3 4 5 6 7 8 9 |
self.context = [[JSContext alloc] init]; NSString *js = @"function add(a,b) {return a+b}"; [self.context evaluateScript:js]; JSValue *n = [self.context[@"add"] callWithArguments:@[@2, @3]]; NSLog(@"---%@", @([n toInt32]));//---5 |
步驟很簡單,建立一個JSContext物件,然後將JS程式碼載入到context裡面,最後取到這個函式物件,呼叫callWithArguments
這個方法進行引數傳值。(JS裡面函式也是物件)
JavaScript -> Objective-C
JS呼叫OC有兩個方法:block和JSExport protocol。
block(JS function):
1 2 3 4 5 6 7 |
self.context = [[JSContext alloc] init]; self.context[@"add"] = ^(NSInteger a, NSInteger b) { NSLog(@"---%@", @(a + b)); }; [self.context evaluateScript:@"add(2,3)"]; |
我們定義一個block,然後儲存到context裡面,其實就是轉換成了JS的function。然後我們直接執行這個function,呼叫的就是我們的block裡面的內容了。
JSExport protocol:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
//定義一個JSExport protocol @protocol JSExportTest - (NSInteger)add:(NSInteger)a b:(NSInteger)b; @property (nonatomic, assign) NSInteger sum; @end //建一個物件去實現這個協議: @interface JSProtocolObj : NSObject @end @implementation JSProtocolObj @synthesize sum = _sum; //實現協議方法 - (NSInteger)add:(NSInteger)a b:(NSInteger)b { return a+b; } //重寫setter方法方便列印資訊, - (void)setSum:(NSInteger)sum { NSLog(@"--%@", @(sum)); _sum = sum; } @end //在VC中進行測試 @interface ViewController () @property (nonatomic, strong) JSProtocolObj *obj; @property (nonatomic, strong) JSContext *context; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //建立context self.context = [[JSContext alloc] init]; //設定異常處理 self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) { [JSContext currentContext].exception = exception; NSLog(@"exception:%@",exception); }; //將obj新增到context中 self.context[@"OCObj"] = self.obj; //JS裡面呼叫Obj方法,並將結果賦值給Obj的sum屬性 [self.context evaluateScript:@"OCObj.sum = OCObj.addB(2,3)"]; } |
demo很簡單,還是定義了一個兩個數相加的方法,還有一個儲存結果的變數。在JS中進行呼叫這個物件的方法,並將結果賦值sum。唯一要注意的是OC的函式命名和JS函式命名規則問題。協議中定義的是add: b:
,但是JS裡面方法名字是addB(a,b)
。可以通過JSExportAs
這個巨集轉換成JS的函式名字。
修改下程式碼:
1 2 3 4 5 6 7 8 9 10 |
@protocol JSExportTest //用巨集轉換下,將JS函式名字指定為add; JSExportAs(add, - (NSInteger)add:(NSInteger)a b:(NSInteger)b); @property (nonatomic, assign) NSInteger sum; @end //呼叫 [self.context evaluateScript:@"OCObj.sum = OCObj.add(2,3)"]; |
我們可以定義自己的異常捕獲,可以把context,異常block改為自己的:
1 2 3 4 |
self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) { [JSContext currentContext].exception = exception; NSLog(@"exception:%@",exception); }; |
記憶體管理
現在來說說記憶體管理的注意點,OC使用的ARC,JS使用的是垃圾回收機制,並且所有的引用是都強引用,不過JS的迴圈引用,垃圾回收會幫它們打破。JavaScriptCore裡面提供的API,正常情況下,OC和JS物件之間記憶體管理都無需我們去關心。不過還是有幾個注意點需要我們去留意下。
1、不要在block裡面直接使用context,或者使用外部的JSValue物件。
1 2 3 4 |
//錯誤程式碼: self.context[@"block"] = ^(){ JSValue *value = [JSValue valueWithObject:@"aaa" inContext:self.context]; }; |
這個程式碼,不用自己看了,編譯器都會提示你的。這個block裡面使用self,很容易就看出來了。
1 2 3 4 5 6 |
//一個比較隱蔽的 JSValue *value = [JSValue valueWithObject:@"ssss" inContext:self.context]; self.context[@"log"] = ^(){ NSLog(@"%@",value); }; |
這個是block裡面使用了外部的value,value對context和它管理的JS物件都是強引用。這個value被block所捕獲,這邊同樣也會記憶體洩露,context是銷燬不掉的。
1 2 3 4 |
//正確的做法,str物件是JS那邊傳遞過來。 self.context[@"log"] = ^(NSString *str){ NSLog(@"%@",str); }; |
2、OC物件不要用屬性直接儲存JSValue物件,因為這樣太容易迴圈引用了。
看個demo,把上面的示例改下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//定義一個JSExport protocol @protocol JSExportTest //用來儲存JS的物件 @property (nonatomic, strong) JSvalue *jsValue; @end //建一個物件去實現這個協議: @interface JSProtocolObj : NSObject @end @implementation JSProtocolObj @synthesize jsValue = _jsValue; @end //在VC中進行測試 @interface ViewController () @property (nonatomic, strong) JSProtocolObj *obj; @property (nonatomic, strong) JSContext *context; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //建立context self.context = [[JSContext alloc] init]; //設定異常處理 self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) { [JSContext currentContext].exception = exception; NSLog(@"exception:%@",exception); }; //載入JS程式碼到context中 [self.context evaluateScript: @"function callback (){}; function setObj(obj) { this.obj = obj; obj.jsValue=callback; }"]; //呼叫JS方法 [self.context[@"setObj"] callWithArguments:@[self.obj]]; } |
上面的例子很簡單,呼叫JS方法,進行賦值,JS物件保留了傳進來的obj,最後,JS將自己的回撥callback賦值給了obj,方便obj下次回撥給JS;由於JS那邊儲存了obj,而且obj這邊也保留了JS的回撥。這樣就形成了迴圈引用。
怎麼解決這個問題?我們只需要打破obj對JSValue物件的引用即可。當然,不是我們OC中的weak。而是之前說的記憶體管理輔助物件JSManagedValue
。
JSManagedValue
本身就是我們需要的弱引用。用官方的話來說叫garbage collection weak reference
。但是它幫助我們持有JSValue物件必須同時滿足一下兩個條件(不翻譯了,翻譯了怪怪的!):
- The JSManagedValue’s JavaScript value is reachable from JavaScript
- The owner of the managed reference is reachable in Objective-C. Manually adding or removing the managed reference in the JSVirtualMachine determines reachability.
意思很簡單,JSManagedValue
幫助我們儲存JSValue,那裡面儲存的JS物件必須在JS中存在,同時 JSManagedValue
的owner在OC中也存在。我們可以通過它提供的兩個方法 + (JSManagedValue )managedValueWithValue:(JSValue )value;
- (JSManagedValue )managedValueWithValue:(JSValue )value andOwner:(id)owner
建立
JSManagedValue物件。通過
JSVirtualMachine的方法
- (void)addManagedReference:(id)object withOwner:(id)owner來建立這個弱引用關係。通過
- (void)removeManagedReference:(id)object withOwner:(id)owner來手動移除他們之間的聯絡。
把剛剛的程式碼改下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
//定義一個JSExport protocol @protocol JSExportTest //用來儲存JS的物件 @property (nonatomic, strong) JSValue *jsValue; @end //建一個物件去實現這個協議: @interface JSProtocolObj : NSObject //新增一個JSManagedValue用來儲存JSValue @property (nonatomic, strong) JSManagedValue *managedValue; @end @implementation JSProtocolObj @synthesize jsValue = _jsValue; //重寫setter方法 - (void)setJsValue:(JSValue *)jsValue { _managedValue = [JSManagedValue managedValueWithValue:jsValue]; [[[JSContext currentContext] virtualMachine] addManagedReference:_managedValue withOwner:self]; } @end //在VC中進行測試 @interface ViewController () @property (nonatomic, strong) JSProtocolObj *obj; @property (nonatomic, strong) JSContext *context; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //建立context self.context = [[JSContext alloc] init]; //設定異常處理 self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) { [JSContext currentContext].exception = exception; NSLog(@"exception:%@",exception); }; //載入JS程式碼到context中 [self.context evaluateScript: @"function callback (){}; function setObj(obj) { this.obj = obj; obj.jsValue=callback; }"]; //呼叫JS方法 [self.context[@"setObj"] callWithArguments:@[self.obj]]; } |
注:以上程式碼只是為了突出用 JSManagedValue
來儲存 JSValue
,所以重寫了 setter
方法。實際不會寫這麼搓的姿勢。。。應該根據回撥方法傳進來引數,進行儲存 JSValue
。
3、不要在不同的 JSVirtualMachine
之間進行傳遞JS物件。
一個 JSVirtualMachine
可以執行多個context
,由於都是在同一個堆記憶體和同一個垃圾回收下,所以相互之間傳值是沒問題的。但是如果在不同的 JSVirtualMachine
傳值,垃圾回收就不知道他們之間的關係了,可能會引起異常。
執行緒
JavaScriptCore
執行緒是安全的,每個context執行的時候通過lock關聯的JSVirtualMachine
。如果要進行併發操作,可以建立多個JSVirtualMachine
例項進行操作。
與UIWebView的操作
通過上面的demo,應該差不多瞭解OC如何和JS進行通訊。下面我們看看如何對 UIWebView
進行操作,我們不再通過URL攔截,我們直接取 UIWebView
的 context
,然後進行對JS操作。
在UIWebView
的finish的回撥中進行獲取
1 2 3 4 5 |
- (void)webViewDidFinishLoad:(UIWebView *)webView { self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; } |
上面用了私有屬性,可能會被蘋果給拒了。這邊要注意的是每個頁面載入完都是一個新的context
,但是都是同一個JSVirtualMachine
。如果JS呼叫OC方法進行操作UI的時候,請注意執行緒是不是主執行緒。
參考: