• OC与JS混合开发


        随着iOS开发的成本增大,越来越多的公司开始使用html5混合开发软件了,因为使用原生的开发花费的成本跟时间都很大,而使用html5来搭建界面会方便很多,效率相对而言也提高了。虽然使用UIWebView实现的交互效果与原生效果相比还是会大打折扣,这类界面通常没有复杂的交互效果,所以现在主流应用大多采用混合开发。花了几天时间,把JS的基础全部看了一遍,又研究了一下巧神的书,写了一个iOS7以前的JS与OC混合开发的demo。

        既然是html5页面搭建的布局,那么肯定是得有html5页面的,所以首先我们得先写一个html5的页面。既然我们做的是App,所以我们针对的是手机页面,需要加入针对移动端页面优化的viewport,然后在body里面加入一个按钮,绑定一个点击事件,在JS里面实现这个方法,通过location.href = ""; 方法进行跳转网页,里面的地址是自己自定义的,然后在OC里面解析。

    <html>
        <head>
            <meta charset = "UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi"/>
            <title>测试网页</title>
            <script type="text/javascript">
                function sum(){
                    location.href = 'jsoc://call_?100'; // 自己写的跳转网址
                }
            
                function ocCallJsNoParamsFunction(){
                    alert("OC调用JS中的无参方法");
                }
            
                function ocCallJsHasParamsFunction(name, food){
                    alert(name+"喜欢吃:"+url);
                }
            </script>
        </head>
        <body>
            <button style = "backgroud:red; 100px; height:30px;" onclick = "click();">点我一下试试</button>
            <br>
            <a href = "http://www.baidu.com">百度一下,你就知道</a>
        </body>
    </html>

      写完了一个简单的html5页面,接下来就要写JS调用OC以及OC调用JS了。首先,使用storyboard来搭建布局,当然你得拖约束。然后把刷新的Item的设置为Refresh。

      把webView拖为属性,把Refresh拖成方法,当点击这个按钮的时候刷新webView。

      - (IBAction)refresh:(id)sender{
          [self.webView reload];
      }

      接下来就可以设置webView的属性了。 使用webView加载网页,可以加载网络上的,也可以加载本地的,如果你需要加载的是网页上面的,那么你就需要在 info.plist 里面添加一个配置。

     

      加载本地的,你只需要得到本地html5页面的路径,就能在webView上面展示了。

        // 系统可以自动检测电话、链接、地址、日历、邮箱
        self.webView.dataDetectorTypes = UIDataDetectorTypeAll;
        self.webView.delegate = self;
        // 根据资源名,扩展名获取该资源对应的 URL
        NSURL *htmlUrl = [[NSBundle mainBundle] URLForResource:@"info.html" withExtension:nil];
        [self.webView loadRequest:[NSURLRequest requestWithURL:htmlUrl]];

        设置好了webView,现在运行起来,你应该是能看到效果的,如下图所示。

                                       

      先来讲一个OC调用JS方法,通过  - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;  方法,我们就可以调用JS里面我们写好的方法了。这里呢,我把调用的方法放在     - (void)webViewDidFinishLoad:(UIWebView *)webView;    当webView加载完页面后调用JS里面的方法。实现OC调用JS。我写了两个方法,第一种是不带任何参数的,所以直接调用方法就可以,而第二种是带参数的,所以第二种方法写法会不一样,stringByEvaluatingJavaScriptFromString 只能带字符串,所以需要先通过stringWithFormat: 拼接字符串,然后把字符串传进去。

    - (void)webViewDidFinishLoad:(UIWebView *)webView{
    //   NSString *js = [webView stringByEvaluatingJavaScriptFromString:@"ocCallJsNoParamsFunction();"];
        NSString *js = [NSString stringWithFormat:@"ocCallJsHasParamsFunction('%@','%@')",@"哈哈",@"苹果"];
        // webView调用JS代码,等webView全部加载html界面之后调用
        [webView stringByEvaluatingJavaScriptFromString:js];
    }

      OC调用JS的代码很简单,但是JS调用OC就有点困难了,因为这是iOS7以前的,那个时候没有出JavaScriptCore,所以开发起来有难度。下面来讲讲JS如何调用OC的代码。JS调用OC,并没有现成的API,可以使用"曲线救国"的方法,间接达到。在UIWebView内发起的所有网络请求,都可以通过delegate函数在原生界面得到通知。所以在html5页面发起一个特殊的网络请求,请求加载的网址内容通常不是真实的地址,OC中判断这个特殊的网络请求来实现不同的功能。

    /**
     *  每当webview发送请求之前就会调用这个方法(js调用oc)
     */
    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
    {
        //获得url全路径
        NSString *url = request.URL.absoluteString;
        NSString *protocol = @"jsoc://";
        
        // 判断url是否以protocol开头
        if([url hasPrefix:protocol]){
            //获得协议后面的路径,substringFromIndex:表示从指定位置开始截取字符串到最后,所截取位置包含该指定位置
            NSString *path = [url substringFromIndex:protocol.length];
            //利用占位符进行切割
            NSArray *subpaths =[path componentsSeparatedByString:@"?"];
            //获得方法名 jsoc://call_?100
            NSString *methodName = [[subpaths firstObject] stringByReplacingOccurrencesOfString:@"_" withString:@":"];
            NSArray *params = nil;
            if (subpaths.count == 2) {
                params = [[subpaths lastObject] componentsSeparatedByString:@"&"];
            }
            
            //调用方法
            SEL sel = sel_registerName([methodName UTF8String]);
            [self jsoc_performSelector:sel withObjects:params];
          
            return NO;
        }
        
        return YES;
        
    }

      sel_registerName([xxx UTF8String]);  和 jsoc_performSelector:withObjects: 用到了runtime,所以还需要具备一些runtime的知识,sel只是一个指向方法的指针,一个根据方法名hash化了的KEY值,能唯一代表一个方法,它的存在只是为了加快方法的查询速度。然后通过自己定义的 jsoc_performSelector:withObjects: 来调用通过sel找到的这个方法。这里我们用了自定义的performSelector,是因为系统给的只能带一个或者两个参数,如果我想要带多个参数,那就只能通过自定义了。通过 sel 指向了一个方法,所以我们得把这个方法实现,这里我就简单的实现。

    - (void)call:(NSString *)number{
        NSLog(@"参数:%@",number);
        NSLog(@"调用了oc的%s方法",__func__);
    }

      下面就来讲讲自定义的 performSelector怎么实现。给NSObject添加一个Category,然后把jsoc_performSelecor:withObjects:实现以下。这里我们会用到 NSMethodSignature 和 NSInvocation,废话不多说,直接上代码。

    - (id)jsoc_performSelector:(SEL)aSelector withObjects:(NSArray *)objects
    {
        //NSInvocation 利用一个NSInvocation对象包装一次方法调用(方法调用者,方法名,方法参数,方法返回值)
        // 通过选择器获取方法签名
        NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
        if(signature == nil){
           NSString *reason = [NSString stringWithFormat:@"** The method[%@] is not find **",NSStringFromSelector(aSelector)];
           @throw [NSException exceptionWithName:@"错误!" reason:reason userInfo:nil];
            
        }
        // iOS中可以直接调用某个对象的消息方式有两种,其中一种就是NSInvocation(对于>2个的参数或者有返回值的处理) 另一种performSelector:withObject
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = self;
        invocation.selector = aSelector;
        NSInteger paras = signature.numberOfArguments - 2;
        paras = MIN(paras, objects.count);
        
        for(NSInteger i=0;i<paras;i++){
            id object = objects[i];
            if([object isKindOfClass:[NSNull class]]) continue;
            // index从2开始 ,原因为:0 1 两个参数已经被target 和selector占用
            [invocation setArgument:&object atIndex:i+2];
        }
        //调用方法
        [invocation invoke];
    
        id returnValue = nil;
        if(signature.methodReturnLength){
            [invocation getReturnValue:&returnValue];
        }
        return returnValue;
    }

      NSMethodSignature 是方法签名,官方定义该类为对方法的参数、返回类似进行封装,协同NSInvocation实现消息转发。signature.numberOfArguments - 2 是因为底层系统本身就带了2个参数,所以我们统计数量的时候需要把系统的减去,下面 setArgument:atIndex:   i + 2 也是一样。

      iOS7 以前的JS OC混合开发就写好了。这里是一个传送门。推荐几个开源库,WebViewJavascriptBridge、EasyJSWebView等。

      上面讲了iOS 7 以前如何实现混合开发,下面来讲讲iOS7 之后如何实现混合开发,iOS7开始,出现了一个JavaScriptCore 这么一框架,通过它来实现JS调用OC代码,会简单很多,不过代码就有点不一样。JavaScriptCore是webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境。代码是开源的,可以下下来看看源码

      JSContext

      JS执行的环境,同时也通过JSVirtualMachine管理着所有对象的声明周期,每个JSValue都和JSContext相关联并且强引用context。

      JSValue

      JS对象在JSVirtualMachine中的一个强引用,其实就是Hybird对象。我们对JS的操作都是通过它。并且每个JSValue都是强引用一个context。同时,OC和JS对象之间的转换也是通过它。

      JSManageValue

      JS和OC对象的内存管理辅助对象。由于JS内存管理是垃圾回收,并且JS中的对象都是强引用,而OC是引用计数。如果双方相互引用,势必会造成循环引用,而导致内存泄露。我们可以用JSManagedValue保存JSValue来避免。

      JSVirtualMachine

      JS运行的虚拟机,有独立的堆空间和垃圾回收机制。

      JSExport

      一个协议,如果JS对象想直接调用OC对象里面的方法和属性,那么这个OC对象只要实现这个JSExport协议就可以了。

       

      好了,下面直接上代码吧。先来看一下html5页面。这里面JS调用OC需要在onclick中加入调用的方法,前面是跟OC规定好的一个名称,点后面的是方法的名称,这个名称在OC代码中必须得一致。

    <div style="300px; margin: 0 auto;background:red;">
            <input type="button" value="带两个参数的方法" onclick="Native.testMethodWithParam1Param2('param1_value', 'param2_value')" />
            <br />
            <input type="button" value="带两个参数的方法2" onclick="Native.testMethod(1111, '2222')" />
            <br /> 
            <input type="button" value="带一个参数的方法" onclick="Native.testLog('带一个参数')" />
            <br />
            <input type="button" value="参数为数组的方式" onclick="Native.testArray([1111, '2222'])" />
            <br />
            <input type="button" value="测试log" onclick="log('测试')" />
            <br />
            <input type="button" value="JS调用OC原生提示框" onclick="alert('alert')" />
            <br />
            <input type="button" value="添加视图" onclick="addSubView('view')" />
            <br />
            <div>
                <span>输入一个整数:</span>
                <input id="input" type="text" />
                <br />
                结果为:<p id="result"></p>
            </div>
            <input type="button" value="计算阶乘" onclick="Native.calculateForJS(input.value)" />
        </div>

      当然还有JS中的代码,下面便是JS中的代码。

    <script type="text/javascript">
        function showResult(resultNumber) {
            document.getElementById("result").innerText = resultNumber;
        }
    </script>

      然后回到Xcode中,我们需要自己写代理方法,通过代理方法,当点击按钮的时候就可以直接调用相对应的方法。

    @protocol JSOCExport <JSExport>
    
    /** 带两个参数的方法 */
    - (void)testMethodWithParam1:(NSString *)param1 Param2:(NSString *)param2;
    /** 带两个参数的方法(2) */
    - (void)test:(NSNumber *)param1 method:(NSString *)param2;
    /** 带一个参数的方法 */
    - (void)testLog:(NSString  *)logText;
    /** 参数以数组的方式 */
    - (void)testArray:(NSArray *)dataArray;
    - (void)calculateForJS:(NSNumber *)number;
    
    @end

      既然是JavaScriptCore框架,需要用到JSContext,声明一个成员变量,然后再webViewDidFinishLoad:中初始化。

    - (void)webViewDidFinishLoad:(UIWebView *)webView {
        // 设置导航栏title
        self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
        // 设置页面元素
    //    [webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.display = 'none'"];
        context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        // 打印异常
        context.exceptionHandler = ^(JSContext *context, JSValue *exceptions) {
            context.exception = exceptions;
            NSLog(@"%@", exceptions);
        };
        
        // 以 JSExport 协议    关联 Native
        context[@"Native"] = self;
        
        // 以block 形式关联 JS中的func
        context[@"log"] = ^(NSString *str) {
            NSLog(@"log = %@", str);
        };
        
        UIViewController *vc = self;
        context[@"alert"] = ^(NSString *str) {
            dispatch_async(dispatch_get_main_queue(), ^{
                UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"msg from js" message:str preferredStyle:UIAlertControllerStyleAlert];
                UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
                [alert addAction:action];
                [vc presentViewController:alert animated:YES completion:nil];
            });
        };
        
        context[@"addSubView"] = ^(NSString *str) {
            UIView *v = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
            v.backgroundColor = [UIColor redColor];
            [v addSubview:[[UISwitch alloc] init]];
            [vc.view addSubview:v];
        };
    }

      最后把剩下的方法也给实现下,这里OC调用JS的方法会有点不一样,用到了 - (JSValue *)evaluateScript:(NSString *)script; 方法,直接调用JS的代码。

      这里提供了一个传送门

  • 相关阅读:
    ASP.NET 中通过Form身份验证 来模拟Windows 域服务身份验证的方法
    中华枣文化三字经
    佛祖保佑、永无BUG!!!
    fatal error C1010: 在查找预编译头时遇到意外的文件结尾 (转)
    VisualSVN Server搭建SVN服务器<转>
    AMF_OBJECT 数据结构浅析
    rtmp聊天相关归总
    signal(SIGPIPE, SIG_IGN) (转)
    malloc()与calloc区别 (转)
    select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET(转)
  • 原文地址:https://www.cnblogs.com/shensq/p/5848668.html
Copyright © 2020-2023  润新知