iOS程序中网页与OC代码互相调用的原理+一个实现方案的分析

导语

越来越多的iOS程序都内嵌了H5页面,微信也提供了公众平台方便第三方应用开发者以网页JS的形式向用户提供更丰富的界面展示和内容。iOS移动客户端想尽可能多的提供接口让网页能获取App内的信息,与此同时,App也希望能有一种手段让能控制网页JS的行为。这种控制与被控制,调用与被调用,就是这篇文章所关注核心技术点。

核心技术点

稍加分析,我们可以将App内的网页JS与Native Code的互动分解为两大难点。网页JS如何向Native Code传递其想调用的Native Code接口和参数 Native Code收到了网页JS调用后,执行相关的逻辑结束后,如何将执行的结果通知回网页JS

一、网页JS如何向Native Code传递其想调用的Native Code接口和参数
  1. 当我们用 UIWebView 作为App内嵌的浏览器进行网络访问时,每次网页访问一个新的url,都会触发其delegate协议 UIWebViewDelegate 的回调方法
 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

在这个方法内部我们可以通过 request 参数获取网页即将访问的url

[[request URL] absoluteString]

网页可以通过触发访问新的url,将想调用的Native Code接口和参数通过JSON格式化后,放在新的url中,通知回Native Code的 UIWebViewDelegate 的回调方法

  1. 网页中触发新的url有两种常用的方式
  • 网页JS通过修改doucument.loaction
document.location='http://new.url/params?key=value'
  • 新建一个看不见的iframe标签,修改它的属性 src
newIframe = document.createElement('iframe');
newIframe.style.display = 'none';
document.documentElement.appendChild(newIframe);
newIframe.src = 'http://new.url/params?key=value';
document.documentElement.removeChild(newIframe);
  • 这两种方法一个很大的区别在于,前者因为是修改的主frame的location,极有可能破坏整个网页的JS逻辑,并造成网页卡顿等异常现象。后者因为创建了一个新的不可见子frame,逻辑上只影响这个空的iframe,UI上因为不显示也不会造成任何UI干扰和渲染耗时。

二、Native Code如何调用网页JS接口并向其传递参数
  1. 核心方法是 UIWebView 的方法
 - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

参数 script 就是需要执行的JS代码,返回结果为JS执行结果。这里建议如果JS需要返回大量数据,最好将数据放入数组或对象,再转化成JSON字符串返回给Native Code。

  1. 使用方法
  • 如果Native Code想让网页JS返回用户选购的商品ID列表,首先需要准备JS代码。我们把选购的商品ID放入数组,再将数组转化成JSON字符串返回。代码如下
function getOrderList() {
  var orderList = ['10001', '10045', '54321'];
  var orderListString = JSON.stringify(orderList);
  orderList = [];
  return orderListString;
};
  • Native Code只需要执行
NSString *js = @"getOrderList();";
NSString *result = [webView stringByEvaluatingJavaScriptFromString:js];

就可以让网页JS执行相关的代码,并同步的取得返回值

行文至此,我们已经掌握了Native Code和网页JS进行交互的两个核心技术方法。接下来,讲介绍如何运用这些方法来打造一套专属于App的JSSDK。

三、一个完整的JSSDK实现方案

我们依然以一个购物页面作为例子来讲述,如何实现一套JSSDK。

  1. 构建Native Code接口和JSSDK接口
    我们在Native Code层先定义好,交互接口的名称字符串。这个字符串就是第三方应用开发者看见的,由App开发者提供的JSSDK的接口。
NSString *const jssdk_getUserInfo = @"jssdk.getUserInfo";
NSString *const jssdk_payMyBill = @"jssdk.payMyBill";
NSString *const jssdk_showLogistics = @"jssdk.showLogistics";
  1. JS构建一次合法的调用Native Code
    上面已经介绍了如何从JS层将信息传递给Native Code,那么哪些信息是网页需要传递给App的呢?SDK的提供者需要提供: 接口名称 接口参数 callback回调ID 。其中 callback回调ID 是隐式调用的,第三方应用开发者并不需要关心。它是用于SDK内部正确的回调到调用者指定方法的一个索引。
var callback_index = 1;
var callback_map = {};
function callNativeCode(func, params, callback) {
        if (!func || typeof func !== 'string') return;
        if (typeof params !== 'object') params = {};

        var callbackID = (callback_index++).toString();

        if (typeof callback === 'function')
          callback_map[callbackID] = callback;

        var paramsObj = {'func':func,'params':params,'callbackID':callbackID};
        var paramsForNative = JSON.stringify(paramsObj);
        callNativeCode(paramsForNative);
}
callNativeCode('jssdk.getUserInfo',{'code':'0fec3575758a06c203868edcca748565'},function(openid, name, sex){
// to something to process return value
});

上面代码中的callNativeCode实际上进行了两个工作。将接口名称、参数、回调callbackID对象经过JSON处理的字符串保存起来。以及通过通过改变iframe属性的方式,通知Native主动来取数据。Native Code在回调中就可以在回调中收到JS传递过来的新url。

var callNativeCodeQueue = [];
function callNativeCode(message) {
     callNativeCodeQueue.push(message);

     messagingIframe = document.createElement('iframe');
     messagingIframe.style.display = 'none';
     messagingIframe.src = 'http://new.url/params?key=value';
     document.documentElement.appendChild(messagingIframe);
     document.documentElement.removeChild(messagingIframe);
};
  1. Native Code主动获取JS想调用的接口列表
 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
