资讯专栏INFORMATION COLUMN

MXBridge - 插件式JS与OC交互框架

KavenFan / 1643人阅读

摘要:概述提供一个插件式的与交互的框架通过实现插件式扩展接口以供调用前往查看主要的类大致画了一下类图结合上图先介绍一下这里几个类的方法持有一个以截获的页面加载的回调以触发注入和环境初始化的操作委托的代理持有一个真正的并持有一个这样将与绑定在一

概述

MXBridge,提供一个插件式的JavaScriptObjective-C交互的框架,通过JavaScriptCore实现,插件式扩展Obejctive-C接口以供JavaScript调用.前往Github查看

主要的类

大致画了一下类图:

结合上图,先介绍一下这里几个类的方法:

UIWebView(MXBridge) : category,持有一个MXWebViewDelegateProxy以截获UIWebView的页面加载的回调,以触发JS注入和bridge环境初始化的操作.

MXWebviewDelegateProxy : 委托的代理.持有一个真正的UIWebViewDelegate,并持有一个MXWebViewBridge,这样将bridge与UIWebView绑定在一起,一个UIWebView中只有一个bridge,并跟随UIWebView的释放一起释放代理和bridge.

MXWebViewBridge : 与JS交互,主要通过这个桥来实现. 持有JSContext,也就是当前WebView的JS运行环境.通过JSExport暴露三个接口供JS直接调用,同时持有一个从js中获取的jsBridge对象,即对应了JS代码中的 JSBridgeForOC以供异步回调时调用JS代码. 除了一个setupJSContext的初始化Webview的JS环境的方法外,还有一个cleanJSContext,在UIWebView释放时,释放持有的JS对象指针,以使对象正常释放.

MXWebViewConntext : 一个单例的全局上下文,放置一些全局的系统信息,以及加载mxbridge.js的代码以字符串的形式放在内存中. 还持有一个插件列表,插件列表的信息放在应用中的 plugins.plist文件中,以键值对形式储存插件名和插件对应的OC类名.还有一个setUp方法,用于初始化MXBridge的功能,调用这个方法后,会通过Method Swizzling来为应用中所有的UIWebView赋予该功能.

MXWebViewPlugin : 插件,所有OC对JS所提供的方法,都是基于插件的形式,即用户实现一个插件,然后JS代码就可以根据插件名和插件方法名来调用这个插件的功能.

MXMethodInvocaton : 方法调用,JS对OC的一次方法调用中,将参数以及调用信息记录在这个Model中.

实现原理

结合上图,来介绍一下MXBridge的实现原理,在介绍实现原理之前,建议先去学习一下JavaScriptCore的使用方法,MXBridge是基于JavaScriptCore实现的,所以只支持iOS7以上的设备.

通过Method swizzling来替换了UIWebView的三个方法的实现:

-(instancetype)mx_initWithFrame:(CGRect)frame {
    [self mx_initWithFrame:frame];
    if (self) {
        [self mx_setup];
    }
    return self;
}

-(nullable instancetype)mx_initWithCoder:(NSCoder *)aDecoder {
    [self mx_initWithCoder:aDecoder];
    if (self) {
        [self mx_setup];
    }
    return self;
}

-(void)mx_setDelegate:(id)delegate {
    //  设置上真正的代理。
    if ([self.delegate isKindOfClass:[MXWebviewDelegateProxy class]]) {
        ((MXWebviewDelegateProxy *)self.delegate).realDelegate = delegate;
    }else {
        [self mx_setDelegate:delegate];
    }
}

在初始化UIWebView的时候,就会为webview添加一个 MXWebviewDelegateProxy对象作为webviewDelegate,而在使用者使用 setDelegate方法时,将要设置的delegate赋值给MXWebviewDelegateProxy对象的realDelegate属性,以让这个设置的delegate能够正常运行.

method swizzling的执行是放在MXwebViewContextsetUp方法中的,这个方法作为在整个应用中初始化MXBridge环境,初始化后才能在应用里的UIWebView中进行JavaScriptObjective-C之间的交互.

而设置代理的主要目的,是为了给UIWebView当前界面的JSContext注入我们的MXBridge.js,以获取交互功能. 在JavaScriptCore中JS代码都是执行在JSContext这个运行环境中的,JSContext表示JS代码在OC中的运行环境,我们可以通过这个环境以执行JS代码,或者让JS直接调用OC方法,具体关于JavaScriptCore的一些简介,可以看一下这篇简陋的文章.

我们要获取这个JSContext,可以通过KVC :

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

但是UIWebView中的这个JSContext是一直在变化的,我们通过观察,可以发现,在UIWebViewDelegate的三个状态中shouldStartLoadWithRequest , webViewDidStartLoadwebViewDidFinishLoad时,UIWebViewJSContext都是指向不同地址,对于这个问题,我们一开始是选取最后一个状态,即webViewDidFinishLoad中的JSContext来使用,因为这个JSContext也是UIWebView加载结束后一直使用的JSContext.所以我们设置一个delegateproxy对象,以获取webViewDidFinishLoad事件,在此时将所需的js注入到从UIWebView中获取的JSContext中,就可以赋予JS与OC交互的功能,而这个阶段的主要操作就是 :

