2020 年终总结

2020 对每个人来说都是多变的一年,突如其来的疫情打乱了一切...

总算在新年第一个月结束前写完了这个年终总结,拖延症还是没有改掉。这一年发生了许多事情,就按类别纬度分别写一下吧。

疫情

似乎早在回家之前就听说武汉出现了奇怪的肺炎,每天都在增加新的病例。但是当时所有人都大意了,我当时得知这个事情以后也是丝毫没有在意,感觉与我无关。

说巧不巧,回家以后跟所有同学都约过了,就等春节了。

结果,疫情爆发了,全国范围的...

春节之后我们取消了所有的聚会,走亲访友都没有,就老老实实地在家里待着。因为没有口罩,如果不是为了买生活必备品,出门都不会出。所有人都非常恐慌,每天都在关注新闻,各种媒体、社交网络的报道。

不知道从什么时候开始,疫情仿佛忽然得到了控制。外出吃饭、聚会、看电影又恢复了正常,我们的生活也慢慢恢复平静,好像疫情从未发生一样...

但有些人有些事,我不会忘记...

工作

毕业正好一年,在阿里也呆了一年多的时间,这个时间不长,但对我来说已经足够了。

在家办公

春节之后,疫情爆发了,几乎所有互联网公司都开始了在家办公。我也不例外,在家呆了大约一个月的时间,3 月 1 号才回到杭州,然后又开始为期两周的居家隔离。

在家办公是种很特别的体验,就是你可以在不影响工作的同时想做什么做什么,跟家里人吃饭、逛街、聊聊天。但是唯一要忍受的就是更长的工作时间,因为在家,没有了上下班时间的限制(钉钉要求所有员工 9:30 必须离开公司,否则罚款),大家的工作和生活更融为一体。早晨一睁眼,直接拿过手机就开始开早会;晚上在床上提交完最后一行代码,合上电脑就能睡觉。有的人能承受,有的人忍不了。

矛盾产生

因为在家办公,生活作息都会影响家人,有的时候一个电话会议能从早上 10 点开到下午 1 点,午饭都来不及吃。家里人觉得这种工作不适合长期做。有的时候需要平衡两方的压力,自己确实也会很烦躁。其他的事情记不清了,只记得那时争吵不断...

公司文化

来阿里之前,很多事情没有想清楚:不知道为什么来,不知道下一步该怎么走。可能就觉得,害,阿里牛逼啊。毕业前确实会有这样的学生思维。但是来了以后却发现这里的氛围、文化与我格格不入,我无法说服自己变成他们所谓的“价值观正确”的人。也不是说那种文化不好,没有绝对的好不好,只有自己觉得合不合适。去拼多多的人也不见得全都是要死要活的,看自己怎么选择了。

我为那些真心喜欢阿里文化的人高兴,为那些不喜欢却被无形力量束缚的人惋惜...

开始面试

在确定了这里不再适合自己发展下去之后,我开始了各种面试。这期间面过很多公司,也包括微软。因为完全没有准备(尤其是算法方面),还是在微软第 5 面 failed 掉了。也没错,“如果这真是你的 dream company,你总要为此付出点什么”。

非常幸运的是,剩下几个公司的面试都通过了,感谢这些公司的面试官和 HR。

面试的经历其实没什么好讲的,我没有换新的方向,原来在钉钉做 Mac app 开发,现在继续自己的老本行做 iOS。面试嘛,八股文再复习一遍就没问题了。

绩效 & 离职

再拿到 offer 以后我始终没有跟主管提离职的想法,毕竟绩效还没有出,我不奢望会有多么超出预期的绩效结果,但也不想什么都拿不到地走人。

快 4 月底的时候,绩效终于出了:3.75B。对于这个结果,我其实没有太意外,以 P5 的身份做了很多 P6 应该做的事情,在 BU 整体绩效很好的前提下,给一个 3.75 也不是什么过分的事情。不管是出于什么想法,我感谢那些曾经给予我支持和认可的人。

虽然很无情,我还是跟主管提了离职。中途肯定也经历了各种层级领导的劝说,也有内心的小动摇,但走还是要走的。大概又经历了不到一个月的时间吧,我离开了阿里...

入职新公司

关注过我的朋友应该都知道我离开阿里来了字节,字节对于我来说是一个非常新鲜的地方,用一句话说就是:"It's big, yet simple." 没有那么多令自己不适的文化,同事也都非常年轻。虽然不是完美的,但对我来说尚好。在字节遇到了很多老朋友,不管是同学还是网友,仿佛这个公司让我离圈子更近了,感觉还是很有趣的一件事。

