当前位置: 代码网 > it编程>App开发>苹果IOS > iOS开发WebViewJavascriptBridge通讯原理解析

iOS开发WebViewJavascriptBridge通讯原理解析

2024年05月18日 苹果IOS 我要评论
前言h5页面具有跨平台、开发容易、上线不需要跟随app的版本等优点,但h5页面也有体验不如native好、没有native稳定等问题。所以目前大部分app都是使用hybrid混合开发的。当然有了h5页

前言

h5页面具有跨平台、开发容易、上线不需要跟随app的版本等优点,但h5页面也有体验不如native好、没有native稳定等问题。所以目前大部分app都是使用hybrid混合开发的。

当然有了h5页面就少不了h5与native交互,交互就会用到bridge的能力了。

webviewjavascriptbridge是一个native与js进行消息互通的第三方库,本章会简单解析一下webviewjavascriptbridge的源码和实现原理。

通讯原理

javascriptcore

javascriptcore作为ios的js引擎为原生编程语言oc、swift 提供调用 js 程序的动态能力,还能为 js 提供原生能力来弥补前端所缺能力。 ios中与js通讯使用的是javascriptcore库,正是因为javascriptcore这种起到的桥梁作用,所以也出现了很多使用javascriptcore开发app的框架,比如rn、weex、小程序、webview hybrid等框架。 如图:

当然js引擎不光有苹果的javascriptcore,谷歌有v8引擎、mozilla有spidermoney

javascriptcore本章只简单介绍,后面主要解析webviewjavascriptbridge。因为uiwebview已经不再使用了,所以后面提到的webview都是wkwebview,demo也是以wkwebview进行解析。

源码解析

代码结构

除了引擎层外,还需要native、h5和webviewjavascriptbridge三层才能完成一整个信息通路。webviewjavascriptbridge就是中间那个负责通信的sdk。

webviewjavascriptbridge的核心类主要包含几个:

  • webviewjavascriptbridge_js:是一个js的字符串,作用是js环境的bridge初始化和处理。负责接收native发给js的消息,并且把js环境的消息发送给native。
  • wkwebviewjavascriptbridge/webviewjavascriptbridge:主要负责wkwebview和uiwebview相关环境的处理,并且把native环境的消息发送给js环境。
  • webviewjavascriptbridgebase:主要实现了native环境的bridge初始化和处理。

初始化

webviewjavascriptbridge是如何完成初始化的呢,首先要有webview容器,所以要对webview容器进行初始化,设置代理,初始化webviewjavascriptbridge对象,加载url。

    wkwebview* webview = [[nsclassfromstring(@"wkwebview") alloc] initwithframe:self.view.bounds];
    webview.navigationdelegate = self;
    [self.view addsubview:webview];
    // 开启打印
    [webviewjavascriptbridge enablelogging];
    // 创建bridge对象
    _bridge = [webviewjavascriptbridge bridgeforwebview:webview];
    // 设置代理
    [_bridge setwebviewdelegate:self];

这里加载的就是jsbridgedemoapp这个本地的html文件。

    nsstring* htmlpath = [[nsbundle mainbundle] pathforresource:@"jsbridgedemoapp" oftype:@"html"];
    nsstring* apphtml = [nsstring stringwithcontentsoffile:htmlpath encoding:nsutf8stringencoding error:nil];
    nsurl *baseurl = [nsurl fileurlwithpath:htmlpath];
    [webview loadhtmlstring:apphtml baseurl:baseurl];

再看一下jsbridgedemoapp这个html文件。

function setupwebviewjavascriptbridge(callback) {
// 第一次调用这个方法的时候,为false
    if (window.webviewjavascriptbridge) { return callback(webviewjavascriptbridge); }
// 第一次调用的时候,为false
    if (window.wvjbcallbacks) { return window.wvjbcallbacks.push(callback); }
// 把callback对象赋值给对象
    window.wvjbcallbacks = [callback];
// 加载webviewjavascriptbridge_js中的代码
// 相当于实现了一个到https://__bridge_loaded__的跳转
    var wvjbiframe = document.createelement('iframe');
    wvjbiframe.style.display = 'none';
    wvjbiframe.src = 'https://__bridge_loaded__';
    document.documentelement.appendchild(wvjbiframe);
    settimeout(function() { document.documentelement.removechild(wvjbiframe) }, 0)
    }