// 获取js执行环境
JSContext *context = [_webview valueForKeyPath: @"documentView.webView.mainFrame.javaScriptContext"];
// 注入bridge.JS
[_context evaluateScript:[MXWebviewContext shareContext].bridgeJS];
// 从js环境中获取 JSbridgeForOC, 在MXWebviewBridge中持有
_jsBridge = [_context[@"mxbridge"] valueForProperty:@"JSbridgeForOC"];
// 将MXWebviewBridge放入js的环境中,由mxbridge持有
[_context[@"mxbridge"] setValue:self forProperty:@"OCBridgeForJS"];

但由于webViewDidStartLoad的限制,我们的mxbridge必须在页面加载完成后,才会初始化完成,而js有些代码会在页面加载过程中执行,为了处理这个时间差,我们有一个变量来表示mxbridge的加载状态,即mxbridge.isReady, 还有一个bridgeReady的Event会在初始化完成时发送出去.所以js调用插件时,首先需要检测mxbridge.isReady,如果mxbridge没有成功初始化,就需要等待bridgeReady事件发生了. 如:

execSafely : function (pluginName, functionName, args,successCallback,failCallback) {
        if (window.mxbridge && window.mxbridge.isReady) {
            window.mxbridge.exec(pluginName, functionName, args,successCallback,failCallback);
        } else {
            document.addEventListener("bridgeReady",  function() {
                                      window.mxbridge.exec(pluginName, functionName, args,successCallback,failCallback);
                                      }, true);
        }
    },

继续讨论实现原理,刚才说到初始化js环境,OC端持有一个JS的桥,而JS端也持有了一个OC端的桥,这样我们就可以使用JavaScriptCore相关的知识进行两者之间的交互了.

Objective-CJavaScript持有一个MXWebviewBridge对象,而这个对象实现了一个继承了JSExport协议的MXNativeBridgeExport ,继承JSExport后,可以将OC中的方法直接在JS中使用,所以提供了三个接口给JS使用:

// 在Native端打日志,方便调试
-(void)loggerWithLevel:(NSArray *)arguments;
// 异步调用插件
-(void)callAsyn:(NSDictionary *)arguments;
// 同步调用插件
-(JSValue *)callSync:(NSDictionary *)arguments;

JavaScript通过callAsyn:callSync:调用OC提供的插件,这两个函数中的具体实现,也比较简单,以callAsyn:举例说明一下:

-(void)callAsyn:(NSDictionary *)arguments {
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程中执行。
        MXMethodInvocation *invocation = [[MXMethodInvocation alloc] initWithJSCall:arguments];
        if (invocation == nil) {
            NSDictionary *error = @{@"errorCode":MXBridge_ReturnCode_PLUGIN_INIT_FAILED,@"errorMsg":@"传递参数错误,无法调用函数!"};
            NSLog(@"异步调用 ,失败 %@",error);
        }
        MXWebviewPlugin *plugin = _pluginDictionarys[invocation.pluginName];
        if (!plugin) {
            Class cls = [MXWebviewContext shareContext].plugins[invocation.pluginName];
            if (cls == NULL) {
                NSDictionary *error = @{@"errorCode":MXBridge_ReturnCode_PLUGIN_NOT_FOUND,@"errorMsg":[NSString stringWithFormat:@"插件 %@ 并不存在 ",invocation.pluginName]};
                [self callBackSuccess:NO withDictionary:error toInvocation:invocation];
            }
            plugin = [[cls alloc] initWithBridge:self];
            _pluginDictionarys[invocation.pluginName] = plugin;
        }
        // 调用 插件中相应方法
        SEL selector = NSSelectorFromString(invocation.functionName);
        if (![plugin respondsToSelector:selector]) {
            selector = NSSelectorFromString([invocation.functionName stringByAppendingString:@":"]);
            if (![plugin respondsToSelector:selector]) {
                NSDictionary *error = @{@"errorCode":MXBridge_ReturnCode_METHOD_NOT_FOUND_EXCEPTION,@"errorMsg":[NSString stringWithFormat:@"插件对应函数 %@ 并不存在 ",invocation.functionName]};
                [self callBackSuccess:NO withDictionary:error toInvocation:invocation];
            }
        }
        // 调用插件
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [plugin performSelector:selector withObject:invocation];
#pragma clang diagnostic pop
    });
}

上段代码中,当JS调用OC的数据传到后,先将调用数据转换为一个MXMethodInvocation对象,然后检测参数合法性. 然后检测插件是否存在,不存在则去创建插件,但插件不在plugins.plist中或者类不存在,也会有相应地错误提示.拿到插件后,就可以根据方法名和js传递的参数调用插件相应地方法了.

对于异步调用的插件,js调用时,会传递调用成功和失败的回调 :

        var list = {
            "success":successCallback,
            "fail":failCallback
        };
            mxbridge.JSbridgeForOC.callBackLists[jscall.invocationID] = list;

