When iOS loves JS

想起了以前在慕课网看到的 @大城小胖 JS混合开发的一个视频,在此整理一下iOS与JS之间的相关知识点。

JSBinding技术


JSBinding技术不是Hybrid技术,它是JavaScript与Native之间的桥接。

JSBinding依赖于JSEngine,而iOS 7首次开放了JavaScriptCore的API,使得JSBinding得以打通JS与原生语言的鸿沟。

JavaScriptCore的核心类:

  • JavaScriptCore.h
  • JSContext
  • JSValue
  • JSExport

首先需要导入JavaScriptCore头文件
#import <JavaScriptCore/JavaScriptCore.h>
然后创建运行的上下文

JSContext *context = [[JSContext alloc] init];  // JSContext 是 JS运行环境

增加JS异常处理器

context.exceptionHandler = ^(JSContext *ctx, JSValue *exception) {
    NSLog(@"%@", exception);
};

1. Native中调JS

  • 执行JS代码:

    JSValue *value = [context evaluateScript:@"1+2"];
    NSLog(@"1 + 2 = %f",[result toDouble]);    //  1 + 2 = 3.000000
    
  • 执行JS函数:

    NSString *script = @"var function sum(a,b) { return a+b; }";
    [context evaluateScript:script];
    
    JSValue *sum = context[@"sum"];
    JSValue *result = [sum callWithArguments:@[@1,@2]];
    NSLog(@"sum(1,2) = %f",[result toDouble]);    // sum(1,2) = 3.000000
    
  • 创建JS的值:

    JSValue *intVar = [JSValue valueWithInt32:123 inContext:context];
    context[@"bar"] = intVar;  //不创建全局变量指向值对象
    [context evaluteScript:@"bar++"];
    

    另一种方式,直接在context中创建全局变量

    [context evaluateScript:@"var bar = 123;"]; 
    

2. JS通过Block调Native

context[@"sum"] = ^(int a, int b) {
    return a + b;
};
JSValue *result = [context evaluateScript:@"sum(1,2)"];
NSLog(@"sum(1,2) = %f", [result toDouble]);  // sum(1,2) = 3.000000

可以传递多个参数

context[@"sum"] = ^{
    NSArray *args = [JSContext currentArguments];
    __block double sum = 0;
    [args enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        sum += [obj toDouble];
    }];
    return sum;
};
JSValue *result = [context evaluateScript:@"sum(1,2,3,4,5)"];
NSLog(@"sum = %f", [result toDouble]); // sum = 15.000000

3.通过JSExport调Native

实例化JSExport类加入到JSContext中,再用JS调用对象

新建一个JSExport类Point3D

#import <JavaScriptCore/JavaScriptCore.h>

//声明一个协议
@protocol Point3DExport <JSExport>
// 将属性暴露给JS
@property double x;
@property double y;
@property double z;
// 将方法暴露给JS
- (double)length;
@end

@interface Point3D : NSObject <Point3DExport>{
    JSContext *context;
}

- (instancetype)initWithContext:(JSContext *)ctx;
@end

@implementation Point3D

@synthesize x,y,z;

- (instancetype)initWithContext:(JSContext *)ctx {
    if (self = [super init]) {
        context = ctx;
        // 尝试将Point3D类放到上下文中,但实际只是加了一个object,而不是一个类
        context[@"Point3D"] = [Point3D class];
    }
    return self;
}

-(double)length {
    return sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
}

@end

执行部分

Point3D *point3D = [[Point3D alloc] initWithContext:context];
point3D.x = 1;
point3D.y = 2;
point3D.z = 3;
context[@"point3D"] = point3D;
NSString *script = @"point3D.x=0;point3D.y=4;point3D.length()";
JSValue *result = [context evaluateScript: script];
NSLog(@"Result of %@ is %f", script, [result toDouble]);  // Result of point3D.x=0;point3D.y=4;point3D.length() is 5.000000

4.直接加载JS文件

void loadScriptContext(JSContext *ctx, NSString *fileName) {
    NSString *filePath = [NSString stringWithFormat:@"%@/JS/%@",[[NSBundle mainBundle] resourcePath], fileName];
    NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    [ctx evaluateScript:script];
}

可以借用3中的方式,模拟控制台的功能,然后利用[JSContext currentArguments]获取所有的参数打印。

关于JS与OC之间类型转换

OC与JS的类型转换

关于JS与OC之间Retain cycle的处理

使用JSManagedValue包装JS对象,系统会正确处理垃圾回收及内存管理。

[JSManagedValue managedValueWithValue:value];

关于JS与OC之间多线程的情况

同一个JSVirtualMachine相当于处于同一个线程下,不用担心锁及并发问题。

// 初始化虚拟机,类似于创建了一个新线程
JSVirtualMachine *jsvm = [[JSVirtualMachine alloc] init];
JSContext *ctx = [[JSContext alloc] initWithVirtualMachine:jsvm];

Hybrid


即混合开发,由Native通过JSBridge等方法提供统一的API,然后用Html5+JS来写实际的逻辑,调用API,这种模式下,由于Android,iOS的API一般有一致性,而且最终的页面也是在webView中显示,所以有跨平台效果。比较有名的框架有PhoneGap(Cordova),AppCan等。
Hybrid 是Web技术与Native之间的桥梁!

