2023年3月30日星期四

熟练使用 Google 或 Facebook 内部工具毫无用处?

有一部分 Google 和 Facebook 员工总是在担心一件事情:公司内部使用的所有工具都不是来源的,离开了公司就不会再用到,花那么多年把这些工具用到熟手了,一旦离职就会变成毫无用处的沉没成本。这种忧虑显得非常围城:在大厂外面的人觉得自己使用的工具不够好,想要进大厂看看到底人家有什么黑科技;在大厂里面的人觉得自己只用闭源工具,担心因为自己不懂其它公司都在用的来源工具而失去竞争力,想要花时间学习开源工具但又没有机会。

担心这种事情并非毫无道理,但我觉得这不是一个真正值得解决的问题,而且不是一个解决后就一了百了的问题。如果此时此刻在大厂,还不如专心做好眼前的工作,等到真正换工作的时候再根据工作需要现学。真正重要的能力,不是你现在懂什么,而是需要在未知环境中摸索时你能够学得有多快。(学校里建立激励机制是错的。根据你此时此刻懂多少来决定给予多少正向或反向激励,一旦你把这内化了就会造成职业上长远的负面影响。)


为什么说「不懂外部开源工具」这个问题不可能被彻底解决?这本质上还是因为技术发展的速度太快,如果一门技术此时此刻用不上,你学了之后就会逐渐贬值。

很多人只看到了现在成功的独角兽在用什么技术,但现在已经成功了的独角兽都是 10 后那一代创业公司了,它们创业时赶上了 AWS 的浪潮,但那时候大家用的 AWS 跟今时今日 AWS 提供的服务根本不是同一回事。那时候大家还是用 EC2 这样赤裸裸的机器,运维还是工作的一大部分,大家尝试做运维自动化但成熟度跟今时今日比简直就是玩具。

随着这一批 10 后创业公司的成长,它们对 AWS 的用量越来越大,遇到的运维问题越来越复杂,然后才出现了现在常见的这一些解决方案:用 AWS 管理界面太手工了,所以用 Terraform 来统一管理需要用到的 AWS 资源。自己在 EC2 上安装和维护软件太麻烦,关键的是缺乏一致性,公司内部做一套统一的容器镜像吧,把所有常用的东西打包进去。很多这些解决方案都是为大规模 AWS 运维设计出来的。

然而如果你看一下 20 后新生代的创业公司在用什么,你会发现你学会前面的一切都没用。20 后创业公司一上来就用 Firebase。EC2 是什么?虚拟机是什么?容器是什么?统统不知道,也不需要知道,赶紧把东西做出来,获得用户和拿到融资最重要。Firebase 当然有它的弱点和限制,大多是 20 后创业公司在扩大规模时都会遇到 Firestore 无索引筛选慢的问题,然后要把数据迁移到 GCP 或干脆把服务迁移走。

现实情况就是这样子,如果你离开大厂加入一家 10 后创业成功的独角兽,你就必须接受过去 10 年大家在 AWS 上建立起来的复杂生态环境,但即使你学会了你依然摆脱不了原来的忧虑:这些技术仅适用于这一批公司。你跳回去大厂,这些技术就没用了。你跳到 20 后早期创业公司,这些技术也没用。等它们 10 年后成长为独角兽时,你需要为它们解决新一代技术带来的问题,而不是去纠结上一代技术已经把上一代的问题解决得有多好。


你的职业发展如何,受你控制且最重要的部分是你能够为别人创造多大的价值。(这也是学校激励机制从来没有帮助你内化的东西。)从懂什么技术出发思考如何优化职业发展,这本身就是错误的方向。你必须从如何创造价值出发进行思考,如果没有充足的信息进行思考那就先尽心观察收集数据。

每一家公司要解决的问题都不一样,具体的某一种技术可能为解决特定的某一个问题提供了所需的手段,但越是通用的技术越需要针对特定问题通过定制化来落地。我见过很多人把「这在我上一家公司非常成功地解决了这个问题」带到新公司,尝试重复一边解决同样的问题,最终发现没有两个问题是一样的。在新公司不接地气,过去成功过的解决方案在新公司不能落地,最终都会失败。

