Hybrid App: 了解JavaScript如何與Native實現混合開發

  • 2019 年 11 月 13 日
  • 筆記

一、簡介

Hybrid Development混合開發是目前移動端開發異常火熱的新興技術,它能夠實現跨平台開發,極大地節約了人力和資源成本。跨平台開發催生了很多新的開源框架,就目前而言,在混合開發中比較流行的有FaceBook開源React Native,有Goggle開源的Flutter。React Native實現的是通過下發JS腳本的方式達到JS與Native交互。Flutter實現的則是通過採用現代響應式框架來構建UI,Flutter與ReactiveCocoa框架配合使用最佳。當然開發者也可以在Native中內嵌WebView的方式(WebKit)實現混合開發。雖然方式不同,但目的相同,都是跨平台,殊途同歸吧。對跨平台有了粗略的了解後,再來看看iOS系統中對JS與Native是如何交互的,其實,系統是給開發者提供了一個極其強大的框架來實現這個功能的,即JavaScriptCore框架。這個框架通過定義JSValue值對象和聲明JSExport協議作為橋樑完成Native與JS的通訊。JS雖然是單執行緒語言,但是iOS是支援多執行緒執行任務的,開發者可以在非同步情況下執行任意一個環境的JavaScript程式碼。大概結構圖如下:

 

二、分析

參考這上圖,可以看出JavaScriptCore框架結構還是很清晰的,JavaScriptCore中有那麼幾個核心的類在開發者是很常用的,需要弄懂它們代表的意思。

  

 

三、API

知道了這幾個核心類的概念已經對這個框架有了個基本的認識,具體的API如何使用,我們可以選擇性點擊去深入研究一下。只有對它們的屬性和方法都了如指掌,開發起來才能得心應手,手到擒來。哎呀媽,不廢話了。。。例如JSContext和JSValue開發中必用的類,額外的可能還會用JSManagerValue,如下:

JSContetxt類:

//初始化,可以選擇對應的虛擬機  - (instancetype)init;  - (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;    //執行js程式碼,返回js值對象  - (JSValue *)evaluateScript:(NSString *)script;  - (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL;    //獲取當前的js上下文  + (JSContext *)currentContext;    //獲取當前的js執行函數,返回js值對象  + (JSValue *)currentCallee;    //獲取當前的js函數中this指向的對象,返回js值對象  + (JSValue *)currentThis;    //獲取當前的js函數中的所有參數  + (NSArray *)currentArguments;    //js的全局對象  @property (readonly, strong) JSValue *globalObject;    //js執行的異常數據  @property (strong) JSValue *exception;  @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);    //js運行的虛擬機  @property (readonly, strong) JSVirtualMachine *virtualMachine;    //js上下文名稱  @property (copy) NSString *name;    //分類  @interface JSContext (SubscriptSupport)  //獲取和設置屬性為js全局對象  - (JSValue *)objectForKeyedSubscript:(id)key;  - (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;  @end    //分類(C函數風格)  @interface JSContext (JSContextRefSupport)  //獲取和設置全局上下文  + (JSContext *)contextWithJSGlobalContextRef:(JSGlobalContextRef)jsGlobalContextRef;  @property (readonly) JSGlobalContextRef JSGlobalContextRef;  @end

JSValue類:

//js上下文  @property (readonly, strong) JSContext *context;    //使用OC數據初始化js值對象,創建有值的JSValue  + (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context;  + (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context;  + (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;  + (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;  + (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context;  + (JSValue *)valueWithPoint:(CGPoint)point inContext:(JSContext *)context;  + (JSValue *)valueWithRange:(NSRange)range inContext:(JSContext *)context;  + (JSValue *)valueWithRect:(CGRect)rect inContext:(JSContext *)context;  + (JSValue *)valueWithSize:(CGSize)size inContext:(JSContext *)context;    //使用OC數據初始化js值對象,創建空的JSValue  + (JSValue *)valueWithNewObjectInContext:(JSContext *)context;  + (JSValue *)valueWithNewArrayInContext:(JSContext *)context;  + (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context;  + (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context;  + (JSValue *)valueWithNewPromiseInContext:(JSContext *)context fromExecutor:(void (^)(JSValue *resolve, JSValue *reject))callback;  + (JSValue *)valueWithNewPromiseResolvedWithResult:(id)result inContext:(JSContext *)context;  + (JSValue *)valueWithNewPromiseRejectedWithReason:(id)reason inContext:(JSContext *)context;  + (JSValue *)valueWithNewSymbolFromDescription:(NSString *)description inContext:(JSContext *)context;  + (JSValue *)valueWithNullInContext:(JSContext *)context;  + (JSValue *)valueWithUndefinedInContext:(JSContext *)context;    //js數據轉OC數據  - (id)toObject;  - (id)toObjectOfClass:(Class)expectedClass;  - (BOOL)toBool;  - (double)toDouble;  - (int32_t)toInt32;  - (uint32_t)toUInt32;  - (NSNumber *)toNumber;  - (NSString *)toString;  - (NSDate *)toDate;  - (NSArray *)toArray;  - (NSDictionary *)toDictionary;  - (CGPoint)toPoint;  - (NSRange)toRange;  - (CGRect)toRect;  - (CGSize)toSize;    //js值對象判斷  @property (readonly) BOOL isUndefined;  @property (readonly) BOOL isNull;  @property (readonly) BOOL isBoolean;  @property (readonly) BOOL isNumber;  @property (readonly) BOOL isString;  @property (readonly) BOOL isObject;  @property (readonly) BOOL isArray;  @property (readonly) BOOL isDate;  @property (readonly) BOOL isSymbol;  - (BOOL)isEqualToObject:(id)value;  - (BOOL)isEqualWithTypeCoercionToObject:(id)value;  - (BOOL)isInstanceOf:(id)value;    //js調用函數  - (JSValue *)callWithArguments:(NSArray *)arguments;  - (JSValue *)constructWithArguments:(NSArray *)arguments;  - (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;    //js屬性設置  - (JSValue *)valueForProperty:(JSValueProperty)property;  - (void)setValue:(id)value forProperty:(JSValueProperty)property;  - (BOOL)deleteProperty:(JSValueProperty)property;  - (BOOL)hasProperty:(JSValueProperty)property;  - (void)defineProperty:(JSValueProperty)property descriptor:(id)descriptor;  - (JSValue *)valueAtIndex:(NSUInteger)index;  - (void)setValue:(id)value atIndex:(NSUInteger)index;  - (JSValue *)objectForKeyedSubscript:(id)key;  - (JSValue *)objectAtIndexedSubscript:(NSUInteger)index;  - (void)setObject:(id)object forKeyedSubscript:(id)key;  - (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index;  + (JSValue *)valueWithJSValueRef:(JSValueRef)value inContext:(JSContext *)context;    //OC與JS類型對應關係    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)   ---------------------+---------------------

JSManagerValue類:

//對JSValue進行一層包裝,對記憶體進行有效的管理,防止提前或者過度釋放  + (JSManagedValue *)managedValueWithValue:(JSValue *)value;  + (JSManagedValue *)managedValueWithValue:(JSValue *)value andOwner:  - (instancetype)initWithValue:(JSValue *)value;  @property (readonly, strong) JSValue *value;

 

四、案例

[1] 首先打開Safari瀏覽器的web檢查器,會用來查看js運行的效果 ,控制台列印

[2] 導入JavaScriptCore框架

[3] 導入頭文件開始測試,Native調用JS

[3-1] 調用無參數的JS函數

native.js

