iOS_JS与OC的交互详解

背景:

UIWebView: iOS 用来展示 web 端内容的控件。

1. 核心方法:

- (NSString*)stringByEvaluatingJavaScriptFromString:(NSString *)script;

script 就是 JS 代码,返回结果为 js 执行结果。 比如一个 JS function 为

function testFunction(abc){

  return abc;

};

webview 调用此 JS 代码如下:

NSString *js = @"testFunction('abc')";

NSString *result = [webView stringByEvaluatingJavaScriptFromString:js];

2. 重要回调:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

webview 每当需要去加载一个 request 就先回调这个方法,让上层决定是否加载。一般在这里截获,进行本地的处理。

Native 调用 JS:

本质就一个方法,通过 stringByEvaluatingJavaScriptFromString,都是同步。

下面重点说说JS怎么回调Native:

1.通常方法:js修通过改doucument的loaction或者新建一个看不见的iFrame,修改它的 src,就会触发回调 webView 的 shouldStartLoadWithRequest,参数 request 的 url 就是新赋值的 location 或者 url,上层截获这个 url 的参数,对此分发即可。 这个都是异步调用的。

如 JS function:

    var messagingIframe;

    messagingIframe = document_create Element('iframe');

    messagingIframe.style.display = 'none';

    document.documentElement.a(messagingIframe);

    function TestIOSJS(){

        messagingIframe.src = "ios/test/click";

    };

当触发上面的JS时,webview会收到下面的回调:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

{

    NSString *url = request.URL.absoluteString;

    if([url hasSuffix:@"ios/test/click"]){

        //do something you want

        return NO;

    }

    return YES;

}

通过截获这个request的参数就可以做native需要做的事情。

有个开源的代码挺不错的,大家可以看看:https://github.com/marcuswestin/WebViewJavascriptBridge

2.通过XMLHttpRequest:

  (1) Native子类化一个NSURLProtocol类,并通过[NSURLProtocol registerClass:self];把自己注册。

  (2) JS function 创建一个 XMLHttpRequest 对象,然后可以设置携带的参数,设置同步或者异步,然后通过 send 发送请求。

    function iOSExec(){

        var execXhr = new XMLHttpRequest();

        execXhr.open('HEAD', "/!test_exec?" + (+new Date()), true); //设置scheme

        var vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];

        execXhr.setRequestHeader('vc', vcHeaderValue);//设置参数等

        execXhr.setRequestHeader('rc', 1);

        // 发起请求

        execXhr.send(null);

    };

  (3) 因为步骤1已经把自己注册,所以每个客户端的网络请求都会请求这个类 的+(BOOL)canInitWithRequest:(NSURLRequest *)request,让此决定是否需要生成这个request。

@implementation TestURLProtocol

+(void)initProtocol

{

    [NSURLProtocol registerClass:self];

}

+(BOOL)canInitWithRequest:(NSURLRequest *)request{

    NSString *url = request.URL.absoluteString;

    if([url containsString:@"!test_exec"]){

        //do something

    }

    return NO;

}

  (4) 通过获取这个request的参数,上层可以进行拦截,然后进行本地的相 关操作。 

这个方法比较少用,不过能解决JS同步回调Native的方法。

这里也有一个开源库,大家可以看一下:

https://github.com/apache/cordova-ios/tree/master/CordovaLib

app分类:

要想明白你的问题,首先的知道app的分类,app通常被分为3类:

webapp:用html css 和js开发的运行在服务器端的app;

Native app:根据手机系统的默认开发语言开发的app

hybrid app:基于两者之间的app

而你在问题里说的webapp本身就是一个用手机访问的网站,部署在服务器端,不需要安装,直接通过浏览器访问的,如果是需要安装的app,可以百度一下后面两种,你应该就会明白了。

 

同一框架内的两个页面,怎实现JS交互?

function getFrame(frameId)

{

if (typeof window.my_iframe == "undefined") {

window.my_iframe = document.get ElementById(frameId);

if (typeof window.my_iframe == "undefined")

throw "fatal: iframe object not found";

}

return window.my_iframe;

}

function getFrameWin()

{

var f = getFrame();

var win = f.contentWindow || f.contentDocument;

return win;

}

function getFrameDoc()

{

var win = getFrameWin();

return win.contentDocument || win.document;

}

var doc = getFrameDoc();

doc.body.getElementById('div1').style.display = 'block';

代码实例演示

先了解一下JavaScriptCore.framework框架

涉及到的几种类型:

  • JSContext, JSContext是代表JS的执行环境,通过-evaluateScript:方法就可以执行一JS代码
  • JSValue, JSValue封装了JSObjC中的对应的类型,以及调用JSAPI
  • JSExport, JSExport是一个协议,遵守此协议,就可以定义我们自己的协议,在协议中声明的API都会在JS中暴露出来,才能调用

iOS7开始 苹果公布了JavaScriptCore.framework 它使得JSOC的交互更加方便了。

首先导入framework,方法如下:

iOS_JS与OC的交互详解

点击Linked Frameworks and Libraries 的添加后选择 JavaScriptCore.framework

