iOS 原生和H5交互《DSBridge》原理

hogwarts.png

iOS和H5交互离不开原生层面的支持

  1. WKWebView 执行一段js代码,可以通过:
webView.evaluateJavaScript("console.log('hello word!')")
  1. JS可以通过prompt传递参数到Native ,WKUIDelegate中的prompt方法会拦截到
var rsult = prompt(method,args)

WKUIDelegate中的prompt:

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

以上就是iOS Native和H5交互的基础。

DSBridge是跨平台JavaScript Bridge,通过它,您可以在JavaScript和Native之间同步或异步调用彼此的函数。

集成方式:

pod "dsBridge"

初始化:

  1. H5需要引入dsbridge.js
<script src="https://cdn.jsdelivr.net/npm/dsbridge/dist/dsbridge.js"> </script>
  1. iOS需要:
  • 创建DWKWebView对象,或者继承DWKWebView
  • 注册方法:可以扩展InternalApis类型,或者自己实现,然后通过addJavascriptObject:注册相应的方法
  InternalApis *  interalApis= [[InternalApis alloc] init];
  interalApis.webview=self;
 [self addJavascriptObject:interalApis namespace:@"_dsb"];

原生调用js方法

  1. 需要js先注册对应的方法,比如:addValue
<script>
 dsBridge.register('addValue', function (r, l) {
        return r + l;
    })
</script>

register方法做了如下事情:

  • 通过_dsb.dsinit初始化 dsbridge
  • 待注册方法保存到_dsaf或_dsf,object 保存到 _dsaf._obs或_dsf._obs
    window属性下有如下数据结构:
    /*
   // 存储 同步方法,对象
   _dsf: { _obs: {} },
   // 存储 异步方法,对象
   _dsaf: { _obs: {} },
  */
  //js注册方法
  register: function (b, a, c) {
    c = c ? window._dsaf : window._dsf;
    if (window._dsInit) {
      window._dsInit = true;
      setTimeout(function () {
        bridge.call("_dsb.dsinit");
      }, 0);
    }
    // 方法直接保存到  _dsaf或_dsf
    // object 保存到 _dsaf._obs或_dsf._obs
    if ("object" == typeof a) {
      c._obs[b] = a;
    } else {
      c[b] = a;
    }
  }
  1. 原生通过callHandler:调用js方法,最终会在js中执行 _handleMessageFromNative
  • Native会创建一个唯一的callbackId和handle对应
  • 保存handle到全局,传递callbackId到js
[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber* value){
        NSLog(@"%@",value);
}];
...
// evaluateJavaScript 执行 js的 _handleMessageFromNative(p),参数p:包含{method,callbackId,data}
- (void) dispatchJavascriptCall:(DSCallInfo*) info{
    NSString * json=[JSBUtil objToJsonString:@{@"method":info.method,@"callbackId":info.id,
                                               @"data":[JSBUtil objToJsonString: info.args]}];
    [self evaluateJavaScript:[NSString stringWithFormat:@"window._handleMessageFromNative(%@)",json]
           completionHandler:nil];
}

_handleMessageFromNative()的实现如下:

  • json序列化,取出参数和callbackId
  • 定义同步/异步方法
  • 执行对应的方法,然后通过_dsb.returnValue将callbackId回传给Native
        // 原生调用js会来到这 {method,callbackId,data}
        _handleMessageFromNative: function (a) {
          var e = JSON.parse(a.data);
          var b = { id: a.callbackId, complete: !0 };
          var c = this._dsf[a.method];
          var d = this._dsaf[a.method];
          // 同步js方法
          var h = function (a, c) {
            b.data = a.apply(c, e);
            // js 执行完成处理回调,原生returnValue会被执行
            bridge.call("_dsb.returnValue", b);
          };
          // 异步js方法
          var k = function (a, c) {
            // 异步保存 block
            e.push(function (a, c) {
              b.data = a;
              b.complete = !1 !== c;
              bridge.call("_dsb.returnValue", b);
            });
            //  js 执行
            a.apply(c, e);
          };

          if (c) {
            // 同步
            h(c, this._dsf);
          } else if (d) {
            // 异步
            k(d, this._dsaf);
          } else {
            c = a.method.split(".");
            // c.length >= 2 , 命名空间对象
            if (!(2 > c.length)) {
              a = c.pop();
              var c = c.join("."),
                d = this._dsf._obs, // 同步
                d = d[c] || {},
                f = d[a];
            }
            if (f && "function" == typeof f) {
              h(f, d);
            } else {
              var d = this._dsaf._obs; // 异步
              d = d[c] || {};
              f = d[a];
              if (f && "function" == typeof f) {
                k(f, d);
              }
            }
          }
        }