在公司 landing 的时间也比较短,就习惯了这里的工作方式,继续做着一些我擅长的事情。非常有幸在这半年的时间里也收获了一些小惊喜,比如 Spot Bonus,感谢大家的肯定。

技术

工作方面写得看似很多,但文中的部分也只占了现实的 10% 不到。今年在技术方面感觉没有什么太值得写得东西,扒了扒自己之前写的代码,就简单总结一下吧。

钉钉之前想调研接入 Flutter 的可能性,因为我负责 Mac 端,所以也做了很多尝试:比如熟悉 engine 层,尝试修改 embedder 加入对 Metal surface 的支持,Dart 与 native 通讯如何复用现有 JavaScript Bridge 等等。期间还做了很多衍生的小项目,比如用 Rust 写的 IDL parser & code generator,第一次用 parser combinator 的方式写 parser,感觉写起来还是非常爽快的。

然后也做了一些 SwiftUI 和 Swift 的研究,比如 Preference 机制、SIL、Swift 类型系统实现等等,从之前的不了解到现在的一知半解了吧,哈哈。然后也小小地参与了一把 Swift async 提案的讨论和 code review,希望自己的想法可以有机会被放大。

其他的就是一些所谓的屠龙技了,一些看起来很厉害实则没有任何用的东西,比如第一次学会用 C++ template 写快排...

电子产品 & 软件

身为果粉,今年算是把所有常用设备都更新了一遍:

  • Apple Watch Series 6
  • iPhone 12 mini
  • Mac mini (Apple Silicon)

全是今年发布的新产品,iPad 就算了,一方面之前的老款 iPad Pro 一直吃灰,对我来说本来就不是高频使用的产品,另一方面今年 Apple 也没有更新 iPad Pro,明年目测会有更牛逼的新款出现。

我对今年购买的这些产品的感受就是:Leap Forward。尤其是 Mac mini,表现十分亮眼,第一次让我感觉到了 Apple 产品也是有性价比的。iPhone 12 mini 则是用差不多 1/3 的价格购买了一个比 iPhone Xs 更好一点的体验。总的来说,这波不亏。

今年购买的软件倒是不多,大部分都是游戏了,比如 Switch 上的《Overcooked 2》,Xbox 上的《Unravel 2》,还有一些 Steam 游戏。然后升级了一下 Surge v4。

感情

12 月 3 号:我终于跟他在一起了。

本来我已经对这个充满情侣的世界绝望了,因为我是个低社交爱好者,除了同学和朋友,我不会主动接触任何陌生人。平时用工作和研究技术来填满自己的时间,可能也是一种麻痹吧。

直到遇见他...

从来没有那么在乎一个人,那么想为一个人做些什么。也许每个人内心都有一个属于 ta 的天使,非常巧的是,那个天使就出现在了我的面前。我知道,这条爱情道路上必然会有诸多坎坷,但我也知道,没有任何人可以动摇。Because love is love.

今年在一起的时间不长,那个圣诞夜,那个元旦,那些个工作日的夜晚... 那些事情都会留在我们的记忆里,这个故事才刚刚开始,未完待续...

总结

用两个词总结这一年那就是**「Changes & Chances」**,意外的改变创造了新的机遇,而新的机遇又带来新的改变。

2020 年,工作的事情太多。2021 年,希望能投入更多到自己的生活。

从 SwiftUI 谈声明式 UI 与类型系统

Hello, SwiftUI

Apple 在 WWDC19 上正式发布了 Project Catalyst(原 Marzipan),使得开发者能够将 iPadOS app 移植到 macOS 上。同时 SwiftUI 也压轴亮相,正式统一了 Apple 全平台的 UI 开发解决方案。恰逢前些时候,Google 在其 I/O 大会上亮相了 Jetpack Compose —— 一个全新的 Android 原生 UI 开发框架,标志着两大移动操作系统阵营全面拥抱声明式 UI 开发模式。

声明式 UI 的前世今生

其实声明式 UI 并不是什么新技术,早在 2006 年,微软就已经发布了其新一代界面开发框架 WPF,其采用了 XAML 标记语言,支持双向数据绑定、可复用模板等特性。