iOS_JS与OC的交互详解

可以看到工程中添加了JavaScriptCore.framework框架,里面有很多头文件:

iOS_JS与OC的交互详解

下来我们创建一个UIWebView用来测试

//

//  TestViewController.m

//  JavaScriptCoreDemo

//

//  Created by XDS on 16/6/21.

//  Copyright © 2016年 xds. All rights reserved.

//

#import "TestViewController.h"

#import 《JavaScriptCore/JavaScriptCore.h》

//导入框架的头文件(新浪博客特殊字符限制,将书名号改为尖括号即可)

@interface TestViewController ()//遵循UIWebView的代理方法

@property (nonatomic, strong)UIWebView *myWebView;

@end

@implementation  TestViewController

- (void) viewDidLoad {

    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];

    self.title = @"JS交互";

//取消自动调整布局

  self.automaticallyAdjustsScrollViewInsets =NO;

//初始化WebView并添加到当前视图上

_myWebView= [[UIWebViewalloc] initWithFrame:CGRectMake(0,64, [UIScreenmainScreen].bounds.size.width, [UIScreenmainScreen].bounds.size.height - 64)];

 _myWebView.delegate = self;

 [self.view addSubview:_myWebView];

//加载网址(百度)

  NSString *httpStr = @"https://www.baidu.com";

    NSURL *httpUrl = [NSURL URLWithString:httpStr];

    NSURLRequest *httpRequest = [NSURLRequestrequestWithURL:httpUrl];

    [_myWebView loadRequest:httpRequest];

}

- (void)didReceiveMemoryWarning {

    [super didReceiveMemoryWarning];

   // Dispose of any resources that can be recreated.

}

@end

//

//  AppDelegate.m

//  JavaScriptCoreDemo

//

//  Created by XDS on 16/6/21.

//  Copyright © 2016年 xds. All rights reserved.

//

#import "AppDelegate.h"

#import "JSWebViewController.h"

#import "TestViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

   // Override point for customization after application launch.

   self.window= [[UIWindow alloc] initWithFrame:[UIScreenmainScreen].bounds];

   TestViewController *testVc = [[TestViewControlleralloc] init];

   UINavigationController *nav = [[UINavigationControlleralloc]initWithRootViewController:testVc];

   self.window.rootViewController = nav;

    [self.window makeKeyAndVisible];

    return YES;   

}

运行后报错:

网址为https://www.baidu.com时报错:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

网址为http://www.baidu.com时报错:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

解决办法:

iOS9中,苹果将原http协议改成了https协议,使用 TLS1.2 SSL加密请求数据。

iOS_JS与OC的交互详解

运行结果:

iOS_JS与OC的交互详解

UIWebView的代理方法如下:

@optional

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType;

- (void)webViewDidStartLoad:(UIWebView*)webView;

- (void)webViewDidFinishLoad:(UIWebView*)webView;

- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullableNSError *)error;

一、OC调用JS方法:

在TestViewController中添加下面的方法:

#pragma mark 网页加载完毕后

- (void)webViewDidFinishLoad:(UIWebView*)webView {

//首先创建JSContext 对象(此处通过当前webView的键获取到jscontext

   JSContext *context=[webView valueForKeyPath :@"documentView.webView.mainFrame.javaScriptContext"];

    //OC调用JS 

   //使用\\n来换行,其中第一个'\'是转义字符,使用HTML中的br标签无法换行

   NSString *alertJS= @"alert('Test\\nOC调用JS方法')"; //准备执行的js代码

    [context evaluateScript:alertJS];//通过oc方法调用jsalert

}

运行结果:

iOS_JS与OC的交互详解

可以看到提示框和alertView一致,点击OK后消失。提示标题为网址。

如果想自定义提示框标题,可以在项目中新建一个UIWebView的类拓展

iOS_JS与OC的交互详解
iOS_JS与OC的交互详解

//

//  UIWebView+JavaScriptAlert.h

//  JavaScriptCoreDemo

//

//  Created by XDS on 16/6/21.

//  Copyright © 2016年 xds. All rights reserved.

//

#import 《UIKit/UIKit.h》

@interface UIWebView (JavaScriptAlert)

-(void) webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString*)message initiatedByFrame:(id)frame;

- (BOOL)webView:(UIWebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString*)message initiatedByFrame:(id)frame;

@end

//

//  UIWebView+JavaScriptAlert.m

//  JavaScriptCoreDemo

//

//  Created by XDS on 16/6/21.

//  Copyright © 2016年 xds. All rights reserved.

//

#import "UIWebView+JavaScriptAlert.h"

@implementation UIWebView (JavaScriptAlert)

- (void)webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString*)message initiatedByFrame:(id)frame {

   UIAlertView * customAlert = [[UIAlertView alloc] initWithTitle:@"温馨提示"message:message delegate:nilcancelButtonTitle:@“好的otherButtonTitles:nil];

    [customAlert show];

}


static BOOL diagStat = NO;

static NSInteger bIdx = -1;

