mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
mod
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
<audio id="audio" title="39 | 打通前端与原生的桥梁:JavaScriptCore 能干哪些事情?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cb/37/cbdb1a93d452658cfa773b7fab1bf837.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
今天这篇文章是原生与前端共舞模块的第一篇,我来跟你聊聊前端和原生之间的桥梁 JavaScriptCore,看看它是什么、能做哪些事儿。
|
||||
|
||||
总结来说,JavaScriptCore 为原生编程语言 Objective-C、Swift 提供调用 JavaScript 程序的动态能力,还能为 JavaScript 提供原生能力来弥补前端所缺能力。
|
||||
|
||||
正是因为JavaScriptCore的这种桥梁作用,所以出现了很多使用 JavaScriptCore 开发 App 的框架 ,比如React Native、Weex、小程序、WebView Hybird等框架。
|
||||
|
||||
接下来,我们再回过头来看看,JavaScriptCore 的来头是啥,为什么这些框架不约而同地都要用 JavaScriptCore 引擎来作为前端和原生的桥梁呢?
|
||||
|
||||
要回答这个问题的话,你还需要**了解JavaScriptCore 的背景**。
|
||||
|
||||
JavaScriptCore,原本是 WebKit中用来解释执行 JavaScript 代码的核心引擎。解释执行 JavaScript 代码的引擎自 JavaScript 诞生起就有,不断演进,一直发展到现在,如今苹果公司有 JavaScriptCore 引擎、谷歌有 V8 引擎、Mozilla 有 SpiderMonkey。对于 iOS 开发者来说,你只要深入理解苹果公司的 JavaScriptCore 框架就可以了。
|
||||
|
||||
iOS7 之前,苹果公司没有开放 JavaScriptCore 引擎。如果你想使用 JavaScriptCore 的话,需要手动地从开源WebKit 中编译出来,其接口都是 C 语言,这对于iOS开发者来说非常不友好。
|
||||
|
||||
但是从iOS7开始,苹果公司开始将 JavaScriptCore 框架引入 iOS 系统,并将其作为系统级的框架提供给开发者使用。这时,接口使用 Objective-C 进行包装,这对于原生 Objective-C 开发者来说,体验上就非常友好了。
|
||||
|
||||
JavaScriptCore 框架的框架名是 JavaScriptCore.framework。由于苹果公司的系统已经内置了JavaScriptCore 框架,而且性能不逊色于 V8 和 SpiderMonkey 等其他引擎,所以前端开发 App 框架就都不约而同将 JavaScriptCore 框架作为自己和原生的桥梁。
|
||||
|
||||
接下来,我就和你详细分析一下JavaScriptCore框架的原理。
|
||||
|
||||
## JavaScriptCore 框架
|
||||
|
||||
苹果官方对JavaScriptCore框架的说明,你可以点击[这个链接](https://developer.apple.com/documentation/javascriptcore)查看。从结构上看,JavaScriptCore 框架主要由 JSVirtualMachine 、JSContext、JSValue类组成。
|
||||
|
||||
JSVirturalMachine的作用,是为 JavaScript 代码的运行提供一个虚拟机环境。在同一时间内,JSVirtualMachine只能执行一个线程。如果想要多个线程执行任务,你可以创建多个 JSVirtualMachine。每个 JSVirtualMachine 都有自己的 GC(Garbage Collector,垃圾回收器),以便进行内存管理,所以多个 JSVirtualMachine 之间的对象无法传递。
|
||||
|
||||
JSContext 是 JavaScript 运行环境的上下文,负责原生和 JavaScript 的数据传递。
|
||||
|
||||
JSValue 是 JavaScript 的值对象,用来记录 JavaScript 的原始值,并提供进行原生值对象转换的接口方法。
|
||||
|
||||
JSVirtualMachine、JSContext、JSValue 之间的关系,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/24/00306bd8193cd606d6e62340cffcbb24.png" alt="">
|
||||
|
||||
可以看出,JSVirtualMachine 里包含了多个 JSContext, 同一个JSContext 中又可以有多个 JSValue。
|
||||
|
||||
JSVirtualMachine 、JSContext、JSValue 类提供的接口,能够让原生应用执行 JavaScript 代码,访问 JavaScript 变量,访问和执行 JavaScript 函数;也能够让 JavaScript 执行原生代码,使用原生输出的类。
|
||||
|
||||
那么,**解释执行 JavaScript 代码的 JavaScriptCore 和原生应用是怎么交互的呢?**
|
||||
|
||||
要理解这个问题,我们先来看看下面这张图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/c3/316d9ba836fd6fd14155e941e21b27c3.png" alt="">
|
||||
|
||||
可以看到,每个 JavaScriptCore 中的 JSVirtualMachine 对应着一个原生线程,同一个 JSVirtualMachine 中可以使用 JSValue 与原生线程通信,遵循的是JSExport协议:原生线程可以将类方法和属性提供给 JavaScriptCore 使用,JavaScriptCore 可以将JSValue提供给原生线程使用。
|
||||
|
||||
JavaScriptCore 和原生应用要想交互,首先要有 JSContext。JSContext 直接使用 init 初始化,会默认使用系统创建的 JSVirtualMachine。如果 JSContext 要自己指定使用哪个 JSVirtualMachine,可以使用 initWithVirtualMachine 方法来指定,代码如下:
|
||||
|
||||
```
|
||||
// 创建 JSVirtualMachine 对象 jsvm
|
||||
JSVirtualMachine *jsvm = [[JSVirtualMachine alloc] init];
|
||||
// 使用 jsvm 的 JSContext 对象 ct
|
||||
JSContext *ct = [[JSContext alloc] initWithVirtualMachine:jsvm];
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示,首先初始化一个 JSVirtualMachine 对象 jsvm,再初始化一个使用 jsvm 的 JSContext 对象 ct。
|
||||
|
||||
下面我再举一个**通过JavaScriptCore在原生代码中调用JavaScript变量的例子**。
|
||||
|
||||
这里有一段 JavaScript 代码,我定义了一个 JavaScript 变量 i ,然后我们一起看看如何通过 JavaScriptCore 在原生中调用变量i。代码如下:
|
||||
|
||||
```
|
||||
JSContext *context = [[JSContext alloc] init];
|
||||
// 解析执行 JavaScript 脚本
|
||||
[context evaluateScript:@"var i = 4 + 8"];
|
||||
// 转换 i 变量为原生对象
|
||||
NSNumber *number = [context[@"i"] toNumber];
|
||||
NSLog(@"var i is %@, number is %@",context[@"i"], number);
|
||||
|
||||
```
|
||||
|
||||
上面代码中,JSContext 会调用 evaluateScript 方法,返回 JSValue 对象。
|
||||
|
||||
JSValue 类提供了一组将 JavaScript 对象值类型转成原生类型的接口,你可以点击[这个链接](https://developer.apple.com/documentation/javascriptcore/jsvalue),查看官方文档中对 JSValue 接口的详细说明。
|
||||
|
||||
其中,有3个转换类型的接口比较常用,我来和你稍作展开:
|
||||
|
||||
- 在这个示例中,我们使用的是 toNumber 方法,来将 JavaScript 值转换成 NSNumber 对象。
|
||||
- 如果 JavaScript 代码中的变量是数组对象,可以使用 toArray方法将其转换成 NSArray对象。
|
||||
- 如果变量是 Object类型,可以使用 toDictionary方法将其转换成 NSDictionary。
|
||||
|
||||
如果你想在原生代码中使用JavaScript 中的函数对象,可以通过 callWithArguments 方法传入参数,然后实现它的调用。使用示例如下:
|
||||
|
||||
```
|
||||
// 解析执行 JavaScript 脚本
|
||||
[context evaluateScript:@"function addition(x, y) { return x + y}"];
|
||||
// 获得 addition 函数
|
||||
JSValue *addition = context[@"addition"];
|
||||
// 传入参数执行 addition 函数
|
||||
JSValue *resultValue = [addition callWithArguments:@[@(4), @(8)]];
|
||||
// 将 addition 函数执行的结果转成原生 NSNumber 来使用。
|
||||
NSLog(@"function is %@; reslutValue is %@",addition, [resultValue toNumber]);
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示:首先,JSContext 通过 evaluateScript 方法获取 JavaScript 代码中 addition 函数,并保存为一个 JSValue 对象;然后,通过 JSValue 的 callWithArguments 方法,传入 addition 函数所需参数 x、y 以执行函数。
|
||||
|
||||
而如果要在原生代码中调用JavaScript 全局函数,你需要使用 JSValue 的 invokeMethod:withArguments 方法。比如,[Weex](https://github.com/apache/incubator-weex/)框架就是使用这个方法,来获取 JavaScript 函数的。
|
||||
|
||||
相关代码路径是 incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXJSCoreBridge.mm ,核心代码如下:
|
||||
|
||||
```
|
||||
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args {
|
||||
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
|
||||
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
可以看到,JSContext 中有一个 globalObject 属性。globalObject 是 JSValue 类型,里面记录了 JSContext 的全局对象,使用 globalObject 执行的 JavaScript 函数能够使用全局 JavaScript 对象。因此,通过 globalObject 执行 invokeMethod:withArguments 方法就能够去使用全局 JavaScript 对象了。
|
||||
|
||||
通过上面的分析,我们可以知道,通过 evaluateScript 方法,就能够在原生代码中执行 JavaScript 脚本,并使用 JavaScript 的值对象和函数对象。那么,**JavaScript又是如何调用原生代码呢?**
|
||||
|
||||
我先给出一段代码示例,你可以思考一下是如何实现的:
|
||||
|
||||
```
|
||||
// 在 JSContext 中使用原生 Block 设置一个减法 subtraction 函数
|
||||
context[@"subtraction"] = ^(int x, int y) {
|
||||
return x - y;
|
||||
};
|
||||
|
||||
// 在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数
|
||||
JSValue *subValue = [context evaluateScript:@"subtraction(4,8);"];
|
||||
NSLog(@"substraction(4,8) is %@",[subValue toNumber]);
|
||||
|
||||
```
|
||||
|
||||
可以看出,JavaScript 调用原生代码的方式,就是:
|
||||
|
||||
- 首先,在 JSContext 中使用原生 Block 设置一个减法函数subtraction;
|
||||
- 然后,在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数。
|
||||
|
||||
除了 Block外,我们还可以通过 JSExport 协议来实现在JavaScript中调用原生代码,也就是原生代码中让遵循JSExport协议的类,能够供 JavaScript 使用。Weex 框架里,就有个遵循了 JSExport 协议的WXPolyfillSet 类,使得JavaScript 也能够使用原生代码中的 NSMutableSet 类型。
|
||||
|
||||
WXPolyfillSet 的头文件代码路径是 incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXPolyfillSet.h ,内容如下:
|
||||
|
||||
```
|
||||
@protocol WXPolyfillSetJSExports <JSExport>
|
||||
|
||||
// JavaScript 可以使用的方法
|
||||
+ (instancetype)create;
|
||||
- (BOOL)has:(id)value;
|
||||
- (NSUInteger)size;
|
||||
- (void)add:(id)value;
|
||||
- (BOOL)delete:(id)value;
|
||||
- (void)clear;
|
||||
|
||||
@end
|
||||
|
||||
// WXPolyfillSet 遵循 JSExport 协议
|
||||
@interface WXPolyfillSet : NSObject <WXPolyfillSetJSExports>
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
可以看到,WXPolyfillSet 通过 JSExport 协议,提供了一系列方法给 JavaScript 使用。
|
||||
|
||||
现在我们已经理解了原生和 JavaScript 的互通方式,知道了它们的互通依赖于虚拟机环境JSVirtualMachine。接下来,我们需要对JavaScriptCore引擎进行更深入地理解,才能更好地用好这个框架。比如,JavaScriptCore 是怎么通过直接使用缓存 JIT 编译的机器码来提高性能的,又是怎么对部分函数进行针对性测试编译优化的。
|
||||
|
||||
JSVirtualMachine 是一个抽象的 JavaScript 虚拟机,是提供给开发者进行开发的,而其核心的 **JavaScriptCore 引擎则是一个真实的虚拟机,包含了虚拟机都有的解释器和运行时部分**。其中,解释器主要用来将高级的脚本语言编译成字节码,运行时主要用来管理运行时的内存空间。当内存出现问题,需要调试内存问题时,你可以使用 JavaScriptCore 里的 Web Inspector,或者通过手动触发 Full GC 的方式来排查内存问题。
|
||||
|
||||
接下来,我跟你说下 JavaScriptCore 引擎内部的组成。
|
||||
|
||||
## JavaScriptCore 引擎的组成
|
||||
|
||||
JavaScriptCore内部是由 Parser、Interpreter、Compiler、GC 等部分组成,其中 Compiler 负责把字节码翻译成机器码,并进行优化。你可以点击[这个链接](https://trac.webkit.org/wiki/JavaScriptCore),来查看WebKit 官方对JavaScriptCore 引擎的介绍。
|
||||
|
||||
JavaScriptCore 解释执行 JavaScript 代码的流程,可以分为两步。
|
||||
|
||||
第一步,由 Parser 进行词法分析、语法分析,生成字节码。
|
||||
|
||||
第二步,由 Interpreter 进行解释执行,解释执行的过程是先由 LLInt(Low Level Interpreter)来执行 Parser 生成的字节码,JavaScriptCore 会对运行频次高的函数或者循环进行优化。优化器有 Baseline JIT、DFG JIT、FTL JIT。对于多优化层级切换, JavaScriptCore 使用 OSR(On Stack Replacement)来管理。
|
||||
|
||||
如果你想更深入地理解JavaScriptCore 引擎的内容,可以参考我以前的一篇博文“[深入剖析 JavaScriptCore](https://ming1016.github.io/2018/04/21/deeply-analyse-javascriptcore/)”。
|
||||
|
||||
## 小结
|
||||
|
||||
今天这篇文章,我主要和你分享的是 iOS 中 JavaScriptCore 能干的事情。
|
||||
|
||||
总结来说,JavaScriptCore 提供了前端与原生相互调用的接口,接口层上主要用的是 JSContext 和 JSValue 这两个类,通过 JSValue 的 evaluateScript 方法、Block 赋值 context、JSExport 协议导出来达到互通的效果。
|
||||
|
||||
前端的优势在于快速编写UI,原生的优势在于对平台特性的天然支持,现在我们有了能够打通前端和原生的武器,就可以充分利用二者的优势,互为补充地去做更多、更有意思的事儿。而你,也可以充分发挥自己的想象力,去实现更有意思的App。
|
||||
|
||||
## 课后作业
|
||||
|
||||
如果原生方法没有遵循 JSExport 协议,也没有使用 Block 方式设置给 JSContext,那还有没有其他办法可以在JavaScript中调用原生代码呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<audio id="audio" title="40 | React Native、Flutter 等,这些跨端方案怎么选?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/e3/f5e5aad44d57f0b6ae9878b52eb670e3.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
为了一份代码能够运行在多个平台,从而节省开发和沟通成本,各公司都开始关注和使用跨端方案。目前,主流的跨端方案,主要分为两种:一种是,将 JavaScriptCore 引擎当作虚拟机的方案,代表框架是 React Native;另一种是,使用非 JavaScriptCore 虚拟机的方案,代表框架是 Flutter。
|
||||
|
||||
使用跨端方案进行开发,必然会替代原有平台的开发技术,所以我们在选择跨端方案时,不能只依赖于某几项指标,比如编程语言、性能、技术架构等,来判断是否适合自己团队和产品,更多的还要考虑开发效率、社区支持、构建发布、 DevOps、 CI 支持等工程化方面的指标。
|
||||
|
||||
所以说,我们在做出选择时,既要着眼于团队现状和所选方案生态,还要考虑技术未来的发展走向。
|
||||
|
||||
接下来,我就以React Native和Flutter为例,和你说明如何选择适合自己的跨端方案。
|
||||
|
||||
## React Native框架的优势
|
||||
|
||||
跨端方案的初衷是要解决多平台重复开发的问题,也就是说,使用跨端方案的话,多个平台的开发者可以使用相同的开发语言来开发适合不同系统的App。
|
||||
|
||||
React Native 使用 JavaScript 语言来开发,Flutter 使用的是 [Dart 语言](https://dart.dev/guides/language/language-tour)。这两门编程语言,对 iOS 开发者来说都有一定的再学习成本,而使用何种编程语言,其实决定了团队未来的技术栈。
|
||||
|
||||
JavaScript 的历史和流行程度都远超 Dart ,生态也更加完善,开发者也远多于 Dart 程序员。所以,从编程语言的角度来看,虽然 Dart 语言入门简单,但从长远考虑,还是选择React Native 会更好一些。
|
||||
|
||||
同时,从页面框架和自动化工具的角度来看,React Native也要领先于 Flutter。这,主要得益于 Web 技术这么多年的积累,其工具链非常完善。前端开发者能够很轻松地掌握 React Native,并进行移动端 App 的开发。
|
||||
|
||||
当然,方案选择如同擂台赛,第一回合的输赢无法决定最后的结果。
|
||||
|
||||
## Flutter框架的优势
|
||||
|
||||
除了编程语言、页面框架和自动化工具以外,React Native 的表现就处处不如 Flutter 了。总体来说,相比于React Native框架,Flutter的优势最主要体现在性能、开发效率和体验这两大方面。
|
||||
|
||||
**Flutter的优势,首先在于其性能。**
|
||||
|
||||
我们先从最核心的**虚拟机**说起吧。
|
||||
|
||||
React Native 所使用的 JavaScriptCore, 原本用在浏览器中,用于解释执行网页中的JavaScript代码。为了兼容 Web 标准留下的历史包袱,无法专门针对移动端进行性能优化。
|
||||
|
||||
Flutter 却不一样。它一开始就抛弃了历史包袱,使用全新的 Dart 语言编写,同时支持 AOT 和 JIT两种编译方式,而没有采用HTML/CSS/JavaScript 组合方式开发,在执行效率上明显高于 JavaScriptCore 。
|
||||
|
||||
除了编程语言的虚拟机,Flutter的优势还体现在**UI框架的实现**上。它重写了UI 框架,从 UI 控件到渲染,全部重新实现了,依赖 Skia 图形库和系统图形绘制相关的接口,保证了不同平台上能有相同的体验。
|
||||
|
||||
想要了解 Flutter 的布局和渲染,你可以看看这两个视频“[The Mahogany Staircase - Flutter’s Layered Design](https://www.youtube.com/watch?v=dkyY9WCGMi0)”和“[Flutter’s Rendering Pipeline](https://www.youtube.com/watch?v=UUfXWzp0-DU&t=1955s)”。
|
||||
|
||||
**除了性能上的优势外,Flutter在开发效率和体验上也有很大的建树**。
|
||||
|
||||
凭借热重载(Hot Reload)这种极速调试技术,极大地提升了开发效率,因此Flutter 吸引了大量开发者的眼球。
|
||||
|
||||
同时,Flutter因为重新实现了UI框架,可以不依赖 iOS 和 Android 平台的原生控件,所以无需专门去处理平台差异,在开发体验上实现了真正的统一。
|
||||
|
||||
此外,Flutter 的学习资源也非常丰富。Flutter的[官方文档](https://flutter.dev/docs),分门别类整理得井井有条。YouTube 上有一个专门的[频道](https://www.youtube.com/flutterdev),提供了许多讲座、演讲、教程资源。
|
||||
|
||||
或许,你还会说Flutter 包大小是个问题。Flutter的渲染引擎是自研的,并没有用到系统的渲染,所以App包必然会大些。但是,我觉得从长远来看,App Store对包大小的限制只会越来越小,所以说这个问题一定不会成为卡点。
|
||||
|
||||
**除了上面两大优势外,我再和你说说Flutter对动态化能力的支持。**
|
||||
|
||||
虽然 Flutter 计划会推出动态化能力,但我觉得动态化本身就是一个伪命题。软件架构如果足够健壮和灵活,发现问题、解决问题和验证问题的速度一定会非常快,再次发布上线也能够快速推进。而如果软件架构本就一团糟,解决问题的速度是怎么也快不起来的,即使具有了动态化能力,从解决问题到灰度发布再到全量上线的过程也一定会很曲折。
|
||||
|
||||
所以,我认为如果你想通过动态化技术来解决发布周期不够快的问题的话,那你首先应该解决的是架构本身的问题。长远考虑,架构上的治理和优化带来的收益,一定会高于使用具有动态化能力的框架。
|
||||
|
||||
当然,如果你选择使用动态化能力的框架,是抱着绕过App Store审核的目的,那就不在本文的讨论范围之内了。
|
||||
|
||||
## 如何选择适合自己的跨端方案?
|
||||
|
||||
看到这,你一定在想,跨端方案不是只有 Rect Native 和 Flutter,还有小程序、快应用、Weex 等框架。没错,跨端方案确实有非常多。
|
||||
|
||||
但,**我今天与你分享的 React Native 代表了以 JavaScriptCore 引擎为虚拟机的所有方案,对于这一类方案的选择来说,道理都大同小异**。只要你打算转向前端开发,选择它们中的哪一个方案都差不多,而且方案间的切换也很容易。
|
||||
|
||||
着眼未来,决定跨端方案最终赢家的关键因素,不是编程语言,也不是开发生态,更不是开发者,而是用户。
|
||||
|
||||
如果谷歌的新系统 Fuchsia 能够如谷歌所计划的五年之内应用到移动端的话,那么五年后即使使用 Fuchsia 的用户只有10%,你的 App 也要去支持 Fuchsia。Fuchsia 系统的最上层就是 Flutter,这时使用 Flutter 来开发 App就成了首选。而Flutter 本身就是一种跨端方案,一旦使用Flutter开发成为团队的必选项,那么其他技术栈就没有存在的价值了。
|
||||
|
||||
其实,我本人还是很看好 Fuchsia 系统的。它的内核是 Zircon,Fuchsia 是整个系统的统称,在 Fuchsia 技术的选择上,谷歌选择了微内核、优于 OpenGL 高内核低开销的图像接口 Vulkan、3D 桌面渲染 Scenic、Flutter 开发框架。谷歌的打算是,三年内在一些非主流的设备上对 Fuchsia 内核进行完善,待成熟后推向移动端。
|
||||
|
||||
Fuchsia 架构分为四层,包括微内核的第一层 Zircon,提供系统服务的第二层 Garnet,用户体验基础设施的第三层 Peridot,Flutter所在基础应⽤的第四层 Topaz。结合 Android 系统的经验,在设计架构之初,谷歌就考虑了厂商对深度定制的诉求,使得每层都可以进行替换,模块化做得比 Android系统更加彻底。
|
||||
|
||||
Fuchsia 架构,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/e3/0deca9e023f5e092824e6b44808dc7e3.png" alt="">
|
||||
|
||||
你可以通过这个视频,查看[Fuchsia 最近的动向](https://youtu.be/83SDXL65W9k)。如果你有 Pixel 3 XL 手机,可以动手尝试下。你可以点击[这个链接](https://github.com/Pixel3Dev/zircon-pixel3),来查看支持 Pixel 3 XL 的 Fuchsia 项目。Fuchsia 官方 Git 仓库的地址是[https://fuchsia.googlesource.com](https://fuchsia.googlesource.com),你可以点击查看其源码。
|
||||
|
||||
当然,不管操作系统多么牛,最后还要由用户来选。
|
||||
|
||||
所以,跨端技术方案的赢家是谁,最终还是要看使用移动设备的用户选择了谁,就好像游戏机市场中的 Nintendo Switch 和 PlayStation Vita。PlayStation Vita 在硬件、性能、系统各方面都领先 Nintendo Switch,但最终游戏开发者还是选择在 Nintendo Switch 上开发,而这其实都取决于购买游戏机的玩家。当 Nintendo Switch 成为了流行和热点以后,所有的游戏开发者都会跟着它走。
|
||||
|
||||
虽然我们不能决定未来,但我们可以去预测,然后选择一款大概率会赢的跨端框架,以此来奠定自己的竞争力。
|
||||
|
||||
## 总结
|
||||
|
||||
在今天这篇文章中,我将跨平台方案分成了两种:一种是,将 JavaScriptCore 引擎当作虚拟机的方案,代表框架是 React Native;另一种是,使用非 JavaScriptCore 虚拟机的方案,代表框架是 Flutter。
|
||||
|
||||
然后,在此基础上,我从编程语言、性能、开发效率和体验等方面和你分析了这两类方案。但是,选择一款适合自己团队的跨平台开发方案,仅仅考虑这几个方面还不够,我们还要着眼于未来。
|
||||
|
||||
在我看来,从长远考虑的话,你可以选择 Flutter作为跨平台开发方案。但是,最终 Flutter 是否能成功,还要看谷歌新系统 Fuchsia 的成败。
|
||||
|
||||
## 课后作业
|
||||
|
||||
如果最终 Fuchsia 失败了,而 iOS 继续突飞猛进,SwiftUI也支持跨端了,那你也就不用换技术栈了,继续使用 Swift 开发就好了。你对此是什么看法呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。<br>
|
||||
|
||||
125
极客时间专栏/iOS开发高手课/原生与前端共舞/41 | 原生布局转到前端布局,开发思路有哪些转变?.md
Normal file
125
极客时间专栏/iOS开发高手课/原生与前端共舞/41 | 原生布局转到前端布局,开发思路有哪些转变?.md
Normal file
@@ -0,0 +1,125 @@
|
||||
<audio id="audio" title="41 | 原生布局转到前端布局,开发思路有哪些转变?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/b6/804047978a05ea2cffc7add1387a4eb6.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我来跟你聊聊原生布局转到前端布局的过程中,开发思路会有哪些转变。
|
||||
|
||||
最开始的时候,iOS 原生布局只支持通过横纵坐标和宽高确定布局的方式,后来引入了 Auto Layout 来优化布局。但,Auto Layout 的写法繁琐,开发者需要编写大量的代码,无法将精力集中在界面布局本身。再后来,苹果公司意识到 Auto Layout的这个问题,于是推出了VFL(Visual Format Language,可视化格式语言)来简化 Auto Layout 的写法。
|
||||
|
||||
其实,包装 Auto Layout 的第三方库,通过支持链式写法,也能达到简化编写 Auto Layout 的目的。
|
||||
|
||||
比如,适用于 Objective-C 的 [Masonry](https://github.com/SnapKit/Masonry) 和适用于 Swift 的 [SnapKit](https://github.com/SnapKit/SnapKit),都是非常优秀的第三方库。这两个库的实际使用数量,明显高于苹果自身推出的 VFL。关于这两个库的实现原理和源码分析,你可以查看我以前写的“[读 SnapKit 和 Masonry 自动布局框架源码](https://ming1016.github.io/2018/04/07/read-snapkit-and-masonry-source-code/)”这篇文章。
|
||||
|
||||
## UIStackView
|
||||
|
||||
虽然 Masonry 和 SnapKit 能够简化布局写法,但和前端的布局思路相比,Auto Layout 的布局思路还处在处理两个视图之间关系的初级阶段,而前端的 Flexbox 已经进化到处理一组堆栈视图关系的地步了。
|
||||
|
||||
>
|
||||
关于 Flexbox 布局的思路,我在[第27篇文章](https://time.geekbang.org/column/article/94708)中已经跟你详细分析过了。你可以借此机会再复习一下相关内容。
|
||||
|
||||
|
||||
苹果公司也意识到了这一点,于是借鉴Flexbox 的思路创造了 UIStackView,来简化一组堆栈视图之间的关系。
|
||||
|
||||
和 Flexbox 一样,按照UIStackView设置的规则,一组堆栈视图在可用空间中进行动态适应。这组视图按照堆栈中的顺序,沿着轴的方向排列。这里的轴,可以设置为横轴或纵轴。所以,UIStackView 和 Flexbox布局框架一样,布局都取决于这组堆栈视图设置的各个属性,比如轴方向、对齐方式、间距等等。
|
||||
|
||||
UIStackView虽然在布局思路上,做到了和Flexbox对齐,但写法上还是不够直观。前端布局通过 HTML + CSS 组合,增强了界面布局的可读性。那么,苹果公司打算如何让自己的布局写法也能和Flexbox一样既简洁,可读性又强呢?
|
||||
|
||||
## SwiftUI
|
||||
|
||||
在WWDC 2019 上,苹果公司公布了新的界面布局框架 [SwiftUI](https://developer.apple.com/xcode/swiftui/)。SwiftUI在写法上非常简洁,可读性也很强。
|
||||
|
||||
GitHub 上有个叫 [About-SwiftUI](https://github.com/Juanpe/About-SwiftUI) 的项目,收集了 SwiftUI的相关资料,包括官方文档教程、WWDC SwiftUI 相关视频、相关博客文章、基于 SwiftUI 开源项目、各类视频,非常齐全,可以全方位地满足你的学习需求。
|
||||
|
||||
除了支持简洁的链式调用外,它还通过 DSL 定制了 UIStackView 的语法。这套 DSL 的实现,使用的是 Function Builders 技术,可以让 DSL 得到编译器的支持。有了这样的能力,可以说苹果公司未来可能会诞生出更多编译器支持的特定领域 DSL。
|
||||
|
||||
可以想象,未来 iOS 的开发会更加快捷、方便,效率提高了,门槛降低了,高质量 App的数量也会增加。这,也是苹果公司最想看到的吧。
|
||||
|
||||
至此,**原生布局的开发思路从布局思路优化转向了 DSL。**
|
||||
|
||||
DSL 编写后的处理方式分为两种:
|
||||
|
||||
- 一种是,通过解析将其转化成语言本来的面目,SwiftUI 使用的就是这种方式;
|
||||
- 另一种是,在运行时解释执行 DSL。SQL 就是在运行时解释执行的 DSL。
|
||||
|
||||
对于这两种 DSL,我都实践过。所以接下来,我就跟你分享下我以前对这两种 DSL 的实现。理解了这两种实现方式以后,你也就可以根据项目的实际情况去选择适合自己的方式。
|
||||
|
||||
## 解析转换 DSL
|
||||
|
||||
在做iOS开发之前,我做过很长一段时间的前端开发。转到 iOS 开发后,我就一直觉得布局思路不如前端简单,编写也不够简洁。于是,我就想能不能通过 Flexbox 这种布局思路将前端和原生结合在一起,使用前端 HTML + CSS 的组合作为布局 DSL,通过解析将其转换成原生代码。
|
||||
|
||||
后来,我按照这个思路实现了一个项目,叫作[HTN](https://github.com/ming1016/HTN)(HTML To Native):通过解析 HTML ,将其生成 DOM 树,然后解析 CSS,生成渲染树,最后计算布局生成原生 Texture 代码。
|
||||
|
||||
下图展示的是,我借鉴Flexbox布局思路,使用 HTML + CSS编写的在浏览器中的显示页面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/9b/c7ba9448393eccf6570dd59a445afe9b.png" alt="">
|
||||
|
||||
可以看到,通过 Inspect 观察,HTML 结合 CSS 能够简洁直观地描述界面元素的各种属性和多组界面元素的布局。
|
||||
|
||||
通过 HTN 的转换生成的代码,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/cd/dfbb67ef7933057a5ee33cd094eb0bcd.png" alt="">
|
||||
|
||||
可以看出,和前端代码相比,原生 Texture的代码繁琐、难读。转换后的完整代码在 HTN 工程中的路径是 HTN/HTNExamples/Sample/Flexbox.m。编译后的效果如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/0a/a6734e0f8577545a1e95bdd4ae5d550a.png" alt="">
|
||||
|
||||
可以看到,手机上显示的内容布局和浏览器上基本一致,从而实现了用前端布局编写原生界面布局的目标。
|
||||
|
||||
我专门写了一篇文章用于记录这个项目的开发过程,“[HTML 转原生 HTN 项目开发记录](https://ming1016.github.io/2017/10/16/html-to-native-htn-development-record/)”,你可以参考解析 HTML 生成 DOM 树的部分,解析使用的是状态机,能够很轻松地处理复杂的逻辑判断。
|
||||
|
||||
HTML 是标准界面布局 DSL,语法上还是会有些啰嗦,这也是 XML 格式和 JSON 格式的区别。基于这点,我设计了一个基于前端布局思想的 DSL,同时编写了能够解释执行这个 DSL 的程序。之所以不使用 JSON,是为了在运行时对 DSL 的解释更快。在这个项目里,我精简了冗余的格式。
|
||||
|
||||
另外,GitHub 上有个利用 Swift 5.1 的 Function Builders 开发了一个能通过 Swift DSL 编写 HTML 的项目 [Vaux](https://github.com/dokun1/Vaux)。你也可以通过这个项目学习如何自定义一个 Swift DSL。
|
||||
|
||||
接下来,我和你说说我对第二种运行时解释执行的 DSL ,是怎么设计实现的。
|
||||
|
||||
## 运行时解释执行 DSL
|
||||
|
||||
我设计的这个 DSL 库,叫作[STMAssembleView](https://github.com/ming1016/STMAssembleView)。对于这种水平居中排列:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/d4/18b321c779c84a0ecfc4afa721bd15d4.png" alt="">
|
||||
|
||||
STMAssembleView 中的 DSL 如下:
|
||||
|
||||
```
|
||||
{
|
||||
hc(padding:30)
|
||||
[(imageName:starmingicon)]
|
||||
[(imageName:starmingicon)]
|
||||
[(imageName:starmingicon)]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面代码中,hc 的两个单词分别表示了轴方向和对齐方式:
|
||||
|
||||
- 第一个字母 h 表示按水平轴方向排列,取的是horizontal 的首字母。如果要表示垂直轴方向排列,可以取 vertical 的首字母,也就是用v表示。
|
||||
- 第二个字母 c 表示居中对齐方式。如果要表示左对齐可以用 l(left),表示右对齐可以用r(right),表示上对齐用 t(top),表示下对齐可以用b(bottom)。
|
||||
|
||||
padding 表示的是,大括号里视图之间的间距。其中大括号表示的是一组堆栈视图的集合,中括号表示的是单个视图单元,中括号内的小括号表示的是描述视图的属性。
|
||||
|
||||
设计的 DSL 解释执行的实现代码,在 STMAssembleView 工程中的代码路径是 STMAssembleView/Classes/STMAssembleView.m。
|
||||
|
||||
## 小结
|
||||
|
||||
总结来说,原生和前端都是面向用户做交互的,只是前端在某些方面,比如布局,比原生发展得要快些。不过,原生后来者居上,通过融合演进、相互促进,实现了原生和前端共舞的局面。由此可以看出,和文化的发展一样,技术只有融合才能够丰富多彩,相互刺激才会进步。
|
||||
|
||||
苹果公司对技术演进节奏的把握和对产品一样,都是一步一步递进。也就是说,新技术都依赖于上一个技术,只有上一个技术完善后才会推出新的技术,而不是一次快速打包推出后再依赖后期完善。
|
||||
|
||||
这样,苹果公司就可以把每一步都做到最好,每次推出的技术都是用户真正想要的。除此之外,一步一步推出技术,有两个好处:一方面可以将眼前的技术做到极致;另一方面,能够有足够时间验证已推功能的完善性,并观察用户下一步需要的是什么,然后通过观察,砍掉计划中的用户不需要的功能,将精力集中在用户急需的功能上,将其做到极致,形成良性循环。
|
||||
|
||||
比如,SwiftUI 可能很早就在苹果公司的计划中了,当时的方案应该远没有现在的优秀,于是苹果公司优先解决 Auto Layout处理视图关系繁琐的问题,推出了UIStackView。之后,苹果公司继续观察用户使用情况,发现仅仅吸取布局思想还不够,编程语言写法不够简洁、编译器没有突破,用户依然不会买单。
|
||||
|
||||
于是,苹果公司推出了语法更加简洁的 Swift 语言和支持 Hot Reload(热重载)的 Playground,得到了很多开发者的认同。这样,原生编写布局就具备了和前端编写布局的基本竞争条件。
|
||||
|
||||
最后只差一个 DSL ,苹果公司就能够将原生布局开发,推到和前端一样的便利程度。就这一步,苹果公司考虑得更加长远:通过一种能和编译器相结合的编程语言特性 Function Builders ,不仅支持了现在的界面开发 DSL,也具备了结合其他领域 DSL 的能力。之所以苹果公司不急着发布SwiftUI,也符合它一贯的作风,没想清楚,做不到极致,就不推出来。
|
||||
|
||||
有了 DSL,配合编译器的Hot Reload 调试强力支持,再加上 Swift 语言本身的优势,最后的胜者不言而喻。
|
||||
|
||||
通过苹果公司从原生布局转到前端布局的思路演进,你会发现,苹果公司对技术演进的判断思考方式很独特,也很有效。这种思想,非常值得我们学习。同时,对于我们开发者来说,苹果公司布局思路的演进,也会推动着我们跟上技术的发展。拥抱技术变化,让开发更高效。
|
||||
|
||||
## 课后作业
|
||||
|
||||
不光 iOS 开发者会用 SwiftUI,macOS 和 iPadOS 的应用开发也会用到。因为写法简单,SwiftUI必将成为广大苹果开发者的首选。因此,你就更加应该好好理解 SwiftUI ,以及它是如何利用 Swift 语言特性来简化代码的。比如,@State 这样的写法到底简化了什么呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
103
极客时间专栏/iOS开发高手课/原生与前端共舞/42 | iOS原生、大前端和Flutter分别是怎么渲染的?.md
Normal file
103
极客时间专栏/iOS开发高手课/原生与前端共舞/42 | iOS原生、大前端和Flutter分别是怎么渲染的?.md
Normal file
@@ -0,0 +1,103 @@
|
||||
<audio id="audio" title="42 | iOS原生、大前端和Flutter分别是怎么渲染的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/d6/3eb8d8ae545770e6789f9844a05c3bd6.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我来和你聊聊iOS原生、大前端和Flutter分别是怎么渲染的。
|
||||
|
||||
用户在使用 App 时,界面的设计、流畅程度是最直接的体验。为此,苹果公司提供了各个层级的库,比如 SwiftUI、UIKit、Core Animation、Core Graphic、OpenGL ,以方便App界面的开发。
|
||||
|
||||
说起来,即使你不了解这些库的实现原理,也可以通过它们提供的易用接口上手去开发 App,特别是 SwiftUI 大大简化了界面的开发,也确实能够解决大部分问题。但是,一旦遇到性能问题,完全依靠搜索获得的不完整的、拼凑来的知识,大概率只能解一时之需,要想系统地解决问题,还是要知道这些库的实现原理。
|
||||
|
||||
而这些与界面相关的库,背后的知识其实就是渲染。接下来,我就和你说说渲染的原理。
|
||||
|
||||
## 渲染原理
|
||||
|
||||
我们看到的 App 界面,都是由 CPU 和 GPU 共同计算处理的。
|
||||
|
||||
CPU 内部流水线结构拥有并行计算能力,一般用于显示内容的计算。而 GPU的并行计算能力更强,能够通过计算将图形结果显示在屏幕像素中。内存中的图形数据,经过转换显示到屏幕上的这个过程,就是渲染。而负责执行这个过程的,就是GPU。
|
||||
|
||||
渲染的过程中,GPU需要处理屏幕上的每一个像素点,并保证这些像素点的更新是流畅的,这就对 GPU 的并行计算能力要求非常高。
|
||||
|
||||
早期,图形渲染是由 VGA(Video Graphics Array,视频图形阵列)来完成的,随着3D加速的需要,带来了比如三角形生成、光栅化、纹理贴图等技术。处理这一系列技术的处理器,就被统称为 GPU。
|
||||
|
||||
GPU的主要工作是将 3D 坐标转化成 2D 坐标,然后再把2D 坐标转成实际像素,具体实现可以分为顶点着色器(确定形状的点)、形状装配(确定形状的线)、几何着色器(确定三角形个数)、光栅化(确定屏幕像素点)、片段着色器(对像素点着色)、测试与混合(检查深度和透明度进行混合)六个阶段。
|
||||
|
||||
为了能够更方便地控制 GPU 的运算,GPU 的可编程能力也不断加强,开始支持 C 和 C++ 语言。通过 OpenGL 标准定义的库,可以更容易地操作 GPU。
|
||||
|
||||
在渲染过程中,CPU 专门用来处理渲染内容的计算,比如视图创建、布局、图片解码等,内容计算完成后,再传输给 GPU 进行渲染。
|
||||
|
||||
在这个过程中,CPU 和 GPU 的相互结合,能够充分利用手机硬件来提升用户使用 App 的体验。当然,在这个过程中,如果CPU 的计算时间超过了屏幕刷新频率要求的时间,界面操作就会变得不流畅。
|
||||
|
||||
那么,如果你想要知道原生、大前端和 Flutter 谁会更流畅,就要分别去了解在渲染过程中,谁的CPU 计算内容会更快。
|
||||
|
||||
接下来,我们先看看原生渲染中的计算。
|
||||
|
||||
## 原生渲染
|
||||
|
||||
原生界面更新渲染的流程,可以分为以下四步。
|
||||
|
||||
**第一步**,更新视图树,同步更新图层树。
|
||||
|
||||
**第二步**,CPU 计算要显示的内容,包括视图创建(设置 Layer 的属性)、布局计算、视图绘制(创建 Layer 的 Backing Image)、图像解码转换。当 runloop 在 BeforeWaiting 和 Exit 时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程 Render Server。
|
||||
|
||||
**第三步**,数据到达 Render Server后会被反序列化,得到图层树,按照图层树中图层顺序、RGBA值、图层 frame 过滤图层中被遮挡的部分,过滤后将图层树转成渲染树,渲染树的信息会转给 OpenGL ES/Metal。前面 CPU 所处理的这些事情统称为 Commit Transaction。
|
||||
|
||||
**第四步**,Render Server 会调用 GPU,GPU 开始进行前面提到的顶点着色器、形状装配、几何着色器、光栅化、片段着色器、测试与混合六个阶段。完成这六个阶段的工作后,再将 CPU 和 GPU 计算后的数据显示在屏幕的每个像素点上。
|
||||
|
||||
整个渲染过程,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d4/8f/d4ae58f7b0d09725757dca84ea7a318f.png" alt="">
|
||||
|
||||
如上图所示,CPU 处理完渲染内容会输入到 Render Server 中,经图层树和渲染树的转换,通过 OpenGL 接口提供给 GPU,GPU 处理完后在屏幕上显示。
|
||||
|
||||
渲染过程中 Commit Trasaction 的布局计算会重载视图 LayoutSubviews 方法,以及执行 addSubview 方法来添加视图。视图绘制会重载视图的 drawRect 方法。这几个方法都是 iOS 开发中常用的。
|
||||
|
||||
移动视图位置、删除视图、隐藏或显示视图、调用 setNeedsDisplay 或 setNeedsDisplayInRect 方法,都会触发界面更新,执行渲染流程。
|
||||
|
||||
这些就是原生渲染计算的所有内容了,接下来我们再一起看看大前端的渲染。
|
||||
|
||||
## 大前端渲染
|
||||
|
||||
大前端的开发框架主要分为两类:第一类是基于 WebView 的,第二类是类似 React Native 的。
|
||||
|
||||
对于第一类 WebView 的大前端渲染,主要工作在 WebKit 中完成。WebKit 的渲染层来自以前 macOS 的 Layer Rendering 架构,而iOS 也是基于这一套架构。所以,从本质上来看,WebKit 和 iOS 原生渲染差别不大。
|
||||
|
||||
第二类的类React Native 更简单,渲染直接走的是iOS 原生的渲染。那么,我们为什么会感觉 WebView 和类React Native 比原生渲染得慢呢?
|
||||
|
||||
**从第一次内容加载来看**,即使是本地加载,大前端也要比原生多出脚本代码解析的工作。
|
||||
|
||||
WebView需要额外解析 HTML + CSS + JavaScript 代码,而类 React Native方案则需要解析JSON + JavaScript。HTML + CSS 的复杂度要高于 JSON,所以解析起来会比 JSON 慢。也就是说,首次内容加载时, WebView 会比类React Native 慢。
|
||||
|
||||
**从语言本身的解释执行性能来看**,大前端加载后的界面更新会通过 JavaScript 解释执行,而JavaScript 解释执行性能要比原生差,特别是解释执行复杂逻辑或大量计算时。所以,大前端的运算速度,要比原生慢不少。
|
||||
|
||||
除了首次加载解析要耗时,以及 JavaScript 语言本身解释慢导致的性能问题外,WebView 的渲染进程是单独的,每帧的更新都要通过 IPC 调用 GPU 进程。频繁的IPC 进程通信也会有性能损耗。
|
||||
|
||||
WebView的单独渲染进程还无法访问 GPU 的 context,这样两个进程就没有办法共享纹理资源。纹理资源无法直接使用 GPU 的 Context 光栅化,那就只能通过 IPC 传给 GPU 进程,这也就导致 GPU 无法发挥自身的性能优势。由于 WebView 的光栅化无法及时同步到 GPU,滑动时容易出现白屏,就很难避免了。
|
||||
|
||||
说完了大前端的渲染,你会发现,相对于原生渲染,无论是 WebView 还是类 React Native都会因为脚本语言本身的性能问题而在存在性能差距。那么,对于 Flutter 这种没有使用脚本语言,并且渲染引擎也是全新的框架,其渲染方式有什么不同,性能又怎样呢?
|
||||
|
||||
## Flutter 渲染
|
||||
|
||||
Flutter 界面是由 Widget 组成的,所有 Widget 组成 Widget Tree,界面更新时会更新 Widget Tree,然后再更新 Element Tree,最后更新 RenderObject Tree。
|
||||
|
||||
接下来的渲染流程,Flutter 渲染在 Framework 层会有 Build、Wiget Tree、Element Tree、RenderObject Tree、Layout、Paint、Composited Layer 等几个阶段。将 Layer 进行组合,生成纹理,使用 OpenGL 的接口向 GPU 提交渲染内容进行光栅化与合成,是在 Flutter 的 C++ 层,使用的是 Skia 库。包括提交到 GPU 进程后,合成计算,显示屏幕的过程和 iOS 原生基本是类似的,因此性能也差不多。
|
||||
|
||||
Flutter 的主要优势,在于它能够同时运行于 Android 和 iOS这两个平台。但是,苹果公司在WWDC 2019 上推出 SwiftUI 和 Preview 后,Flutter 在界面编写和 Hot Reload 上的优势会逐渐降低。
|
||||
|
||||
## 小结
|
||||
|
||||
今天这篇文章,我首先和你说了渲染的原理,然后分别和你展开了原生、大前端、Flutter 是怎么渲染的。整体来看,大前端中的 WebView 方式渲染性能会差些,React Native 和其他方案在渲染上的性能都差不多。
|
||||
|
||||
而关于如何选择这三种开发方案,我的建议是结合自身情况和工作需要来确定就好。
|
||||
|
||||
当你所在团队已经偏向于大前端时,那么你可以选择拥抱变化,毕竟前端技术生态已经非常完善了。
|
||||
|
||||
如果你开始喜欢谷歌的技术,也想多了解 Android 或者谷歌的新操作系统 Fuchsia的话,Flutter 无疑是最好的选择。
|
||||
|
||||
当然,如果你和我一样是一名果粉的话,那我相信苹果公司的产品会不断给你惊喜,可以继续你的iOS 原生开发之旅。相信在 SwiftUI、Project Catalyst、Combine 这些项目的帮助下,你一定能够开发出更多、更优秀的 App 。
|
||||
|
||||
## 课后作业
|
||||
|
||||
在你看来,Chrome 和 WebKit 的渲染引擎,有什么区别呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
109
极客时间专栏/iOS开发高手课/原生与前端共舞/43 | 剖析使 App 具有动态化和热更新能力的方案.md
Normal file
109
极客时间专栏/iOS开发高手课/原生与前端共舞/43 | 剖析使 App 具有动态化和热更新能力的方案.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="43 | 剖析使 App 具有动态化和热更新能力的方案" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/a9/684cd40e986504106c34939e5a6d61a9.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我来和你聊聊iOS开发中的动态化和热更新方案。
|
||||
|
||||
热更新能力的初衷是,能够及时修复线上问题,减少Bug 对用户的伤害。而动态化的目的,除了修复线上问题外,还要能够灵活更新App 版本。
|
||||
|
||||
要实现动态化,就需要具备在运行时动态执行程序的能力。同时,实现了动态化,也就具备了热更新能力。通常情况下,实现动态化的方案有三种,分别是 JavaScriptCore 解释器方案、代码转译方案、自建解释器方案。接下来,我就和你详细说说这三种方案。
|
||||
|
||||
## JavaScriptCore 解释器方案
|
||||
|
||||
iOS 系统内置的JavaScriptCore,是能够在 App 运行过程中解释执行脚本的解释器。
|
||||
|
||||
JavaScriptCore 提供了易用的原生语言接口,配合 iOS 运行时提供的方法替换能力,出现了使用 JavaScript 语言修复线上问题的 [JSPatch](https://github.com/bang590/JSPatch),以及把 JavaScriptCore 作为前端和原生桥梁的 [React Native](https://github.com/facebook/react-native) 和 [Weex](https://github.com/apache/incubator-weex)开发框架。这些库,让 App 具有了动态化能力。
|
||||
|
||||
但是,对于原生开发者来说,只能解释执行 JavaScript 语言的解释器 JSPatch、React Native 等,我们用起来不是很顺手,还是更喜欢用原生语言来开发。那么,有没有办法能够解决语言栈的问题呢?
|
||||
|
||||
## 代码转译方案
|
||||
|
||||
DynamicCocoa 方案将 Objective-C 转换成 JavaScript 代码,然后下发动态执行。这样一来,原生开发者只要使用原生语言去开发调试即可,避免了使用 JavaScript 开发不畅的问题,也就解决了语言栈的问题。
|
||||
|
||||
当然,语言之间的转译过程需要解决语言差异的问题,比如 Objective-C 是强类型,而 JavaScript 是弱类型,这两种语言间的差异点就很多。但,好在 JavaScriptCore 解释执行完后,还会对应到原生代码上,所以我们只要做好各种情况的规则匹配,就可以解决这个问题。
|
||||
|
||||
手段上,语言转译可以使用现有的成熟工具,比如类 C 语言的转译,可以使用LLVM 套件中 Clang 提供的LibTooling,通过重载 HandleTranslationUnit() 函数,使用 RecursiveASTVistor 来遍历 AST,获取代码的完整信息,然后转换成新语言的代码。
|
||||
|
||||
在这里,我无法穷尽两种编程语言间的转译,但是**如果你想要快速了解转译过程的话,最好的方法就是看一个实现的雏形。**
|
||||
|
||||
比如,我以前用 Swift 写过一个 Lisp 语言到 C 语言转译的雏形。你可以点击[这个链接](https://github.com/ming1016/study/tree/master/LispToC),查看具体的代码。通过这个代码,你能够了解到完成转译依次需要用到词法分析器、语法分析器、遍历器、转换器和代码生成器。它们的实现分别对应 LispToC 里的 JTokenizer.swif、JParser.swift、JTraverser.swift、JTransformer.swift和CodeGenerator.swift。
|
||||
|
||||
再比如,你可以查看[SwiftRewrite项目](https://github.com/LuizZak/SwiftRewriter)的完整转译实现。SwiftRewriter 使用 Swift 开发,可以完成 Objective-C 到 Swift 的转换。
|
||||
|
||||
## 自建解释器方案
|
||||
|
||||
可以发现,我在前面提到的JSPatch、React Native等库,到最后能够具有动态性,用的都是系统内置的 JavaScriptCore 来解释执行 JavaScript 语言。
|
||||
|
||||
虽然直接使用内置的 JavaScriptCore 非常方便,但却限制了对性能的优化。比如,系统限制了第三方 App 对 JavaScriptCore JIT(即时编译)的使用。再比如,由于 JavaScript 使用的是弱类型,而类型推断只能在 LLInt 这一层进行,无法得到足够的优化。
|
||||
|
||||
再加上 JSContext 多线程的处理也没有原生多线程处理得高效、频繁的 JavaScriptCore 和原生间的切换、内存管理方式不一致带来的风险、线程管理不一致的风险、消息转发时的解析转换效率低下等等原因,使得JavaScriptCore 作为解释器的方案,始终无法比拟原生。
|
||||
|
||||
**虽然通过引入前端技术栈和利用转译技术能够满足大部分动态化和热修复的需求,但一些对性能要求高的团队,还是会考虑使用性能更好的解释器。**
|
||||
|
||||
如果想要不依赖系统解释器实现动态化和热修复,我们可以集成一个新的解释器,毕竟解释器也是用代码写出来的,使用开源解释器甚至是自己编写解释器,也不是不可以。
|
||||
|
||||
因此,腾讯公司曾公布的 OCS方案,自己实现了一个虚拟器 OCSVM 作为解释器,用来解释执行自定义的字节码指令集语言 OCScript,同时提供了将 Objective-C 转成 OCScript 基于 LLVM 定制的编译器 OCS。
|
||||
|
||||
腾讯公司自研一个解释器的好处,就是可以最大程度地提高动态化的执行效率,能够解释执行针对 iOS 运行时特性定制的字节码指令。这套定制的指令,不光有基本运算指令,还有内存操作、地址跳转、强类型转换指令。
|
||||
|
||||
OCSVM 解释执行 OCScript 指令,能够达到和原生媲美的稳定和高性能,完成运行时 App 的内存管理、解释执行、线程管理等各种任务。OCS 没有开源,所以你无法直接在工程中使用 OCS 方案,但是有些公司自己内部的动态化方案其实就是参考了这个方案。这些方案都没有开源,实现的难度也比较大。
|
||||
|
||||
因此,你想要在工程中使用高效的解释器,最好的方案就是,先找找看有没有其他的开源解释器能够满足需求。
|
||||
|
||||
**这时,如果你仔细思考,一定会想到 LLVM**。LLVM 作为标准的 iOS 编译器套件,对 iOS 开发语言的解析是最标准、最全面的。那么,LLVM 套件里面难道就没有提供一个解释器用来动态解释执行吗?
|
||||
|
||||
按理说,LLVM来实现这个功能是最合适不过了。其实 LLVM 里是有解释器的。
|
||||
|
||||
只不过,ExecutionEngine 里的 Interpreter,是专门用来解释 LLVM IR 的,缺少对 Objective-C 语法特性的支持,所以无法直接使用。除此之外,ExecutionEngine 里还有个 MCJIT,可以通过 JIT 来实现动态化,但因为iOS 系统的限制也无法使用。
|
||||
|
||||
其实,LLVM 之所以没有专门针对 iOS 做解释器,是因为 iOS 动态化在 LLVM 所有工作中的优先级并不高。
|
||||
|
||||
不过,好在 **GitHub 上有一个基于 LLVM 的 C++ 解释器** [**Cling**](https://github.com/root-project/cling)**,可以帮助我们学习怎样通过扩展 LLVM 来自制解释器。**
|
||||
|
||||
解释器分为解释执行 AST 和解释执行字节码两种,其中Cling 属于前者,而 LLVM 自带解释器属于后者。
|
||||
|
||||
从效率上来说,解释执行字节码的方案会更好一些,因为字节码可以在编译阶段进行优化,所以使用 LLVM IR 这种字节码,可以让你无需担心类似寄存器使用效率,以及不断重复计算相同值的问题。LLVM 通过优化器可以提高效率,生成紧凑的 IR。而这些优化都在编译时完成,也就提高了运行时的解释效率。
|
||||
|
||||
那么,LLVM 是怎么做到的呢?
|
||||
|
||||
LLVM IR 是 SSA(Static Single-Assignment,静态单赋值) 形式的,LLVM IR 通过 mem2reg Pass 能够识别 alloca 模式,将局部变量变成 SSA value,这样就不再需要 alloca、load、store 了。
|
||||
|
||||
SSA 主要解决的是,多种数据流分析时种类多、难以维护的问题。它可以提供一种通用分析方法,把数据流和控制流都写在 LLVM IR 里。比如,LLVM IR 在循环体外生成一个 phi 指令,其中每个值仅分配一次,并且用特殊的 phi 节点合并多个可能的值,LLVM 的 mem2reg 传递将我们初始堆栈使用的代码,转成带有虚拟寄存器的 SSA。这样 ,LLVM 就能够更容易地分析和优化 IR 了。
|
||||
|
||||
LLVM 只是静态计算0和1地址,并且只用0和1处理虚拟寄存器。在高级编程语言中,一个函数可能就会有几十个变量要跟踪,虚拟寄存器计算量大后,如何有效使用虚拟寄存器就是一个很大的问题。SSA 形式的 LLVM IR 的 emitter 不用担心虚拟寄存器的使用效率,所有变量都会分配到堆栈里,由 LLVM 去优化。
|
||||
|
||||
其实,我和你分享的OCS 和 Cling 解释器,都是基于 LLVM 扩展实现的。那么,**如果我们不用 LLVM 的话,应该怎么写解释器呢?**
|
||||
|
||||
要了解如何写解释器,就要先了解解释器的工作流程。
|
||||
|
||||
解释器首先将代码编译为字节码,然后执行字节码,对于使用频次多的代码才会使用 JIT 生成机器代码执行。因此,解释器编译的最初目标不是可执行的机器代码,而是专门用在解释器里解释执行的字节码。
|
||||
|
||||
因为编译器编译的机器代码是专门在编译时优化过的,所以解释器的优化就需要推迟到运行时再做。这时,就需要Tracing JIT来跟踪最热的循环优化,比如相同的循环调用超过一百万次,循环就会编译成优化的机器代码。浏览器的引擎,比如 JavaScriptCore、V8,都是基于字节码解释器加上 Tracing JIT 来解释执行 JavaScript 代码的。
|
||||
|
||||
其实,**JIT 技术就是在 App 运行时创建机器代码,同时执行这些机器代码**。编译过程,将高级语言转换成汇编语言,Assembler(汇编器) 会将汇编语言转换成实际的机器代码。
|
||||
|
||||
仅基于字节码的解释器的实现,我们只需要做好解析工作,然后优化字节码和解释字节码的效率,对应上原生的基本方法执行,或者方法替换就可以实现动态化了。
|
||||
|
||||
但是,自己实现 JIT 就难多了,一方面编写代码和维护代码的成本都很高,另一方面还需要支持多 CPU 架构,如果搭载 iOS 系统的硬件 CPU 架构有了更新还要再去实现支持。所以,JIT 的标签和跳转都不对外提供调用。
|
||||
|
||||
那如果要想实现一个自制 JIT 的话,应该如何入手呢?
|
||||
|
||||
用 C++ 库实现的 JIT [AsmJit](https://github.com/asmjit/asmjit),是一个完整的 JIT 和 AOT 的 Assembler,可以生成支持整个x86和x64架构指令集(从 MMX 到 AVX512)的机器代码。AsmJit的体积很小,在300KB 以内,并且没有外部依赖,非常适合用来实现自己的 JIT。使用 AsmJit 库后,我们再自己动手去为字节码编写 JIT 能力的解释器,就更容易了。
|
||||
|
||||
## 小结
|
||||
|
||||
今天这篇文章,我跟你分享了使 App 具有动态化和热更新能力的方案,其中包含了目前大多数项目在使用的 JavaScriptCore 解释器方案。
|
||||
|
||||
但由于 JavaScriptCore 方案更适合前端开发者,于是出现了对原生开发者更友好的代码转译方案,代码转译最终解释执行还是 JavaScriptCore,在效率上会受到种种限制。为了更好的性能,便有了在 App 内集成自建解释器的方案。
|
||||
|
||||
我觉得热更新用哪种方案问题都不大,毕竟只是修复代码。但是,动态化方案的选择,就要更慎重些了,毕竟整个业务都要用。
|
||||
|
||||
动态化方案的选择主要由团队人员自身情况决定,比如原生开发者居多时可以选择代码转译或自建解释器方案;前端开发者居多或者原生开发者有意转向前端开发时,可以选择 JavaScriptCore 方案。
|
||||
|
||||
另外,动态化方案本身,对大团队的意义会更加明显。因为大团队一般会根据业务分成若干小团队,由这些不同团队组成的超级大 App 每次发版,都会相互掣肘,而动态化就能够解决不同团队灵活发版的问题,让各个小团队按照自己的节奏来迭代业务。
|
||||
|
||||
## 课后作业
|
||||
|
||||
如果你负责的 App 出现了线上问题,你是采用什么方案来修复这个问题的呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user