-(void)nativeCallJs {        //方式一      //從js文件獲取js程式碼      NSString *path = [[NSBundle mainBundle] pathForResource:@"native" ofType:@"js"];      NSData *jsData = [NSData dataWithContentsOfFile:path];      NSString *script = [[NSString alloc] initWithData:jsData encoding:NSUTF8StringEncoding];        //執行js程式碼      [self.jsContext evaluateScript:script];  }

-(void)nativeCallJs {        //方式二      //js程式碼寫在端上      NSString *script = @"                      (function(){                           console.log("native call js ------- Wellcome Native");                      })();";        //執行js程式碼      [self.jsContext evaluateScript:script];  }

- (void)viewDidLoad {      [super viewDidLoad];        //js上下文      self.jsContext = [[JSContext alloc] init];        //native調用js      [self nativeCallJs];  }

[3-2] 調用有參數的JS函數

native.js

-(void)nativeCallJsWithArguments:(NSString *)argument {        //方式一      //從js文件獲取js程式碼      NSString *path = [[NSBundle mainBundle] pathForResource:@"native" ofType:@"js"];      NSData *jsData = [NSData dataWithContentsOfFile:path];      NSString *jsString = [[NSString alloc] initWithData:jsData encoding:NSUTF8StringEncoding];        //拼接js參數      NSString *script = [NSString stringWithFormat:jsString,argument];        //執行js程式碼      [self.jsContext evaluateScript:script];  }

-(void)nativeCallJsWithArguments:(NSString *)argument {        //方式二      //js程式碼寫在端上      NSString *jsString = @"                      function receive(argument) {                           console.log("native call js ------- Wellcome "+argument);                      };                      receive('%@')";        //拼接js參數      NSString *script = [NSString stringWithFormat:jsString,argument];        //執行js程式碼      [self.jsContext evaluateScript:script];  }

- (void)viewDidLoad {      [super viewDidLoad];        //js上下文      self.jsContext = [[JSContext alloc] init];        //native調用js      [self nativeCallJsWithArguments:@"我的老哥"];  }

[4] 導入頭文件開始測試,JS調用Native

注意:調用包括無參數和有參數的OC方法,這裡使用程式碼塊Block為例

-(void)jsCallNative {        //定義無參數block      void (^Block1)(void) = ^(){          NSLog(@"js call native ------- hello JavaScript");      };        //定義有參數block      void (^Block2)(NSString *) = ^(NSString *argument){          NSLog(@"js call native ------- hello JavaScript----Wellcome %@",argument);      };        //設置block為JSContext全局對象的屬性,然後可以在safari控制台執行函數oc_block()輸出列印;      [self.jsContext setObject:Block1 forKeyedSubscript:@"oc_block1"];      [self.jsContext setObject:Block2 forKeyedSubscript:@"oc_block2"];  }

- (void)viewDidLoad {      [super viewDidLoad];        //js上下文      self.jsContext = [[JSContext alloc] init];        //js調用native      [self jsCallNative];  }

[5]導入頭文件開始測試, OC和JS對象的映射

//OC與JS數據傳遞的數據就是JSValue值對象,存儲在JS的全局對象中  //存和取的過程  [self.jsContext setObject:(id) forKeyedSubscript:(NSObject<NSCopying> *)];  [self.jsContext objectForKeyedSubscript:(id)]

[5-1] 系統提供的OC數據類型,不用特殊存儲,可以直接存取

//系統提供的OC數據類型,不用特殊存儲,可以直接存取  [self.jsContext setObject:@"mac" forKeyedSubscript:@"os"];  JSValue *osValue = [self.jsContext objectForKeyedSubscript:@"os"];  NSString *osName = [osValue toString];  NSLog(@"-----osName = %@-----",osName);

2019-11-12 14:58:17.471840+0800 混合開發[10499:365654] -----osName = mac-----

[5-2] 特殊的OC類型,如自定義對象,則必須遵守JSExport協議,JS才能拿到自定義對象的所有屬性和方法

#import <UIKit/UIKit.h>  #import <JavaScriptCore/JavaScriptCore.h>    //遵守JSExport協議,使得JS在上下文中可以獲取到OC中定義的屬性和方法  @protocol PersonProtocol <JSExport>  @property (nonatomic,   copy) NSString *name;  @property (nonatomic, assign) int age;  @property (nonatomic, assign) int grade;  @property (nonatomic, assign) float score;  -(void)description;  @end    @interface Person : NSObject<PersonProtocol>  @property (nonatomic,   copy) NSString *name;  @property (nonatomic, assign) int age;  @property (nonatomic, assign) int grade;  @property (nonatomic, assign) float score;  -(void)description;  @end    #import "Person.h"  @implementation Person  -(void)description {      NSLog(@"姓名:name = %@",self.name);      NSLog(@"年齡:age = %d",self.age);      NSLog(@"年級:grade = %d",self.grade);      NSLog(@"分數:score = %.1f",self.score);  }  @end

//特殊的OC類型,自定義對象,則必須遵守JSExport協議,JS才能拿到自定義對象的所有屬性和方法  Person *person = [[Person alloc] init];  person.name = @"張三";  person.age = 20;  person.grade = 5;  person.score = 98;  [self.jsContext setObject:person forKeyedSubscript:@"personEntity"];  JSValue *personValue = [self.jsContext objectForKeyedSubscript:@"personEntity"]; //personEntity為OC在JS的對象形式  Person *xyq_person = (Person *)[personValue toObject];  [xyq_person description];

2019-11-12 14:58:17.472563+0800 混合開發[10499:365654] 姓名:name = 張三  2019-11-12 14:58:17.472709+0800 混合開發[10499:365654] 年齡:age = 20  2019-11-12 14:58:17.472810+0800 混合開發[10499:365654] 年級:grade = 5  2019-11-12 14:58:17.472889+0800 混合開發[10499:365654] 分數:score = 98.0

 

五、實踐

到現在為止,相信我們對JS和Native的交互原理有了自己的理解。在案例中使用了js文件下發和解析的方式實現了Native執行JS程式碼,這個正是Facebook開源的React Native的設計思路。React Native支援跨平台,通過一套js文件就可以在Andriod和iOS上完成Native的介面渲染。現在我們通過一個小測試來模擬Hybrid App的構建原理,通過按鈕點擊切換控制器視圖的背景色。

(1) 創建JavaScript腳本,在腳本中創建Native需要的任意UI控制項存到數組,作為函數的返回值

UIKit.js

//定義一個自調用函數,JavaScript腳本載入完成立即執行  (function(){      return renderUI();   })();    /* JavaScript腳本  定義一個Label類   * rect:尺寸   text:文本  color:顏色   */  function Label(rect,text,fontSize,textColor,textAlignment,bgColor){      this.rect = rect;      this.text = text;      this.fontSize = fontSize;      this.textColor = textColor;      this.textAlignment = textAlignment; //NSTextAlignmentCenter = 1      this.bgColor = bgColor;      this.type = "Label";  }    /* JavaScript腳本  定義一個Button類   * rect:尺寸   text:文本  color:顏色  callFunction:函數   */  function Button(rect,title,fontSize,titleColor,bgColor,callFunction){      this.rect = rect;      this.title = title;      this.fontSize = fontSize;      this.titleColor = titleColor;      this.bgColor = bgColor;      this.callFunction = callFunction;      this.type = "Button";  }    /* JavaScript腳本  Rect類   * x:坐標x   y:坐標y  w:寬度  h:高度   */  function Rect(x,y,w,h){      this.x = x;      this.y = y;      this.w = w;      this.h = h;  }    /* JavaScript腳本  Color類   * r:red  g:green  b:black  a:alpa   */  function Color(r,g,b,a){      this.r = r;      this.g = g;      this.b = b;      this.a = a;  }    //渲染方法,實例化上面類的對象  function renderUI() {        //創建js標籤對象      var screenWidth = 375.0;      var labeWidth = 200;      var labelRect = new Rect((screenWidth-labeWidth)*0.5, 100, labeWidth, 44);      var labeFontSize  = 20;      var labelTextColor = new Color(1,0,0,1);      var labelBgColor = new Color(1,1,0,1);      var label = new Label(labelRect, "I From JS", labeFontSize, labelTextColor, 1, labelBgColor);        //創建js按鈕對象      var buttonWidth = 200;      var buttonRect = new Rect((screenWidth-buttonWidth)*0.5, 200, buttonWidth, buttonWidth);      var buttonFontSize  = 40;      var buttonTitleColor = new Color(1,0,1,1);      var buttonbgColor = new Color(1,1,1,1);      var button = new Button(buttonRect,"Button",buttonFontSize,buttonTitleColor,buttonbgColor,function(r,g,b){                                  var randColor = new Color(r,g,b,1);                                  configEntity.chageViewColor(randColor);                            });        //返回js對象      return [label, button];    }

(2) 創建自定義對象,遵守JSExport協議,添加為JS的全局對象的屬性,作為與Native交互的橋接器

//自定義Config類  #import <Foundation/Foundation.h>  #import <UIKit/UIKit.h>  #import <JavaScriptCore/JavaScriptCore.h>    NS_ASSUME_NONNULL_BEGIN    @protocol ConfigProtocol <JSExport>  -(void)chageViewColor:(JSValue *)colorValue;  @end    @interface Config : NSObject<ConfigProtocol>  @property (nonatomic, strong) UIViewController *currentVc;  -(void)chageViewColor:(JSValue *)colorValue; //改變當前控制器視圖背景色  @end    NS_ASSUME_NONNULL_END

//Created by 夏遠全 on 2019/11/12.    #import "Config.h"    @implementation Config    -(void)chageViewColor:(JSValue *)colorValue {        CGFloat red = colorValue[@"r"].toDouble;      CGFloat green = colorValue[@"g"].toDouble;      CGFloat blue = colorValue[@"b"].toDouble;      CGFloat alpha = colorValue[@"a"].toDouble;        self.currentVc.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:alpha];  }  @end

(3) 在VC中解析JavaScript腳本,獲取UI控制項元素,進行介面的渲染

#import "ViewController.h"  #import <JavaScriptCore/JavaScriptCore.h>  #import "Person.h"  #import "Config.h"    @interface ViewController ()  @property (nonatomic, strong) JSContext *jsContext;  @property (nonatomic, strong) NSMutableArray *actions; //所有的回調函數  @end    @implementation ViewController    - (void)viewDidLoad {      [super viewDidLoad];      self.view.backgroundColor = [UIColor redColor];        //js上下文      self.jsContext = [[JSContext alloc] init];        //從JS文件獲取UI進行渲染      [self renderUIFromJs];  }    -(void)renderUIFromJs {        //創建Config對象      Config *config = [[Config alloc] init];      config.currentVc = self;      [self.jsContext setObject:config forKeyedSubscript:@"configEntity"];        //從js文件獲取js程式碼      NSString *path = [[NSBundle mainBundle] pathForResource:@"UIKit" ofType:@"js"];      NSData *jsData = [NSData dataWithContentsOfFile:path];      NSString *script = [[NSString alloc] initWithData:jsData encoding:NSUTF8StringEncoding];        //執行js程式碼      JSValue *jsValue = [self.jsContext evaluateScript:script];      for (int i=0; i<jsValue.toArray.count; i++) {            //取出每一個控制項對象值          JSValue *subValue = [jsValue objectAtIndexedSubscript:i];            //創建控制項          NSString *type = [subValue objectForKeyedSubscript:@"type"].toString;          if ([type isEqualToString:@"Label"]) {                //this.rect = rect;              //this.text = text;              //this.fontSize = fontSize;              //this.textColor = textColor;              //this.textAlignment = textAlignment; //NSTextAlignmentCenter = 1              //this.bgColor = bgColor;              //this.type = "Label";                CGFloat X = subValue[@"rect"][@"x"].toDouble;              CGFloat Y = subValue[@"rect"][@"y"].toDouble;              CGFloat W = subValue[@"rect"][@"w"].toDouble;              CGFloat H = subValue[@"rect"][@"h"].toDouble;              NSString *text = subValue[@"text"].toString;              NSInteger fontSize = subValue[@"fontSize"].toInt32;              UIColor *textColor = [UIColor colorWithRed:subValue[@"textColor"][@"r"].toDouble green:subValue[@"textColor"][@"g"].toDouble blue:subValue[@"textColor"][@"b"].toDouble alpha:subValue[@"textColor"][@"a"].toDouble];              UIColor *bgColor = [UIColor colorWithRed:subValue[@"bgColor"][@"r"].toDouble green:subValue[@"bgColor"][@"g"].toDouble blue:subValue[@"bgColor"][@"b"].toDouble alpha:subValue[@"bgColor"][@"a"].toDouble];              NSTextAlignment alignment = subValue[@"textAlignment"].toInt32;                UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(X, Y, W, H)];              label.text = text;              label.font = [UIFont systemFontOfSize:fontSize];              label.textColor = textColor;              label.textAlignment = alignment;              label.backgroundColor = bgColor;              [self.view addSubview:label];            }          if ([type isEqualToString:@"Button"]) {                //this.rect = rect;              //this.title = title;              //this.fontSize = fontSize;              //this.titleColor = titleColor;              //this.bgColor = bgColor;              //this.type = "Button";              //this.callFunction = callFunction;                CGFloat X = subValue[@"rect"][@"x"].toDouble;              CGFloat Y = subValue[@"rect"][@"y"].toDouble;              CGFloat W = subValue[@"rect"][@"w"].toDouble;              CGFloat H = subValue[@"rect"][@"h"].toDouble;              NSInteger fontSize = subValue[@"fontSize"].toInt32;              NSString *title = subValue[@"title"].toString;              UIColor *titleColor = [UIColor colorWithRed:subValue[@"titleColor"][@"r"].toDouble green:subValue[@"titleColor"][@"g"].toDouble blue:subValue[@"titleColor"][@"b"].toDouble alpha:subValue[@"titleColor"][@"a"].toDouble];              UIColor *bgColor = [UIColor colorWithRed:subValue[@"bgColor"][@"r"].toDouble green:subValue[@"bgColor"][@"g"].toDouble blue:subValue[@"bgColor"][@"b"].toDouble alpha:subValue[@"bgColor"][@"a"].toDouble];                JSValue *actionValue = subValue[@"callFunction"];              [self.actions addObject:actionValue];                UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(X, Y, W, H)];              [button setTitleColor:titleColor forState:UIControlStateNormal];              [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];              [button setTitle:title forState:UIControlStateNormal];              button.titleLabel.font = [UIFont systemFontOfSize:fontSize];              button.backgroundColor = bgColor;              button.tag = self.actions.count-1;              [self.view addSubview:button];          }      }  }    -(void)buttonClick:(UIButton *)button {        JSValue *actionValue = [self.actions objectAtIndex:button.tag];      [actionValue callWithArguments:@[@(arc4random_uniform(2)),@(arc4random_uniform(2)),@(arc4random_uniform(2))]];    }    - (NSMutableArray *)actions {      if (!_actions) {          _actions = [NSMutableArray array];      }      return _actions;  }

(4) 演示gif