- (BOOL)webView:(UIWebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString*)message initiatedByFrame:(id)frame {

    UIAlertView *confirmDiag = [[UIAlertViewalloc] initWithTitle:@"温馨提示"

message:message  delegate:self cancelButtonTitle:@"取消otherButtonTitles:@"确定", nil];

        [confirmDiag show];

    bIdx= -1;

        while (bIdx==-1) {

      //[NSThread sleepForTimeInterval:0.2];

        [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1f]];

    }

    if (bIdx == 0){//取消;

        diagStat= NO;

    }    else if (bIdx == 1) {//确定;

        diagStat= YES;

    }

   return diagStat;

}

- (void)alertView:(UIAlertView *)alertView clicked ButtonAtIndex:(NSInteger)buttonIndex{

    bIdx= buttonIndex;

}

@end

运行效果如下:

iOS_JS与OC的交互详解

二、JS调用iOS

通过JSContext,我们有两种调用方法:

1) 直接调用JS方法

2)OC中通过JSContext注入模型,然后调用模型的方法

首先我们看第一种,直接调用方法。

其中用到了iOSblock

#pragma mark 网页加载完毕后调用此方法

- (void)webViewDidFinishLoad:(UIWebView *)webView {

   //首先创建JSContext 对象(此处通过当前webView的键获取到jscontext

   JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

       //OC调用JS

   //使用\\n来换行,其中第一个'\'是转义字符,使用HTML中的br标签无法换行

   NSString *alertJS=@"alert('Test\\nOC调用JS方法')";//准备执行的js代码

    [context evaluateScript:alertJS];//通过oc方法调用jsalert

    

      //JS调用OC

//第一种情况:JS里面直接调用方法

   //其中test1就是js的方法名称,赋给是一个block 里面是iOS代码

   //此方法最终将打印出所有接收到的参数,js参数是不固定的我们测试一下就知道

    context[@"test1"] = ^() {

        NSArray *args = [JSContextcurrentArguments];

        for (id obj in args) {

            NSLog(@"%@",obj);

        }

    };

   //此处我们没有写后台(但是前面我们已经知道iOS是可以调用js的,我们模拟一下)

   

//首先准备一下js代码,来调用js的函数test1然后执行

   

//一个参数

    NSString *jsFunctStr=@"test1('参数1')";

    [context evaluateScript:jsFunctStr];

    

   

//二个参数   

NSString *jsFunctStr1=@"test1('参数a','参数b','参数c')";

    [context evaluateScript:jsFunctStr1];

    

   //JS直接执行JS代码

    [context evaluateScript :@"var num = 4"];

//    [context evaluateScript:@"var squareFunc = function(value) {return value * value}"];

//pow是js中的幂函数,用来获得x的y次方; PI是js中的圆周率,大约等于3.141592653589793

    [context evaluateScript:@"var squareFunc = function(value) {return Math.pow(value, 2)}"];

    [context evaluateScript:@"var circleFunc = function(value) {return Math.pow(value, 2) * Math.PI}"];

   //计算正方形的面积

    JSValue *square = [context evaluateScript:@"squareFunc(num)"];

    NSLog (@"%@", square.toNumber);

   //也可以通过下标的方式获取到方法

    JSValue *squareFunc = context[@"squareFunc"];

    JSValue *value = [squareFunc callWithArguments:@[@"8"]];

    NSLog (@"%@", value.toNumber);

    //计算圆形的面积

    JSValue *circleFunc = context[@"circleFunc"];

    JSValue *circleArea = [circleFunc callWithArguments:@[@"10"]];

    NSLog(@"%@", circleArea.toNumber);

}

给test1赋值的block要是执行了那么结果就是对的,JS调用了iOS

控制台打印:

2016-06-21 14:55:09.659 JavaScriptCoreDemo[1960:1773853]参数1

2016-06-21 14:55:09.659 JavaScriptCoreDemo[1960:1773853]参数a

2016-06-21 14:55:09.659 JavaScriptCoreDemo[1960:1773853]参数b

2016-06-21 14:55:09.659 JavaScriptCoreDemo[1960:1773853]参数c

2016-06-21 14:55:09.659 JavaScriptCoreDemo[1960:1773853] 16

2016-06-21 14:55:09.660 JavaScriptCoreDemo[1960:1773853] 64

2016-06-21 14:55:09.660 JavaScriptCoreDemo[1960:1773853] 314.1592653589793

第二种情况,就是JS中是通过注入一个模型对象来调用方法的。

第一种方式是没有注入模型到JS中的。这种方式使用起来不太合适,通常在JS中有很多全局的函数,为了防止名字重名,使用模型的方式是最好不过了。通过我们协商好的模型名称,在JS中直接通过模型来调用我们在ObjC中所定义的模型所公开的API。

我们需要使用到

JSExport

凡事添加了JSExport协议的协议,所规定的方法,变量等就会对js开放,我们才可以通过js调用到。

如果js是一个参数或者没有参数的话就比较简单,我们的方法名和js的方法名保持一致即可。

比如: js方法为  

testobject.TestOneParameter('参数1')  

那么,我们在oc中添加的代理方法就为

-(void)TestOneParameter:(NSString *)message;  

