如何编写自己的 Native Bridge

AdrianaTrot 5年前
   <p><img src="https://simg.open-open.com/show/922d7f35ccc45fb81408d59d02dfc6e0.png"></p>    <p>和很多人一样,在我弄清楚 React Native 的实现机制之前,其实已经在实际项目中用过一段儿时间了。不过在我学习 React Native 实现机制的过程中,逐渐开始给这个项目贡献代码,最终成为核心开发者中的一员。</p>    <p>尽管如此,这个项目中的 Native Bridge 对我来说还是很神秘的,它到底是通过什么『魔法』打通了 JavaScript 和 Objective C 这两种不同编程语言之间的边界。正因为缺少对这部分内容的了解,才促使我花了一些时间做了这些调研,然后把我的理解通过这几篇文章分享给大家。</p>    <p>毋庸置疑,在没有一个对项目清晰的规划设计之前就写代码是没有任何意义的。架构设计对于软件开发来说是很重要的一步,我会讲解整个思考过程,详细解释我们做的一些决策以及背后的原因。</p>    <h2>第一章:设计一个架构</h2>    <h3>第一个迭代</h3>    <p>在开始一个新项目之前,首先需要确定一下在这个项目中应该采用哪些技术方案:</p>    <p>在我们这个场景下,选择的技术方法如下:</p>    <ol>     <li>Objective C 来控制 Cocoa UI</li>     <li>C++ 来和 JSVM 交互</li>     <li>JSVM (V8 或 Chakra)</li>     <li>JavaScript</li>     <li>React + 自定义渲染器</li>    </ol>    <p>我默认主要的执行逻辑是用 C++ 写的,它内部有一个自定义全局上下文的 JS 引擎(基本上所有主流的 JS 引擎 都提供扩展内置函数的能力),所以当看到自定义全局上下文的时候不要惊讶,其实并没有什么特殊的地方,只是通过 C++ 扩充了一些内置的 JS 函数,稍候我们会介绍相关的内容。</p>    <p>一旦我们调用这些自定义的 JS 函数,JS 虚拟机就会执行我们在 C++ 里面实现的那部分代码,然后就可以继续调用 Objective C 的代码来绘制 UI。听起来有点儿复杂,但是不要灰心,很快就看到曙光了。</p>    <p>上述所有的逻辑都可以在同一个线程(主线程)中运行,但是可能会导致一些性能问题。为了避免性能问题,我门给通过 Objective C 进行 UI 渲染的工作专设了一个新的线程。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/bff3ae77fb73fcb6c0fd2af5f7239f98.png"></p>    <p>尽管听起来比较合理,可是实际上却并不管用。</p>    <p>问题在于 Apple 限制了只可以在主线程中渲染 UI,这也就导致我们只能在主线程中去执行 Objective C 的代码。可惜的是,这样应用的入口就和平台绑定了(至少要让它跑在其他平台上会变的非常复杂)。但事已至此,我们只能调整我们最初的设计,来满足这项限制。</p>    <h3>修正方案</h3>    <p>如果必须在主线程渲染 UI,那我们就接受这个限制。不过我们要运行一个 Cocoa 应用,而非 C++ 的程序。在它启动时我们创建一个包含了 JSVM 的后台线程来运行打包好的 JS 代码。和前一种方法里一样,因为有 JSVM 的存在,我们可以在 JS 代码中调用 C++ 写的函数。一旦这样的函数被调用,主线程就会收到指令并绘制 UI。</p>    <p>在主线程中,(JS 线程)发来的指令通过 Objective C 渲染出对应的界面元素。如果成功,Objective C 会回调一个从 C++ 传递过来的函数(代表 JSVM 对 JS 逻辑的回调)。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/163318c12bba5a79e979c791d4924a7d.png"></p>    <h3>最终的架构</h3>    <p>好了,最后总结一下:</p>    <ol>     <li> <p>因为需要在主线程中绘制 UI,所以我们需要一个 Cocoa 类型的应用;</p> </li>     <li> <p>当程序启动的时候,我们需要创建一个 JS 线程来初始化 JS 引擎,然后执行我们打包在一起的 JS 代码。之前说过,我们给 JSVM 上下文打过补丁,暴露了一些额外的 API 来操作 UI 层;</p> </li>     <li> <p>一旦JS线程需要绘制UI的时候,就给主线程发一个命令,主线程收到之后,Objective C 代码就开始执行对应的绘制逻辑;</p> </li>     <li> <p>最后,Objective C 执行一系列的回调函数,从而把最终的执行结果传递通过 JS 线程 传递给我们在 JS 里面逻辑。</p> </li>    </ol>    <h3>搭建基础平台</h3>    <p>动手实践的时候到了!首先,我们创建一个空白的 Cocoa 应用,如下图所示:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e65c0a2bc5b31750a6ee3252920ec916.png"></p>    <p>新建 Mac OS X Cocoa Application 窗口</p>    <p>第一步完成之后,我们就有了一个基础的程序架构。之后,编辑 AppDelegate.m 文件里面的 applicationDidFinishLaunching 函数,在这个函数里面应该启动一个新的线程来初始化 JS 引擎,代码如下:</p>    <pre>  - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {      _jsvmThread = [[NSThread alloc] initWithTarget:self                                            selector:@selector(runJSVMThread)                                              object:nil];      [_jsvmThread start];  }  </pre>    <p>现在你可能有两个疑问:</p>    <pre>  _jsvmThread  @selector(runJSVMThread)  </pre>    <p>我们是通过创建一个 NSThread 的实例来启动一个新的线程的, _jsvmThread 就是 NSThread 的一个实例。后续如果要执行 C++ 回调函数的时候,我们需要 NSThread 实例的引用,因此通过 _jsvmThread 变量保存了下来。</p>    <p>关于 Selector 的问题,根据 Apple 的文档所述,是一个用来选择一个对象要执行那个方法的名称,或是在源码编译后用来代替这个名称的唯一标识符。更多细节建议还是参考 <a href="/misc/goto?guid=4959731640361149141" rel="nofollow,noindex">Apple的官方文档</a> 。</p>    <p>换句话说,当创建了 NSThread 之后就会执行当前实例上面的 runJSVMThread 方法来完成 JS引擎 的初始化工作:</p>    <pre>  - (void)runJSVMThread {      [[[ChakraProxy alloc] init] run];            NSRunLoop *runloop = [NSRunLoop currentRunLoop];            while (true) {          [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];      }  }  </pre>    <p>正如在代码中看到的,我这里用了 ChakraCore 而没有选择 V8。原因是:</p>    <ol>     <li> <p>编译 V8 花了我1个多小时的时间,然后又花了2个小时才能把 HelloWorld 运行起来;</p> </li>     <li> <p>编译 ChakraCore 以及运行 HelloWorld 总共花了 10 多分钟的时间。</p> </li>    </ol>    <p>由于我们只是编写一个原型,所以我把 ChakraCore 提供的更好的开发体验放在了首位。</p>    <p>ChakraProxy 初始化之后,我们必须通过一个 runloop 来保证线程不会退出(如果没有这个机制的话,线程的代码执行完毕之后就结束了)</p>    <p>现在我们的程序可以正常启动了,而且还启动了一个新的线程等待 UI 绘制的命令。现在我们来着手实现 ChakraProxy 这个类。</p>    <p>ChakraProxy 的逻辑很简单,简单来说就是用来整合 Objective C 和 C++ 代码的一个封装:</p>    <p>ChakraProxy.h</p>    <pre>  #import <Foundation/Foundation.h>    @interface ChakraProxy : NSObject    -(void)run;    @end  </pre>    <p>ChakraProxy.m</p>    <pre>  #import "ChakraProxy.h"    @implementation ChakraProxy    -(void)run {    NSLog(@"Hello from the JSVM thread!");  }    @end  </pre>    <p>显而易见,ChakraProxy 除了输出一行日志之外,什么也没有做,但是我们可以在此基础上开展更多的工作了。在下一篇我们会添加更多的功能,比如整合 ChakraCore,基本的桥接模型,以及其他好多好多内容。</p>    <p> </p>    <p> </p>    <p>来自:http://efe.baidu.com/blog/how-to-create-you-own-native-bridge/</p>    <p> </p>