bridge将成功失败的回调以 一次调用的唯一标示记录在JSbridgeForOC, 而在异步回调JavaScript的处理函数时,也就是调用JSbridgeForOCcallbackAsyn方法时,就会从callBackLists中找到对应的回调函数,以执行相应的回调:

        callbackAsyn : function (callbackID,status,args) {
            // 执行异步调用,然后OC对JS的调用立即返回。
            window.setTimeout(function() {
                                mxbridge.JSbridgeForOC._callbackAsyn(callbackID,status,args);
                              },0);
        },
        // 真正的回调函数.
        _callbackAsyn : function(callbackID , status ,args) {
            var callbackfuns = mxbridge.JSbridgeForOC.callBackLists[callbackID];
            if (callbackfuns) {
                if (status == mxbridge.OK) {
                    callbackfuns.success && callbackfuns.success(args);
                } else {
                    callbackfuns.fail && callbackfuns.fail(args);
                }
                delete mxbridge.JSbridgeForOC.callBackLists[callbackID];
            };
        }
使用步骤

导入代码.

创建插件 ,插件的编写要注意以下几点 :

继承 MUWebviewPlugin 类,这个类中提供了几个在插件中常用的属性,bridge,containerVCwebView,一些异步时的回调函数,如- (void)callBackSuccess:(BOOL)success withDictionary:(NSDictionary *)dict toInvocation:(MUOCMethodInvocation *)invocation;- (void)callBackSuccess:(BOOL)success withString:(NSString *)string toCallbackID:(NSString *)callbackID; ,返回给JS的值,可以是一个字符串,也可以是以NSDictionary表示的
JSON对象.

- (instancetype)initWithBridge:(MUWebviewBridge *)bridge,可以在这个初始化函数中作一些初始化的操作.

对于插件方法,形式是这样的 : - (NSDictionary *)syncFunction(:(MUOCMethodInvocation *)invocation); ,同步方法返回值必须是 NSDictionary * ,而参数可以有也可以没有. 对于异步方法 - (void)asynFunction(:(MUOCMethodInvocation *)invocation),返回值类型为void,参数也可以有,可以没有. 传递的参数放在MUOCMethodInvocation中.

创建 plugins.plist文件,以 键值对的形式,插件名和插件类名的对应关系.

在需要该功能时,调用 MUWebviewContextsetUp方法,激活功能,使项目中所有的webview都能进行交互.

MUWebViewContext中提供了几个接口,以供设置 :

appName,appVersion,osType,osVersion ,等应用系统信息.

loggerBlock,一个打日志的block,用于调试JS..

注意事项

JS调用插件,传递的参数是json对象的形式.而调用参数传递到插件中时,是以NSDictionary的形式.同理,在回调JS时,OC传递的类型是NSDictionary,而到达JS的返回值是 json对象. 这与JavaScriptCore相关.

UIWebView页面加载完成时,才会初始化MXBridge以支持插件调用功能,所以,调用插件前,要进行检测,以确保mxbridge已经初始化完成.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79846.html

相关文章

  • 精读《前端未来展望》

    摘要:精读前端可以从多个角度理解,比如规范框架语言社区场景以及整条研发链路。同是前端未来展望,不同的文章侧重的格局不同,两个标题相同的文章内容可能大相径庭。作为使用者,现在和未来的主流可能都是微软系,毕竟微软在操作系统方面人才储备和经验积累很多。 1. 引言 前端展望的文章越来越不好写了,随着前端发展的深入,需要拥有非常宽广的视野与格局才能看清前端的未来。 笔者根据自身经验,结合下面几篇文章...

    MadPecker 评论0 收藏0
  • JavaScriptCore 使用

    摘要:在上有这样一个项目可以拿到了上下文创建的事件,只不过也是改获取方法也是苹果的私有,原来项目中使用了这个库上架苹果应用商店没有问题,现在审核情况不太了解。 前言 动态化是移动开发技术中的重要的一部分 ,当前普遍的动态化方案 , 如 React Native 、Weex 、Hybrid部分解决方案及之前流行的热修复框架 JSPatch ,背后都用到了 JavaScriptCore 框架 ,...

    张迁 评论0 收藏0
  • iOS引入JavaScriptCore引擎框架

    摘要:在中,我们可以引入框架,这样,我们可以层来操作层代码的执行。都会发送相应的消息给。在端,由于只有暴露在全局的函数声明才能够让端访问,这就限制了端的灵活性。我们有理由憧憬未来在和下更方便的集成引擎来完成建议的双向通信。 JavaScriptCore引擎     我们都知道WebKit是个渲染引擎,简单来说负责页面的布局,绘制以及层的合成,但是WebKit工程中不仅仅有关于渲染相关的逻辑,...

    xavier 评论0 收藏0
  • WebViewJavascriptBridge原理解析

    摘要:否则按照正常流程处理。如果是表示是初始化环境的消息,如果是则表示是发送消息。则立即发送消息。回调主动调用获取注册的函数调用中的对应函数处理把消息从发送到,执行具体的发送操作。处理从返回的消息。从而找到具体的实现执行。 基本说明 我们的项目是一个OC与javascript重度交互的app,OC与javascript交互的那部分是在WebViewJavascriptBridge的githu...

    yck 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<