如果js是多个参数的话  我们代理方法的所有变量前的名字连起来要和js的方法名字一样。

比如: js方法为  

testobject.TestTwoParameterSecondParameter('参数A','参数B')  

JS方法有两个参数,那么我们的代理方法就是把JS的方法名TestTowParameterSecondParameter 任意拆分成两段作为代理方法名(此处我们拆分为TestTwoParameter  SecondParameter  那么我们的代理方法就是

-(void)TestTwoParameter:(NSString *)message1 SecondParameter:(NSString *)message2;  

直接看代码:

首先创建一个类TestJSObject,继承NSObject

并且规定一个协议TestJSObjectProtocol,而且这个协议必须要遵守JSExport协议

iOS_JS与OC的交互详解 
iOS_JS与OC的交互详解

//

//  TestJSObject.h

//  JavaScriptCoreDemo

//

//  Created by XDS on 16/6/21.

//  Copyright © 2016年 xds. All rights reserved.

//

#import 《Foundation/Foundation.h》

#import 《JavaScriptCore/JavaScriptCore.h》

//导入框架头文件

//首先创建一个实现了JSExport协议的协议

@protocol TestJSObjectProtocolJSExport》//将书名号改为尖括号即可

//测试几种参数的情况

- (void)TestNoParameter;

- (void)TestOneParameter:(NSString *)message;

- (void)TestTwoParameter:(NSString *)message SecondParameter:(NSString *)message2;

- (void)TestTypeParameter:(NSArray *)message;

- (void)TestDifferentTypeParameter:(NSDictionary *)dict SecondParameter:(NSArray *)array;

@end

//

实现协议方法

@interface TestJSObject :NSObject《TestJSObjectProtocol》

@end

//

//  TestJSObject.m

//  JavaScriptCoreDemo

//

//  Created by XDS on 16/6/21.

//  Copyright © 2016年 xds. All rights reserved.

//

#import "TestJSObject.h"

@implementation TestJSObject

- (void)TestNoParameter {

   NSLog(@"====TestNoParameter====");

}

- (void)TestOneParameter:(NSString *)message {

   NSLog(@"====TestOneParameter:%@====", message);

}

- (void)TestTwoParameter:(NSString *)message SecondParameter:(NSString *)message2 {

NSLog(@"====TestTwoParameter:%@====%@====", message, message2);

}

- (void)TestTypeParameter:(NSArray *)message {

   

NSLog(@"====TestTypeParameter:%@", message);

}

- (void)TestDifferentTypeParameter:(NSDictionary *)dict SecondParameter:(NSArray *)array {

   NSLog(@"====TestDifferentTypeParameter:====\ndict:%@\narray:%@", dict, array);

}

@end

调用方法:

#pragma mark 网页加载完毕后调用此方法

- (void)webViewDidFinishLoad:(UIWebView*)webView {

//首先创建JSContext 对象(此处通过当前webView的键获取到jscontext),类似JS中的window

   JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

//OC调用JS

   //使用\\n来换行,其中第一个'\'是转义字符,使用HTML中的br标签无法换行

   NSString *alertJS=@"alert('Test\\nOC调用JS方法')"; //准备执行的js代码

    [context evaluateScript:alertJS];//通过oc方法调用jsalert

       

//JS调用OC

   //第一种情况:JS里面直接调用方法

   //其中test1就是js的方法名称,赋给是一个block 里面是iOS代码

   //此方法最终将打印出所有接收到的参数,js参数是不固定的我们测试一下就知道

    context[@"test1"] = ^() {

        NSArray *args = [JSContext currentArguments];

        for (id obj inargs) {

            NSLog (@"%@",obj);

        }

    };

   

//此处我们没有写后台(但是前面我们已经知道iOS是可以调用js的,我们模拟一下)

   

//首先准备一下js代码,来调用js的函数test1 然后执行

   

//一个参数

    NSString *jsFunctStr=@"test1('参数1')";

    [context evaluateScript:jsFunctStr];

    

   

//二个参数

   NSString *jsFunctStr1=@"test1('参数a','参数b','参数c')";

    [context evaluateScript:jsFunctStr1];

    

   //JS直接执行JS代码

    [context evaluateScript:@"var num = 4"];

//    [context evaluateScript:@"var squareFunc = function(value) {return value * value}"];

   //powjs中的幂函数,用来获得xy次方;PIjs中的圆周率,大约等于3.141592653589793

    [context evaluateScript:@"var squareFunc = function(value) {return Math.pow(value, 2)}"];

    [context evaluateScript:@"var circleFunc = function(value) {return Math.pow(value, 2) * Math.PI}"];

   

//计算正方形的面积

    JSValue *square = [context evaluateScript:@"squareFunc(num)"];

    NSLog (@"%@", square.toNumber);

       //也可以通过下标的方式获取到方法

    JSValue *squareFunc = context[@"squareFunc"];

    JSValue *value = [squareFunc callWithArguments:@[@"8"]];

    NSLog(@"%@", value.toNumber);

//计算圆形的面积

    JSValue *circleFunc = context[@"circleFunc"];

    JSValue *circleArea = [circleFunc callWithArguments:@[@"10"]];

    NSLog (@"%@", circleArea.toNumber);

    

//第二种情况,js是通过注入模型对象调用的,我们假设js里面有一个对象 testobject 在调用方法

   

//首先创建我们新建类的对象,将它赋值给js的对象

       TestJSObject *testJO=[TestJSObjectnew];

    context[@"testobject"]=testJO;

   

//同样我们也用刚才的方式模拟一下JS调用方法

   

NSString *jsStr1=@"testobject.TestNoParameter()";

    [context evaluateScript:jsStr1];

   

NSString*jsStr2=@"testobject.TestOneParameter('参数1')";

    [context evaluateScript:jsStr2];

   

NSString*jsStr3=@"testobject.TestTwoParameterSecondParameter('参数A', '参数B')";

    [context evaluateScript:jsStr3];

   

NSString*jsStr4=@"testobject.TestTypeParameter(['a','b','c'])";

    [context evaluateScript:jsStr4];

   

NSString*jsStr5=@"testobject.TestDifferentTypeParameterSecondParameter({'name' : 'xds', 'age': 23, 'height' : 178}, [{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}, ['age', 23], 'height=178'])";

    [context evaluateScript:jsStr5];

    

//异常处理

    context.exceptionHandler= ^(JSContext *context, JSValue *exceptionValue) {

        context.exception= exceptionValue;

        NSLog(@"异常信息:%@", exceptionValue);

    };

}

控制台打印:

2016-06-21 14:59:36.445 JavaScriptCoreDemo[1969:1805567]参数1

2016-06-21 14:59:36.445 JavaScriptCoreDemo[1969:1805567]参数a

2016-06-21 14:59:36.445 JavaScriptCoreDemo[1969:1805567]参数b

2016-06-21 14:59:36.445 JavaScriptCoreDemo[1969:1805567]参数c

2016-06-21 14:59:36.445 JavaScriptCoreDemo[1969:1805567] 16

2016-06-21 14:59:36.445 JavaScriptCoreDemo[1969:1805567] 64

2016-06-21 14:59:36.446 JavaScriptCoreDemo[1969:1805567] 314.1592653589793

2016-06-21 14:59:36.446 JavaScriptCoreDemo[1969:1805567]

====TestNoParameter====

2016-06-21 14:59:36.446 JavaScriptCoreDemo[1969:1805567]

====TestOneParameter:参数1====

2016-06-21 14:59:36.446 JavaScriptCoreDemo[1969:1805567] ====TestTwoParameter:参数A====参数B====

2016-06-21 14:59:36.446 JavaScriptCoreDemo[1969:1805567] ====TestTypeParameter:(

    a,

    b,

    c

)

2016-06-21 14:59:36.447 JavaScriptCoreDemo[1969:1805567] ====TestDifferentTypeParameter:====

dict:{

    age = 23;

    height = 178;

    name = xds;

}

array:(

        {

        key1 = value1;

        key2 = value2;

        key3 = value3;

    },

        (

        age,

        23

    ),

    "height=178"

)

PS:

JavaScript和Objective-C交互的那些事

最近公司的运营瞎搞了个活动,其活动要服务端提供数据支持,web前端

在微信公众账号内作为主要的运营阵地,而iOSAndroid要提供相应的入口及页面进行配合。一个活动,动用了各个端的程序猿。而在这里面技术方面主要就是涉及到

web端和服务端的交互,web前端iOSAndroid的交互。本人作为一个

iOS开发者,今天就聊聊webiOSAndroid三端的交互,其实在说明白一点就是方法的互相调用而已。这里主要讲解iOSAndroid会稍微提一下,仅作参考。

iOS_JS与OC的交互详解

概述

iOS原生应用和web页面的交互大致上有这几种方法

iOS7之后的JavaScriptCore、拦截协议、第三方框架WebViewJavaScriptBridge

iOS8之后的WKWebView。

在这里主要讲解JavaScriptCore拦截协议这两种办法。

WebViewJavaScriptBridge是基于拦截协议进行的封装。学习成本相对

JavaScriptCore较高,使用也不如JavaScriptCore方便本文不做叙述。

WKWebView是iOS8之后推出的,还没有成为主流使用,所以本篇文章也不做详细叙述。

Objective-C执行JavaScript代码

相关方法

// UIWebView的方法

- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

// JavaScriptCore中JSContext的方法

- (JSValue *)evaluateScript:(NSString *)script;

- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL

相关应用

用这些方法去执行大段的

JavaScript

代码是没什么必要的,但是有些小场景用起来还是比较顺手和实用的,列举两个例子作为参考:

// 获取当前页面的title

NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];