我观察到一些人的成功路线,是借助在上一家公司打开的眼界,在下一家公司结合实际地解决问题,然后迅速地成长。例如说,在大厂当个 L5 接手维护一个曾经非常有开创性的系统,保证它的服务质量同时在上面添加功能。没错,这开创性的工作是轮不到你做了,几年前做这件事情的人升了 L6,但这样的机会只有一次,你不是第一个把路从无到有地踩出来的人,你就没机会升 L6。但你可以把这个系统和它解决的问题搞明白,接着去一家尚未解决过这个问题的独角兽,帮助他们解决这个问题。

对你来说真正有价值的是你见过这件事情能做成的视野。一件探索性的事情知道它能做成,你就已经成功了一半。(有很多探索性的事情,最终是做不成的,至少是在你有生之年人类无法达成。)但你不能把大厂的方案直接抄一遍,你需要理解在这个相似的领域这家公司独有的问题是怎样的,然后定制一个能在这家公司落地的方案。在一家独角兽迅速成长的那几年里,很多问题都是由于规模迅速扩张而造成的,曾经帮助这家公司成功的那批人对这些问题一无所知,而你在大厂见过规模成功扩张后的方案,所以他们会寄望于你来帮助他们解决问题。因为这件事情在这家公司还没有发生过,这个问题也没有被解决过,你在这家公司做出来就算是开创性的了,于是你可以在这里升 L6。

等你升完 L6,环视四周,发现升 L7 的好机会也都被别人挖掘完了,接下来在这家公司升 L7 会变得越来越难。没关系,你需要做的是把前面这个过程再重复一遍,这家公司已经被人利用过的 L7 机会,总有下一家还在路上的公司还没有遇到过相关的问题,等着你去解决。闭源的大厂,尤其是 Google 和 Facebook 这两家,里面有很多黑科技是外面大家根本不知道能做得这么好的,知道能做得这么好就是你最大的优势。(说得直白点,小厂是「贫穷限制了想象力」,而你见识过拥有几乎无限资源的大厂的想象力能去到的地方。)


回到文章的主题上来,熟练使用内部工具本身没有什么特别的价值,但知道这样的工具能做出来(而别人不知道这样的工具竟然能做出来)就很有价值,深入理解这些工具是如何被设计出来的以及它们被设计出来时的历史背景也很有价值。

2023年3月26日星期日

从人工编辑到算法排序

为什么作为企业内部沟通工具,Slack 在公司还小的时候那么好用,但在公司变得越来越大之后 Slack 会变得如此低效?这个问题我思考有一段时间里,我觉得这是一个数据规模的问题,在规模还小的时候 Slack 是一个很好的解决方案,但一旦规模大到一定程度 Slack 就不再有效率。

如果要跟 Slack 做对比,在 Facebook 内部使用 Facebook Workplace 的效率就很高。Workplace 有它自己的问题,例如说导致员工不停地在刷 newsfeed,因为总是害怕自己错过了什么重要的信息,所以一有空停下来就要刷。除此之外,在 Facebook 内部使用 Workplace 的体验很好,重要的信息总是会在 newsfeed 上出现。因为 Facebook 知道你是谁、你跟哪些同事的交互比较多、他们又跟哪些内容交互比较多,所以 Workplace 的排序算法能够把对你重要的内容有限显示出来。

为什么 Slack 和 Facebook Workplace 存在这样的区别?我思考的结果是,Slack跟 Workplace 的区别本质上很像 Yahoo 作为一个门户网站跟 Google 的区别。前者依赖于人工编辑,而后者使用算法排序。在数据规模有限时,人工编辑能够应付得过来而且效果可以非常好,毕竟这是让人亲自审阅这些内容然后决定什么重要、什么不重要、如何归类等等。随着数据规模变得越来越大,人工编辑最终会应付不过来,这就是 Yahoo 作为门户网站最终会输给 Google 的原因,同时这也是 Slack 不好用的原因。


面对大量没经过整理的信息,我们有两个非常不一样的选择:

  1. 尝试控制这些信息的产生和流动。
  2. 放弃对信息产生和流动进行控制。

这两个不同的方向,导致了截然不同的产品设置。