...
    NSRange jssdkSchemaRange = [urlString rangeOfString:@"'http://new.url/params?key=value"];
    if (jssdkSchemaRange.location == 0) {
        NSString *jssdkInputQueueJSON = [webView stringByEvaluatingJavaScriptFromString:@"fetchJSSDKInputQueueJSON();"];
        [self unpackJSSDKInputQueueJSON:jssdkInputQueueJSON];

        return NO;
    }
...
}

 - (void)unpackJSSDKInputQueueJSON:(NSString *)jssdkInputQueueJSON {
    NSArray *inputQueue = [jssdkInputQueueJSON JSONArray];
    if (nil == inputQueue) return;
    for (int i = 0; i < inputQueue.count; i++) {
            NSString *oneJSCall = [inputQueue stringAtIndex:i];
            NSDictionary  *jsCallParams = [oneJSCall JSONDictionary];
            if (nil == jsCallParams) return;

            NSString *funcName = [jsCallParams stringForKey:@"func"];
            NSDictionary *params = [jsCallParams dictionaryForKey:@"params"];
            NSString *callbackID = [dic stringForKey:@"callbackID"];
            if ((funcName.length == 0) || (params == nil)) return;

            [self jsInvokeNativeCode:funcName withParams:params withCallbackID:callbackID];
}

 - (void)functionCall:(NSString *)funcName withParams:(NSDictionary *)params withCallbackID:(NSString *)callbackID {
    if ("jssdk.getUserInfo" == funcName) return [self getUserInfo:params withCallbackID:callbackID];
    if ("jssdk.payMyBill" == funcName) return [self payMyBill:params withCallbackID:callbackID];
    if ("jssdk.showLogistics" == funcName) return [self showLogistics:params withCallbackID:callbackID];
 }

似乎JS可以把想传递的内容放在url中传递给Native Code,但这里强烈不推荐这样实现。上面OC回调方法中的 request 中的长度是有限制的。一旦数据超常依然只能从Native Code层发起调用,主动获取数据,如上面的代码所示。document.location还有一个很严重的问题,就是异步带来的请求被忽略。如果我们连续 2 个 js 调 Native Code,连续 2 次改 document.location 的话,在 Native Code 的 delegate 方法中,只能截获后面那次请求,前一次请求由于很快被替换掉,所以被忽略掉了。

  1. Native Code将处理结果返回JS层
    和第二步中取JS调用数据的方式一样。Native Code把返回数据构造成NSDictionary并用JSON格式化后传递给JS层。
 -(NSString*) callbackToJS:(NSString*)callbackID withResult:(NSDictionary*)nativeResult {
    NSMutableDictionary *toJSResult = [NSMutableDictionary dictionary];
    [toJSResult setObject:nativeResult ? nativeResult:[NSDictionary dictionary] forKey:@"params"];
    [toJSResult setObject:callBackID forKey:@"callBackID"];
    NSString *toJSResultJSON = [toJSResult JSONRepresentation]; //将NSDictionary格式化成NSString字符串
    NSString *jsFinalResult = [_webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"processFinalResultFromNativeCode(%@);", toJSResultJSON]];

    return jsFinalResult;
 }
  1. JS拿到Native Code执行结果,并将其返回给初始调用时注册的callback函数
function processFinalResultFromNativeCode(jsFinalResult) {
      var msgWrap = jsFinalResult;
      var ret = callback_map [jsFinalResult['callbackID']](jsFinalResult['params']);
      delete callback_map [jsFinalResult['callbackID']];
      return JSON.stringify(ret);
};
  1. 将JSSDK框架注入第三方网页
    从整体流程来讲,JSSDK框架的注入应该是第一步,但却被放到了文章的末尾。如果理解了Native Code和JS如何互相调用交互,框架的注入那就是水到渠成的事情。将上面讲到的JS逻辑封装到一个自完成自调用的JS函数里面,通过Native Code将处理结果返回JS层一模一样的方式,就可以给WEB注入我们的JSSDK框架了。奉上文章最后一段代码来解释,JSSDK是如何开始其生命。
NSString *jssdkPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"jssdk.js"];
NSString *jssdkContent = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
[_webView stringByEvaluatingJavaScriptFromString:jssdkContent];

到这里一个简单但完整的JSSDK的demo就给各位看官呈现完毕。实际实现中会加入数据合法性、消息派发机制来辅助整个框架的更高效和安全的实现。这篇文章囿于篇幅限制只介绍了最关键的一些部分,像JSON格式化、HTML调用JSSDK接口、UIWebView等都没有涉及。有兴趣的读者可以阅读一些相关文章和代码,从整体上补全JSSDK的全流程。

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

推荐阅读更多精彩内容

  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 25,266评论 7 249
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,652评论 0 15
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 在Android中,JSBridge已经不是什么新鲜的事物了,各家的实现方式也略有差异。大多数人都知道WebVie...
    Jannonx阅读 1,306评论 0 5
  • 个人情况:睫毛短塌、不算硬 目前使用产品:贝印睫毛夹、日上睫毛膏(自然型) 主要问题:刷出来的睫毛不翘,起不了大眼...
    林婷是阅读 164评论 0 0