// 获取当前页面的url

NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];

JavaScriptCore

iOS7之后苹果推出了JavaScriptCore这个框架,从而让web页面和本地原生应用交互起来非常方便,而且使用此框架可以做到Android那边和iOS相对统一,web前端写一套代码就可以适配客户端的两个平台,从而减少了web前端的工作量。

web前端在三端交互中,web前端要强势一些,一切传值、方法命名都按web前端开发人员来定义,让另外两端去做适配。在这里以调用摄像头和分享为例来详细讲解,测试网页代码取名为test.html,其代码内容如下:

​test.html代码内容

iOS_JS与OC的交互详解

test.html代码解释

可能有些同学对web前端的一些知识不太熟悉,稍微对这段代码做下解释,先说

ToyuniOSAndroid这两边在本地要注入的一个对象【参考下面iOS的代码更容易明白】,充当原生应用和web页面之间的一个桥梁。页面上定义了两个按钮名字分别为

CallCameraShare。点击CallCamera会通过Toyun这个桥梁调用本地应用的方法

- (void)callCamera,没有传参;而点击Share会先调用本文件中的JavaScript

方法callShare这里将要分享的内容格式转成JSON字符串格式(这样做是为了适配

AndroidiOS可以直接接受JSON对象)然后再通过Toyun这个桥梁去调用原生应用的