2010 年,由诺基亚领导的 Qt 团队也正式发布了其下一代界面解决方案 Qt Quick,同样也是声明式,甚至 Qt Quick 起初的名字就是 Qt Declarative。QML 语言同样支持数据绑定、模块化等特性,此外还支持内置 JavaScript,开发者只用 QML 就可以开发出简单的带交互的原型应用。

声明式 UI 框架近年来飞速发展,并且被 Web 开发带向高潮。React 更是为声明式 UI 奠定了坚实基础并一直引领其未来的发展。随后 Flutter 的发布也将声明式 UI 的思想成功带到移动端开发领域...

声明式到底是什么

想象我们要实现下面这个界面:

SwiftUI Previews

打开开关就让下面的 label 显示 on,反之显示 off。如果我们要用非声明式的方式实现,即命令式,那么需要:

1. 创建一个 `UISwitch`,设置它的 change 事件 handler
2. 创建一个 `UILabel`
3. 创建一个 `UIStackView`,设置方向为垂直
4. 将 1、2 创建的两个视图添加到 `UIStackView` 中
5. change 事件触发时读取开关的当前状态,设置相应字符串到 label 中

这样做面对一个状态,我们尚且能够正确处理,但随着应用日渐复杂,状态也越来越多并且错综复杂,状态变化的顺序甚至也能影响应用逻辑的正确性,因为我们对每个事件的处理都是对界面的增量修改。一旦前一个状态有错误,后面就会错上加错,接下来多线程混入,然后 boom,你的应用可能就 crash 了。

声明式的意思就是让我们描述我们需要一个什么样的界面,而不是告诉计算机一步一步干什么。那么上面的例子用声明式就是这样:

“我需要一个界面,它是一个 VStack(垂直布局),里面有一个开关,开关的值与 switchValue 的布尔值绑定,VStack 里接下来是一个 Text,它的值当 switchValue 为 true 时是 foo,否则是 bar”

我们可以发现,全文没有命令,都是在描述界面是怎样的。switchValue 我们称之为 “The Source of Truth”,Toggle 的状态、Text 的文本内容都与它相绑定。状态变化时,界面按照先前描述的重新“渲染”即可得到状态绝对正确的界面。这正是声明式的优势所在,降低状态增加时界面维护的复杂度

SwiftUI 与其他框架的异同

SwiftUI 自亮相以来,全网就在讨论其与 React、Flutter 之间的关系云云。经过这两天的研究,我想简单谈谈我的观点:(免责声明:没有看过源码,也没有参与现场 Lab,一切都是个人想法)

首先是与 Flutter 的对比,Flutter 的思路是从 0 开始,即语言、基础库、渲染引擎、排版引擎即框架本身全部由自己实现,其渲染引擎 Skia 只需要操作系统为止提供一个 GL Context 便可以完成所有图形渲染,这使得其跨平台性变得十分强大,到目前为止 Windows、Linux、macOS、Fuchsia 都已经得到了 Flutter 官方的支持。

这种做法我认为有利有弊,首先好处是所有平台下行为一致,不管是滚动视图、Material Design 控件还是模糊效果这些在其他平台没有的都得到了全平台的支持,开发者并不需要为这些去做平台间的适配,反观 React Native... 当然缺点也是存在的,Flutter 这种做法类似于游戏引擎,平台提供的 UI 特性它一概不用,因此 Flutter View 与原生视图的交互就没有那么容易了,同时新的 Dart 语言貌似也不是非常受社区和开发者喜爱。

SwiftUI 没有像 Flutter 那样从头再来,这个全新的框架依旧使用了 UIKit、AppKit 等作为基础。但它并不是一个 UIKit 的声明式封装,通过 Xcode 的调试视图可以看出这一点:

Xcode View Debugger

许多基础组件,像 Text、Button 等都并不是直接使用 UILabelUIButton 而是一个名为 DisplayList.ViewUpdater.Platform.CGDrawingViewUIView 子类。它们使用了自定义绘制,但又承载于 UIKit 的环境中,因此我猜测 SwiftUI 只提供了组件的自定义渲染和布局引擎,它使用到的底层技术还是 Core Animation、Core Graphics、Core Text 等。使用自定义绘制去实现组件可以理解成为跨平台提供便利,毕竟一个按钮还要区分 UIButtonNSButton 来实现未免有些麻烦。但是部分复杂的控件还是采用了 UIKit 中已有的类,比如 UISwitch 等。由于未脱离 UIKit 体系,嵌入一个 UIView 非常容易,你不需要搞什么外部纹理(Flutter 需要),因为它们的上下文是同一个,坐标系也是同一个。