// 驱动所有hander的初始化
setupwebviewjavascriptbridge(function(bridge) {
...
}

在jsbridgedemoapp的script标签下,声明了一个名为setupwebviewjavascriptbridge的方法,在加载html后直接进行了调用。 setupwebviewjavascriptbridge方法中最核心的代码是:

创建一个iframe标签,然后加载了链接为 https://bridge_loaded 的内容。相当于在当前页面内容实现了一个到 https://bridge_loaded 的内部跳转。 ps:iframe标签用于在网页内显示网页,也使用iframe作为链接的目标。

html文件内部实现了这个跳转后native端是如何监听的呢,在webview的代理里有一个方法:decidepolicyfornavigationaction 这个代理方法的作用是只要有webview跳转,就会调用到这个方法。代码如下:

// 只要webview有跳转,就会调用webview的这个代理方法
- (void)webview:(wkwebview *)webview decidepolicyfornavigationaction:(wknavigationaction *)navigationaction decisionhandler:(void (^)(wknavigationactionpolicy))decisionhandler {
    if (webview != _webview) { return; }
    nsurl *url = navigationaction.request.url;
    __strong typeof(_webviewdelegate) strongdelegate = _webviewdelegate;
    // 如果是webviewjavascriptbridge发送或者接收消息,则特殊处理。否则按照正常流程处理
    if ([_base iswebviewjavascriptbridgeurl:url]) {
        if ([_base isbridgeloadedurl:url]) {
            // 是否是 https://__bridge_loaded__ 这种初始化加载消息
            [_base injectjavascriptfile];
        } else if ([_base isqueuemessageurl:url]) {
            // https://__wvjb_queue_message__
            // 处理web发过来的消息
            [self wkflushmessagequeue];
        } else {
            [_base logunkownmessage:url];
        }
        decisionhandler(wknavigationactionpolicycancel);
        return;
    }
    // webview的正常代理执行流程
...

从上面的代码中可以看到,如果监听的webview跳转不是webviewjavascriptbridge发送或者接收消息就正常执行流程,如果是webviewjavascriptbridge发送或者接收消息则对此拦截不跳转,并且针对消息进行处理。 当消息url是https://bridge_loaded 的时候,会去注入webviewjavascriptbridge_js到js中:

// 将webviewjavascriptbrige_js中的方法注入到webview中并且执行
- (void)injectjavascriptfile {
    nsstring *js = webviewjavascriptbridge_js();
    // 把javascript代码注入webview中执行
    [self _evaluatejavascript:js];
    // javascript环境初始化完成以后,如果有startupmessagequeue消息,则立即发送消息
    if (self.startupmessagequeue) {
        nsarray* queue = self.startupmessagequeue;
        self.startupmessagequeue = nil;
        for (id queuedmessage in queue) {
            [self _dispatchmessage:queuedmessage];
        }
    }
}

[self _evaluatejavascript:js];就是执行webview中的evaluatejavascript:方法。把js写入webview。所以执行完此处代码js当中就有bridge这个对象了。初始化完成。

总结:在加载h5页面后会调用setupwebviewjavascriptbridge方法,该方法内创建了一个iframe加载内容为 https://bridge_loaded ,该消息被decidepolicyfornavigationaction监听到,然后执行injectjavascriptfile去读取webviewjavascriptbridge_js将webviewjavascriptbridge对象注入到当前h5中。

webviewjavascriptbridge 对象

整个webviewjavascriptbridge_js文件其实就是一个字符串形式的js代码,里面包含webviewjavascriptbridge和相关bridge调用的方法。

// 初始化bridge对象,oc可以通过webviewjavascriptbridge来调用js里面的各种方法
window.webviewjavascriptbridge = {
    registerhandler: registerhandler, // js中注册方法
    callhandler: callhandler, // js中调用oc的方法
    disablejavscriptalertboxsafetytimeout: disablejavscriptalertboxsafetytimeout,
    _fetchqueue: _fetchqueue, // 把消息转换成json串
    _handlemessagefromobjc: _handlemessagefromobjc // oc调用js的入口方法
};

webviewjavascriptbridge对象里核心的方法有:

  • registerhandler:js中注册方法
  • callhandler: js中调用native的方法
  • _fetchqueue: 把消息转换成json字符串
  • _handlemessagefromobjc:native调用js的入口方法

当初始化完成后,webviewjavascriptbridge对象和对象里的方法就已经存在并且可用了。

js和native是如何相互传递消息的呢?从上面的代码中可以看到如果js想要发送消息给native就会调用callhandler方法;如果native想要调用js方法那js侧就必须先注册一个registerhandler方法。

相对应的我们看一下native侧是如何与js传递消息的,其实接口标准是一致的,native调js的方法使用callhandler方法:

id data = @{ @"datafromoc": @"aaaa!" };
    [_bridge callhandler:@"octojshandler" data:data responsecallback:^(id response) {
        nslog(@"js回调的数据是:%@", response);
    }];

js调native方法在native侧就必须先注册一个registerhandler方法:

    // 注册事件(h5调app)
    [_bridge registerhandler:@"jstooccallback" handler:^(id data, wvjbresponsecallback responsecallback) {
        nslog(@"jstooccallback called: %@", data);
        responsecallback(@"response from jstooccallback");
    }];

也就是说native像js发送消息的话,js侧要先注册该方法registerhandler,native侧调用callhandler; js像native发送消息的话,native侧要先注册registerhandler,js侧调用callhandler。这样才能完成双端通信。

如图:

native向js发送消息

现在要从native侧向js侧发送一条消息,方法名为:"octojshandler",并且拿到js的回调,具体实现细节如下:

js侧

native向js发送数据,首先要在js侧去注册这个方法:

bridge.registerhandler('octojshandler', function(data, responsecallback) {
    ...
})

这个registerhandler的实现在webviewjavascriptbridge_js是:

// web端注册一个消息方法,将注册的方法存储起来
function registerhandler(handlername, handler) {
    messagehandlers[handlername] = handler;
}

就是将这个注册的方法存储到messagehandlers这个map中,key为方法名称,value为function(data, responsecallback) {}这个方法。

native侧

native侧调用bridge的callhandler方法,传参为data和一个callback回调

id data = @{ @"datafromoc": @"aaaa!" };
[_bridge callhandler:@"octojshandler" data:data responsecallback:^(id response) {
    nslog(@"js回调的数据是:%@", response);
}];

接下来会走到webviewjavascriptbridgebase的-senddata: responsecallback: handlername:方法,该方法中将"data"和"handlername"存入到一个message字典中,如果存在callback会生成一个callbackid一并存入到message字典中,并且将该回调存入到responsecallbacks中,key为callbackid,value为这个callback。代码如下:

// 所有信息存入字典
nsmutabledictionary* message = [nsmutabledictionary dictionary];
if (data) {
    message[@"data"] = data;
}
if (responsecallback) {
    nsstring* callbackid = [nsstring stringwithformat:@"objc_cb_%ld", ++_uniqueid];
    self.responsecallbacks[callbackid] = [responsecallback copy];
    message[@"callbackid"] = callbackid;
}
if (handlername) {
    message[@"handlername"] = handlername;
}
[self _queuemessage:message];

将message存储到队列等待执行,执行该条message时会先将message进行序列化,序列化完成后将message拼接到字符串webviewjavascriptbridge._handlemessagefromobjc('%@');中,然后执行_evaluatejavascript执行该js方法。

// 把oc消息序列化、并且转化为js环境的格式,然后在主线程中调用_evaluatejavascript
- (void)_dispatchmessage:(wvjbmessage*)message {
    nsstring *messagejson = [self _serializemessage:message pretty:no];
    nsstring* javascriptcommand = [nsstring stringwithformat:@"webviewjavascriptbridge._handlemessagefromobjc('%@');", messagejson];
    [self _evaluatejavascript:javascriptcommand];
}

_handlemessagefromobjc方法会将messagejson传递给_dispatchmessagefromobjc进行处理。 首先将messagejson进行解析,根据handlername取出存储在messagehandlers中的方法。如果该message中存在callbackid,将callbackid作为参数生成一个回调放到responsecallback中。 代码如下:

function _dodispatchmessagefromobjc() {
// 解析发送过来的json
    var message = json.parse(messagejson);
    var messagehandler;
    var responsecallback;
    // 主动调用
    // 如果有callbackid
    if (message.callbackid) {
    // 将callbackid当做callbackresponseid再返回回去
        var callbackresponseid = message.callbackid;
        responsecallback = function(responsedata) {
        // 把消息从js发送到oc,执行具体的发送操作
            _dosend({ handlername:message.handlername, responseid:callbackresponseid, responsedata:responsedata });
            };
        // 获取js注册的函数,取出消息里的handlername
        var handler = messagehandlers[message.handlername];
        // 调用js中的对应函数处理
        handler(message.data, responsecallback);
            }
    }

handler方法其实就是名为"octojshandler"的方法,这时就走到了registerhandler里的那个function(data, responsecallback) {}方法了。我们看一下方法内部的具体实现:

bridge.registerhandler('octojshandler', function(data, responsecallback) {
    // oc中传过来的数据
    log('从oc传过来的数据是:', data)
    // js返回数据
    var responsedata = { 'datafromjs':'bbbb!' }
    responsecallback(responsedata)
})

data就是从native传过来的数据,responsecallback就是保存的回调,然后又生成了新数据作为参数给到了这个回调。

responsecallback的实现是:

responsecallback = function(responsedata) {
    // 把消息从js发送到oc,执行具体的发送操作
    _dosend({ handlername:message.handlername, responseid:callbackresponseid, responsedata:responsedata });
};

将该方法的handlername、生成的callbackresponseid(也就是callbackid)以及js返回的数据一起给到_dosend方法。

_dosend方法将message存储到sendmessagequeue消息列表中,并使用messagingiframe加载了一次https://wvjb_queue_message

// 把消息从js发送到oc,执行具体的发送操作
    function _dosend(message, responsecallback) {
    // 把消息放入消息列表
    sendmessagequeue.push(message);
    // 发出js对oc的调用,让webview执行跳转操作,可以在decidepolicyfornavigationaction:中拦截到js发给oc的消息
    messagingiframe.src = custom_protocol_scheme + '://' + queue_has_message;
    }

这时webview的监听方法decidepolicyfornavigationaction监听到了https://wvjb_queue_message 消息后还是执行webviewjavascriptbridge._fetchqueue()去取数据,取到数据后根据responseid当初在_responsecallbacks中存储的callback,然后执行callback、移除responsecallbacks中的数据。到此为止,整个native向js发送消息的过程就完成了。

总结:

  • js中先调用registerhandler将方法存储到messagehandlers中
  • native调用callhandler:方法,将消息内容存储到message中,回调存储到responsecallbacks中。
  • 将message消息序列化通过_evaluatejavascript方法执行_handlemessagefromobjc
  • 将message解析,通过message.handlername从messagehandlers取出该方法;根据message.callbackid生成回调
  • 执行该方法,回调

js向native发送消息

从js向native发消息其实和native向js发消息的接口层面是差不多的。

native侧

native侧首先要注册一个jstooccallback方法

[_bridge registerhandler:@"jstooccallback" handler:^(id data, wvjbresponsecallback responsecallback) {
    responsecallback(@"response from jstooccallback");
}];

该方法也同样是将该方法的callback存储起来,存储到messagehandlers当中,key就是方法名"jstooccallback",value就是callback。

js侧

js侧会调用callhandler方法:

// 调用oc中注册的那个方法
bridge.callhandler('jstooccallback', {'foo': 'bar'}, function(response) {
    log('js 取到的回调是:', response)
})

这个callhandler方法同样会调用_dosend方法:将callback存储到responsecallbacks中,key为callbakid;将消息存储到sendmessagequeue中;messagingiframe执行https://wvjb_queue_message

native的decidepolicyfornavigationaction方法监听到该消息后同样通过webviewjavascriptbridge._fetchqueue()去取消息。

根据callbackid创建一个responsecallback,根据message的handlername从messagehandlers取出该回调,然后执行:

wvjbresponsecallback responsecallback = null;
nsstring* callbackid = message[@"callbackid"];
if (callbackid) {
    responsecallback = ^(id responsedata) {
        if (responsedata == nil) {
            responsedata = [nsnull null];
        }
        wvjbmessage* msg = @{ @"responseid":callbackid, @"responsedata":responsedata };
        [self _queuemessage:msg];
    };
} else {
    responsecallback = ^(id ignoreresponsedata) {
        // do nothing
    };
}
wvjbhandler handler = self.messagehandlers[message[@"handlername"]];
handler(message[@"data"], responsecallback);

调用完这个方法后,该消息已经收到,然后将回调的内容回调给js。 通过上面的代码可以看到,回调js的内容就是callbackid和responsedata生成的message,调用_queuemessage方法。

_queuemessage方法上面已经看过了,就是序列化消息、加入队列、执行webviewjavascriptbridge._handlemessagefromobjc('%@');方法。

js收到该消息后,处理返回的消息,从responsecallbacks中根据message中的responseid取出callback并且执行。最后删除responsecallbacks中的数据,js向native发送数据就完成了。

小结:

  • native侧调用registerhandler方法注册方法,方法名为jstooccallback,将消息存储到messagehandlers中,key为方法名,value为callback。
  • js侧调用callhandler方法:将responsecallback存储到responsecallbacks中;将message存储到sendmessagequeue中;messagingiframe执行 http://wvjb_queue_message
  • native侧监听到该消息后调用webviewjavascriptbridge._fetchqueue()去取数据
  • 根据handlername从messagehandlers中取出该callback;根据callbackid创建callback对象作为参数放到handlername的方法中;执行该回调。

总结

综上,webviewjavascriptbridge的核心流程就分析完了,最核心的点是js通过加载iframe来通知native侧;native侧通过evaluatejavascript方法去执行js。

从整个sdk来看,设计的非常好,值得借鉴学习:

  • 使用外观模式统一调用接口,比如初始化webviewjavascriptbridge的时候,不需要关心使用方使用的是uiwebview还是wkwebview,内部已经处理好了。
  • 接口统一,不管是native侧还是js侧,调用方法就是callhandler、注册方法就是registerhandler,不需要关注内部实现,使用非常方便。
  • 代码简洁,逻辑清晰,层次分明。从类的分布就能很清晰的看出各自的功能是什么。
  • 职责单一,比如decidepolicyfornavigationaction方法只负责监听事件、_fetchqueue是负责把消息转换成json字符串返回、_dosend是发送消息到native、_dispatchmessagefromobjc是负责处理从oc返回的消息等。虽然decidepolicyfornavigationaction也能接收消息,但这样就不会这么精简了。
  • 扩展性好,目前decidepolicyfornavigationaction虽然只有初始化和发消息两个事件,如果有其他事件还可以再扩展,这也得益于方法设计的职责单一,扩展对原有方法影响会很小。

以上就是ios开发webviewjavascriptbridge通讯原理解析的详细内容,更多关于ios webviewjavascriptbridge通讯的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com