- (void)share:(NSString *)shareInfo方法这个是有传参的,参数为shareInfo。而下面的两个方法为原生方法调用后的回调方法,其中picCallback为获取图片成功的回调方法,并且传回拿到的图片photosshareCallback为分享成功的回调方法。

iOS

这边根据前端定义的方法名来写代码,但是有些时候web前端会让我们定义,但是我们定义好之后他又要修改,这时候就会很烦啊。所以碰到三端交互的时候最好就是让web前端

去定义方法名,iOSAndroid根据web前端定义好的去写代码。

JavaScriptCoreweb页面调用原生应用的方法可以用DelegateBlock

两种方法,此文以按Delegate讲解。

JavaScriptCore中类及协议:

JSContext:给JavaScript提供运行的上下文环境

JSValue:JavaScriptObjective-C数据和方法的桥梁

JSManagedValue:管理数据和方法的类

JSVirtualMachine:处理线程相关,使用较少

JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议

ViewController中的代码

#import "ViewController.h"

#import 《JavaScriptCore/JavaScriptCore.h》

@protocol JSObjcDelegate 《JSExport》

- (void)callCamera;

- (void)share:(NSString *)shareString;

@end

@interface ViewController ()

@property (nonatomic, strong) JSContext *jsContext;

@property (weak, nonatomic) IBOutlet UIWebView *webView;

@end

@implementation ViewController

#pragma mark - Life Circle

- (void)viewDidLoad {

    [super viewDidLoad];

    NSURL *url = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"html"];

    [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:url]];

}

#pragma mark - UIWebViewDelegate

- (void)webViewDidFinishLoad:(UIWebView *)webView {

    self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    self.jsContext[@"Toyun"] = self;

    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {

        context.exception = exceptionValue;

        NSLog(@"异常信息:%@", exceptionValue);

    };

}

#pragma mark - JSObjcDelegate

- (void)callCamera {

    NSLog(@"callCamera");

    // 获取到照片之后在回调js的方法picCallback把图片传出去

    JSValue *picCallback = self.jsContext[@"picCallback"];

    [picCallback callWithArguments:@[@"photos"]];

}

- (void)share:(NSString *)shareString {

    NSLog(@"share:%@", shareString);

    // 分享成功回调js的方法shareCallback

    JSValue *shareCallback = self.jsContext[@"shareCallback"];

    [shareCallback callWithArguments:nil];

}

@end

ViewController中的代码解释

自定义JSObjcDelegate协议,而且此协议必须遵守JSExport这个协议,自定义协议中的方法就是暴露给web页面的方法。在webView加载完毕的时候获取JavaScript

运行的上下文环境,然后再注入桥梁对象名为Toyun,承载的对象为self即为此控制器,控制器遵守此自定义协议实现协议中对应的方法。在JavaScript调用完本地应用的方法做完相对应的事情之后,又回调了JavaScript中对应的方法,从而实现了web页面本地应用

之间的通讯。

JavaScriptCore使用注意

JavaScript调用本地方法是在子线程中执行的,这里要根据实际情况考虑线程之间的切换,而在回调JavaScript方法的时候最好是在刚开始调用此方法的线程中去执行那段

JavaScript方法的代码,我在实际运用中开始没注意,就被坑惨了啊。什么,说的太绕,看下面的代码解释:

//  假设此方法是在子线程中执行的,线程名sub-thread

- (void)callCamera {     

    // 这句假设要在主线程中执行,线程名main-thread

    NSLog(@"callCamera");  

    // 下面这两句代码最好还是要在子线程sub-thread中执行啊

    JSValue *picCallback = self.jsContext[@"picCallback"];

    [picCallback callWithArguments:@[@"photos"]];

}

运行效果

iOS_JS与OC的交互详解


拦截协议

拦截协议这个适合一些比较简单的一些情况,不需要引入什么框架,只需要web前端配合一下就好。但是在具体调用哪一个方法上,以及在传值的时候可能会有些不方便,而且调用完后无法在回调JavaScript的方法。

web前端test.html中的代码

iOS_JS与OC的交互详解

test.html中的代码解释

这段代码相比上面的那段测试代码是很简单的,同样有一个按钮,名字为CallCamera