门户网站和搜索引擎的区别能够很好地解释这两个方向的差异有多大。门户网站相信自己可以人工整理和索引世界上所有的网站,或者至少是重要的网站。因此如果你要找某个网站,在门户网站上你要么能找到这个网站(有索引)要么不能找到(没索引)。这就好比图书馆,你不知道是否存在一本某个题材的书,但你可以根据图书索引找到这个题材的书所在的书架,浏览完书架上所有的书你就知道你想要的这本书是否存在了。

搜索引擎做了一个本质上截然不同的假设。沿用图书馆的例子,因为数据规模如此之大,你就算根据图书索引找到了你想要的书所存在的书架,你也会发现符合这一分类的书架如此之多你根本浏览不过来。在一个小图书馆里,可能计算机科学只是一个书架。在人类知识的图书馆里,计算机科学的书架区域可能有好几平方公里大。使用搜索引擎的前提,就是你选择了主动放弃对信息的控制,你让图书馆管理员帮你挑 10 本跟你想要题材最相关的书,他选好后拿过来给你浏览,你从中挑一本你觉得最合适的拿去读。

使用 Google 进行搜索时,虽然 Google 会告诉你总共有多少条结果、分开多少页显示,但大多数人根本就不会在乎,因为前几条有你想要的信息就是有,没有的话再翻几页也不太可能有。这就是我所定义的放弃控制:选择相信算法把对你重要的信息放在前面,放弃排在后面的信息。无论你从哪一个点开始选择不再看排在后面的信息,你都可以相信你已经浏览了相关性高的信息,而你不看的信息的相关性肯定不如你已经看过的高。


Slack、邮件以及公司内部人工编辑的门户网站,其实都属于第一种选择,相信人能够控制信息。Slack 可以通过 channel 控制信息分类,可以通过加入和退出 channel 来控制你具体接收的信息。邮件无论使用 Gmail 还是 Outlook 接收,都可以设置复杂的规则对邮件进行控制,例如说什么邮件不看,什么邮件不那么频繁地看。公司的内部门户网站也一样,编辑往往站在公司的立场来思考公司想要让什么员工获取什么信息,然后编写文章并发布到对应的频道。

Facebook Workplace 属于第二种选择。使用 Workplace 意味着你放弃了手工(包括通过设置规则)判断什么信息重要、什么信息不重要,你选择相信 Facebook 的排序算法帮你把重要的信息排在前面,因此无论你刷 newsfeed 刷到哪里停下来你都可以相信你已经阅读了比较重要的信息,排在后面你不去阅读的信息一定没有前面的那么重要。正是因为 Workplace 的这种特性,在 Facebook 这种规模已经非常大的公司内部 Workplace 会显得很好用。

今时今日已经没有人使用门户网站了,大家都使用搜索引擎,这是因为全球公开的信息的规模已经非常之大。然而企业面对的情况不一样,每一家企业都是从小开始做大的,在它们还小的时候 Slack 就会显得非常好用。(我自己经营一家几个人的公司,一个 Facebook Messenger 的群聊就够了,甚至不需要分多个 channel。所有人知道公司内部所有正在进行的讨论,因为根本就没有多少讨论。)然而当企业做大之后,切换到 Workplace 就很难。因为信息架构的本质区别,Slack 里面的信息不可能导入到 Workplace 里面,这就使得 Slack 拥有巨大的粘性。

Workplace 有些很成功的跨国公司客户,例如说 Walmart 和 Starbucks。对于这种规模的公司来说,使用 Workplace 非常合适。但创业公司上了 Slack 的船后就很难下来了,这就导致了今时今日很多创业公司有一定规模后需要想办法应对 Slack 带来的各种负面影响。我当然希望能够直接从 Slack 切换到 Workplace,但实际上这对于任何一家这种规模的公司来说这都是一个痛苦的迁移。

2023年3月19日星期日

在 React Native WebView 中使用自定义字体

在 React Native 中使用自定义字体(自己提供 woff2 或其他格式的字体文件)很常见,官方文档也说得很清楚。在 WebView 中使用自定义字体也是有文档可以参考的。但在 React Native 里面使用 WebView 并且要在 React Native WebView 里面使用自定义字体,我搜索了一下没找到现成的文档,只好自己根据 iOS 和 Android 原生 WebView 的文档研究对应的 React Native 方法。