所以我认为 SwiftUI 更加类似 React Native,使用系统框架提供的组件,只不过绘制和布局可以自己来实现,这在 SwiftUI 之前也有相关的框架这样实践的,比如 YogaComponentKit 等。

SwiftUI 的类型系统

Flutter、React 的类型系统并不是强约束,一个界面里有一个 Text 和有两个 Text 类型是一样的,React 使用 JavaScript 更是无类型。SwiftUI 与它们不同,它使用了强类型约束。举个例子:

VStack {
  Text("Hello")
}

VStack {
  Text("Hello")
  Text("World")
}

VStack {
  Text("Hello")
      .color(Color.red)
}

类型都是不同的。首先上面这种语法叫做 Function Builders,是 Apple “私自”夹带到 Swift 里的私货。上面这些表达式最后都会得到一个实现了 View 协议的具体类型,SwiftUI 里基本使用的都是具体类型,而不是协议类型,首先 VStack 是一个 struct 同时也是一个具体类型,它的构造方法里接受一个闭包,这个闭包使用了通过 @functionBuilder 修饰的 ViewBuilder 结构体作为 builder,因此上面的第二段代码在编译时会被转化成:

VStack {
  let v1 = ViewBuilder.buildExpression(Text("Hello"))
  let v2 = ViewBuilder.buildExpression(Text("World"))

  return ViewBuilder.buildBlock(v1, v2)
}

然后我们看一下上面这个 ViewBuilder.buildBlock 重载的签名:

static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View

所以一个 Text 和两个 Text,它们的父容器 VStack 的类型都是不同的!另外提一下,buildBlock 的范型参数最多有 10 个:

划重点:也就是你的一个视图层级(目前)不能有超过 10 个子视图。 且超过后编译器的错误提示丝毫不会体现这一点,了解这个将会非常节约你的时间!

不同的状态对应的视图也不同,但是它们的类型是相同的,这意味着什么呢?那就是,不需要 Diff-Patch 了

我们想象下面的场景:

VStack {
  if something {
      Text("something is true")
  }
  Text("something else")
  if !something {
      Text("something is not true")
  }
}

something 变化时,视图应该怎么变化?对于 React、Flutter 来说,它们没有类型的概念,每次只能拿到两个快照(一个当前状态的,一个新状态的)。它们有两个选择去完成界面的更新:

1. 把老的视图全部移除,重新添加新视图
2. 找出它们的差异,根据差异去修改视图

第一种方法最简单,但是性能很差,且不能保存视图自身的状态。第二种方法需要高效的算法加持,看起来能解决我们的问题,但是它不是必要的

SwiftUI 的做法是根据类型来更新界面,上面这段代码的类型是:

VStack<TupleView<Text?, Text, Text?>>

有了类型框架就能做静态优化,这类似前端框架 Svelte 和 Vue.js 3.0 所做的一些优化,可以称之为 AOT。

在没有类型的情况下,每次状态变化,界面中都只有两个 Text,只不过内容不一样,这时候框架通过 diff 认为界面中的 Text 控件本身没变,只是内容变了,于是给它们设置了新的内容。

但事实并不是这样,something 变化时,界面显示的 Text 是不同的,中间的 Text 始终显示 “something else”,变化的是它上下两个相邻的 Text。框架拿到新视图时就可以按范型参数的顺序去检查他们的差异:

Before update:
VStack(TupleView(Text(...), Text(...), nil))

After update:
VStack(TupleView(nil, Text(...), Text(...)))

它们的相对位置写在了类型中,这样就能避免中间的视图被修改,没有类型信息或其他元信息,这点是绝对做不到的。

SwiftUI 对于类型做得其实更多,所有的字体调整、位置调整等操作在 SwiftUI 中都是通过 ViewModifier 实现的,调整后的视图类型为 View.Modified<some ViewModifier>,因此有无这些参数调整的视图,类型也是不同的,这些都将有助于框架去做一些静态优化。

关于 SwiftUI 的详细使用方面,我之后可能还会再更新文章,本文就是简单谈谈我对框架宏观层面的理解。祝大家 WWDC 周玩得开心~


References:

  1. Function builders - Swift Forums
  2. Function builders (draft proposal)
  3. Opaque Result Types
  4. Evolution discussion of the new DSL feature behind SwiftUI
  5. WWDC 19 - 402
  6. https://svelte.dev/
  7. https://twitter.com/unixzii/status/1136330564582092800?s=20