点击之后调用自己的callCamera方法,window.location.href这里是改变主窗口的指向从而马上发出一个链接为toyun://callCamera请求,而想要传给原生应用的参数也可已包含到此请求中,而在iOS方法中我们要拦截这个请求,根据请求内容去判断JavaScript

想要做的事情,从而实现web页面本地应用之间的交互。

iOS

iOS对应的代码

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

{

    NSString *url = request.URL.absoluteString;

    if ([url rangeOfString:@"toyun://"].location != NSNotFound) { 

        // url的协议头是toyun

        NSLog(@"callCamera");

        return NO;

    }

    return YES;

}

iOS对应的代码的解释

​在webView的代理方法中去拦截自定义的协议Toyun://如果是此协议则据此判断

JavaScript想要做的事情,调用原生应用的方法,这些都是提前约定好的,同时阻止此链接的跳转。

总结

随着手机硬件的配置越来越强大和HTML5的兴起,一个App完全可以由web页面

来写。现在已经有部分应用这么干了,我是遇见过的,如古诗文网。尽管比较少但是

web页面本地应用的交互不论是iOS还是Android都是会有遇到的。

iOS我还是比较推荐JavaScriptCore,这样三端可以相对统一起来,写的时候都比较简单。随着时间的推移iOS8推出的WKWebView会逐渐成为主流,这个的功能更强大。

拦截协议也只能说用到比较简单的一些情况吧,复杂的情况处理相互之间参数的传递还是比较麻烦的,而且这个不能回调JavaScript的方法,确实喜欢拦截协议的同学可以研究

WebViewJavaScriptBridge这个第三方库。

对于Android本人也就是略知皮毛而已,就不班门弄斧了,对于一些

Android开发者来说,可以看它第一段的test.html这个页面的写法完全是可以适配

Android的。

JavaScript和Objective-C交互的那些事(续)

已经写过交互了,为什么相隔几个月来还要在出一片续集呢?这是因为过去几个月的使用的过程中出现了几个深坑,在这里特别强调一下。深坑主要包括内存管理和什么时候注入交互对象才是合理的。

内存管理

内存泄露问题

在我的第一篇文章里面注入的交互对象为控制器self,这样JSContext环境引用控制器self

,在退出控制器的时候,因为控制器selfJSContext引用而不释放,而JSContext

只有等控制器释放了才能随之释放,所以就引起了循环引用,造成内存泄露。

解决办法

关于这个问题有三种解决办法。

可以使用我参考文章中提到的,注入一个中间的对象去交互,而不是直接使用控制器self

这样可能需要在对象中在加一层代理,或者Block来进行和控制器之间的通信。

注入对象改为注入类[self class],这样倒是可以防止内存泄露,但是所写的代理方法就要改为类方法,全部使用类方法在实际开发中会带来一些不便,也不会太好。

使用Block进行交互替掉JSExport协议

合适时机注入交互对象UIWebView什么时机创建JSContext环境

什么时候

UIWebView

会创建

JSContext环境

,分两种方式,第一在渲染网页时遇到

时,就会创建

JSContext环境

去运行

JavaScript代码

。第二就是使用方法

[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]

去获取

JSContext环境

时,这时无论是否遇到

,都会去创造出来一个

JSContext环境

,而且和遇到

再创造环境是同一个。

我的错误做法

刚开始的时候,我是在

- (void)webViewDidFinishLoad:(UIWebView *)webView

中去注入交互对象,但是这时候网页还没加载完,

JavaScript

那边已经调用交互方法,这样就会调不到原生应用的方法而出现问题。后来我就改成在

- (void)viewDidLoad

中去注入交互对象,这样倒是解决了上面的问题,但是同时又引起了一个新的问题就是在一个网页内部点击链接跳转到另一个网页的时候,第二个页面需要交互,这时

JSContext环境

已经变化,但是

- (void)viewDidLoad

仅仅加载一次,跳转的时候,没有再次注入交互对象,这样就会导致第二个页面没法进行交互。当然你可以在

- (void)viewDidLoad

- (void)webViewDidFinishLoad:(UIWebView *)webView

都注入一次,但是一定会有更优雅的办法去解决此问题。

解决办法

那么交互对象到底该什么时候注入呢?其实网上已有很好的解决办法,就是在每次创建

JSContext环境

的时候,我们都去注入此交互对象这样就解决了上面的问题。具体解决办法参考了此开源库

UIWebView-TS_JavaScriptContext

。关于这个开源库,我说一点在

- (void)webView:(id)unused didCreateJavaScriptContext:(JSContext*)ctx forFrame:(id)frame

此方法中使用到代理方法

parentFrame

可能会被认为是私有

API

而遭拒,在

Issues

中有人提到。此开源库的实现思路可以参考

readme

写的很不错,除了解决这个问题,也可以学习到一些思考问题的思路。有些不太愿意读英文的同学,我这里也有中文版仅供参考。

UIWebView-TS_JavaScriptContext的readme译文

我曾经做过很多的混合iOS应用,但是我不屑于承认。这些应用的一个主要痛点是通过web/native边界(运行在

UIWebView

中的JavaScript与运行在App中的ObjectiveC之间)进行交互。