1. 原生代码调用网页中的 JavaScript 函数

假设我们的网页中有如下代码

[script type="text/javascript"]

function myFunc() {
    return "Text from web"
}

[/script]

原生代码可以用如下方式调用myFunc()

NSString * result = [self.webView stringByEvaluatingJavaScriptFromString:@"myFunc()"];
NSLog(@"%@",result);

打印结果为 Text from web

2. 网页中的 JavaScript 调用系统的原生代码

这一步比上边的要复杂一些,iOS 不像 Android 可以直接给网页中的 JavaScript 函数注入一个原生代码的接口。这里我们会用一个比较曲折的方式来实现。

假设 Objective-C 的类里有一个方法

-(void)nativeFunction:(NSString*)args {
}

JavaScript 里我们用下边的方法来最终调用到上边这个方法

window.JSBridge.callFunction("callNativeFunction", "some data");

在我们的页面里,是没有JSBridge.callFunction存在的,这一步我们要在原生代码端注入。

在 webView 的 delegate 的- (void)webViewDidFinishLoad:(UIWebView *)webView里我们用下边的方式注入 JavaScript

NSString *js = @"(function() {
    window.JSBridge = {};
    window.JSBridge.callFunction = function(functionName, args) {
        var url = \"bridge-js://invoke?\";
        var callInfo = {};
        callInfo.functionname = functionName;
        if (args) {
            callInfo.args = args;
        }
        url += JSON.stringify(callInfo);
        var rootElm = document.documentElement;
        var iFrame = document_createElement_x_x_x(\"IFRAME\");
        iFrame.setAttribute(\"src\",url);
        rootElm.a(iFrame);
        iFrame.parentNode.removeChild(iFrame);
    };
    return true;
})();";

[webView stringByEvaluatingJavaScriptFromString:js];

简单解释一下,首先我们在 window 里创建一个叫JSBridge的对象,然后在里边定义一个方法 callFunction,这个方法的作用是把两个参数打包为 JSON 字符串,然后附带到我们自定义的URL bridge-js://invoke?后边,最后用IFRAME的方式来加载这个 URL

这么做的原因是,当加载IFRAME的时候,就会调用 webView 的 delegate 的- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType方法,其中 request 就是我们刚才自定义的那个 URL,在这个方法里我们做如下处理

NSURL *url = [request URL];
NSString *urlStr = url.absoluteString;
return [self processURL:urlStr];

processURL 函数如下

- (BOOL)processURL:(NSString *) url {
    NSString *urlStr = [NSString stringWithString:url];
    NSString *protocolPrefix = @"bridge-js://invoke?";
    if ([[urlStr lowercaseString] hasPrefix:protocolPrefix]) {
        urlStr = [urlStr substringFromIndex:protocolPrefix.length];
        urlStr = [urlStr stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSError *jsonError;
        NSDictionary *callInfo = [NSJSONSerialization JSONObjectWithData:[urlStr dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&jsonError];
        NSString *functionName = [callInfo objectForKey:@"functionname"];
        NSString * args = [callInfo objectForKey:@"args"];
        if ([functionName isEqualToString:@"callNativeFunction"]) {
            [self nativeFunction:args];
        }
        return NO;
    }
    return YES;
}

bridge-js://invoke?这个自定义的 URL 里边把附带在后边 JSON 字符串解析出来,然后判断 function name key的值如果是callNativeFunction那么就去调用原生方法nativeFunction, 如果需要实现更多的方法调用,只要添加这个映射关系就行了。

至此,JavaScript 和 Objective-C 代码的双向调用就都实现了。

思路

更好的一个JSBridge的使用方案:WebViewJavascriptBridge

React Native


Facebook发起的开源的一套新的APP开发方案,使用JS+部分原生语法来实现功能,底层会把React转换为原生API,相比Weex,iOS与Android不共用同一套代码。
基于React,宣称“Learn once, write anywhere”
抽空准备学习中...

Weex


Weex来自阿里系,最底层的原理是和React-Native相同的,就是将JS代码渲染成原生组件。
基于Vue,宣称"Write once, run anywhere"
犹豫到底学哪个😂
学习推荐作者:@一缕殇流化隐半边冰霜

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • JavaScriptCore框架主要是用来实现iOS与H5的交互。由于现在混合编程越来越多,H5的相对讲多,所以研...
    水灵芳蕥阅读 1,389评论 1 8
  • 本文由我们团队的 纠结伦 童鞋撰写。 写在前面 本篇文章是对我一次组内分享的整理,大部分图片都是直接从keynot...
    知识小集阅读 15,230评论 11 172
  • 随着H5技术的兴起,在iOS开发过程中,难免会遇到原生应用需要和H5页面交互的问题。其中会涉及方法调用及参数传值等...
    Chris_js阅读 3,062评论 1 8
  • 写在前面 本篇文章是对我一次组内分享的整理,大部分图片都是直接从keynote上截图下来的,本来有很多炫酷动效的,...
    等开会阅读 14,402评论 6 69
  • 月,是故乡圆;酒,是故乡醇;茶,是故乡浓。粤北的群山之中深藏着我的故乡,和大多数中国的农村一样,它并无什么盛产。沿...
    仓央格桑阅读 518评论 1 2