JavaScriptCore
其實也是一個很老的API了,之前都是在mac應用中使用,是純c的代碼,在iOS7中,用Objective-C進行了封裝。JavaScriptCore
允許我們在不使用UIWebView
和WKWebView
的情況下去執行JavaScript代碼。而JavaScript作為web開發的一門腳本語言,在跨平臺的應用上,有著很重要的作用。許多跨平臺方案,比如JSPatch,React-Native都是基于JavaScriptCore
作為iOS 端的實現方式。
JavaScriptCore
首先我們需要認識一下JavaScriptCore這個庫里有什么東西。其實這個頭文件只是導入了5個頭文件
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
對于這個五個類,簡單總結如下
-
JSContext
是JavaScript的運行上下文,是執行JavaScript代碼和注冊native方法接口的執行環境。 -
JSValue
是JSContext
執行后的返回結果,可以是任何JavaScript類型,JSValue
提供很多的方法進行JavaScript和Objective-C類型的轉換。但是需要注意的是JSValue
是一個強引用類型,JavaScript內部通過垃圾回收機制進行內存管理。JavaScript和Objective-C類型的轉換可以參考JSValue
提供的方法。 -
JSManagedValue
是JSValue
的封裝,用它可以解決JavaScript和native代碼之間循環引用的問題。 -
JSVirtualMachine
管理JavaScript運行時和管理JavaScript暴露的native對象的內存。一般情況下是不需要直接和這個類交流的。但是如果你需要并行執行JavaScript代碼,JSContext
就需要運行在不同的JSVirtualMachine
之上。每一個JSVirtualMachine
有著自己的 -
JSExport
是一個協議,通過實現它可以完成把一個native對象暴漏給JavaScript。
引用一張圖來說明上述類型的一些關系
Objective-C 使用JavaScript
JavaScript沒有類的概念,可以通過原型繼承的方式實現繼承。Objective-C可以調用JavaScript的函數和JavaScript屬性。這些函數和屬性需要先以NSString
的形式被加載到JSContext
環境上去,執行成功后,如果有返回值的話,返回一個JSValue
對象。一般情況下,JavaScript文件都會存在本地,打包進你的程序中去,當然也可以從網絡中獲取JavaScript文件,加載到context中去,這就是我們講的熱跟新。
函數和屬性
現有如下JavaScript代碼
//jscore.js
var jsGlobleVar = "JS Demo";
function min(a,b){
return a-b;
};
那么這個min方法就可以在Objective-C中以類似key-value的形式被檢索到,這個value就是一個JSValue
,然后調用JSValue
實例的callWithArguments
傳入Objective-C的類型的參數,在JavaScript環境中會進行相應的類型轉換
//JSCoreViewController.m
NSString *jsCode = [self readFile]; // jscore.js
[self.context evaluateScript:jsCode]; //將上述min函數加載到context環境中去
JSValue *min = [self.context[@"min"] callWithArguments:@[@2,@4]]; // self.context[@"min"] 對應的就是js中的 min函數或者min屬性
JSValue *jsVar = self.context[@"jsGlobleVar"];
NSLog(@"jsGlobleVar-----%@",[jsVar toString]);// "JS Demo"
NSLog(@"min+++++%d",[min toInt32]); //-2
JavaScript 調用 Objective-C
JavaScript調用Objective-C的block
如下代碼,向context注冊了key 為multi的 JSValue
對象
self.context[@"multi"] = ^(NSInteger a,NSInteger b){
return a*b;
};
相當在JavaScript中定義一個如下的函數
function multi (a,b){
return a*b;
}
那么在context調用這個block的方式和Objective-C中調用JavaScript的形式是一致的即
JSValue *multi = [self.context[@"multi"] callWithArguments:@[@3,@4]];
// 或者
multi = [self.context evaluateScript:@"multi(10,2)"];
自定義類型和方法
除了使用JSContext
下標方法暴露JavaScript對象以外,還可以使用JSExprot
協議把Objective-C中的自定義類轉換為JSValue
,并暴露給JavaScript對象,在JavaScript的環境中操作這個Objective-C對象。
首先需要定義個遵循JSExport
的子協議。在子協議中規定了哪些屬性和方法可以在JavaScript環境中可用。由于JavaScript中函數參數沒有類型,所以所有的Objective-C方法都會以駝峰命名的方式被調用,比如-(void)minuse:(NSInteger)a b:(NSInteger)b c:(NSInteger)c;
將以minuseBC(a,b,c)
的方式被調用。也可以通過JSExportAs
宏重自定義在JavaScript中被調用的方法名//JSExportAs(add, - (NSInteger)add:(NSInteger)a b:(NSInteger)b)
。總結一下JSExport
的主要功能有
- 將Objective-C的函數和屬性傳給JavaScript
- @property---> JavaScript的getter/setter
- Objective-C 實例方法 ---> JavaScript 函數Objective-C
- Objective-C類方法----> 在全局類對象上的JavaScript函數
Objective-C對象
先定義如下的Objective-C協議,并寫個類遵循這個協議(這里使用JSExportModel
來定義)
@protocol JSModelProtocol <JSExport>
-(void)minuse:(NSInteger)a b:(NSInteger)b c:(NSInteger)c;
@property (assign,nonatomic)NSInteger sum;
@end
接下來就可以將遵循了這個協議的類JSExportModel
的實例對象加到執行環境中去了,并調用在JavaScript中已經被加載到context中的useOCObject
方法。
-(void)excuteOCCdoeInJS{
JSExportModel *model = [JSExportModel new];
self.context[@"model"] = model;
[self.context[@"useOCObject"] callWithArguments:nil];
NSLog(@"%ld",(long)model.intV);//14
}
useOCObject的JavaScript代碼如下
function useOCObject(){
model.minuseBC(100,12,12);
model.intV = 14;
};
這里調用了Objective-C的對象的方法,并對傳入的對象的屬性進行了修改。
傳入一個類對象
因為JavaScript沒有類的概念,所有傳入到JavaScript環境中的Objective-C類就變為了一個Constructor
對象,如果你需要在JavaScript環境中生成一個對象,你需要使這個Objective-C類有個類方法來生成實例對象,即在之前的協議中,我們先定義一個類方法用來生成實例對象
//在協議中聲明,來暴露給JavaScript
+(instancetype)createWithIntV:(NSInteger)value;
然后在Objective-C中進行如下調用
-(void)excuteOCClassInJS{
self.context[@"Model"] = [JSExportModel class];
JSValue *returned = [self.context[@"useOCClass"] callWithArguments:nil];
JSExportModel *m= [returned toObjectOfClass:[JSExportModel class]];
NSLog(@"%ld",(long)m.intV); //12
}
JavaScript代碼如下所示
function useOCClass(){
var m = Model.createWithIntV(12);
m.minuseBC(10,1,1); // 調用Objective-C方法
return m
}
可以看到我們,上述Objective-C代碼打印輸出了12。
返回一個JavaScript函數
在JavaScript中函數也是變量,是一等公民,也可以被返回。在JavaScript文件中有個JavaScriptFunc函數,這個函數如下所示
function callback (){
// 這里打印的東西可以在 safari瀏覽器上的開發選項中打開
console.log("method----");
};
function jsFunc(){
Obj.jsValue = callback // 直接對變量賦值
return callback; //將function 以callback的形式返回
}
這個函數就是將callback函數返回給Objective-C對象。上述第一種是將
函數對象直接賦值給Objective-C對象,第二種直接返回函數對象。可以在Objective-C中用如下方式調用函數。
-(void)jsReturnBlock{
self.obj.jsValue = [self.context[@"jsFunc"] callWithArguments:nil];
[ self.obj.jsValue callWithArguments:nil];
self.context[@"Obj"] = self.obj;
[self.context[@"jsFunc"] callWithArguments:nil];
[ self.obj.jsValue callWithArguments:nil];
}
內存管理
我們都知道Objective-C使用ARC進行內存管理,JavaScriptCore(virtualMechine)內部通過垃圾回收機制來進行內存管理,所有的引用都是強引用。JavaScriptCore內部則保證了大多數的內存管理都是自動進行的,不需要我們進行額外的內存管理。但是還是得注意兩種情況下的內存管理
- 在Objective-C對象中存儲JavaScript的值
- 將JavaScript區域(主要是函數)加到Objective-C對象上中去
self.jsValue = [JSValue new];
self.context[@"block"] = ^(){
JSValue *value = self.jsValue;
NSLog(@"%@",value);
};
這里和普通block對象造成循環引用的類似,self.context 引用self,而本身self保有context。這種情況下,其實編譯器也會告訴你這里產生了循環引用。 最好的方法就是將這個jsValue作為block的參數傳到JavaScript環境中去。
還有一種情況就是在block中需要使用JS中其他的對象或者函數,需要從當前的JSContext
獲取,這時候就造成了循環引用,推薦的方式則是通過+[JSContext currentContext]
來獲取當前的context,即
self.jsValue = [JSValue new];
self.context[@"block"] = ^(){
// 循環引用
// context = self.context;
JSContext *context = [JSContext currentContext];
NSLog(@"%@",value);
};
還有一種情況是你必須要用一個Objective-C對象去保存一個JavaScript的值或者函數,你必須使用JSManagedValue
去弱引用JavaScript對象。JSManagedValue
本身就是一個弱引用JavaScript對象的對象。-addManagedReference:withOwner:
將JSManagedValue
對象加入到了垃圾回收的引用。這個函數的意思就是JavaScript的垃圾回收機制觀察引用Objective-C對象ower,如果存在,那么它就不回收這個對象,否則就回收這個對象。
線程
JSVirtualMachine
保證了JavaScript代碼執行的線程安全,鎖都是JSVirtualMachine
來控制。使用不同的JSVirtualMachine
來實現串行或并發。
JavaScriptCore 和UIWebView
在UIWebView
中能夠調用JavaScript代碼,其本質上還是UIWebView
將JavaScript代碼在其內部的JSContext
上執行。可以在UIWebView
加載完后調用,獲取到這個_context。
-(void)webViewDidFinishLoad:(UIWebView *)webView{
_context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[_context evaluateScript:@"showWebViewAlert()"];
}
網上有說documentView.webView.mainFrame.javaScriptContext
是私有API,會有被拒的風險,需謹慎。
JavaScriptCore 和WKWebView
WKWebView
和JavaScriptCore
之間的通信,可以通過WKWebView
的代理實現。在js代碼中 通過發送如下 消息
window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'});
WKWebView
只需要在啟動的時候綁定了AppModel就可以接收到JS發過來的消息了。具體如下
WKUserContentController *contentVC = [WKUserContentController new];
//然后就可以通過代理取到js發來的消息了
[contentVC addScriptMessageHandler:self name:@"AppModel"];
config.userContentController = contentVC;
這樣就可以WKScriptMessageHandler
受到消息了
// MARK: - WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
// 收到消息
// window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'});
NSLog(@"%@",message.body);
}
demo
demo下載地址iOS 中使用JS的demo ,喜歡的就star一下吧。。哈哈
參考資料
Java?Script?Core
JavaScriptCore by Example
JavaScriptCore and iOS 7
JavaScriptCore Tutorial for iOS: Getting Started
Integrating JavaScript into Native Apps