我们都知道官方只给出一种方法从ObjectiveC调到网页里,是通过

stringByEvaluatingJavaScriptFromString

方法。还有一种调用JavaScript的典型办法是人为设置

window.location

去触发

UIWebView

的代理方法

shouldStartLoadWithRequest:

。另一种常常使用到的技术是实现自定义的

NSURLProtocol

并拦截通过

XMLHttpRequest

发出的请求。

在iOS7中苹果给出了一个公开的框架JavaScriptCore(WebKit的一部分),这个框架提供了简单机制在ObjectiveC和JavaScript的环境中互相调用对象和方法。众所周知,

UIWebView

建立在WebKit之上最终也是使用了JavaScriptCore,不幸的是苹果没有暴露一些途径给我们去访问这套框架。

可以使用KVC简单粗暴的获取这个深植于

UIWebView

内部官方文档却未定义的属性

JSContext

这篇博客介绍了这个技术

。当然,这个方法的主要缺点是他依赖

UIWebView

的内部构造。

我介绍一个可供选择的方法去获取

UIWebView

JSContext

。当然我的方法也不是官方的,可能被拒。我应该不会尝试提交一个这样的应用到AppStore。但它看来至少不那么容易被拒,我认为它并没有特别地依赖于

UIWebView

的内部结构不同于

UIWebView

自己用WebKit和JavaScriptCore。(这有个小警告,一会解释)

基本原理是这样的:WebKit用

WebFrameLoadDelegate

回调与客户端进行通讯就好像UIWebView传达页面加载事件通过他自己的UIWebViewDelegate。

WebFrameLoadDelegate

其中一个方法是

webView:didCreateJavaScriptContext:forFrame:

就像所有事件源,WebKit的代码去检测他的代理是否实现了回调方法,如果实现了就调用此方法。下面是WebKit的部分源码(

WebFrameLoaderClient.mm

)

if (implementations->didCreateJavaScriptContextForFrameFunc) {

    CallFrameLoadDelegate(implementations->didCreateJavaScriptContextForFrameFunc, webView, @selector(webView:didCreateJavaScriptContext:forFrame:), script.javaScriptContext(), m_webFrame.get());

}

证实在iOS,

UIWebView

内,不论任何对象实现WebKit的

WebFrameLoadDelegate

方法,并不是真的实现

webView:didCreateJavaScriptContext:forFrame:

所以WebKit从不会调用此方法。如果此方法存在于代理对象中,它将会被自动调用。

既然如此,在OC中有很多的办法给现有的类和对象动态的增添一个方法。最简单的办法就是通过扩展。我给已有的类

NSObject

添加一个扩展去实现

webView:didCreateJavaScriptContext:forFrame:

方法。

的确,添加这个方法让WebKit开始调用它,因为任何对象(包括UIWebView中的一些sink object)都继承自

NSObject

,现在都实现了

webView:didCreateJavaScriptContext:forFrame:

这个方法。如果未来

UIWebView

内部的sink object实现了这个代理方法,那么这个途径就是失效因为我们自己实现的分类永远不会被调用。

当我们的方法被WebKit调用的时候会传给我们一个WebKit中的WebView(不是

UIWebView

),一个JavaScriptCore的

JSContext

对象和WebKit的WebFrame。因为没有一个公开的WebKit框架的头文件提供给我们,所以WebView和WebFrame对我们来说非常透明。但是JSContext正是我们寻找的,通过JavaScriptCore框架对我们来说完全是适用的。(在实际中,我最终在WebFrame中调用方法,作为一个最佳状态)

问题现在就变成怎样根据JSContext反找到对应的UIWebView。首先我尝试使用WebView对象我们控制和沿着继承的view去找到他拥有的UIWebView.但是后来证明这个对象是一些UIView的代理,并不是一个真正的UIView。并且因为他对我们来说是透明的,我也没有打算使用它。

我的解决方案是迭代所有在app中所创建的UIWebViews(参考代码,我是怎么样做的)并且使用

stringByEvaluatingJavaScriptFromString:

去储存一个token"cookie"在JavaScriptContext中,然后我在JSContext中查找已经存在的这个token,如果他存在这个UIWebView就是我所要找的。

一旦我们有了JSContext我们就可以做一些很有趣的事情。我的测试App展示了我们怎样映射ObjectiveC的blocks和对象到全局命名空间并且通过JavaScript访问和调用它们。

总结

在使用一项技能的时候,一定要挖透,理解其原理,这样在遇到问题的时候才能更从容的应对。这也算是给我的一点点启示和教训吧,更希望大家能引以为戒。关于readme的译文,作者水平有限,欢迎指正。


 

参考:

http://www.bkjia.com/IOSjc/889945.html

http://blog.csdn.net/lwjok2007/article/details/47058101

http://blog.sina.com.cn/s/blog_a5243c7f0102wd3b.html

http://www.jianshu.com/p/f896d73c670a

http://www.jianshu.com/p/939db6215436

JavaScriptCore在实际项目中的使用的坑

UIWebView-TS_JavaScriptContext