美文网首页iOS开发技巧
WKWebView_JSTrade介绍

WKWebView_JSTrade介绍

作者: youlianchun | 来源:发表于2017-07-19 14:08 被阅读159次

    一前文

    拖这么久才补JSTrade的实现介绍,之前一直在对这货进行逻辑结构梳理重构和功能扩展,也没一个像样的demo,到目前JSTrade的实现方式和对外接口算是比较稳定,所以补个说明。
    最开始的是JSExport,后台增加了JSImport,再后来JSExport增加了JSHandler,再后来JSExport增加了返回值直接return功能,到目前采用协议头(解除原本的对象继承),使用方式也由原来比较杂乱慢慢规整(效仿JavaScriptCore使用风格),提供一个更加熟悉和友好的交互方式。

    二采用技术点

    JSTrade的实现其实并不是很难,其中主要使用了runtime、WK的js注入、runloop线程等待、NSJSONSerialization结构数据序列化处理、NSInvocation参数处理、NSMethodSignature参数类型处理、WK的js调用、NSBlock转NSMethodSignature。
    runtime方面主要使用到这几点知识1:方法欺骗(class_replaceMethod),2:对象协议方法列表获取和解析(protocol_copyMethodDescriptionList),3:未实现方法转发(forwardInvocation:),4:动态添加,获取属性(objc_setAssociatedObject),5:面向切面编程前半部分原理

    三实现原理简介

    1、js到oc:

    采用js注入往web内添加支持对象(JSExportModel)和方法体(和JSExportModel内部函数关联),web通过调用函数进入JSExportModel内部函数,并且将所带的参数和一些必要数据进行格式化转换换,然后调用WK的postMessage(block返回结果)方法或者prompt(return返回结果)函数,由于WK交互接口无法实现直接返回结果,所以采用prompt函数,对特殊关键字进行拦截。JSExport接收到message后对数进行解析,并且根据关键字获取到消息指定的本地方法(需要注册),然后结合NSMethodSignature和NSInvocation进行动态参数设置和转发。
    对于本地方法的注册由两种方式,一种是NSBlock注册,一种是继承JSExportProtocol协议,JSExportProtocol和JavaScriptCore的在使用上一致。JSExport协议内的方法和web端js方法一一对应。

    2、oc到js:

    JSImport的实现简单来书就是通过协议声明一些和web端对应的js函数,然后通过oc的消息转发把方法调用指向到指定的代码上通过WK统一调用js函数,JSImportProtocol协议声明函数如果在.m文件内实现了方法体就等于开发者手动控制js调用,不实现则由JSImportModelManager统一处理。JSImport模块通过引入面向切面编程原理来实现原本的继承关系解绑,仅通过继承协议,让import在使用上更加灵活。

    三使用与实现详解

    1、使用介绍:

    之前使用介绍

    介绍比较草,后期补充一个颜值高些的内容

    2、实现详解

    这边就不对每个部分都做一一解说了,说实话,我也不知道怎么介绍好一些,这边就挑几个点着重介绍一下:1.JSExportAs 实现(JSImport协议的那几个原理是一样的),2.面向切面编程实现,3.oc调用js集中处理,4.js-oc直接返回值处理,5.好像没有了,剩下的网上有很多文章。

    JSExportAs 实现

    首先,来看看JSTrade 的JSExportAs 和 JavaScriptCore的区别

    //JSTrade
    #define JSExportAs(PropertyName, Selector) \
        @optional Selector __JS_EXPORT_AS__##PropertyName:(id)argument NS_UNAVAILABLE; @required Selector
    
    //JavaScriptCore
    #define JSExportAs(PropertyName, Selector) \
        @optional Selector __JS_EXPORT_AS__##PropertyName:(id)argument; @required Selector
    
    

    除了多了一个NS_UNAVAILABLE,其它的一模一样,这个关键字宏的作用大家应该晓得,这里使用这家伙的目的其实是为了让编译器识别不到这个方法,为了避免调用引起问题(应该没有谁那么无聊)。
    通过JSExportAs的作用是给一个oc的方法取个js一样的名字,毕竟oc跟上参数后sel直接转NSString的结果带有“:”,不是合法的js函数名,JSExportAs定义出来的结果是两个方法名,一个是直观上的函数名,另一个是吧别名和函数名进行拼接后的一个可选函数名。

    JSExportAs(doFoo,
        - (void)doFoo:(id)foo withBar:(id)bar
        );
    //结果是
    @required - (void)doFoo:(id)foo withBar:(id)bar;
    @optional - (void)doFoo:(id)foo withBar:(id)bar __JS_EXPORT_AS__doFoo:(id)argument NS_UNAVAILABLE;
    

    在原来的函数名后边在加上一个参数来做区分
    函数名结构晓得后就好办了,接下来runtime出场了:
    采用@protocol(协议名)来获取协议对象,接着用class_copyProtocolList()获取对象的协议列表,然后检查一下有没有JSExportProtocol或者JSExportProtocol子协议(jsProtocol),没有就不用搞了,接着用protocol_copyMethodDescriptionList获取协议内的函数列表,对列表内的协议进行遍历,并且转换成js函数和本地函数一一对应的JSExportMethod对象,再用一个字典保存到Model对象内(动态添加个属性)具体实现函数如下,包含JSExportAs和一般函数:

    NSDictionary<NSString *,JSExportMethod *>* jsExportMethods(id<JSExportProtocol> model) {
        Protocol *jsProtocol = @protocol(JSExportProtocol);
        Class cls = [model class];
        if (class_conformsToProtocol(cls,jsProtocol)){
            unsigned int listCount = 0;
            Protocol * __unsafe_unretained *protocolList =  class_copyProtocolList(cls, &listCount);
            for (int i = 0; i < listCount; i++) {
                Protocol *protocol = protocolList[i];
                if(protocol_conformsToProtocol(protocol, jsProtocol)) {
                    jsProtocol = protocol;
                    break;
                }
            }
            free(protocolList);
            struct objc_method_description * methodList = protocol_copyMethodDescriptionList(jsProtocol, YES, YES, &listCount);
            NSMutableArray *methodArray = [NSMutableArray arrayWithCapacity:listCount];
            NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
            NSMutableDictionary *modelMethodDict = [NSMutableDictionary dictionary];
            
            for(int i=0;i<listCount;i++) {
                SEL sel = methodList[i].name;
                NSString *selName = NSStringFromSelector(sel);
                JSExportMethod *method = [JSExportMethod methWithTarget:model sel:sel];
                NSString *jsFuncName;
                if (method.callBack) {
                    jsFuncName = [selName componentsSeparatedByString:@":"][0];
                }else{
                    jsFuncName = [selName stringByReplacingOccurrencesOfString:@":" withString:@""];
                }
                method.jsFuncName = jsFuncName;
                methodArray[i] = method;
                methodDict[selName] = method;
                modelMethodDict[jsFuncName] = method;
            }
            free(methodList);
            struct objc_method_description * methodListAs = protocol_copyMethodDescriptionList(jsProtocol, NO, YES, &listCount);
            for(int i=0;i<listCount;i++) {
                SEL sel = methodListAs[i].name;
                NSString *selName = NSStringFromSelector(sel);
                if ([selName containsString:@"__JS_EXPORT_AS__"]) {
                    NSArray *keys = [selName componentsSeparatedByString:@"__JS_EXPORT_AS__"];
                    NSString *selName = keys[0];
                    JSExportMethod *method = methodDict[selName];
                    [modelMethodDict removeObjectForKey:method.jsFuncName];
                    NSString *selNameAs = keys[1];
                    NSString *jsFuncName = [selNameAs stringByReplacingOccurrencesOfString:@":" withString:@""];
                    method.jsFuncName = jsFuncName;
                    modelMethodDict[jsFuncName] = method;
                }
            }
            free(methodListAs);
            return modelMethodDict;
        }else{
            return nil;
        }
    }
    

    JSImportVarAs、JSImportFuncAs的实现原理和JSExportAs是类似的,只是在property的set和get方法上多做了一些区分的条件判断

    } else {//__JS_IMPORT_AS_VAR_
                        [varNameArray addObject:selName];
                        BOOL isSet = NO;
                        NSString *getName = selName;
                        if ([selName hasPrefix:@"set"]) {
                            NSString *tmp =  [selName substringWithRange:NSMakeRange(3, selName.length-4)];
                            if ([varNameArray containsObject:tmp]) {
                                getName = tmp;
                                isSet = YES;
                            }else{
                                tmp = [tmp stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[tmp substringToIndex:1] lowercaseString]];
                                if ([varNameArray containsObject:tmp]) {
                                    getName = tmp;
                                    isSet = YES;
                                }
                            }
                        }
                        if ([selName containsString:@"__JS_IMPORT_AS_VAR_AS__"]) {
                            jsFuncName = [getName componentsSeparatedByString:@"__JS_IMPORT_AS_VAR_AS__"][1];
                            selName = [selName componentsSeparatedByString:@"__JS_IMPORT_AS_VAR_AS__"][0];
                            if (isSet) {
                                selName = [selName stringByAppendingString:@":"];
                            }
                        }else {
                            jsFuncName = [getName stringByReplacingOccurrencesOfString:@"__JS_IMPORT_AS_VAR__" withString:@""];
                            selName = [selName stringByReplacingOccurrencesOfString:@"__JS_IMPORT_AS_VAR__" withString:@""];
                        }
                        method = methodDict[selName];
                        if (!method) {
                            method = [[JSImportMethod alloc] init];
                            methodDict[selName] = method;
                        }
                        method.isSet = isSet;
                        method.isVar = YES;
                        method.jsFuncName = jsFuncName;
                        method.isAs = YES;
                    }
    
    
    面向切面编程实现

    面向切面编程的实现原理来源于Aspects,这里有我对Aspects实现原理的理解图和运用demo,主要是runtime的知识运用JSImport内用到的只是当中的一部分。下边是实现函数,代码比起Aspects简单了很多:

    static void jsTrade_forwardInvocation(id self) {
        NSCParameterAssert(self);
        Class selfClass = object_getClass(self);
        char buffer[100] = "";
        const char *selfclass = object_getClassName(self);
        strcpy(buffer,selfclass);
        strcat(buffer,"_miSub");
        char *subclass = buffer;
        Class subClass = objc_getClass(subclass);
        if (subClass == nil) {
            subClass = objc_allocateClassPair(selfClass, subclass, 0);
            Method method_forwardInvocation = class_getInstanceMethod(subClass, @selector(forwardInvocation:));
            const char *encode_forwardInvocation = method_getTypeEncoding(method_forwardInvocation);
            IMP imp_forwardInvocation = imp_implementationWithBlock(^(id self, NSInvocation *anInvocation ) {
                forwardInvocation(self, anInvocation);
            });
            BOOL b = class_addMethod(subClass, @selector(forwardInvocation:), imp_forwardInvocation, encode_forwardInvocation);
            if (b) {
                class_replaceMethod(subClass, sel_registerName(name_forwardInvocation), method_getImplementation(method_forwardInvocation), encode_forwardInvocation);
            }else{
                //添加失败
            }
            
            Method method_class = class_getInstanceMethod(subClass, @selector(class));
            IMP imp_class = imp_implementationWithBlock(^Class(id self) {
                return selfClass;
            });
            const char *encode_class = method_getTypeEncoding(method_class);
            class_replaceMethod(subClass, @selector(class), imp_class, encode_class);
            objc_registerClassPair(subClass);
            jsTrade_addFunction(subClass);
        }
        object_setClass(self, subClass);
    }
    
    oc调用js集中处理

    JSImport 这个功能刚开始也是突发奇想,想着js调用oc可以通过代码桥接让oc函数好比是一种语言在被调用,那oc调用js是不是也可以这样,于是就有JSImport。JSImport在实现过程中主要有一个思路难点:如何让所有的函数最后都走向wk的js调用函数,毕竟函数名是千变万化的,恰好NSObject有这么【forwardInvocation:】个方法,消息转发,结合协议的可选性能可以发现协议方法未被实现的函数一旦被调用都会走到这个方法,代码找不到方法实现,而被实现的方法则不会,这也刚好可以满足JSImport的另一个功能:oc-js自定义调用。
    消息转发函数有个NSInvocation参数,NSInvocation的功能是通过消息方式直接调用某个对象的方法,但是我们这里的方法可大多没有实现,怎么搞呢,就是对NSInvocation指定一个新的SEL,让NSInvocation可以顺利执行,当然,这样还不够,方法调用往往伴随着各种类型参数,这些就要通过NSInvocation结合NSMethodSignature来处理,关键点油三点:1.参数类型处理,2.参数设置,3.返回参数设置。搞定了这几个,就可以让一个没有方法体的函数能在用起来就能有一种很健全的感觉。由于对上个版本JSImportModel的模型继承关系移除,采用了面向切面编程思想对【forwardInvocation:】进行了拦截处理,转向指定的c函数:

    static void forwardInvocation(JSImportObject self, NSInvocation *anInvocation) {
        NSString *selName = NSStringFromSelector(anInvocation.selector);
        JSImportMethod *method = [getMethodDict(self) objectForKey:selName];
        if (method) {
            if (!anInvocation.argumentsRetained) {
                [anInvocation retainArguments];
            }
            NSMethodSignature *signature = anInvocation.methodSignature;
            NSString *spaceName = getSpaceName(self);
            NSString *jsFuncName = spaceName.length>0 ? [NSString stringWithFormat:@"%@.%@", spaceName, method.jsFuncName] : method.jsFuncName;
            WKWebView *webView = getWebView(self);
            id returnValue;
            if (method.isVar) {
                if (method.isSet) {
                    id param = [signature getInvocationValue:anInvocation atIndex:2];
                    if (!param) {
                        param = [[NSNull alloc] init];
                    }
                    [webView jsSetVar:jsFuncName value:param];
                }else{
                    returnValue = [webView jsGetVar:jsFuncName];
                }
            }else{
                NSInteger paramsCount = signature.numberOfArguments - 2; // 除self、_cmd以外的参数个数
                NSMutableArray * params = [NSMutableArray array];
                for (NSInteger i = 0; i < paramsCount; i++) {
                    id argument = [signature getInvocationValue:anInvocation atIndex:i + 2];
                    if (!argument) {
                        argument = [[NSNull alloc] init];
                    }
                    [params addObject:argument];
                }
                returnValue = [webView jsFunc:jsFuncName arguments:params];
            }
            anInvocation.selector = sel_registerName(name_forwardFunction);
            [anInvocation invoke];
            
            if (signature.methodReturnLength>0) {
                [signature setInvocation:anInvocation value:returnValue atIndex:-1];
            }
        }else {
            if ([selName isEqualToString:@"webView"]||[selName isEqualToString:@"setWebView:"]||[selName isEqualToString:@"spaceName"]) {//JSImportBase 函数未实现,不做处理
                return;
            }
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, sel_registerName(name_forwardInvocation), anInvocation);
        }
    }
    

    对于方法调用的擦函数处理具体在NSMethodSignature+JSTrade.m文件和WKWebView+JSTrade.m文件,这里就不细说了。

    js-oc直接返回值处理

    js-oc直接返回值是最近才增加的一个功能,做过WK交互的都晓得,原声交互api(postMessage)并不支持直接把结果返回给js,都是采用oc反调用js把之前的结果传回去,这让我大大的羡慕JavaScriptCore。WK交互能够直接返回值给js的地方只有两个,一个是alert提示框,另一个是prompt对话框,alert返回结果类型为BOOL型,满足不了,但prompt返回的是NSString,这就有的搞了。
    这里采用代理拦截器拦截了prompt代理函数,对message进行关键字匹配,正常的对话操作则将代理转发给原代理对象,属于交互的就拦截下来处理。代理拦截器实现看这里。这边贴一下拦截下来的处理:

    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
        if ([prompt isEqualToString:kJSExport_registerKey]) {
            id resault;
            NSDictionary *dict = [NSJSONSerialization unserializeJSON:defaultText toStringValue:NO];
            NSString *spaceName = dict[@"spaceName"];
            if (spaceName.length>0) {
                JSExportMessage *msg = [[JSExportMessage alloc] initWithWebView:webView message:dict];
                JSExportScriptManager *sm = self.manager.handlerDict[spaceName];
                resault = [sm jsHandlerCallWithMessage:msg];
            }
            NSMutableDictionary *res = [NSMutableDictionary dictionary];
            res[@"funcName"] = dict[@"funcName"];
            res[@"result"] = [resault?resault:[NSNull alloc] init];;
            NSString *resault_json = [NSJSONSerialization serializeToJSON:res];
            completionHandler(resault_json);
        }else{
            if ([self.UIDelegateReceiver respondsToSelector:_cmd]) {
                [self.UIDelegateReceiver webView:webView runJavaScriptTextInputPanelWithPrompt:prompt defaultText:defaultText initiatedByFrame:frame completionHandler:completionHandler];
            }
        }
    }
    

    到这里应该算是把JSTrade内比较有趣的实现点都列了一下了,其它的代码就看源码吧:https://github.com/youlianchun/WKWebView_JSTrade

    待续

    后期版本讲对OS X进行兼容处理,以及代码逻辑稳定优化

    相关文章

      网友评论

        本文标题:WKWebView_JSTrade介绍

        本文链接:https://www.haomeiwen.com/subject/lirlkxtx.html