Fork me on GitHub

如何用JavaScriptCore做andriod和iOS都兼容的JS-NativeSDK

Hybrid混合开发模式越来越趋向主流了,于是就避免不了原生Native和JS的交互,也即是通过JavaScriptCore实现JS和Native的通信,总体来说,苹果的JavaScriptCore的API还是很简单易用的,主要的操作步骤如下:

基本使用

  • 获取JSContext
1
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  • 处理某个js调用Native的方法
1
2
3
4
5
6
7
_jsContext[@"log"] = ^() { 
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
//需要注意这里的obj都还是JSValue
NSLog(@"%@",obj);
}
};
  • Native调用JS方法
1
2
[self.jsContext evaluateScript:@"log('arg1')"];
[self.jsContext evaluateScript:@"logCallback('arg1')"];
  • 重定义某个JS方法
1
2
3
4
5
6
7
[self.jsContext evaluateScript:@"checkAPI = function(){\
return [\
'selectImage',\
'startRecord',\
'login',\
];\
}"];

上述几个步骤,其实已经能满足我们开发的基本使用了。可是后来发现由于平台差异性问题,在android端JS调用原生Native的时候,需要指定interfaceName接口名称,所以android和js的通信方式为:

1
2
// js调用android方法的方式
xxxname.log('xxx');

可是这种方式在iOS平台下,是无法调用iOS原生方法的。

1
2
3
4
5
6
7
8
// 这个不能被执行到 
self.jsContext[@"xxname.log"] = ^() {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
//需要注意这里的obj都还是JSValue
NSLog(@"%@",obj);
}
};

那么有什么好的解决办法吗?那是肯定的,要不然不白说废话了吗?

高级用法

方案一:

使用JSExport协议,来达到android的处理效果

  • 定义一个遵守协议的协议类LJNWebViewJSHelperProtocol,并在协议中定义JS要调用原生的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#import <JavaScriptCore/JavaScriptCore.h>

NS_ASSUME_NONNULL_BEGIN

@protocol LJNWebViewJSHelperProtocol <JSExport>

/**
调用原生方法
*/
- (void)callNativeMethod:(NSString *)arg;

@end

NS_ASSUME_NONNULL_END
  • 创建一个继承自NSObject的类LJNWebViewJSHelper并遵守LJNWebViewJSHelperProtocol,实现协议方法
1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
#import "LJNWebViewJSHelperProtocol.h"

NS_ASSUME_NONNULL_BEGIN

@interface LJNWebViewJSHelper : NSObject<LJNWebViewJSHelperProtocol>

@end

NS_ASSUME_NONNULL_END
1
2
3
4
5
6
7
8
9
10
11
12
13
#import "LJNWebViewJSHelper.h"

@implementation LJNWebViewJSHelper

/**
调用原生方法
*/
- (void)callNativeMethod:(NSString *)arg
{
NSLog(@"JS调用原生方法, 参数arg:%@",arg);
}

@end
  • 注意该类的实例到JSContext中
1
2
3
4
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.jsContext[@"xxxname"] = [[LJNWebViewJSHelper alloc] init];
}

至此JS就可以通过xxxname.callNativeMethod的形式调用Native方法了。

注意:

不要编写self.jsContext[@"xxxname"] = self;这样的代码,因为会导致self实例被JS端持有从而导致内存无法释放。

方案二:

对比方案一,无需实现协议方法

  • 创建一个LJNWebViewJSExport类,遵守JSExport协议
1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>

NS_ASSUME_NONNULL_BEGIN

@interface LJNWebViewJSExport : NSObject<JSExport>

@end

NS_ASSUME_NONNULL_END
  • 注册给JSContext
1
self.jsContext[@"xxxname"] = [[LJNWebViewJSExport alloc] init];
  • 处理JS调用Native的方法
1
2
3
4
5
6
7
8
9
10
self.jsContext[@"callNativeMethod"] = ^(NSString *arg) {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
// 需要注意这里的obj还是JSValue
NSLog(@"%@",obj);
}
};

// 重定义xxxname.log方法
[self.jsContext evaluateScript:@"xxxname.log = log"];

至此两种解决方案都已讲述完毕。个人觉得解决方案二有点更多一些,更贴近JS习惯,其次不需要定义那么多的方法,其次对于JS端调用来说,不定参数个数也更容易一些。

总结

  • 使用 NSArray *args = [JSContext currentArguments];得到的args,内部都是JSValue,千万注意这一点,如果想要使用,最好转成相应的类型,比如[obj toString]之类的,具体可以看JSValue的头文件
  • 要注意循环引用的问题,不要写出 self.jsContext[@”xxname”] = self; 这样的代码来,其他的就是block中的循环引用问题。
  • 注意JS和Native的生命周期,一般情况下不会出现什么问题,但是必要时需要使用JSVirtualMachine包装一下。
    • JS的内存管理方式是GC
    • iOS的内存管理方式是引用计数