最后,js处理完,通过原生returnValue回传参数(包含最初的callbackId),根据 callbackId 取出对应的 handle并执行;
整个native->H5过程结束

- (id) returnValue:(NSDictionary *) args{
    // 通过 callbackId 取出对应的 block 完成整个过程
    void (^ completionHandler)(NSString *  _Nullable)= handerMap[args[@"id"]];
    if(completionHandler){
        if(isDebug){
            completionHandler(args[@"data"]);
        }else{
            @try{
                completionHandler(args[@"data"]);
            }@catch (NSException *e){
                NSLog(@"%@",e);
            }
        }
        if([args[@"complete"] boolValue]){
            [handerMap removeObjectForKey:args[@"id"]];
        }
    }
    return nil;
}

js调用原生方法

  1. js调用原生方法 ,需要等原生先注册好对应的方法,注册方式写法如下:
    可以创建自己的javascriptObject,namespace,也可通过扩展InternalApis添加交互方法。
    InternalApis *  interalApis= [[InternalApis alloc] init];
    interalApis.webview=self;
    [self addJavascriptObject:interalApis namespace:@"_dsb"];
  1. js通过 prompt调用原生方法:
    dsBridge.call()调用原生,方法内部会将callback保存到window下,比将callback id传给原生
dsBridge.call("testAsyn","hello", function (v) {
            alert(v)
        })

call方法的定义如下:

  • 处理无参数,有callback的情况
  • callback:id 保存到window下,id传给Native
  • 通过prompt函数传递方法名和参数
call: function (b, a, c) {
    var e = "";
    // 处理无参数,有callback的情况
    if ("function" == typeof a) {
      c = a;
      a = {};
    }
    a = { data: void 0 === a ? null : a };
    // callback,保存到window下
    if ("function" == typeof c) {
    // callback id
      var g = "dscb" + window.dscb++;
      window[g] = c;
      a._dscbstub = g;
    }
    a = JSON.stringify(a);
    if (window._dsbridge) {
      e = _dsbridge.call(b, a);
    } else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) {
      // iOS WKUIDelegate中的prompt方法会被调用
      e = prompt("_dsbridge=" + b, a);
    }
    return JSON.parse(e || "{}").data;
  }

prompt会触发WKUIDelegate中的runJavaScriptTextInputPanelWithPrompt方法:(方法很长,只看关键部分)

  • 获得方法名prompt,参数defaultText
  • call:method:
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
    defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame
completionHandler:(void (^)(NSString * _Nullable result))completionHandler
{
    NSString * prefix=@"_dsbridge=";
    if ([prompt hasPrefix:prefix])
    {
        //返回一个新字符串,其中包含从给定索引处的字符到结尾的接收器字符。
        NSString *method= [prompt substringFromIndex:[prefix length]];
        NSString *result=nil;
        if(isDebug){
            result =[self call:method :defaultText ];
        }else{
            @try {
                result =[self call:method :defaultText ];
            }@catch(NSException *exception){
                NSLog(@"%@", exception);
            }
        }
        completionHandler(result);
    }
    // ......
}

此处有一个关键的方法,call:method:

  • 截取method中的前缀namespace
  • 根据namespace取出对应的javascriptObject;通过runtime遍历出javascriptObject 类中的所有方法。
  • 匹配method,封装返回参数(包括id) 并执行
  • navite处理完成,通过evalJavascript执行对应的js回掉
    整个JS->Navite调用过程完毕