这里说的是在 React Native WebView 中嵌入构建时已经打包进去的本地自定义字体文件。如果使用网络上的自定义字体文件的话,标准的 CSS @font-face 就能解决,唯一需要注意的是网络上的自定义字体文件是否跨源(cross origin)。跨源在 WebView 里也不是解决不了的问题,可以通过改变页面的 baseUrl 把本地生成的 HTML 页面变成同源。也可以把字体部署到自己控制的服务器上,把 Access-Control-Allow-Origin header 设置好。

iOS

React Native WebView 在 iOS 上使用的是 WkWebView,React Native 中的组件 API 跟原生 WkWebView 的很相似。原生有一个这样的 API 用来加载一个 HTML 页面:

func loadHTMLString(
    _ string: String,
    baseURL: URL?
) -> WKNavigation?

原生应用可以通过 Bundle.main.bundleURL 来获得应用目录的绝对路径 URL,也就是以 file:// 开头的一个 URL。在 loadHTMLString 时把 baseURL 指向这个 URL 那打开页面的绝对路径也就是应用目录的绝对路径了。假设打包时已经把 custom-font.woff2 文件当作资源打包进去放在应用根目录了,那在 CSS 中就可以以相对路径的方式指向这个文件了:

@font-face {
  font-family: "Custom Font";
  src: url("custom-font.woff2") format("woff2");
}

在这里 custom-font.woff2 等同于 ./custom-font.woff2,也就是在应用根目录的同名文件。这在原生很容易解决的问题,在 React Native 中的主要障碍是缺乏一个 JavaScript 可以直接读取的 Bundle.main.bundleURL。为此我们要自己写一个简单调用 Bundle.main.bundleURLNative Module