-(NSString *)call:(NSString*) method :(NSString*) argStr
{
    NSArray *nameStr=[JSBUtil parseNamespace:[method stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];

    // 根据命名空间取 interfaceObject
    id JavascriptInterfaceObject=javaScriptNamespaceInterfaces[nameStr[0]];
    NSString *error=[NSString stringWithFormat:@"Error! \n Method %@ is not invoked, since there is not a implementation for it",method];
    // 结果返回值
    NSMutableDictionary*result =[NSMutableDictionary dictionaryWithDictionary:@{@"code":@-1,@"data":@""}];
    if(!JavascriptInterfaceObject){
        NSLog(@"Js bridge  called, but can't find a corresponded JavascriptObject , please check your code!");
    }else{
        
        method=nameStr[1];
        // methodOne xxx:
        NSString *methodOne = [JSBUtil methodByNameArg:1 selName:method class:[JavascriptInterfaceObject class]];
        // methodTwo xxx:handle:
        NSString *methodTwo = [JSBUtil methodByNameArg:2 selName:method class:[JavascriptInterfaceObject class]];
        SEL sel=NSSelectorFromString(methodOne);
        SEL selasyn=NSSelectorFromString(methodTwo);
        // 解析参数 argStr
        NSDictionary * args=[JSBUtil jsonStringToObject:argStr];
        id arg=args[@"data"];
        if(arg==[NSNull null]){
            arg=nil;
        }
        NSString * cb;
        do{
            if(args && (cb= args[@"_dscbstub"])){
                if([JavascriptInterfaceObject respondsToSelector:selasyn]){
                    __weak typeof(self) weakSelf = self;
                    // 对于异步方法,创建completionHandler
                    void (^completionHandler)(id,BOOL) = ^(id value,BOOL complete){
                        NSString *del=@"";
                        result[@"code"]=@0;
                        if(value!=nil){
                            result[@"data"]=value;
                        }
                        value=[JSBUtil objToJsonString:result];
                        value=[value stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
                        
                        if(complete){
                            del=[@"delete window." stringByAppendingString:cb];
                        }
                        /** 
                         cb(JSON.parse(decodeURIComponent("value")).data);
                         delete window.cb
                         */
                        // 通过window下的cb,回调数据
                        NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
                        __strong typeof(self) strongSelf = weakSelf;
                        @synchronized(self)
                        {
                            UInt64  t=[[NSDate date] timeIntervalSince1970]*1000;
                            (*self).jsCache=[(*self).jsCache stringByAppendingString:js];
                            if(t-(*self).lastCallTime<50){
                                if(!(*self).isPending){
                                    [strongSelf evalJavascript:50];
                                    (*self).isPending=true;
                                }
                            }else{
                                [strongSelf evalJavascript:0];
                            }
                        }
                        
                    };
                    void(*action)(id,SEL,id,id) = (void(*)(id,SEL,id,id))objc_msgSend;
                    action(JavascriptInterfaceObject,selasyn,arg,completionHandler);
                    break;
                }
            }
            
            // 无handle,同步
            else if([JavascriptInterfaceObject respondsToSelector:sel]){
                id ret;
                id(*action)(id,SEL,id) = (id(*)(id,SEL,id))objc_msgSend;
                ret=action(JavascriptInterfaceObject,sel,arg);
                [result setValue:@0 forKey:@"code"];
                if(ret!=nil){
                    [result setValue:ret forKey:@"data"];
                }
                break;
            }
            NSString*js=[error stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
            if(isDebug){
                js=[NSString stringWithFormat:@"window.alert(decodeURIComponent(\"%@\"));",js];
                [self evaluateJavaScript :js completionHandler:nil];
            }
            NSLog(@"%@",error);
        }while (0);
    }
    return [JSBUtil objToJsonString:result];
}

源码地址:DSBridge
最后附上包含详细注释的 js源码

var bridge = {
  default: this,
  // js调用原生(method,参数,callback)
  call: function (b, a, c) {
    var e = "";
    // 处理无参数,有callback的情况
    if ("function" == typeof a) {
      c = a;
      a = {};
    }
    a = { data: void 0 === a ? null : a };
    // callback,保存到window下
    if ("function" == typeof c) {
      var g = "dscb" + window.dscb++;
      window[g] = c;
      a._dscbstub = g;
    }
    a = JSON.stringify(a);
    if (window._dsbridge) {
      e = _dsbridge.call(b, a);
    } else if (window._dswk || -1 != navigator.userAgent.indexOf("_dsbridge")) {
      // iOS WKUIDelegate中的prompt方法会被调用
      e = prompt("_dsbridge=" + b, a);
    }
    return JSON.parse(e || "{}").data;
  },
  //js注册方法
  register: function (b, a, c) {
    c = c ? window._dsaf : window._dsf;
    if (window._dsInit) {
      window._dsInit = true;
      setTimeout(function () {
        bridge.call("_dsb.dsinit");
      }, 0);
    }
    // 方法直接保存到  _dsaf或_dsf
    // object 保存到 _dsaf._obs或_dsf._obs
    if ("object" == typeof a) {
      c._obs[b] = a;
    } else {
      c[b] = a;
    }
  },

  registerAsyn: function (b, a) {
    this.register(b, a, true);
  },
  hasNativeMethod: function (b, a) {
    // 原生中提前写好的 hasNativeMethod
    return this.call("_dsb.hasNativeMethod", { name: b, type: a || "all" });
  },
  disableJavascriptDialogBlock: function (b) {
    // 原生中提前写好的,禁用Javascript对话框
    this.call("_dsb.disableJavascriptDialogBlock", { disable: !1 !== b });
  },
};
// 立即执行函数
!(function () {
  if (!window._dsf) {
    var b = {
        // 存储 同步方法,对象
        _dsf: { _obs: {} },
        // 存储 异步方法,对象
        _dsaf: { _obs: {} },
        dscb: 0,
        dsBridge: bridge,
        close: function () {
          bridge.call("_dsb.closePage");
        },
        // 原生调用js会来到这 {method,callbackId,data}
        _handleMessageFromNative: function (a) {
          var e = JSON.parse(a.data);
          var b = { id: a.callbackId, complete: !0 };
          var c = this._dsf[a.method];
          var d = this._dsaf[a.method];
          // 同步js方法
          var h = function (a, c) {
            b.data = a.apply(c, e);
            // js 执行完成处理回调,原生returnValue会被执行
            bridge.call("_dsb.returnValue", b);
          };
          // 异步js方法
          var k = function (a, c) {
            // 异步保存 block
            e.push(function (a, c) {
              b.data = a;
              b.complete = !1 !== c;
              bridge.call("_dsb.returnValue", b);
            });
            //  js 执行
            a.apply(c, e);
          };

          if (c) {
            // 同步
            h(c, this._dsf);
          } else if (d) {
            // 异步
            k(d, this._dsaf);
          } else {
            c = a.method.split(".");
            // c.length >= 2 , 命名空间对象
            if (!(2 > c.length)) {
              a = c.pop();
              var c = c.join("."),
                d = this._dsf._obs, // 同步
                d = d[c] || {},
                f = d[a];
            }
            if (f && "function" == typeof f) {
              h(f, d);
            } else {
              var d = this._dsaf._obs; // 异步
              d = d[c] || {};
              f = d[a];
              if (f && "function" == typeof f) {
                k(f, d);
              }
            }
          }
        },
      },
      a;
    //将b的所有成员赋值给window
    for (a in b) window[a] = b[a];
    // 注册  _hasJavascriptMethod
    bridge.register("_hasJavascriptMethod", function (a, b) {
      b = a.split(".");
      if (2 > b.length) return !(!_dsf[b] && !_dsaf[b]);
      a = b.pop();
      b = b.join(".");
      return (b = _dsf._obs[b] || _dsaf._obs[b]) && !!b[a];
    });
  }
})();

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

推荐阅读更多精彩内容