// WebViewBaseUrl.m

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(WebViewBaseUrlModule, NSObject)
RCT_EXTERN_METHOD(getBaseUrl:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
+ (BOOL)requiresMainQueueSetup
{
  return YES;
}
@end
// WebViewBaseUrl.swift

import Foundation
@objc(WebViewBaseUrlModule)
class WebViewBaseUrlModule: NSObject {
  @objc
  func getBaseUrl(_ resolve: @escaping RCTPromiseResolveBlock,
                  rejecter reject: @escaping RCTPromiseRejectBlock) {
    resolve(Bundle.main.bundleURL.absoluteString)
  }
}

获取到应用路径后,把它用于 React Native WebView 的 baseUrl,本质上跟 iOS 原生的 baseURL 没什么区别,只不过前者是 string 后者是 NSURL

const baseUrl = await NativeModules.WebViewBaseURL.getBaseUrl();
return (
  <WebView
    source={{
      html,
      baseUrl,
    }}
  />
);

(考虑到 macOS 的相似性,这个方法估计在 macOS 上也有效,但我没有测试过。)

Android

Android 的应用根目录路径跟 iOS 不一样,此外 Android 给 WebView 提供了一个神奇的但仅限于 WebView 的应用根目录路径,那就是 file:///android_asset/。在 Android 的原生代码里面是不能使用 file:///android_asset/ 访问应用内的文件的,但在 WebView 里面却可以使用这个神奇的绝对路径。

有了这个神奇的路径后,Android 上需要做的事情就很简单了。我们不需要从原生代码获取应用路径,只需要使用准确的相对路径就可以了。假设 custom-font.woff2 文件正确地放置到了 assets/fonts/custom-font.woff2 目录,便宜打包后这个文件就可以在 WebView 里面通过 file:///android_asset/fonts/custom-font.woff2 访问。为了跟 iOS 使用相似的 CSS 相对路径,我们可以把 baseUrl 指向 file:///android_asset/fonts/(假设 WebView 不需要用到其他本地编译时打包好的资源的话)。

let baseUrl;
if (Platform.os === 'ios') {
  baseUrl = await NativeModules.WebViewBaseURL.getBaseUrl();
} else {
  baseUrl = 'file:///android_asset/fonts/';
}
return (
  <WebView
    source={{
      html,
      baseUrl,
    }}
  />
);

这样就可以同时覆盖 iOS 和 Android 的场景了。React Native WebView 其实还支持 Windows,但我不需要支持 Windows 所以也没去研究。

2023年3月6日星期一

工程师成长到最后最重要的是什么?

有一个问题我最近在 career coaching 时连续被不同的人问了几次,第一次被问到时我想到什么就说什么,但被连续问了几次之后我觉得我需要好好思考一下。这个问题问的是,「工程师成长到最后,最重要的是什么?」

刚刚进入互联网行业的新人,往往觉得自己懂得还不够多,别人比自己厉害、比自己资深,是因为别人比自己懂得更多。按照这个逻辑进行推导,正确的目标自然是让自己懂得越来越多。再往前推导,既然每一个人的时间是有限的,那必须优先弄懂那些重要的事情,因此有了上述这个问题。大家都不想走弯路,希望找到直达终点的直线。


我觉得这个问题的答案并不是清晰可知的,因为清晰可知的事情迟早会被大家优化到极点。能够通过面试进入大厂的人,都聪明和勤奋到一定程度。如果存在这样一条直线,大家都或多或少地都沿着这条直线全力往前跑,最终跑出来的结果会存在一定的差别但不会存在巨大的差别,尤其是不会存在那种走直线比走曲线所应该获得的巨大优势。

那真正能够产生差异的是什么?那必然是一些别人没办法直接告诉你的事情。这是可以通过反证法证明的。如果有人能够教会你如何能够获得别人不能获得的巨大优势,他会选择教你吗?为什么?大家很容易产生一种错觉,「比我厉害的人肯定懂得比我多,这跟在学校里一样,但是他们不愿意教我所以我不如他们厉害。他们联合起来保守秘密,阻止我们获得他们拥有的知识」。

按照这样的假设,比你厉害的人当中只要有一个人守不住秘密,这种知识早就传播开来了。而且教授这种知识肯定是能赚钱的,所以比你厉害的人其实都有动机通过教授这种知识来赚钱,他们不可能真的联合起来保守秘密。这就可以反证,不是别人拥有你没有的知识而且不愿意教你,现实更有可能是他们没办法教你,或者说教你的成本太高所以不知道花时间去教。


有一个概念叫做「隐性知识」,英文叫做「tacit knowledge」,指代的是那些难以言述的知识。隐性知识类似于通过深度机器学习训练出来的一个模型:它可以用,它大多数时候是正确的,少数时候会出错。你没办法通过人和人之间很容易就能沟通的若干条规则把它描述清楚,因为能描述清楚的话就不需要用到机器学习了,用规则系统就可以了。你没办法很好地预测它什么时候对什么时候错,你也不太能解释它错的时候为什么出错了。

面对这样的模型,你可以选择把它整个拷贝走。但如果你不能拷贝呢?(因为人类还没有进行人脑到人脑拷贝的技术,也不确定这样的技术是否真能被发明出来。)如果你能使用这个模型但不能拷贝这个模型,你可以用这个模型作为基准和反馈从零开始训练一个相似的模型。这也是现在已知的传播隐性知识的方法:找一个把某件「只能意会不能言传」的事情做得非常好的人,你在进行训练时请他提供反馈,最终你训练出来的结果不可能跟他完全一样,但你的目标也只是获得相似效果而已。至于能否训练出来,以及要训练多久才能达到你想要的效果,这都是要尝试过才知道的。


现实就是这样子的:有能力进入大厂的人,通过过书本(或其他媒体)学习显性知识的效率肯定不会差。瓶颈往往出现在显性知识能学的都学了,学更多也不能变得更厉害了,但又没有意识到需要甚么样的隐性知识,或者是不知道从哪里能够获得需要的隐性知识。

所以工程师成长到了一定的阶段以后,搞明白自己需要什么以及找到跟谁能获得有效的训练是最重要的。具体怎么做就不是通过写一篇文章就能说清楚的,否则这就不叫做隐性知识了。我能给的建议是:找你相信真心关注你成长的人,请他们花时间观察你工作,问他们觉得你的成长需要什么、谁能够帮助你训练你所需要的。