上海品茶

您的当前位置:上海品茶 > 报告分类 > PDF报告下载

阿里云开发者社区:2022闲鱼技术年度白皮书(163页).pdf

编号:113558 PDF   DOCX 163页 33.26MB 下载积分:VIP专享
下载报告请您先登录!

阿里云开发者社区:2022闲鱼技术年度白皮书(163页).pdf

1、封面页(此页面将由下图全覆盖,此为编辑稿中的示意,将在终稿 PDF 版中做更新)推荐语 惟实励新,新消费时代的闲鱼技术探索 作者:长恭 2022 年,随着宏观环境的变化,在数字经济与循环经济政策的双层加持下,线上闲置商品交易市场蓬勃发展,并进一步与线下市场加速融合,形成新的生态。同时,需求侧“90 后”、“千禧一代”逐渐成为市场上的消费主力和价值主张领袖,消费习惯及消费观念亦随之改变。以绿色、循环、社交为关键词的闲置经济,逐渐成长为新消费时代的主旋律之一。对于兼具交易平台与社区属性的闲鱼而言,随着闲置交易的内涵不断丰富,从高性价比的个性“淘货”延展到环保、共享经济、社交分享乃至价值主张交流,巨

2、大的发展空间背后,闲置交易向“横向”社交化与“纵向”专业化同时延伸,也对平台技术本身提出了更深层次的挑战:一、特色体验的敏捷创新。面对闲置品类+特色服务(检测/估价/回收等)的组合式导购体验,以及拼买/竞拍/求购等多元需求驱动的社区化消费模式,如何在用户端技术的通用性、体验的独特性、迭代的敏捷性上取得最大化的平衡,避免烟囱式多种体验技术并存带来的重复建设、选型困难、杂而不深、运维代价高、组织效率低等问题,从而更专注于业务模型与展示交互本身,而不必分散精力在技术工具本身的复杂性上。二、开放灵活的机制策略。随着二手品类向闲置空间、时间等广义资源的拓宽,以及配套服务的丰富,新的闲置业态不断演化。如何

3、实现关键系统基础架构层面的开放性,以支持生态机制策略的快速迭代,并基于自由市场独特的业态结构和商品流通路径,构建闲置交易市场特有的垂直品类策略,结合社会化营销、互动等消费人群经营能力,服务业务在市场调控和分层经营机制方面持续完善。三、质量为先的降本增效。快速演进的产品需求与日益复杂的业态玩法,对质量效能的诉求越来越高,业务发展与有限资源的矛盾愈渐凸显。如何构建测试技术壁垒形成数据与模型驱动的流程体系,保障用户体验、加深专项能力的深度建设,以平台化的思路赋能整体研发组织的质量能力,进而形成工程效能生态。这本技术精选系统化地阐述了闲鱼技术过去一年对以上问题的思考,以及落地的演进路线和探索实践。对于

4、越来越年轻化的闲鱼,适逢新消费趋势与新技术爆发的拐点,这些也是闲鱼技术身处时代洪流、承前启后进一步突破的阶段性回顾与小结,希望能给更多年轻的技术人和创新者带去些许启发。也欢迎更多感兴趣的年轻朋友共同探讨,甚至加入我们,一起打造散发新世代无限活力的新闲鱼和背后的技术乐园!目录 Flutter 主题.6 节日献礼:Flutter 图片库重磅开源!.7 打造 Flutter 高性能富文本编辑器协议篇.19 打造 Flutter 高性能富文本编辑器渲染篇.28 Flutter 富文本编辑器系列文章 3交互篇.41 Flutter 知识小报.54 KUN 主题.64 这一年,我对终端组织与技术架构的思考

5、【专家讲技术】.65 大终端领域的新物种-KUN.77 三代终端容器 KUN 的首次大考【架构演进】.94 服务端主题.106 电商搜索里都有啥?详解闲鱼搜索系统.107 QCon 直击闲鱼推荐大规模应用背后的工程实践.120 闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性.132 互动抽奖背后的随机性与算法实现.140 技术质量主题.151 这半年我做交易链路自动化回归的那些事儿.152 关于闲鱼测试数据构造,我有几条心得.157 Flutter 主题(此页面将由下图全覆盖,此为编辑稿中的示意,将在终稿 PDF 版中做更新)手机内核稳定性的治理与实践 7 节日献礼:Flutter 图片

6、库重磅开源!作者:新宿 一、背景 去年,闲鱼技术团队新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点。比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如,我们在相册中,需要在图片库之外再搭建图片通道。二、简介 PowerImage 是一个充分利用 native 原生图片库能力、高扩展性的 flutter 图片库。

7、我们巧妙地将外接纹理与 ffi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。能力特点:支持加载 ui.Image 能力。在基于外接纹理的方案中,使用方无法拿到真正的ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。支持图片预加载能力。正如原生 precacheImage 一样。这在某些对图片展示速度要求较高的场景下非常有用。新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。支持模拟器。在 flutter-1.23.0-18.1.pre 之前的版本,模拟器无法展示 Texture Widget。完善自定义图片类型通道。解决业务自定义图片

8、获取诉求。手机内核稳定性的治理与实践 8 完善的异常捕获与收集。支持动图。(来自淘特的 PR)三、Flutter 原生方案 在介绍新方案开始之前,先简单回忆一下 flutter 原生图片方案。原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如 frameBuilder、loadingBuilder,最终在图片加载成功后,会 rebuild 出 RawImage,RawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image。Image:负责图片加载的

9、各个状态的展示,如加载中、失败、加载成功展示图片等。ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。ImageStream:图片资源加载的对象。在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?手机内核稳定性的治理与实践 9 四、新一代方案 我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。1.FFI 正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是ui.I

10、mage。我们把native内存地址、长度等信息传递给flutter侧,用于生成 ui.Image。首先 native 侧先获取必要的参数(以 iOS 为例):_rowBytes=CGImageGetBytesPerRow(cgImage);CGDataProviderRef dataProvider=CGImageGetDataProvider(cgImage);CFDataRef rawDataRef=CGDataProviderCopyData(dataProvider);_handle=(long)CFDataGetBytePtr(rawDataRef);NSData*data=CFB

11、ridgingRelease(rawDataRef);self.data=data;_length=data.length;dart 侧拿到后 override FutureOr createImageInfo(Map map)Completer completer=Completer();int handle=maphandle;int length=maplength;int width=mapwidth;int height=mapheight;int rowBytes=maprowBytes;ui.PixelFormat pixelFormat=ui.PixelFormat.value

12、smapflutterPixelFormat?0;Pointer pointer=Pointer.fromAddress(handle);Uint8List pixels=pointer.asTypedList(length);ui.decodeImageFromPixels(pixels,width,height,pixelFormat,(ui.Image image)ImageInfo imageInfo=ImageInfo(image:image);手机内核稳定性的治理与实践 10 plete(imageInfo);/释放 native 内存 PowerImageLoader.insta

13、nce.releaseImageRequest(options);,rowBytes:rowBytes);return completer.future;我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。这里有两个优化方向:解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。与 flutter 官方讨论,尝试从内部减少这次内存拷贝。FFI 这种方式适合

14、轻度使用、特殊场景使用,支持这种方式可以解决无法获取ui.Image 的问题,也可以在模拟器上展示图片(flutter _height;override Future toByteData(ImageByteFormat format=ImageByteFormat.rawRgba)/TODO:implement toByteData throw UnimplementedError();override int get width=_width;这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。实际上,ImageCache 计算大小,完全没必要直接接触到 u

15、i.Image,可以直接找 ImageInfo取,这样的话就没有这个问题了。问题三:关于 native 侧感知 flutter image 释放时机的问题。修改的 ImageCache 释放如下(部分代码):手机内核稳定性的治理与实践 12 typedef void HasRemovedCallback(dynamic key,dynamic value);class RemoveAwareMap implements Map HasRemovedCallback hasRemovedCallback;./-final RemoveAwareMap _pendingImages=RemoveA

16、wareMap();/-void hasImageRemovedCallback(dynamic key,dynamic value)if(key is ImageProviderExt)waitingToBeCheckedKeys.add(key);if(isScheduledImageStatusCheck)return;isScheduledImageStatusCheck=true;/We should do check in MicroTask to avoid if image is remove and add right away scheduleMicrotask()wait

17、ingToBeCheckedKeys.forEach(key)if(!_pendingImages.containsKey(key)&!_cache.containsKey(key)&!_liveImages.containsKey(key)if(key is ImageProviderExt)key.dispose(););waitingToBeCheckedKeys.clear();isScheduledImageStatusCheck=false;);五、整体架构 我们将两种解决方案非常优雅地结合在了一起:手机内核稳定性的治理与实践 13 我们抽象出了 PowerImageProvide

18、r,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了imageBuilder。蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native资源外,我们提供了自定义图片类型的通道

19、,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如相册这种场景,使用方可以自定义 imageType 为 album,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。手机内核稳定性的治理与实践 14 除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。六、数据 1.FFI vs Texture 机型:iPhone

20、11 Pro;图片:300 张网络图;行为:在 listView 中手动滚动到底部再滚动到顶部;native Cache:20 maxMemoryCount;flutter Cache:30MB flutter version 2.5.3;release 模式下 这里有两个现象:FFI:186MB 波动 Texture:194MB 波动 在 2.5.3 版本中,Texture 方案与 FFI,在内存水位上差异不大,内存波动上面与 flutter 1.22 结论相反。图中棋格图,为打开 checkerboardRasterCacheImages 后所展示,可以看出,FFI方案会缓存整个 cell

21、,而 Texture 方案,只有 cell 中的文字被缓存,RasterCache 会使得 FFI 在流畅度方面会有一定优势。手机内核稳定性的治理与实践 15 2.滚动流畅性分析 设备:Android OnePlus 8t,CPU 和 GPU 进行了锁频。Case:GridView 每行 4 张图片,300 张图片,从上往下,再从下往上,滑动幅度从 500,1000,1500,2000,2500,5 轮滑动。重复 20 次。方式:for i in 1.20;do flutter drive-target=test_driver/app.dart profile;done 跑数据,获取 Time

22、Line 数据并分析。结论:UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI 方式波动比较大。Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize 了,其他方式加载的是原图。3.更精简的代码 dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。手机内核稳定性的治理与实践 16 FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到ImageCache 带来的统一管理,也带来了更

23、精简的代码。4.单测 为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近 95%。手机内核稳定性的治理与实践 17 七、关于开源 我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。1.Issues 关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择

24、 Open a blank issue。我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。2.PR 为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了95%(power_image 库)。在提交 PR 时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。手机内核稳定性的治理与实践 18 得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test 任务,只有单测通过才可合入。八、未来 开源是 PowerImage 的开始,而不是结束,PowerI

25、mage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder 如何实现?比如 ffi 方案如何支持动图?再比如 Kotlin 和 Swift。PowerImage 未来将持续演进,在当前 texture 方案与 FFI 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 FFI 发展,正如在上文的对比中,FFI 方案可以天然享用 raster cache 所带来的流畅度的优势。PowerImage也会持续追随flutter的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。打造 Flutter 高性能

26、富文本编辑器协议篇 19 打造 Flutter 高性能富文本编辑器协议篇 作者:光酒 闲鱼作为一个二手闲置交易平台,卖家发布商品产出优质的供给尤为重要。商品发布器希望拥有富文本编辑能力,让用户简单便捷的方式产出更加优质的内容;Flutter本身没有富文本编辑器的能力的,只有最基础的文本编辑器 TextField。对于更加复杂的场景,比如支持自定义表情、主题、有序段落等能力,目前 flutter组件是无法满足我们的业务诉求,另外在交互体验上与 Native 仍然存在一定的差距。为了解决业务中面临的以上问题,我们决定设计并实现一个 Flutter 场景下高性能、可扩展的富文本编辑器。一、富文本编辑

27、器整体架构设计 首先我们来看一看整体的架构设计分层:自下而上主要分四层:协议层:主要负责 Model 的定义、Selection 描述、Commond 事件逻辑处理,以及协议 Normalizing 校验。能力扩展层:能力扩展层提供丰富的 plugin 能力,既有内置的 plugin,如:纯文本转换,undo/redo 等能力,同时也非常方便的支持业务层自定义的扩展,例如支持站外 H5 页面展示的 model to HTML 的序列化 plugin。打造 Flutter 高性能富文本编辑器协议篇 20 渲染层:渲染层主要实现将富文本 Model 转换成 Flutter Widget 渲染,以及

28、光标、选区、ToolBar 等计算和渲染,以及用户手势交互事件等。业务扩展层:在 Mural 的设计之初,可扩展就是设计过程中非常重要的一部分,我们为业务方提供了非常灵活、功能强大的扩展能力,通过自定义 Node、Plugin、Normalizing,实现如自定义表情、主题、段落、语法高亮等能力。二、协议层设计 富文本编辑器对大家来说并不陌生,发展至今,已经涌现出非常多有优秀的开源富文本编辑器;当我们想要做 Flutter 富文本协议的时候,第一个想法就是先了解优秀的开源富文本编辑器方案,避免闭门造车。目前比较优秀的开源富文本编辑器,如 CKEditor、Quill、Prosemirror、D

29、raft、Slate等等;在了解和对比过后,我们决定使用 Slate 作为我们的富文本编辑器的协议。1.why Slatejs 我们为什么选择 Slate?插件是一等公民,能够很好的满足我们对于扩展性的要求。Slatjs 在设计上支持嵌套结构,可以满足复杂的业务场景。与 Dom 相同的 Data model,对于后面 flutter 渲染层的实现,也变得更加方便。直观的指令设计,能够非常好的支持 plugin 的自定义扩展。Slate 在设计上,协议层与渲染层是有明确的核心划分,这让我们可以复用 Slate协议层的设计,渲染层交给 flutter 来处理。打造 Flutter 高性能富文本编辑

30、器协议篇 21 除了上面的原因,我们选择 Slate 另外一个很重要的原因,就是它的单元测试覆盖率和完整度,让我们对它的稳定性更有信心。2.Slate 协议层设计 协议层的整体架构设计如下图:下面我们就以 Slate 为例,来看一看富文本编辑器的协议层设计,需要定义的核心概念和模块:嵌套 Model 定义。原子能力 Operation 设计。秩序维护者 Normalizing 的设计。打造 Flutter 高性能富文本编辑器协议篇 22 1)协议层设计嵌套 Model 设计 Slate 定义了三种类型的 Node 节点:Editor:包含整个文档内容的根节点。Element:在自定义域中拥有语

31、义的容器节点。Text:包含文档文本的叶子节点。a)Editor Editor 抽象接口定义如下:打造 Flutter 高性能富文本编辑器协议篇 23 b)Element Element 节点比较特殊,既是 Ancentor 节点,作为容器节点包含子节点;同时又是Descendant 节点,可以作为其他容器节点的子节点存在。块(Blocks):Element 默认为 Block 类型的节点,也就是独立的一个段落;在 Slate 协议设计中,一个段落是不允许存在换行符的,当输入换行符的时候,就会生成一个新的 Block 类型的 Element。行内(Inlines):同时 Element 也可以

32、是 Inline 类型的节点,作为另外一个Element 的嵌套子节点存在,作为行内元素渲染在一行。空元素(Void):Element 也可以是 Void 类型,这里 Void 与 HTML 中 Void 的是同一个概念:如果某个 Node 为 Void,则表示这个 Node 节点是不可编辑状态,光标无法定位到节点内部,会被整体输入和删除;比如:某个人、主题、富文本中的图片或者视频等等。c)Text Text 节点是树中的最低级叶子节点,描述了文本内容以及其他自定义的渲染元素;所有的自定义属性都包含在 properties 属性中:打造 Flutter 高性能富文本编辑器协议篇 24 我们以下

33、面这这段富文本为例:最终这样一段富文本对应的 Mode 定义如下:打造 Flutter 高性能富文本编辑器协议篇 25 可以看到,Model 的树形结构还是比较简单的,所有的属性都存放在 properties 字段中,这也非常方便实现自定义扩展;Flutter 渲染层根据 Node 节点的 Type 以及properties 属性,将富文本内容渲染到屏幕上。2)协议层设计原子能力 Operation 接下来需要富文本 Commond 协议的设计,用户的每一次的文字输入、删除、文字加粗、换行等操作都是一次 Command 指令;Slate 抽象定义了九个最基本的Operations,协议层所有的

34、 Commond 指令,最终在协议层,都会转换成一个或者多个 operation 操作:insert_node:插入 Node 节点 insert_text:插入文本 merge_node:合并相同属性的 Node 节点 move_node:移动 Node remove_node:删除 Node remove_text:删除文本 set_node:设置 Node 属性 set_selection:设置 Selection split_node:拆分 Node 下面我们通过对选中文本加粗操作为例,来了解 Slate 协议层 Commond 的处理过程:打造 Flutter 高性能富文本编辑器协议

35、篇 26 对选中文本加粗这样一个 Commond,协议层会将这个 Commond 拆解成三个Opeartion:split_node:将一个 Text Node 拆分成三个 Text Node。set_selection:更新光标选择区域 Selection。set_node:设置需要加粗 Text Node 节点 properties 的加粗属性。当一个 Commond 被协议层拆分成一个或者多个 Opeartion 执行之后,会执行一个非常重要的操作Normalizing。3)秩序维护者Normalizing 每一次 Command 操作,绝大部分情况会对 Model 进行相应修改;我们需

36、要一个秩序维护者Normalizing,时刻保证对协议 Model 修改过之后,保持数据结构的正确性。Slate 定义了几个基本的内置 Normalizing 规则:每一次 Commond 之后,Editor 都会调用 normalizeNode 方法,在 Normalizing的过程中,发现存在协议结构错误,需要进行错误修复。Normalizing 的另一个强大之处在于,我们可以通过自定义 Normalizing,添加自定义的校验规则,实现自定义的需求;在后面的业务扩展章节会,我们会具体讲解如何通过自定义 Normalizing 快速实现一个自定义主题的能力。打造 Flutter 高性能富文

37、本编辑器协议篇 27 三、总结 目前 Mural 已经在闲鱼商品发布、商品详情、消息等场景落地,支持了自定义表情、主题等业务能力,用户体验方面也有了非常大的提升。本次主要介绍了富文本编辑器 Mural 整体的架构设计以及协议层的设计;后续我们会系列文章的方式介绍 Mural 在渲染层的设计、自定义扩展设计,以及交互体验、性能方面的优化实践,敬请期待!参考链接:1 Slate:https:/ 打造 Flutter 高性能富文本编辑器渲染篇 28 打造 Flutter 高性能富文本编辑器渲染篇 作者:光酒 一、开篇 协议篇文章,我们介绍了 Flutter 富文本编辑器协议层的设计。以 Slate

38、为例,介绍了协议层设计的几个重要的概念:嵌套 Model、Opeartion、Normalizing;站在 Slate的肩膀上,让我们有了一个强壮、设计完善的富文本协议层,接下来就让我们看看渲染层是如何实现的;让我们回顾一下 Mural 整体的架构设计分层:渲染层主要工作是将协议 Model 转换成 Widget 渲染到屏幕上,以及处理选区、光标的计算和绘制,处理用户的手势交互、键盘交互等一系列工作。1.Textfield 的渲染实现 首先让我们来看下 Flutter 的 TextField 是如何渲染的:打造 Flutter 高性能富文本编辑器渲染篇 29 如上图所示,Textfield 继

39、承自 StatefulWidget,会 build 嵌套的 Widget tree,其中有几个比较关键的 Widget:TextSelectionGestureDetector 处理手势交互相关的逻辑,比如单击移动光标、长按选择文字展示 Toolbar 等等。另一个比较重要的 WidgetEditableText;EditableText 在 build 的时候,通过buildTextSpan 方法,根据 TextEditingValue 的普通文本以及 composing 部分,创建一个 Textspan 对象给_Editable;最终 RenderEditable 通过 TextPaint

40、er 将文本绘制到 canvas 上。2.Mural 的渲染实现 如上图所示,Mural在渲染层的设计上,与原生TextField前面一部分基本是一致的,不同之处从 MuralEditable 开始,对应到 TextField 的 EditableText。打造 Flutter 高性能富文本编辑器渲染篇 30 上面在协议层我们说了,Slate 在协议在设计上是与 Dom 一致的,到 Flutter 渲染层,就会将 Dom 树转换成 Widget tree,最终渲染到屏幕上。MuralEditable 不再是简单的创建一个 TextSpan,而是按照 Dom 树结构,每一个Element 映射成

41、一个 Widget;每个 Element 对应的 Widget,创建的 RenderObject实现了抽象类:RenderEditorInlineBox。接下来我们再来看看 Element 对应的 Widget,是怎么处理它的子节点的:我们以最简单的 EditableTextLine 为例,包含 Leading 和 Body 两部分,Leading负责渲染段落修饰相关的内容,比如有序段落的序号、引用段落前面的装饰竖线等。Body 则负责渲染具体的富文本内容,实现了抽象类:RenderEditorTextBox,最终依然将所有的叶子节点转换成 InlineSpan,通过 TextPainer 将

42、文本绘制到屏幕上。EditorUtils 的 buildChildren 方法实现如下:打造 Flutter 高性能富文本编辑器渲染篇 31 3.光标&选区渲染 光标和选区是富文本编辑器渲染层另外一个需要处理的难点。与原生 TextField 相比,Mural 在处理光标和选区处理更加复杂;TextField 所有输入文本都绘制在一个 TextPainter,前面我们说过,Mural 每个 Element 都是一个独立的段落,对应一个 RenderObject;在 Mural 中,我们需要计算用户手势操作不同段落的光标位置以及段落之间的选区计算。要实现 Mural 的光标和选区渲染,需要解决如

43、下问题:多 Element 点击获取 TextPosition TextPosition to MuralPoint 光标位置计算 打造 Flutter 高性能富文本编辑器渲染篇 32 1)多 Element 点击获取 TextPosition 如上图所示,当用户点击绿色光点位置之后,首先我们可以根据点击事件确认被点击是哪一个 Element 所渲染的 RenderObject。首先我们通过 globalToLocal 方法将手势回调的 globalPosition 转换为相对于Mural 的 localPosition;接下来遍历 MuralRenderEditable 的 child,寻找

44、包含localPosition 的 child。如上面介绍的,Element 渲染的 RenderObject 实现了 RenderEditorInlineBox 抽象类,也就可以通过 getPositionForOffset 方法获取到相对于当前 TextPainter 的TextPosition。2)TextPosition to MuralPoint 接下来就要解决第二个问题,如何将 TextPosition 转换为协议对于光标、选区位置的描述。以上图为例,点击之后,TextPosition 的 Offset 为 12,而 Slate 协议是如何描述这样一个光标位置呢?如上图所示,变成了

45、 Path 为0,2,offset 为 2 的 Point。3)光标位置计算 接下来就是光标位置计算,通过 TextPainter 的 getOffsetForCaret 方法,获取选中Element 对应 RenderObject 的光标位置,然后转换成相对于 Mural 全局的 Offset;整体过程梳理如下:打造 Flutter 高性能富文本编辑器渲染篇 33 4.支持 WidgetSpan 在实现自定义表情的过程中,我们发现在展示状态,复杂的 WidgetSpan 渲染是不存在问题的,但是在编辑状态支持 WidgetSpan 遇到了一系列问题。简单一点的做法就是,在编辑状态将表情变成中

46、括号包裹的文字,变成一个不可编辑的 inline&void 类型的 Element。但我们目标是实现一个所见即所得的富文本编辑器,为了在编辑状态支持WidgetSpan,需要解决如下几个问题:Element 到 WidgetSpan 渲染。TextValue 与 Native 同步问题。光标、选区 TextBox 计算问题。1)Element 到 WidgetSpan 渲染 我们定义了 MuralCustomElement 这样一个自定义 Element 的抽象类,如果要实现自定义表情 Element 的渲染,需要继承自它:打造 Flutter 高性能富文本编辑器渲染篇 34 其中自定义表情长

47、度计算与 Emoji 不同的一点,我们认为自定义表情始终长度为一。因为是 Inline&Void 类型,所以 isInline 和 isVoid 都返回 true。2)TextValue 与 Native 同步问题 Flutter 文本输入组件的基本原理,就是在 Native 侧创建一个 TextField 组件,通过TextInputConnection 实现双端事件交互以及 TextValue 同步等逻辑。当用户操作键盘进行文字的输入删除、键盘收起、移动光标等操作,会同步到 Flutter侧;同样的,在 Flutter 进行插入、复制、手势导致 Selection 变化等操作,通过调用 T

48、extInputConnection 的 setEditingState 同步给 Native 侧的组件。当我们输入一个表情的时候,从 Flutter 角度看,我们输入了一个特殊的长度为 1 的字符,这个时候我们就需要将这个 TextValue 的变化同步给 Native。我们参考 PlaceholderSpan 的实现,使用字符uFFFC 同步给 Native。打造 Flutter 高性能富文本编辑器渲染篇 35 3)光标、选区 TextBox 计算问题 如果我们不做任何处理会发现,当包含 WidgetSpan 的时候,光标的位置总会计算Offset 为零;深入了解代码发现问题所在:我们需要

49、处理 WidgetSpan 的 codeUnitAtVisitor 以及 getSpanForPositionVisitor 方法:自定义表情作为 WidgetSpan 的例子,其实是相对简单的;对于 WidgetSpan 嵌套WidgetSpan,嵌套的 WidgetSpan 可以被选择、光标移动的场景,要怎么实现呢?大家可以想一想。打造 Flutter 高性能富文本编辑器渲染篇 36 5.键盘交互问题 当 用 户 键 盘 输 入 的 时 候,Engine 侧 会 通 过 message channel 发 送TextInputClient.updateEditingState事件,将最新的

50、TextEditingValue同步到Flutter侧。对于 TextField 来说,更新的过程比较简单,整体更新 TextValue 即可;但对于 Mural来说,每一次 TextValue 的更新,都进行一次 TextValue 到 Slate Model 的转换,频繁执行导致编辑状态下的卡顿,性能大大下降;我们采用了 diff 的方式,判断用户输入、删除内容,进而调用 Commond 更新 Model,刷新界面渲染。我们需要对于换行符做特殊的处理,正如之前提到过的,Element 是不包含换行符的,每一次换行都会新增一个新的 Element 节点。另外一个需要处理的问题就是移动光标的处

51、理,如:iOS 的长按移动光标、Android的横扫键盘移动光标以及第三方输入法移动光标的键盘操作;这里的处理方案,iOS主要是处理 TextInputClient.updateFloatingCursor 事件,根据 Offset 计算光标位 打造 Flutter 高性能富文本编辑器渲染篇 37 置,Android以 及 第 三 方 输 入 法 的 操 作,主 要 是 在TextInputClient.updateEditingState 同步处理。二、扩展能力 扩展能力是我们设计之初就非常重视的能力,为接入方提供简单、强大的自定义扩展能力,支持复杂、不断变化的业务诉求;接下来我们就以自定义

52、主题和撤销功能的实现,来看一看 Mural 在扩展能力方面的设计。1.自定义 Node主题能力 原文为 gif 如上面视频演示的,当输入两个#中间包含字符,则变成一个主题的样式,点击可以跳转到对应的主题落地页;可以对主题进行编辑,如果删掉其中一个#,则变成普通的文本。要实现这样一个自定义主题,我们需要实现以下几个步骤:自定义 Element、自定义 Normalizing。首先是定义 Element:打造 Flutter 高性能富文本编辑器渲染篇 38 接下来就轮到强大的自定义 Normalizing 出场了,通过自定义规则,处理主题 Node节点校验:打造 Flutter 高性能富文本编辑器

53、渲染篇 39 只需要这样简单两步,就实现了主题能力的支持;业务还可以根据自己的需求定制更加复杂的场景,比如有序段落等等。2.Plugin 扩展实现撤销功能 原文为 gif 如上面图所示,我们实现了一个简单的 Plugin 层的扩展撤销功能;在前面讲到协议层设计的时候,我们讨论过 Slate 的精简的 Opeartion 设计,每一次交互的Commond,最终都会拆解成一个或者多个 Opeartion 执行;我们可以通过以下三步实现 plugin 的扩展:打造 Flutter 高性能富文本编辑器渲染篇 40 重写 Operation 的 apply 方法,通过过滤、合并等操作,记录 Opeart

54、ion 执行的历史。实现 Opeartion 的 reverse 方法。根据 Opeartion 执行历史,调用 Opeartion 的 reverse 方法,执行 reverse 操作。三、总结 通过两篇文章,我们介绍了富文本编辑器协议层、渲染层设计和实现,完成了一个功能完善的 Flutter 富文本编辑器;接下来我们会介绍 Flutter 富文本编辑器体验优化方面闲鱼的一些实践和挑战。Flutter 富文本编辑器系列文章 3交互篇 41 Flutter 富文本编辑器系列文章 3交互篇 作者:岑彧 之前的系列文章介绍了协议层和渲染层的实现,大家可以知道 Mural 是基于 Flutter T

55、extField 进行渲染层的设计与实现,然后对其底层的渲染逻辑进行改造,从而对富文本编辑能力进行支持。但是我们在改造过程中发现,其实在交互方面,Flutter 有很多相比起 Native 缺失的功能,本文会围绕放大镜模式和选区反向选择两个比较重要的交互点来展开说明。本文将会以官方代码来进行讲解,因为这些优化思路是普适通用的,不与富文本耦合的。一、放大镜模式 1.背景与现状 对于原生控件,不管是 Android 侧的 EditText,还是 iOS 侧的 UITextField,都是默认支持放大镜模式的。将用户进行文本选择时,用户可以通过放大镜来进行精确的光标定位和选区移动。如下图所示:Flu

56、tter 富文本编辑器系列文章 3交互篇 42 这无疑会对用户体验起到很大的改善作用,但是目前 Flutter 提供的 TextField 控件里并没有对该模式进行支持,早在 2017 年就有人提出了相关 issue。Mural 的 UI 渲染层和 Flutter TextField 除了在文本的渲染机制上不同之外,其他的交互逻辑是基本保持一致的。所以我们决定模拟 Android 和 iOS 双端的放大镜交互,在 Flutter 文本编辑器中进行放大镜模式的支持。2.交互分析 众所周知,Android 和 iOS 有着不同的设计与交互规范,文本编辑控件就是一个很好的例子,不过他们的交互也有相似

57、的地方,我们将会求同存异,尽量满足双端的设计交互规范。一般来说,放大镜控件通常在两个场景会出现,一就是光标定位时,二就是在选区移动时。我们接下来对这两个场景进行分析:1)光标定位 对于 Android 来说,点击 EditText 进行聚焦之后,通常光标下方会出现一个把手:通过拖曳这个把手来进行光标的定位,而放大镜随着拖曳开始而出现,拖曳结束消失。如图所示:对于 iOS 来说,点击 UITextField 进行聚焦之后,长按,光标会变成一个浮动游标,然后可以直接进行拖曳,便可以进行光标的定位,而放大镜随着拖曳开始而出现,拖曳结束消失。如图所示:Flutter 富文本编辑器系列文章 3交互篇 4

58、3 对于 Android 来说,选区移动和光标定位非常相似,通过双击或者长按 EditText 可以选中最近的词,然后选区的左右两端会出现两个把手,以及选区上方会出现一个Toolbar,可以对选中的文本进行复制剪切等操作。拖拽这两个把手就可以进行选区的移动,拖曳开始时 Toolbar 会消失,放大镜出现,拖曳结束时放大镜消失,Toolbar重新出现。iOS 和 Android 的选区移动交互比较相似,不同的是,iOS 只能通过双击 UITextField才能选中最近的词,因为长按手势用于光标定位。以及把手的样式不一样。Flutter 富文本编辑器系列文章 3交互篇 44 3.代码实现 通过以上

59、的分析不难发现,放大镜有三个特点:在内容上,放大镜会以光标或是单边选区为中心,展示固定尺寸的区域内的屏幕上的内容。在位置上,放大镜会浮动在光标或是单边选区之上,保持固定的距离。在逻辑上,放大镜一般随着拖曳开始而出现,拖曳结束而消失,以及选区移动场景下还需要进行 Toolbar 的隐藏和恢复,但是双端有一些不同的交互。其实还有一些其他的细节交互,比如 iOS UITextField 放大镜其实是展示在触摸点上方而并非光标和单边选区上方,并且在触摸区域和光标没有重合的时候,放大镜就会消失等。不过此处暂时以以上三个特点为思路来进行实现,后续会对没有对齐的交互进行进一步的优化与对齐。以上三个特点可以转

60、化为三个问题与解决方案:1)如何把放大镜定位在光标或单边选区上方?Flutter还提供了一组叫做CompositedTransformFollower与CompositedTransformTarget 的组件,他们通过同一个 LayerLink 来让 Follower 与Target 的相对位置保持一致,即 Target 的位置移动时,Follower 也会跟着一起移 Flutter 富文本编辑器系列文章 3交互篇 45 动。而且 TextField 中已经存在 startHandleLayerLink 和 endHandleLayerLink 用于展示选区的操作把手组件,所以我们直接使用这

61、两个 LayerLink,便可以让放大镜吸附在光标上方。定位代码如下:可以看到,我们需要判定是把放大镜吸附到左边的把手上,还是右边的把手上,而当选区为光标模式时,光标属于左边的把手。这 个 问 题 我 们 可 以 在 TextSelectionOverlay 中 的 用 于 展 示 把 手 组 件 的TextSelectionHandleOverlay 组件中解决。在把手组件的_handleDragStart 中把当前的 currentTextSelectionHandleType 更新为当前正在交互的把手类型就可以实现。伪代码在后续介绍逻辑部分一并给出。可以看到 Follower 组件中还有

62、一个 offset 参数,这个用于控制 Target 和 Follower的相对位置。可以看到我们向左偏移了半个放大镜宽度,向上偏移了放大镜高度再加上一个距离。这样就可以让放大镜悬浮在光标或者单边选区正上方。2)如何在放大镜内展示屏幕上指定区域内的内容?首先会给大家介绍一个 Flutter 控件叫做 BackdropFilter,他可以接收一个矩阵,对位置被该控件盖住(即 z 轴处于它下方)的组件产生高斯模糊、倾斜等效果。详细的使用和介绍可参考 BackdropFilter。Flutter 富文本编辑器系列文章 3交互篇 46 我们把这个控件放到 Overlay 上,他就可以对被其盖住的屏幕部

63、分进行映射展示,但是我们并非想对该控件正下方(z 轴)的内容做高斯模糊等特效,而是想展示而是光标附近的内容,即位置处于它下面(y 轴)的内容。所以我们在对传入的矩阵做 translate(偏移),scale(放缩)操作,就可以把光标和选区周围的屏幕内容映射到这个放大镜中。代码如下:Flutter 富文本编辑器系列文章 3交互篇 47 deltaOffsetFromFocusPoint 这个参数跟第一个问题中提到的相对位置有关,需要先确定两者的相对位置,然后计算出对应的 deltaOffsetFromFocusPoint,让其刚好可以以光标为放大镜展示内容的中心来进行展示。3)如何处理双端放大镜

64、的不同交互?对于双端相同的交互,即选区出现时出现 Toolbar,拖动选区时隐藏 Toolbar,展示Magnifier,拖动结束时隐藏 Magnifier,展示 Toolbar。我们同样可以在TextSelectionOverlay 中的展示把手组件的 TextSelectionHandleOverlay 进行改造实现,在_handleDragStart 和_handleDragEnd(新增方法)中显示和隐藏逻辑。部分代码如下:Flutter 富文本编辑器系列文章 3交互篇 48 而对于双端不同的交互,在 Android 中,因为光标定位可以看做选区定位的一种特殊场景,光标下方的把手即选区中

65、的左边把手。无需特殊处理,而对于 iOS 来说,UITextField 通过长按然后拖动来进行光标的定位。所以我们需要对 iOS 进行特殊处理,长 按 开 始 时 展 示 放 大 镜,长 按 结 束 时 隐 藏 放 大 镜。我 们 对TextSelectionGestureDetectorBuilder 进行改造即可。部分代码如下:Flutter 富文本编辑器系列文章 3交互篇 49 4.效果展示 原文为 gif 二、选区支持反向选择 1.背景与现状 在平时的使用中我们注意到,iOS 的 UITextField 是支持反选的,即在操作右边把手时,可以一直往左边拖动,超过左边把手时,把手的位置会

66、进行一个互换,可以继续操作左边的把手。而Android很多厂商也支持了这一特性。但是我们发现在Flutter TextField 中,这个操作是被禁止使用的。所以我们决定在富文本编辑器中支持选区的反向选择。Flutter 富文本编辑器系列文章 3交互篇 50 2.交互分析 对 iOS 以及一些支持反向选择的 Android 机型的交互进行分析之后,以右边把手往左边移动为例,有两种交互。一种是在左右把手交汇的时候交换两个把手的位置,继续往前选择移动的是左边样式的把手。还有一种交互是,左右把手交汇的时候不改变两个把手的位置,在拖动结束之后,如果发现右边把手在左边把手的前面,再进行交换。结合 Flu

67、tter TextField 的改造成本以及用户的操作连续性,我们决定采用第二种交互方式,当然 iOS 端应该保持 UITextField 的第一种方式,这个会在后续进行继续对齐和优化。3.代码实现 可能很多读者会猜想,是不是在背景中介绍到那行代码给删掉,就可以实现这个Feature 的支持。一开始和大家的想法一样,但是出现了很多问题,接下来会进行具体实现和分析。上面有说到,去除掉 TextField 之后,出现了一些问题。第一个就是,两个把手交汇的时候,两个把手都消失了,变成了光标形态。原因是因为在 Flutter TextField 中,选区把手和光标把手(仅 Android,iOS 光标

68、形态没有把手)是在同一个地方实现的,当左右选区交汇时,会自动切换成光标形态,导致无法进行反选。Flutter 富文本编辑器系列文章 3交互篇 51 1)如何在选区交汇时不切换为光标形态?我们当然不可能删除这个规则,因为在设定中,本来光标就是收缩态的选区,如果完全删除,那光标态也不可能存在了,因为左右选区收缩到一起时,一定会展示左右两个把手,这就有点舍本求末了。所以在绝大部分情况下我们是需要这个规则的,但是又想实现反选,自然而然会想到,设定一个标记位来标识我们正在操纵选区把手,当处于这种场景下,左右把手交汇时,我们就不将其转化为光标形态。a)设定标记位表示把手拖动状态 Flutter 富文本编辑

69、器系列文章 3交互篇 52 b)处于该状态时,选区收缩时展示展开态 解决了这个问题,我们还剩下一个问题,反选完成之后,如何交换两个把手。2)如何在反选完成之后保证正确的选区把手样式?我 们 需 要 在 在TextSelectionOverlay中 的 展 示 把 手 组 件 的TextSelectionHandleOverlay 进行实现,新增一个_handleDragEnd 方法,交换selection 的 baseOffset 和 extentOffset Flutter 富文本编辑器系列文章 3交互篇 53 4.效果展示 原文为 gif 三、总结与展望 纵观整个系列文章,我们从协议层、渲

70、染层、自定义扩展以及交互体验优化等方面,详细介绍如何实现一个功能完善、可扩展、高性能的 Flutter 富文本编辑器。目前Mural 已经在闲鱼的多个场景落地,整体的体验也有了不错的提升。未来会继续在基础能力、交互体验、性能等方面更深入的完善富文本编辑器的能力:在基础能力方面,跟随富文本编辑器的业界标准,提供更加丰富的富文本组件和扩展 Plugin 能力;完善单元测试覆盖,保证稳定性。在交互体验方面,我们尽量给用户提供 iOS 和 Android 的端侧交互体验,优化Flutter 现有的一些交互体验问题;但是还有一些功能是尚未和双端对齐的,例如 iOS 的实况本文、三指复制粘贴撤销重做等,这

71、些都正在调研实现以及上线中。在性能方面,我们优化了超长文本编辑的卡顿问题,与原生的 TextField 相比,卡顿有了明显的优化;未来会通过两个思路进行优化性能:判断 Model 的 Dom结构是否变化减少不必要的重复刷新渲染,以及判断选区、ToolBar 是否变化减少不必要的重复计算,来提升编辑器的渲染和编辑的性能。Flutter 知识小报 54 Flutter 知识小报 作者:意境、三莅 一、Flutter 桥调用请注意结果反馈 通过桥来拓展 Flutter 的能力,是非常通用的 Flutter 开发场景。常见的包括:网络请求,本地存储,异构页面通信等。实现功能固然重要,但是如果控制不好返

72、回值,可能会是一个灾难。例如,Dart 侧代码:void getResult()async dynamic result=await MethodChannel(methodChannelName).invokeMethod(methodName,params:content);doOtherJobs();Native 侧代码,以 Android 代码为例:Override public void onMethodCall(MethodCall call,Result result)switch(call.method)case methodName:doJob();break;如果按上述代码

73、实现,就会出现一个非常隐藏的问题:dart 侧代码会一直 wait,之后的代码(doOtherJobs)永远不会被执行到了。问题的原因是 MethodChannel 并不知道底层的逻辑是否执行完毕。那怎样通知 MethodChannel 执行完了呢?需要调用如下代码:如下代码调用任一一个即可 result.success();result.error();如果没有对应方法的实现可以调用如下代码:result.notImplemented();Flutter 知识小报 55 问题虽小,但是影响可能很大,一定要小心。二、Flutter await 代码带来的潜在并发 Flutter 使用 Dart

74、 语言进行开发。Dart 语言具备非常友好的并发编程语法。例如async/await。我们在享受并发语法带来便利的同时,也需要深刻理解代码背后的执行逻辑。只有这样,才能避免走入一些“深坑”。首先从最简单的逻辑来看:如果一个函数被标记为 async,意味着该函数会被异步执行,函数会返回一个 Future 对象。函数正常执行的到该函数的时候,并不会停下并等待函数的结果返回,而是直接运行下面的代码。如果想要程序停下,等待函数的执行结果,需要配合 await 关键字来实现。示例如下:这里有一个非常有意思的问题,使用 await 等待异步函数执行,到 doJob2 函数执行,这中间是不是仅仅执行了 do

75、AsyncJob 函数内容?来看下面的例子:bool needReturn=false;Future doJob2()async needReturn=true;Future doJob()async Flutter 知识小报 56 if(needReturn)return;print(needReturn position1 is$needReturn);/needReturn=false?await doSomething();print(needReturn position2 is$needReturn);/needReturn=false?Future doSomething()as

76、ync print(doSomething);第一个问题 position1 位置的时候 needReturn 是不是一定是 false?答案是 yes,因为 needReturn=true 会在之前执行的时候,直接返回。要想执行到 position1,needReturn 一定是 false。第二个问题 position2 位置的时候 needReturn 是不是一定是 false?答案是不一定!为什么不一定呢?doSomething 函数中并没有设置 needReturn 为true。needReturn 会被修改么?答案是有可能,原因是 doJob2 可能在其他控制流中被执行。看起来 p

77、osition2 的上一句就是 doSomething,但是在 await 等待的时候,其他的并发函数也可能被执行,如果 doJob2 被执行,值就会发生了变化。结论:使用 await并发执行以后,记得一定要做变量的重新检查。因为这里虽然代码相邻,但是过程中可能执行大量其他并发函数,核心状态并不像看起来的那么可控。三、Flutter FPS 高不代表一定流畅 流畅滚动是优异体验的核心保障。FPS(Frames Per Second)作为页面流畅度的核心度量指标,被广泛使用。FPS 本质上度量的是每秒播放的帧数。下图直观对比不同帧率的显示效果。Flutter 知识小报 57 原文为 gif Fl

78、utter 开发页面,同样广泛的使用 FPS 来度量页面流畅度。但是 Flutter 一直有一个“细碎抖动”的问题,也就是页面整体是流畅的,但是在滚动的过程中有明显的细碎抖动,这对用户体验产生了伤害。在实际开发过程中,FPS 这一指标对这类抖动问题的度量效率并不高。例如:前900ms 刷了 50 帧,但是最后 100ms 刷了 1 帧,最后的 FPS 值是 51,看起来也是一个不错的值。但是用户会在其中明显感知到卡顿。帧率的连贯性是很重要的,即便刷新只有 30 帧,但是如果一直是这个帧率,用户感知起来也是流畅的。但是如果一下子从 50 帧掉到 30 帧用户还是会感知明显的卡顿。所以流畅度的度量

79、需要感知帧率的变化。那 Flutter 中怎么感知每一帧的变化呢?可以用如下方法获取每一帧的性能数据数据。WidgetsBinding.instance.addTimingsCallback();透过该方法,除了能获取每一帧的整体耗时,还可以细化到 build 和 raster 两个主要阶段的耗时。这样能更加深入的做性能问题的排查。数据结构体如下:factory FrameTiming(required int vsyncStart,required int buildStart,required int buildFinish,required int rasterStart,require

80、d int rasterFinish,required int rasterFinishWallTime,int frameNumber=-1,)Flutter 知识小报 58 那么在知道帧耗时的情况,怎么判定是一次卡顿呢?可以从如下两个维度来度量:帧耗时是之前 N 帧平均耗时的 M 倍(这里 N 和 M 可以根据实际情况调整,例如一般设置成 3 帧和 2 倍)。帧耗时超过两帧电影帧耗时(电影帧单帧耗时:1000ms/2441.67ms,这是下线,帧耗时超过这个标准,用户能明显感知到卡顿)。同时我们也可以通过统计不同分位的帧耗时,更细致感知实际页面渲染情况。例如常见的 90 分位,99 分位帧

81、耗时。大家可以根据实际情况统计。四、Flutter 新渲染引擎 impeller 尝鲜 接着上面的问题,Flutter 有一个 early-onset jank 的公开问题(问题详解可以参见引用【1】)。Flutter 页面的抖动问题跟这个问题有着一定的关联。本质上 impeller是 Skia 的一个替代方案。官方在 Flutter3.0 的版本中首次公开了 Impeller 的预览版本。同时在 Flutter3.3 版本中进行了大量完善。目前可以通过如下方式开启:flutter run 添加-enable-impeller Native 工程配置 在 IOS 工程的 Info.plist

82、文件中添加如下配置:FLTEnableImpeller Android 工程,在 AndroidManifest.xml 添加如下配置:那 impeller 效果如何呢?从我们初步的测试来看,有如下初步结论:注意目前impeller iOS 的成熟度相比 Android 要高很多。我们只测试了 iOS 的场景:Flutter 知识小报 59 从官方 Gallery 场景来看,优化效果显著,impeller 的 debug 包就有了媲美之前 release 包的效果。Flutter 的细碎抖动问题,在官方 Gallery 场景上基本解决。滚动流畅性有显著提升。由于官方 Gallery 比较简单,

83、从闲鱼的实际 benchmark 来看,impeller 目前在复杂场景下的性能未超过 skia 的实现。【测试版本 Flutter3.3.8 手机 iPhone13 Pro】主要原因是 impeller 目前阶段比较早,很多功能还有待完善,测试过程中也出现了大量渲染错误的问题。impeller 距离生产中使用还需时日。impeller skia raster 线程平均帧耗时 5.5ms raster 线程平均帧耗时 1.9ms impeller 是 Flutter 根本上解决卡顿问题的重要尝试,虽然目前状态下还有很多的不完善,但是可以明显感受到 impeller 带来的显著变化,未来可期。五

84、、Flutter NullSafty 有用么?Flutter 从 2.0 版本开始引入 Dart 语言的 NullSafty 特性,并且在 Flutter2.2 版本中默认开启 Dart3.0 版本中已经明确不再支持非 NullSafty 代码。引入 NullSafty 特性能带来的明显收益:Flutter 知识小报 60 a)在编译期对变量的空安全做强保护。并且 IDE 会在代码静态扫描的过程中,直接给出空安全相关的提示。这无疑大幅提升了编码效率和代码质量。b)Dart 编译器能针对支持 NullSafty 的代码进行更多优化以生成体积更小,性能更佳的程序。c)代码表达更加精简。非 Null

85、Safty Flutter 知识小报 61 NullSafty ok 如果你想将自己的现有的代码迁移到 NullSafty。可以有多种方案可用:将 Flutter 项目中的 dart Sdk 版本改成=2.12。整个工程即可开启 NullSafty。如果整体开启工作量太大,可以改成单个文件开启。只需要在 dart 文件的最开始(一定要最顶上写)添加如下注释即可:六、Flutter 代码 Trim 机制的坑 我们都知道Flutter中的dart代码在编译过程中,会对没用调用到的代码进行裁剪。这样做的好处是尽可能减少产物的大小。以闲鱼场景为例关闭代码 Trim,闲鱼的包大小会增加额外的 8M。但是

86、实际开发过程中的一个不起眼的操作,竟然会打入你可能意料之外的代码。上述代码中 dependencyCall 所在代码是会在编译期间被 trim 掉的(如果没有其他任何引用的话)。但是如果代码改成了如下方案,情况就不同了。Flutter 知识小报 62 由于在编译期间,编译器无法 100%确认 needSomething 变量的值。所以就会将dependencyCall 所在代码打入最终的包中。包大小就“意外”变大了。一个非常小的改动,即便最后的值都是 false,看起来是逻辑等价。但是“不经意间”却影响了最终打入的代码。七、Flutter 为什么引入 EngineGroup 众所周知,Flut

87、ter 最早是面向 APP 级别进行设计的。但是实际应用过程中,更多的场景是将 Flutter 纳入现有的 APP 之中,这时 Flutter 多实例就是一个无法回避的问题。闲鱼团队之前的解决方案是 FlutterBoost,目前也已经开源。本质上是多个页面复用一个 Flutter Engine。Flutter 2.0 版本中,官方对 Flutter 多实例场景进行了优化。据官方数据,增加一个新的 Flutter 实例,只会增加约 180K 的内存占用。这无疑对多实例场景是一个重大利好。官方数据如下:但是这个方案真的那么完美么?目前的 EngineGroup 方案也有他的一些不足:每个 Eng

88、ine 都有自己的 Isolate,彼此之间数据独立无法复用。同时不同页面之间的数据通信的成本也大幅提升。每个 Engine 都有自己的桥通道。需要单独注册和管理。多 Engine 即便已经大幅优化,考虑到更多的 Isolate 和数据隔离。真实页面性能成本相比单 Engine 版本会更高。大家可以根据自己业务的实际情况选择使用。Flutter 知识小报 63 引用【1】Flutter 新一代图形渲染器 Impeller 【2】https:/ 64 KUN 主题(此页面将由下图全覆盖,此为编辑稿中的示意,将在终稿 PDF 版中做更新)这一年,我对终端组织与技术架构的思考【专家讲技术】65 这一

89、年,我对终端组织与技术架构的思考【专家讲技术】作者:宗心 一、前言 本文仅以个人观点阐述未来的端研发趋势和人才岗位结构趋势的要求,进而引出闲鱼技术团队今天要做的事情,闲鱼技术团队作为集团创新产品的先头兵,一方面希望通过持续的技术革新为业务带来核心竞争力,另一方面也希望为集团开拓新的技术领域从而引领新的技术风潮,通过技术带来长期的效能红利。KUN 作为闲鱼技术团队在终端技术这一未来岗位的核心转型的重要基础设施,在推进过程中一定也会遇到各种不同的声音,因此更有必要让大家了解我们的长期构想和实现路径。道路是曲折的,我们无法准确预测未来,但是可以肯定的是,闲鱼技术团队一直以创新突破作为团队的核心灵魂,

90、这个灵魂植根于每一次技术设施升级、研发模式升级、人才岗位升级中并长久不衰。长期以来,闲鱼技术团队,尤其是闲鱼客户端团队都在争议和挑战中持续进步,不停的磨练自身的技术能力,我们有理由相信,一个勇于面向自身革命的团队,一个敢做旧时代掘墓人,新时代的引领者的团队,必将乘鲲而行,带领组织奔赴星辰大海。二、终端行业的趋势变化 首先我想从几个角度去阐述一些趋势的变化。终端设备的趋势变化【智能手机仍是未来三年关键】移动互联网的这十年来,设备从 PC 迁移到智能手机,在这个过程中又衍生出了平板、智能 TV、可穿戴设备等新的终端产品,可预见的未来 XR 设备也会层出不穷。Anyway,在近几年的终端设备趋势来看

91、,目前还没有任何一个终端产品具备目前智能手机的三大特性-刚性需求、高频使用、生态开放,因此往后三年看,在终端的硬件布局上,智能手机依然是今天的优质流量入口【1】。这一年,我对终端组织与技术架构的思考【专家讲技术】66 软件架构的趋势变化【跨端跨设备是业务刚需并持续演进】从 PC 互联网到移动互联网转型的这么多年来看,随着技术的不断演进,传统意义上的前端和客户端持续在做相互的渗透,从最早的 Native+Web 的混合架构,再到ReactNative/Weex 的以 JSRuntime 驱动 Native 组件渲染的高性能跨端架构,再到Flutter 以自绘能力推进多端一致性和性能显著提升的新架

92、构的尝试,技术侧我们一直在致力于进一步提升终端设备上的研发效能从而推动业务的高效迭代和创新。另外随着小程序的在近些年作为各公司主要的用增入口或经营主阵地,进一步推动了跨端技术的演进和迭代。以极客邦组织的相应行业大会内容来看,行业所谓的大前端的技术趋势是长期存在的。它有几个典型的特点:上层以类前端技术作为研发的基础(JS/TS,同时 Dart 也是最早面向前端的设计的语言),下层通过抹平各操作系统的 UI 渲染和底层 API 的能力差异形成端侧容器,该类架构具备一次研发多端交付的特点,在具备高性能的前提下拥有较好的动态部署能力(Flutter 在行业内也有很多开源动态化方案,可参考阿里的 Kra

93、ken【2】和腾讯的 MXFlutter【3】)。组织协同的趋势变化【岗位融合,减少无效协同是敏捷组织的核心诉求】技术趋势变化一定会影响组织协同方式的变化,混合架构带来的岗位的重新设定和组织架构的重新调整,在一定程度上你中有我,我中有你的不停相互影响着。以闲鱼为例,从最早的 iOS/Android 分端的岗位职能,到以业务视角分业务线不分端的岗位职能设计,背后既有独立业务线的快速交付的诉求,也是混合架构下释放的组织效能的体现。今天为止,闲鱼在客户端岗位上崇尚的就是一人开发,独立高质量交付。当然这样的岗位趋势也不是个例,在国内各大厂的大前端组织内我们也能看到不但客户端之间在做岗位融合,前端和客户

94、端也在做岗位融合。我们把视野再放到海外,从 google、facebook、amazon 的数据来看,硅谷的巨头们在人才结构上掌握 Front-End(包含前端、客户端等与用户交互相关的岗位)的人才比例有 30%或更多,这更加催生了端到端全栈的应用开发工程师的存在。组织在融合是表象,背后说明一个本质问题,敏捷组织的构建关键在于减少协同。多岗位协同带来的组织间的管理摩擦严重影响大厂的交付效率和创新效率。基于以上的构想,作为闲鱼客户端的负责人,我认为未来的端侧研发的目的应该核心是服务与敏捷组织,激发公司的效能和创新的活力。我们的组织未来首先应该是岗位角色更少,岗位技术能多样,而对应的技术能力要为这

95、个组织的角色服务,因 这一年,我对终端组织与技术架构的思考【专家讲技术】67 此,所有的研发模式都要服务于如何让这件事更容易的发生。由于我们的技术主阵地依然是以智能手机为主的场景,如何有效的提升以智能手机为主、其他设备为辅的端侧交付效率就是我们核心要关注的事情。三、未来研发模式构想及路径 1.端侧组织职能的再分配 在行业的岗位职能中,客户端(iOS/Android)开发工程师、前端(Web)开发工程师,以及一些上层的细分岗位如 Flutter 工程师开发、RN 开发工程师、小程序开发工程师等角色众多。而各类移动端中间件、移动端构建平台的相关研发工程师的岗位角色也需要相互配合,从这个角度上,岗位

96、的细分一方面带来协同成本的显著增加,一方面在人才发展中造成不同的 Job Code 的天花板太低,不利于人才的长期成长。因此在新的研发模式的设计中,我尝试先定义更少的研发角色,岗位的融合一方面推进带来协同规模的显著下降,一方面推进交叉领域的组合创新,在历史上这种组合创新的例子比比皆是,我们的众多端侧容器技术的发展如 RN/Flutter 等,都离不开客户端和前端的深度融合。在效能工具上这种端侧全栈的岗位融合,相信也有机会创造新的生产力 以下我会重点介绍新研发模式设想中的两种核心岗位设计。这一年,我对终端组织与技术架构的思考【专家讲技术】68 应用开发工程师【应用侧多岗位融合带来组织活力】从敏捷

97、组织为原点出发,我们来思考未来的研发角色的角色定义,从组织的角色来看理想中的极致应该是应用开发工程师可以既懂前端又懂后端,懂数据而又可以对常见算法进行应用,在公司的大背景下当然还有“用好云”的要求,从这个角度来看,业务在协同侧只需要与一个技术团队或一个技术人协同而不再需要与多岗位角色的团队进行协同,这当然是最理想的状态。当然这个对人才的能力以及配套的基础设施的能力要求都太高。我们退一步从三年的视角来看,在端侧是否有机会做到岗位的融合渗透,我认为是有这样的机会的。我们设想这样的一个场景,以商品发布为例,一位同学负责商品的客户端链路的发布体验问题,同时由于发布还存在大量的长尾页面如发布引导,发布的

98、一些常用的频道也由其负责。在这样的场景下,各场景的协同串联就不再需要多个岗位来协同,产品和 UED 只需要跟一位同学进行交流,大幅减少信息传递的衰减以及说服技术的成本。另外串联过程中会涉及到的一些联调工作,如发布频道引导到发布的场景,在单一岗位协同的情况下很多技术内部的联调的成本就会被干掉。当 然 以 上 的 描 述 中 对 人 的 要 求 可 能 是 要 做 端 侧 的 全 栈 工 程 师,即 懂iOS/Android/Web 开发。这里面最核心的挑战来源于学习成本。而实际上今天通过基础设施的升级是有机会大幅度减少端侧开发的学习成本的,同样以闲鱼团队目前已有的实践举例,在端侧 80%的需求承

99、接使用 Flutter 进行交付时,大量开发工程师的技术栈的底线要求变为熟练掌握 Dart 及 Flutter 相关知识,而在 iOS 和 Android侧的要求变为了解和做较为初级的使用即可。这大幅降低了一人开发两端的技术要求。同样的,当前端可以使用 JS/TS 进行 App 的产品侧开发时,在产品链路能投入的研发基数就会大幅上升,这部分的前端的要求可能是熟练掌握 React 等相关的框架和前端语言,同时熟悉 Flutter 的相关知识即可。在未来的组织要求中,我认为我们在应用侧的人才应该是 T 型人才,在上层语言和框架上至少要对 Dart&Flutter 或TS&React 要有较深入的理

100、解,同时对客户端或前端的一些基础知识有一定的了解即可,这部分的能力不足可以通过基础设施的建设来补充,在后面的章节会提到。基础设施开发工程师【基础设施侧各领域形成纵深打好基础】由于上层应用开发工程师需要掌握多门应用开发语言或者框架,这注定代表了他在某一类的知识上不会特别深入,这个时候需要基础设施这一侧提供相应的能力,典型的有我们所说的端侧容器,比如 Flutter/RN/Weex。另外包括发包流程,本地研 这一年,我对终端组织与技术架构的思考【专家讲技术】69 发环境部署等都需要有相关的人员提供能力。这部分的开发工程师更关注基础设施的层的能力建设,我认为包含了刚才说的几个大类能力,包括容器建设、

101、IDE 建设、研发的工作流平台相关的能力建设。这部分的开发工程师更多要对 c+/swift/java等语言要有更多的了解,对编译、渲染相关的知识要有更深入的理解,通过完善应用开发工程师所使用的端研发设施和端运行时环境,大幅减少应用开发工程师所需要的知识。同样举个例子,大家在端侧容器如 Flutter 侧研发的过程中,有对应的组件库和 API能力的前提下,应用开发工程师不需要了解不同操作系统之间的差异。同样在环境部署上,本地环境部署和分支管理的一站式工具将屏蔽不同工程(iOS/Android)的依赖更新、构建等问题,应用开发工程师专注在 Dart 侧或 TS 侧的编译构建和调试上即可。当然这是一

102、个相对理想的假设,实际在推进过程中基础设施的建设可能要以 3 年为周期持续迭代,同时两个岗位需要可以高效的相互协作。2.端侧配套能力的再升级 岗位设计背后的需要有配套的能力建设,正如上一章提到的,能力建设的本身是降低上层研发交付的门槛,我认为能力侧会从以下几点能力开始建设。统一的端侧容器能力 端侧容器这里指的是如 Flutter/RN/Weex/小程序等具体的能力实现,一般包含渲染引擎+上层的虚拟机,我们常见的 Webview 也是端侧容器的一种,对于现代 App 来 这一年,我对终端组织与技术架构的思考【专家讲技术】70 说,端侧内置各类容器完成不同的业务场景的目标是非常常见的事情,举例子来

103、看,淘宝的双十一会场由 Weex 承接,带来较好用户体验的同时,减少了消费者原有在Webview 内的加载渲染等待的时间,提升了流畅度,也帮助了业务转化率指标的显著提升。小程序在微信和支付宝场景下为业务引入了丰富的外部生态并保障了基础体验和安全性。但是随着业务的迭代和不同场景的接入,端侧容器在 App 内部一般会有多个,不同场景下容器与容器之间的交互成本较高,多个容器在运行时环境下的内存消耗较大对稳定性和问题排查都造成了更多的问题,在复用上,跨容器之间的组件复用难度较大体验难以一致。这都是现有场景下的问题。从这个角度来看,新的端侧容器能力更多强调统一,这里我给出一个定义:端侧 80%以上的代码

104、及应用场景使用同一个容器。统一容器带来的架构上的同构有复杂度低、分层可替换、性能提升、可维护性提升等众多优势,下一章提到的 KUN 的建设,就是我们以 Flutter 为技术基座的一种实现。同样举例说明:极限情况下当一个 App全由 Flutter 或 RN 进行构建和研发的时候,我认为该 App 就具备了统一的端侧容器的能力,但关键在于,该容器是否能在更大规模的重视体验的 C 端产品上使用,而不是在长尾 App 中进行应用。不同的规模和业务要求下的技术挑战可能完全不同。统一的编程平面 这一年,我对终端组织与技术架构的思考【专家讲技术】71 统一编程平面包括了研发侧使用的 IDE、配套的 De

105、vTools、统一的 API 和组件库以及配套文档。这里的统一是指在应用开发这一侧,面向 Dart 和 TS 两种语言,对应相关设施应该尽量是一套或在描述上完全一致。举例说明,一位应用开发工程师可以通过 Dart 调用我们已经实现的【价格选择器】的组件,同样也可以通过 TS 的调用使用我们已经实现的【价格选择器】组件。而维护该组件的同学在开发之初就会提供一套在 Flutter 侧的实现,但提供两套接口(Dart/TS)。显然这种场景下真正做到了技术侧的完全复用,大家在一套长期可以共建的标准下补充 API 和组件能力。如果单纯从客户端的视角来看,这种方式通过【标准】来约束客户端形成长期稳定的组件

106、库,从 Web 前端的角度来看,这种方式沉淀了可扩展的组件库,减少的 Web前端的工作量的同时又大幅提升的端侧的用户体验。换言之统一的编程平面在容器之上给予支持,在需要提供标准的时候提供标准,在需要扩展的时候给予更轻量级的扩展能力。让研发的体验和效率同步提升。对于统一的编程平面的畅想来看,未来的应用开发工程师可能需要在一个 IDE 下进行开发,无需再在多个 IDE 进行切换。同样 DevTools 中对 App 以及对应 JSBundle的部署工作都会抽象成一些常见的命令,通过 CLI 的方式显著减少大家的环境配置和切换的时间。在 API 和组件库的建设中会有相对完善的准入机制和所见即所得、文

107、档自动化生成的站点来保证不同知识背景下的客户端或者前端工程师顺利转型为终端应用开发工程师,同时面向 Dart 和 TS 生态进行问题排查或者业务代码研读的时候,由于标准一致,成本会进一步下降。统一的研发工作流 这一年,我对终端组织与技术架构的思考【专家讲技术】72 在端侧的研发工作流中,客户端原有的研发工作流是典型的面向整包的构建和发布,有独特的研发到集成再到测试回归验证以及上线灰度的流程,另外客户端伴随着应用商店的审核,整体节奏更慢,由于无法做线上的变更,对稳定性的要求更高。而前端的研发工作流更多是面向页面级别的代码构建,无需集成直接回归验证再到线上灰度,整体节奏更快,但由于是全量的线上代码

108、覆盖,兼容性和稳定性会稍差。面向未来的研发工作流,我认为一方面要解决交付效率的问题,另一方面对稳定性也要有所保障。从之前的岗位分工来看,我认为面向上层的应用开发工程师更多应该使用基于DailyBuild 的线上工作流,该工作流的特点是在合规的条件下相对频繁的进行工作流的变更和部署,使得线上的关键链路可以进行快速的 AB 和迭代。而面向基础设施的开发工程师更多的会在 Weeklybuild 中更新端侧的容器能力,使得容器侧的问题得到快速的响应和收敛。当然在研发工作流之外,我们需要考虑上层部署侧的降级逻辑、工作流内部配套的持续集成和自动化测试能力等等。Anyway,研发工作流通过数字化和自动化的方

109、式服务于两个研发角色,在可观测、显著减少人为因素犯错的同时显著提升整体的交付效率。四、终端容器 KUN,助力闲鱼再出发 以上的未来研发模式的构想部分的内容在闲鱼已经有了较好的基础,一方面我们的Flutter 技术在业务落地侧的占比远高于目前市面上同等体量的 App,这样的业务规模和技术落地规模让我们有机会验证在较为复杂的 C 端场景下基于 Flutter 技术的 这一年,我对终端组织与技术架构的思考【专家讲技术】73 容器化解决方案是否有机会走通,随着团队对 Flutter 技术的理解和沉淀不断提升,在大量体验场景下的富交互细节以及操作系统升级带来的兼容性挑战正在持续被解决和优化,另一方面闲鱼

110、在推进落地一周一版的研发模式的过程中目前通过新的客户端研发平台完成了发版流程的全面数字化和部分场景的技术自动化落地。以研发流程为例,我们力求将核心的 90%的协作通过线上审批流、钉钉机器人通知,自动化的版本邮件来完成,让整个过程可观测和回溯,同时在构建集成、回归验证和版本放量等能力上通过跟自动化能力结合加速交付效能的提升。然而在目前的布局下,闲鱼客户端基于 Flutter 容器化的基础架构以及一周一版的发布模型,在未来依然有较大的困难需要突破:一方面客户端的人员规模增长速度和生态演进速度在应用层这一侧都显然不如前端,在未来的快速迭代能力侧难以进一步突破以应对不确定的未来,另一方面作为 C 端产

111、品又要求体验必须对齐原生,基础链路侧的富交互体验会持续存在,这在前端角度又是极大的成本。从这两个角度来思考,我们的终端容器需要进一步升级迭代。它应该海纳百川容纳更大的研发生态,让前端&客户端的生态为我所用,在需要体验的时候进行更轻量级的能力扩展,如直接引入通过 Flutter 研发的客户端组件,在需要迭代时发挥前端的作用,如研发期的迭代速度,发布期在合规的要求下具备业务线上的快速迭代的能力。因此从 2021 年下半年开始,我们的 KUN 项目应运而生,简单描述 KUN 的定义,KUN 是基于 W3C 标准子集&Flutter 打造的高扩展性、高性能跨端渲染引擎。KUN的目标是通过提供一套统一的

112、容器覆盖端内全部的业务场景来解决端内多套容器带来研发效能低下、业务代码复用难、不同场景叠加带来的性能水位差的问题。为帮助大家理解我这里增加一张 KUN 与其他类似框架的架构对比图。这一年,我对终端组织与技术架构的思考【专家讲技术】74 KUN 具备以下关键特点:高扩展性 针对富交互场景以及高性能场景,Kun 提供低成本的扩展机制,将 FlutterWidget 映射为上层可使用的自定义组件,过程中提供较为方便的文档生成工具给上层开发使用。同样,在 W3C 标准中,KUN 也通过微内核的设计方案将 W3C 的核心 Tag 注册入 KUN 的内核当中。高扩展性的核心目标是通过复用闲鱼 4 年来在

113、Flutter 侧大量的上层交互控件解决线上产品的交互体验和性能问题。高扩展性也是 KUN 的最核心特点,后续围绕Flutter 生态,后续闲鱼有机会孵化出类似 FishDesign 的组件库生态。面向 W3C 标准 上层应用开发通过 JS/TS 开发,并提供面向 W3C 子集的标准,这也意味着您可以通过React/Rax等框架进行上层开发。这部分目前跟常见的跨端框架的原理基本一致,在整体设计上也参考了业内主流的跨端框架,在标准的支持上,我们目前更多关注闲鱼侧业务较长使用的能力,由于扩展能力是我们的第一选择,因此在未来更多KUN 使用的场景,我们会倾向于使用闲鱼内部的高性能自定义组件,这也意味

114、着我们的这部分能力较薄。高性能&多端一致性 通过 Flutter 引擎以及 Flutter 相关组件生态为整个渲染引擎提供高性能交付产物。底层的整体渲染基于 FlutterWidget,保证了多端的一致性和容器复用。这部分的特性完全来源于 Flutter 的基础能力,这里不再赘述。唯一需要关注的是,当整个应用复用 Flutter 容器时,在内存水位和加载性能侧以及组件开发成本上来看,显著好于混合了多个容器如 Flutter+Weex+WebView 的性能。这部分的优势完全来自于整个App 架构的简洁。这一年,我对终端组织与技术架构的思考【专家讲技术】75 为方便大家理解,这里也补充一张扩展组

115、件的核心描述,可以看到更多时候我们会在产品链路上使用混合模式进行开发,保证关键细节的体验与原生一致,而在基础组件的描述上我们更倾向于直接使用 W3C 的标准。KUN 项目较好的打通了现有的前端生态和 Flutter 生态,并通过配套工具进一步减少开发的门槛,使得具备 JS/TS/Dart 开发能力的终端工程师有机会在闲鱼诞生,这类工程师可以在性能敏感型和动态性敏感型的不同场景下选择不同的技术实施方案做业务落地,使得终端岗位独立完成端侧需求这件事情得以成立。目前我们已在导购场景和我发布的等场景下完成了前期的技术落地。后续会针对复杂场景如闲鱼号等业务进行改造和落地,通过技术的革新为业务带来效能的变

116、化,为用户带来更好的产品体验。未来的几个月,我们会针对目前产出的一些关键能力和部分技术细节做系列性的总结和分享,在完成大规模的业务验证和技术优化后,我们将面向社区进行开源。五、后记 写这篇文章的最后,写一点自己的碎碎念。首先要表达一些对行业内同事和朋友的感谢,比如文中提到的 MXFlutter,是较早进行 Flutter 侧动态化方案落地的开源项目,早在我自己做 GMTC 出品人时跟该项目负责人有所交流,在 KUN 的设计的前期调研中也有了解过 MXFlutter 的一些使用方式和落地情况。这里另外要感谢的是 Kraken 团队,KUN 的整个项目落地过程中得到了 Kraken 团队较多的支持

117、,在技术交流和讨论中也让 KUN 的定位和路径更明确,两个团队也做了大量的代码实现的讨论和交流,得益于 Kraken 的开放性,KUN 在这个过程中站在巨人的肩膀上继续演进。这里插播一个彩蛋,KUN 的名字本身就是对 Kraken 项目的一个致敬,大家可以猜一猜原因。未来 KUN 也希望最终能青出于蓝而胜于蓝,并把代码回馈给社区,在前 这一年,我对终端组织与技术架构的思考【专家讲技术】76 期的建设中我们会先以集团内部开源作为目标,并在未来面向社会开源,为行业技术生态建设提供一些力所能及的帮助。另外想讲下我看到的行业差距和不足。在了解整个国外行业对我所定义的终端岗位的情况来看,硅谷的头部公司工

118、程侧的人才结构已经发生了一些变化,工程侧的人才更多面向全栈和终端全栈的方向去走,原因是海外的工程基建能力更发达和完备,这也意味着海外的技术组织通过技术能力和组织岗位的重新整合,达到了更好的生产效率。近几年国内也开始更加强调组织的持续演进和工程师素养的提升,比如提出了DevOps 的概念,减少运维参与,在云原生的大环境下降低开发和运维门槛。提出了测试开发的概念,推进研发的自测和持续集成自动化能力的建设。提出卓越工程的概念,更加强调软件架构和研发基础设施的重要性。但这些变化始终是跟随硅谷的步伐。作为国内的头部的互联网企业的一员,我也希望国内的技术人能在不确定的大环境下练好内功,通过持续的自我革新来

119、形成企业的技术壁垒,正视我们与国际互联网公司之间的技术和基建上的差异,先追平能力,再找到机会持续超越,真正能为技术创造新商业提供一些微小的帮助。日拱一卒无有尽,功不唐捐终入海。未来星辰大海的道路上,闲鱼技术与你一起乘风破浪。参考资料:【1】2021 年互联网行业挑战与机遇白皮书。【2】北海,高性能 Web 渲染引擎,基于 Flutter 构建。【3】MXFlutter 是一套使用 TypeScript/JavaScript 来开发 Flutter 应用的框架。大终端领域的新物种-KUN 77 大终端领域的新物种-KUN 作者:吉丰 一、KUN 的背景/动机 即使已经到了 2022 年,在面向复

120、杂多变的用户端开发领域,我们依然绕不开一个问题?我们选择什么技术更适应我们的业务场景,不管是通用还是独特。这回到一个问题的原点,每一种技术都有它的局限性(短板)。1.单一技术的缺陷 1)Native 技术的局限性 尽管 Native 技术在用户体验上有绝对的天然优势,但在工程化,部署效率,敏捷上又有天然的短板。工程化效率低 工程复杂度高,由于天然的把所有的业务集成在一个工程里,依赖复杂性,工程复杂度高。编译效率低,切环境,编译打包,效率低。这显著影响实际的开发效率。部署效率低 由必须依赖用户的安装更新,生效周期长,导致实际迭代效率慢。2)Web 技术的局限性 尽管 Web 技术是当今最流行的

121、GUI 技术,但受限于浏览器架构/渲染模型,使其在体验上有天然的劣势。特别是在强调“图片/动画/视频控制、长列表容器、多 Tab 容器”等场景显得力不从心。涉及底层基础能力,必须依赖 Native 技术的增强。大终端领域的新物种-KUN 78 更广义而言,这是 C/S 架构和 B/S 架构技术对比的缩影,历史上从以 C/S 架构为开始,到以 B/S 架构的大流行。而 PC 互联网时代后,Native 技术和 Web 技术在移动互联网上的继续相爱相杀。2.烟囱式多种技术并存的挑战 技术上的乌托邦,理想的情况,我们期望日常的业务软件开发关注于业务本身的复杂度(比如前台而言关注与,核心关注于业务模型

122、复杂度/展示复杂度/交互复杂度),而不是更多的关注于技术工具本身的复杂性。但实际的情况却相反,我们往往考虑技术工具的问题,比如什么场景下适合使用什么技术?什么时候使用 native 技术,什么时候使用 web 技术,什么时候使用非 web的跨端技术,什么时候使用某些特定领域的技术,随着业务场景越来越多,越来越复杂,我们的技术工具的数量也在不断的上升。每多增加一种技术工具,背后需要额外的投入,如何学习熟练掌握一种技术工具。如果不断优化提升技术工具,使之在质量和效率上不断提升。技术工具之间的耦合关系使得复杂度进一步上升,为了解决这部分复杂度所需要的额外的成本。组织效率的降低,容易形成更多的开发瓶颈

123、。技术的割裂,对中小规模的技术团队的影响更加明显,因为技术的割裂,更加容易产生因为技术本身导致的人力瓶颈。有没有一种技术,在效率、体验、通用性取得最大化的平衡。甚至打破传统按技术栈粒度进行划分的职能边界,统一到普遍意义上的终端开发工程师职能上。3.KUN 是什么 大终端领域的新物种-KUN 79 KUN 是一个让开发者使用 Javascript,HTML,CSS 进行开发,使用 Flutter 进行增强的跨端开发框架。它最早的雏形是来自 Kraken,正如鲲这个名字所内涵的:北冥(Kraken)有鱼,其名为鲲(Kun)。Kraken 将 Flutter 技术引入并应用于 Web 技术栈,而在和

124、 Kraken团队的交流中吸取了大量的优秀知识。但我们并不希望去做一个仅仅基于 Flutter 渲染带裁剪的 Web 浏览器。相反,我们试图去完美融合 Web 生态和 Flutter 生态。我们希望在面向中小规模的大前端/大终端的组织中,找到一种真正适合的,长期性的,通用型解决方案。来解决包括我们闲鱼技术团队在内的,大前端/大终端所面临的前端用户体验低、客户端效能低、团队技术割裂、技术协作成本高等一系列问题。KUN 项目组是一个技术栈高度互补的团队,包含 flutter、前端、native 多技术栈的关键专家开发者,在这个过程中,缺少任何一方都将大幅度的降低所能达到的上限。就像 KUN Log

125、o 所隐喻的,它既像一条大鱼又像一个猫身,它是对立和统一的结合体。二、KUN 的独特价值 既然已经有了 React Native,WEEX2.0,Kraken 等跨平台开发框架,我们为什么还需要 KUN?React Native 试图去混合和连接 JS 生态和 OEM 的 GUI 生态(Android 生态&iOS 生态)。但最大的问题在于其不同 OEM 生态之间天然的差异,去抹平它们之间的差异,是一件极具挑战的目标。而舍弃操作系统 GUI 生态,试图去重新构建一套独立完整的 GUI 系统去对接 JS 生态,是一件非常有抱负的目标。它可能意味着在对齐W3C 标准上的更高的上限,但同样这个方向上

126、,挑战重重。大终端领域的新物种-KUN 80 1.KUN 站在巨人的肩膀上,做出独特的价值-开放性 1)KUN 从一开始就不试图去达到完备的 W3C 的标准。接受达到 W3C 标准(包括 html 标签标识,CSS 样式标准,WebAPI 标准)的必要的子集对我们而言充分的。我们在一开始就对我们的目标做取舍和精确的定义。2)KUN 基于 Flutter GUI 系统和其生态,以极低的扩展成本,向上层 JS 运行时提供大量丰富的扩展标签/组件。KUN 不试图去构建全新独立的 GUI 系统,也不去试图抹平多个操作系统 GUI 之间的差异。最大程度结合 JS 技术/生态和 Flutter 技术/生态

127、,取长补短,优势互补。KUN 的开放性是 KUN 最显著的特征。通过更加开放和轻量化的容器设计和实现。KUN 试图通过更加开放性的架构设计,去混合兼顾 JS&Flutter。开放性,技术上意味着:有更广泛的通用性 更大的生态/社区的支持 有更敏捷的响应性 有更长期的成长性 结合闲鱼技术在 flutter 技术领域的天然优势,去混合连接 JS 生态&Flutter 生态,通过更加开放&更加轻量化的设计,在效率,体验,通用性上去取的最佳平衡。大终端领域的新物种-KUN 81 基于将 Flutter 生态融入到 Web 生态中,同时高度的开发性,有着更加广泛的通用性,使得从技术和组织的各种为政,从烟

128、囱式的多技术栈,有机会向分层的技术融合转变,走向技术统一,最后进一步到组织融合成为可能。不再按细粒度的技术栈划分职能岗位,而是统一为职能更大类的终端开发工程师或者开发工程师转变。这尤其在中小规模的技术团队组织中,产生更加显著的价值。当然就这一融合目标的达成,需要考虑至少三个因素:技术方案能不能行 组织内同学愿不愿意 外部环境趋势是否符合 三、KUN 的技术方案和面临的挑战 大终端领域的新物种-KUN 82 KUN 项目第一语言是 Dart 语言,也包含少量的 TS(负责 WebAPI 声明)&C+(负责跨语言通信)代码,以及极少的 Java&OC 代码(负责和操作系统相关)。1.如何实现 Wr

129、itten in JavaScript、Html、CSS,Rendered with Flutter。首先我们需要设定一个我们要完成的目标的边界。没有边界的目标是虚无的。我们明确,不需要实现所有的 W3C 标准,即使在未来也没有这一方面的企图。所以我们在 KUN 容器诞生过程中,除了整体参照阿里巴巴集团现有的跨平台标准外,同时考虑适用性、可测性、易开发、易遵循等原则。适用。适用于闲鱼业务,满足闲鱼大前端绝大部分业务需求;适用于移动端,摒除非移动端视角,完全适用于移动端开发的容器标准。可测。标准定义包含完整的功能边界,可依据标准测试用例保障单测。易开放。未来非闲鱼App可快速接入符合标准的容器;

130、大前端同学可快速上手,存量业务可快速迁移。易遵循。定义出明确、合理的优先级,容器可按照优先级阶段性实现最符合大前端业务的 一、二、三环。如果从零开始完成这项工作,那无疑是非常艰巨又漫长的。好在我们可以站在巨人的肩膀上。通过借用大量的优秀的开源基础库/项目来帮忙完美更快更好的完成这一目标。使用开源 QuickJS 解决 JS 运行时问题。使用开源 YOGA 库,解决使用 CSS 布局问题。使用开源 Kraken 部分源码和 CSSLib 库完成 CSS 样式的定义/解析/计算以及选择器能力。复用 Flutter 已有的 Widget 和 API,进行灵活的组合,解决 CSS 渲染问题。使用 CD

131、P 协议,去解决开发者体验问题。使用 Flutter golden test 解决测试用例的效率问题。尽管以上并不解决所有问题,但已经覆盖很多了,给我们更大的空间去解决其他更关注的问题点。大终端领域的新物种-KUN 83 1)关于 CSS 样式的绘制 如何使用已有的 Flutter 组件和 API 的组合,来完成对 CSS 样式渲染和事件的支持能力。典型代码举例:备注:其中任意的一小段函数,比如 增加 Appear/Disappear 事件。大终端领域的新物种-KUN 84 绘制 CSS 样式的复杂度远小于去实现 CSS 样式的布局的复杂度。使用 Flutter 已有的 Widget 和 AP

132、I 去使得我们避免侵入到 Flutter Render-Object 层去做非常繁重的工作。同时我们必须承认,在任意阶段,我们对 W3C 的标准对齐是不完善的,所以需要我们不断通过更小粒度的组合去逐步完善。而某种意义上,我们发现 Flutter 技术和 Web 技术的很深渊源的部分,这使得我们完成这件工作变得简单。2)关于布局 a)CSS 样式布局 基于 yoga&FFI,我们能快速实现必要的基本子集。b)Flutter 扩展标签布局 并不需要额外的设计和处理,走原生 Flutter Layout。c)混合边界和约束处理 在标准 html 标签和 Flutter 扩展标签的互相嵌套中(可以嵌套

133、的非常深),我们需要定义它们的布局边界和上下级之间的大小约束关系。一种简化的脱离文档流和层叠样式实现。大终端领域的新物种-KUN 85 备注:KUN 的 DOM-Tree/Layout-Tree/Render-Tree 本质上是三组不同的指针形成的三棵树,而其Element是同一份。当DOM-Tree首Driver驱动发生变化时,Layout-Tree 和 Render-Tree 会同时发生变化。3)关于更新机制 a)更新主要包含两个动作:标脏、刷新 b)标脏 基于以 JS 驱动的主动变更的标脏。基于以 Flutter 驱动的被动变更的标脏。基于运行时 Widget 构建的自动依赖收集的标脏。

134、c)基于标脏后的最小化刷新机制 最小化必要的 yoga layout 的重新计算。很好地借用了 Flutter 已有的刷新机制,只有真实参与布局/绘制/事件有变化的Widget 才会重新构建,触发最小化的刷新。2.如何实现,几乎零成本的扩展。KUN 试图通过设计强大的开放系统,将 Flutter 生态完整的提供给 JS 生态。大终端领域的新物种-KUN 86 除了在架构分层上,显著的将微内核和开放层做了明晰的边界划分,在具体组件的开发性上做了很多具体的设计和取舍。具体有几点:在 KUN 的标签/组件系统里,一个自定义标签和一个标准 html 标签是平等的。它们都对应一个或一组 flutter-

135、widget,它们都是扩展的组成部分。自定义标签和标准 html 标签之间的相互嵌套、约束关系,也是开放标签/组件系统的组成部分。一个标准的 html 标签不是一个复合的巨型 flutter-widget。它是由一系列可组合的 flutter-widget 构成。原因是对 W3C 标准的实现,是一个过程,更细粒度的组合关系,有利于不断引入 Flutter 生态已有的部分,更高效的迭代演进。引入一个自定义标签/组件,应该几乎是 0 成本的。包括标签/组件的内部实现本身,以及去组合通用能力(比如 style 渲染,常用事件等),应该快速 0 成本的实现。而基于丰富的 Flutter 组件生态,和极

136、低的扩展成本,极好的弥补了 Web 技术的天然缺陷。1)四个步骤扩展并使用一个组件 创建组件 注册标签 生成文档 JS 使用 以一个简单的图片组件为例:a)新建 flutter 组件 可以创建一个 flutter 组件,或引入一个已有的 flutter 组件。大终端领域的新物种-KUN 87 b)通过统一扩展接口 KunDefs,注册标签。大终端领域的新物种-KUN 88 备注是一种能力组合方式,用于让自定义标签快速获得额外的能力,比如通用的style 的样式渲染能力或点击事件能力。c)在组件上加上必要的注解,用于自动生成组件文档和组件的 TS 定义(d.ts)自动化完成,文档的更新发布&组件

137、 TS 定义 npm 发布。大终端领域的新物种-KUN 89 (*)可选的一码多端在 W3C 标准上 KUN 容器是 Web 容器的一个子集,在自定义标签上 KUN 容器是 Web 容器的一个超集。一码多端是前端为了适配 KUN 容器和Web 容器之间的差异,(想象一下历史上为不同浏览器做的兼容处理),但要简单得多,主要负责了如何将 KUN 容器上的增强标签能够降低到 H5 版本。可选。d)前端工程导入 d.ts 定义后,可以像 React 组件一样使用,带 Lint 检查和代码提示。备注,任意内置能力以相同的 api 进行扩展。大终端领域的新物种-KUN 90 3.如何实现更好的面向更广泛的

138、 Web 开发者和 Flutter 开发者的体验。1)面向 Web 开发者 运行时面向标准 Web-API 设计和实现。在这一层的标准上,我们高度借鉴了 Kraken 的理念,面向 WebAPI 设计而不是绑定具体一个前端框架。对不同语言的指责做了明确的定义和划分:a)Typescript 用于对 WebAPI 的声明部分。b)C+用于跨语言通信的部分。c)Dart 用于对接动态库的驱动的部分以及具体的 KUN 实现部分。所以(a)(b)非常薄。工程链路复用前端工程化 DevTools 对齐 Chrome DevTools 基于 Chrome DevTools Protocol,对齐常用的日志

139、查看/元素审查/盒模型查看/CSS调试/样式锚点/属性查看/控制台输入输出/Source 源文件和定位/网络/存储/事件联动等。大终端领域的新物种-KUN 91 而基于 FlutterForWeb 技术,未来有机会将 KUN 开发调试环境集成进浏览器环境。2)面向 Flutter 开发者 a)原生的 flutter 开发环境 运行 KUN 只需要最简单的 flutter 开发环境就够了,它的依赖非常少。b)极简的 playground 四、KUN 的进展和规划 1.KKUN 目前支持超过 100 个 Web-API(包含 DOM-API&BOM-API)1)超过 30 种 html 标签 覆盖

140、文档节点/文档元信息节点/片段节点/内容组/语义内容组标签/语义文本标签/嵌入式内容/脚本等。2)超过 30 种自定义开放组件/标签,覆盖内容/容器/动画/输入等。3)支持超过 100 个属性定义的 CSS 样式,覆盖:覆盖布局/盒模型/字体、文本/颜色、背景/边框、圆角/变形、过渡/动画/效果处理(滤镜、遮罩等)/规则支持/层叠上下文等属性。大终端领域的新物种-KUN 92 覆盖包括绝对单位/相对单位等 15 种单位,和包含继承等在内的 5 种值属性。覆盖至少 3 种基础选择器。较好支持层叠上下文。4)支持 63 个 BOM-API,覆盖定时/跳转/URL/环境/Location/屏幕/存储

141、/日志等。5)并为此建立了超过 1100 个 test-cases 的高效的自动化测试系统。基于 flutter golden test,5 分钟内完成像素级截图对比。微内核和扩展,行覆盖分别超过 80%和 70%。KUN 的业务进展介绍基于极强的业务需求驱动,KUN 已经在闲鱼导购场景/基础链路场景,灰度或已经全量。以闲鱼典型的“我发布的”页面为例:大终端领域的新物种-KUN 93 通过技术升级从 H5 升级到 KUN,降价交互组件体验升级,显著大幅提升了业务的重点指标。后续包括新版闲鱼号等核心基础链路和双 11 核心互动场景都将基于 KUN 落地。2.KUN 的规划 这个项目的从最初是一个

142、带有一点验证性的项目,但随着项目逐步在业务中的落地应用,它让我们的理想变得更加触手可及。一种技术适合闲鱼前端&客户端,覆盖全业务域场景。2022 Q4 Roadmap,会重点关注一下几个方面:1)支持包括双 11 在内的更多的重点业务。2)更多维的性能优化。3)基于 KUN 的组件库建设。4)开发者体验。5)CI&Test&Document 的持续建设。最后:如果你会Web应用开发,你可以通过KUN创建一个原生性能的移动应用程序。如果你会 Flutter 应用开发,你可以通过 KUN 创建一个动态化的移动应用程序。如果你的团队同时会 Web 应用开发和 Flutter 应用开发,你可以通过 K

143、UN,使用 Web 技术开发,Flutter 技术增强你的移动应用程序。结合 Web 技术和 Flutter 技术各自的优势互补,以及它们背后良好的生态和社区支持,你有机会使用一种技术来来覆盖你的所有上层业务。KUN 是一个新物种,有机会去完成这一点。三代终端容器 KUN 的首次大考【架构演进】94 三代终端容器 KUN 的首次大考【架构演进】作者:叶遥、颂晨 闲鱼号在闲鱼业务中一直承担着非常重要的角色,它既是卖家组织商品的货架,又是达人自我表达的载体,既是大 V 私域运营的阵地,又是小铺开店经营的门面。它是闲鱼各产品线的交汇点,号店浑然一体,一定要类比的话,它更像是抖音/小红书个人主页+淘宝

144、店的综合体。闲鱼号是个用户高频访问的场景,产品 Feature 快速迭代,体验上备受关注,当下面临的问题:古董级高度耦合的业务代码、多业务线并行的日常需求时常让前端成为交付瓶颈。基于 Weex 1.0 渲染附带着大量的双端不一致问题和体验顽疾,也限制了交互创新。最近我们对闲鱼号做了架构升级,相信很快就会和大家见面,这里做个小结,概括下来这次升级直接带来的收益:三代终端容器 KUN 的首次大考【架构演进】95 1.体验 中高端机上维持秒开,同时:新增微信朋友圈式的下拉封面交互,手势体感更加连贯,个人表达更加充分。新增贴近原生体验的下拉刷新交互,提升 APP 体验一致性。优化嵌套滚动的交互体验,纵

145、划横划更加自然顺滑,逛起来更高效。2.可维护性 可维护性的提升是产品-设计-实现综合优化的结果,具体:产品侧重新梳理所有 Features,抽象并制订同类功能的表达原则,确定各业务的表达方式、优先级。设计侧综合考虑模块权重、所属角色、用户比例、扩展方式等因素确定设计框架。技术侧通过组件化拆分+全局状态的方式解耦业务逻辑,提高需求并行效率。一、为什么要升级 闲鱼号项目已有超过 5 年历史,目前业务较难向前迭代,原因主要归结为端容器能力受限和架构腐化两方面。1.端容器能力受限 闲鱼号目前是前端页面,容器使用 Weex1.0(后文统称 Weex)。Weex 两年前就少有维护,其既有问题使得承载当下业

146、务有以下问题:难维护。闲鱼号存在较多的舆情顽疾,究其原因,Weex 不是标准前端容器,在布局、组件、动画、事件等方面与预期不一致。一部分绕道解决,一部分只能保持现状依托升级容器解决。难做好体验。业务定位使闲鱼号在体验上有较高要求,但“这个交互是 Native实现的,Weex 做不了”不时会出现在业务迭代的技术评估中。三代终端容器 KUN 的首次大考【架构演进】96 2.项目架构腐化 闲鱼号有较高的业务复杂度和较厚重的历史上下文。承载了人设、电商、内容、信任等领域的业务,同时存在多维度视图(主/客态、B/C 态、内容/电商态),多年下来已发展到 5.x 版本,技术架构仍未进行过本质升级,相关问题

147、已严重影响项目日常维护和迭代,主要体现在:1)ViewModel 过于复杂。ViewModel 是大管家,统一格式化、将数据派发到模块。在平铺了 15+模块数据的场景,ViewModel 改动不时“牵一发而动全身”。2)缺乏统一的状态共享方案,数据流混乱。组件间通信存在 Vue 事件体系、自建事件体系、全局 controller、自建状态共享体系多种方式。二、升级目标 对应上述问题,闲鱼号迎来了技术架构升级。升级核心目标是,未来 1-2 年技术架构不成为项目迭代、维护的吞吐瓶颈,支撑业务快速、平稳、创新的发展,能持续保持高标准体验。其中:容器能力:升级渲染容器,提升容器能力边界。明显减少存量体

148、验顽疾,多场景协助业务、设计完成理想的交互、视觉体验。项目架构:模块解耦,打造清晰的数据流。明显提升迭代效率,减少影响面回归成本和压力,降低不同模块协作冲突次数。三代终端容器 KUN 的首次大考【架构演进】97 其中针对项目架构部分非本文重点,主要通过模块化前端后数据协议、统一状态管理方案进行了解决。后文继续介绍容器能力部分的思考和实践。三、为什么用 KUN 渲染?1.闲鱼号端侧主要问题 为对症下药,对闲鱼号端容器(Weex)侧进行了全面的诊断。问题集中在性能、渲染质量、扩展能力、终端体验一致性四个方面。1)性能:闲鱼号首屏(700ms)和交互性能不错,性能问题主要在内存上,Weex页面重度访

149、问是闲鱼客户端 OOM 的主要场景之一。启动 Weex 容器会产生较大的增量内存,部分控件无回收机制也会导致内存增加,如 waterfall 组件:加载 5 页带来了 3000 个未释放内存节点、40M 内存增量。三代终端容器 KUN 的首次大考【架构演进】98 2)渲染质量:在基础样式、布局、事件体系等方面和前端预期不一致,如:不支持 overflow:visible。不支持 z-index,层叠只能通过节点位置先后实现。不支持 display:inline,替代方案 rich-text 标签不支持 line-height 等基础样式控制。3)终 端 体 验 一 致 性:闲 鱼 号 工 程

150、中 充 斥 着 大 量 形 如 if(isIOS)xxxxelse if(isAndroid)xxx的代码,主要作用之一按端处理,以规避渲染差异。尽管如此,目前两端仍存在不小差异。4)扩展能力:前端标准和生态起源于 PC,无线设备相对于 PC 存在不少特性,无线端原生能力相对于纯前端也更加丰富。受容器扩展能力(成本)限制,前端无法(标准)实现部分业务认为理所当然的体验:无法在元素上屏前获取元素布局信息(宽、高、位置)。大多折叠场景需要。高频动画性能不佳。绑定滚动的动画如曝光、导航透明度渐变等场景。基础控件能力缺失。如:输入框无法自动聚焦、控制聚焦时距离键盘空间;增强控件定制困难,如在嵌套滚动容

151、器上添加回到顶部。为从根本解决上述问题,我们进行了新容器调研。2.渲染容器选型 在无线前端容器演进过程中,前端侧比较固定,框架(React、Rax 等)驱动业务代码生成 Virtual DOM 以抽象视图结构,特定 Driver/容器内置 JS Module 将 Virtual DOM 翻译为容器对应的渲染指令;容器侧主要历经 Webview 容器渲染-客户端渲染衍生-自绘渲染(衍生)三个阶段。其中:1)Webview 渲染容器。为解决 Native 页面双端研发成本、双端渲染不一致、无动态化、页面与客户端耦合的问题,Webview 容器开始承载无线业务。2)客户端渲染衍生容器(React N

152、ative、Weex 1.0)。Webview 解决问题的同时引入了新的问题,渲染性能和能力边界明显逊色,故在 2.0 时代诞生了客户端渲染衍生容器,对接起了前端、客户端生态,用前端生态写,用客户端能力渲染。三代终端容器 KUN 的首次大考【架构演进】99 3)自绘渲染衍生(基于 Flutter/完全自绘)。客户端渲染一定程度了解决 Webview渲染性能、能力边界的问题,但将前端标准/生态“翻译”为 Android/IOS 标准/生态的过程,存在明显的失真,实际渲染与预期不一致,Android 与 IOS 不一致。由此,更彻底的方法是重写渲染能力以进行统一。自绘渲染方案一方面性能优于 Web

153、view,一方面渲染一致性优于 Weex,闲鱼号新容器往自绘(衍生)类选型便是自然的方向。在自绘容器中,基于 Kraken,闲鱼技术团队自研了 KUN 容器,整体思路是对接前端和 Flutter 生态,用前端写,在 Flutter渲染。对于上文提到闲鱼号端侧的 4 个问题,KUN 对应的解决原理如下:1)性能(内存)对于已接入 Flutter 的客户端,打开 KUN 页面的增量内存只有 KUN 引擎本身,没有 Flutter 负担,KUN 引擎较为轻量,内存增量相对于 Webview、客户端衍生方案较少。Flutter 自身提供了良好的回收能力(sliver),在无限流(瀑布流)场景,内存占用

154、不会随内容加载无限上涨。2)染质量。借助 Flutter 像素级渲染能力,上述 overflow:visible、z-index、rich-text 等问题均能解决。3)终端体验一致性。前端对接的生态从两套变成了一套,以 React Native 和 KUN中 Text 组件渲染为例:下图中,Text 业务组件在 React Native 下被转为了 JS 三代终端容器 KUN 的首次大考【架构演进】100 元素,在 UIManager.js 中转为 RN Element RCTVirtualText。接下来就是客户端部分,RCTVirtualText 在 C+层通过 Bridge 将指令传递

155、给 Native UIManager,UIManager 根据所处不同的系统环境进行组件映射,IOS 映射到 UITextView,Android 映射到 TextView。相对之下 KUN 则是一套映射,呈现 Flutter 基础组件,便能有更好的终端一致性。4)扩展能力。经过一层 KUN Element 抽象,自定义 Flutter 组件也可视为基础组件同等公民开放给 KUN 前端。如此,通过低成本对接前端、Flutter 生态等方式,KUN 有了灵活强大的扩展能力,以嵌套滚动容器为例:三代终端容器 KUN 的首次大考【架构演进】101 以上从理论层面推导了 KUN 能解决上述问题,我们便

156、开始了闲鱼号升级到 KUN 容器的实践。现阶段已完成升级,经过了一轮技术灰度。回顾升级过程,也像新生事物一样充斥着标准对齐、性能等诸多细节问题,但最终都悉数解决,整体体验符合预期。四、体验变化效果 1.性能 首屏性能上,KUN 与 Weex1.0 勉强持平,中高端机器上维持秒开。内存水位上初步测试较 Weex、WebView 有所降低,待补充准确数据(上图为 KUN,下图为 Weex)。原文为 gif 三代终端容器 KUN 的首次大考【架构演进】102 原文为 gif 2.渲染质量 基础样式、布局、事件体系等方面已基本对齐前端标准,overflow:visible、z-index、rich-t

157、ext 等均已正常渲染。3.扩展能力 Kun 使得前端享有了闲鱼客户端 Flutter 生态。闲鱼号升级中,快速扩展了前端无法(标准)实现组件 10 余个,包括:流畅嵌套滚动 富交互下拉封面 三代终端容器 KUN 的首次大考【架构演进】103 滚动视频播控 图片加载控制(裁剪、渐显等能力)带高斯模糊背景的弹幕 原文为 gif 借助 Flutter 能力取背景图主色的蒙层 借助 Flutter 在上屏之前能获取布局信息,标准化实现了纯前端难以模拟的行数过多动态折叠功能 三代终端容器 KUN 的首次大考【架构演进】104 原文为 gif 4.终端体验一致性 渲染引擎较少与 OS 渲染能力耦合,解决

158、了双端组件、交互(bounce 效果)、布局等方面不一致问题。除了文字排版和字体外,基本做到双端一致。研发过程中,渲染层面也几乎不出现 if(isAndroid)renderAndroid()if(isIOS)renderIOS()的代码。三代终端容器 KUN 的首次大考【架构演进】105 五、后续思考 如果从闲鱼号端侧诉求的视角出发,我们可以这样看待 KUN 的演进:KUN 在基础规范上会持续扩充,并且与 W3C 规范持续对齐;在扩展能力上通过 CSS扩展、JSAPI 扩展、混合组件扩展等方式持续增强容器能力、拓展容器边界、提升用户体验。闲鱼号架构在持续演进中。有了 KUN 的加持,我们对此

159、充满信心。106 服务端主题(此页面将由下图全覆盖,此为编辑稿中的示意,将在终稿 PDF 版中做更新)电商搜索里都有啥?详解闲鱼搜索系统 107 电商搜索里都有啥?详解闲鱼搜索系统 作者:云钟 搜索是电商平台的核心流量入口,承载着平台主要的成交引导、意图收敛、活动投放。一个稳定、高效、可扩展的搜索系统是电商平台得以生存发展的基石。本文探讨如何构建完善的商品搜索系统,并根据闲鱼二手交易的差异化特性介绍闲鱼搜索系统的时效性优化。一、首先,构建一个搜索系统:电商场景的搜索 1.搜索引擎 搜索系统的核心是搜索引擎,目前 Lucene、ElasticSearch 等开源引擎已十分成熟,阿里云也提供完整的

160、搜索解决方案-OpenSearch,包含基于 Ha3 的搜索引擎(Heaven ask 3)及系列管控工具。这里,我们简单描述下搜索引擎内的基本概念作为导引,不过多深入引擎的具体实现(那将是一个冗长的话题,网络上的资料也随处可见)。1)搜索引擎的基本概念 分词 通过一定规则对文本分出单词,每个单词作为搜索的最小粒度单元。只有单词匹配,文档才能被召回,因此分词的准确是搜索精准的基础。如“红色摩托车”被分词成“红色”,“摩托车”,那它将被“摩托车”或者“红色”召回,如果分词成“红色摩托”,“车”,那它在引擎中被搜索出的概率就将大打折扣。索引?_红色苹果手机,doc1_?_红色苹果,doc2_?“红

161、色”,“苹果”-doc1,doc2?_“手机”-doc1_ 电商搜索里都有啥?详解闲鱼搜索系统 108?倒排索引:称为反向索引、置入档案或反向档案,是一种索引方法。被用来存储在全文搜索下某单词在文档存储位置的映射。它是文档检索系统中最常用的数据结构。?正排索引:也叫 attribute 索引或者 profile 索引,是存储 doc 某特定字段(正排字段)对应值的索引,用来进行过滤、统计、排序或者算分使用。正排索引中“正指的是从 doc-fieldInfo 的过程。?索引内容类型:文本索引、空间索引、向量索引、数值索引。排序方法 匹配召回的结果集,通过特定的排序规则呈现。这里的排序规则,可以是

162、单一维度的排序(如按价格、销量、发布时间);人工设置的权重分;相关性得分;特定场景的模型打分等。基于这三个基本概念,搜索动作就可以简化地理解为“利用搜索词的分词结果,通过倒排索引匹配相应的文档,并依据特定排序方法有序透出”的过程。搜索引擎仅提供搜索的基础能力,现实环境下的搜索场景当然要复杂的多,一款地图搜索和一款商品搜索所面临的挑战大相径庭。作为原材料的搜索引擎,该打造成何种形状,就看面对问题如何去设计模具了。以闲鱼为例,搜索系统的整体架构如下:电商搜索里都有啥?详解闲鱼搜索系统 109 闲鱼搜索系统架构图 2.在线服务 上述架构图中的步骤 18 为一次搜索请求的完整执行流程。1)请求接入模块

163、-应用层 处理客户端或 h5 请求,请求接入模块的主要工作:参数校验、负载均衡、安全拦截。大部分的非法请求在这一层被拦截,避免进入系统核心模块后,导致不可预期的结果。应用层承载面向用户的业务逻辑:实际处理用户的业务请求,进行安全合规检测,同时并行请求投放的各类资源位。电商搜索里都有啥?详解闲鱼搜索系统 110 2)应用层-排序接入层 排序接入层是连接应用层与底层引擎的纽带,也是闲鱼搜索系统的最核心模块。他负责解析应用层的搜索请求,并对其进行合适流程编排:意图预测-请求拼串-搜索引擎召回-精排模型打分-重排规则-外部混排。3)排序接入层-意图预测模块 负责分词并预测搜索请求的实际意图,包括错词改

164、写(例:平果-苹果)、同义词的合并(例:pingguo-苹果),类目预测(例:“苹果”出手机,还是出水果,它们各自的权重又是多少?)。4)排序接入层-搜索引擎 利用意图预测得到的信息,合并应用层参数,拼装出合法的搜索引擎请求,在搜索引擎内部历经“海选”、“粗排”、“精排”三个阶段,得到符合召回条件的商品集。5)排序接入层-精排模型打分 由于 RT 的限制,搜索引擎内部无法完成对海量商品复杂度较高的打分计算。这一步的工作,将引擎召回的商品集送入更精准的打分系统进行算分。为什么不把打分服务放在引擎内部?技术上是可行的,但由于打分服务变更频率频繁,而引擎相对稳定,处于系统迭代稳定性的考虑,独立拆分精

165、排打分服务是更好的选择。6)排序接入层-混排模块 部分业务场景下,合作方有合并混排的诉求。独立拆分混排服务,隔离开发环境,让不熟悉主搜的外部开发同学在独立混排模块内做开发,即使服务异常,也不至于影响闲鱼本身的搜索能力。7)排序接入层-应用层 排序完成的商品列表,在应用层补充实时信息,如各类标签,促销信息等。同时,将商品搜索结果与广告等各类投放组装层最终的搜索结果页。电商搜索里都有啥?详解闲鱼搜索系统 111 8)应用层-接入层-客户端 将最终的搜索结果页返回到客户端或 h5 页面进行渲染。3.离线模块 与在线服务对应,搜索系统的离线模块负责数据的 dump,清洗,索引构建。1)搜索引擎离线模块

166、 全量索引(Fullindex):数据源来自多表 join 后的全量业务数据,包含所有商品信息,由 buildSerive 构建好索引后提供给 Ha3 使用,系统内仅有一份全量索引。批次增量索引(IncIndex):根据周期内(通常 30 分钟到 1 小时)数据产出方发送的增量消息(如:商品修改信息),在 BuildService 上构建成索引段,定期发送给 Ha3 加载,引擎存在多段批次增量。实时索引(RtIndex):将数据产出方实时生产的数据经中转 Topic 发送至 Ha3,由 Ha3 引擎内的 BS lib 构建出实时索引加载使用,时效性为实时。在新的批次增量索引加载后,Ha3 对实

167、时索引作清理。电商搜索里都有啥?详解闲鱼搜索系统 112 4.稳定性 搜索承载闲鱼导购线的核心流量,因此对系统的稳定性和业务的高可用有极高的要求。闲鱼搜索系统,分别在中心机房(张北)和单元机房(南通)进行了异地多机房部署,确保当单一机房故障时,流量可转发至正常机房服务。1)应用层异地多活 2)引擎层异地多活 电商搜索里都有啥?详解闲鱼搜索系统 113 5.外围系统 核心链路以外,搜索业务的高效运作也离不开一系列外围系统。投放系统资源位与配置的投放 电商搜索里都有啥?详解闲鱼搜索系统 114 评测系统算法标注、效果评测 Debug 平台可视化的在线 debug 工具,负责线上请求回放、舆情排查等

168、 二、然后,优化它:闲鱼搜索系统时效性优化 1.差异化场景 电商搜索里都有啥?详解闲鱼搜索系统 115 上一章节提到的搜索系统架构适用于大部分电商平台,但闲鱼搜索场景与常规电商平台之间,又存在着显著差异。其中,闲鱼商品单库存,无明显冷热差异且变更频繁的特性,对全链路的实时处理能力都提出较高的要求。试想一下,热门的商品被买下架后,并没有及时同步引擎。对买家、卖家和平台来说都是一种困扰与损失。闲鱼搜索经年累月的业务迭代,累积了相当量级的实时增量。最严重的时期,引擎的增量延时一度达到 8 小时之久,对用户体验、成交效率形成了巨大冲击。因此过去的一段时间,我们开启了搜索时效优化的专项。电商搜索里都有啥

169、?详解闲鱼搜索系统 116 2.Searcher 扩列 排查链路实时处理能力的瓶颈,如上文离线模块中对实时增量的描述(数据产出方实时生产的数据经中转 Topic 实时发送至 Ha3,由 Ha3 引擎内的 BS_lib 构建出实时索引加载使用),在实时增量到达索引所在 searcher 列之前都未出现延时,因此瓶颈位于在线 searcher 的 BS_lib 的消费能力,最直接的方式是提升 searcher 的处理能力。因此,我们将 searcher 的列数从 16 列扩大至 24 列,提升了 50%的实时增量处理能力,增量延时缓解。3.引擎架构治理 我们重新思考了闲鱼主搜引擎的架构与定位。在之

170、前的架构中,倒排、正排、详情字段部署在同个引擎(Ha3 引擎支持这种能力)。倒排、正排用作查询索引,详情字段补充商品详细信息,并对外提供信息补全服务。但查询的请求量是极其不对等的,详情字段的 qps 为索引字段的 5.3 倍。倒排与正排字段的数量远大于详情字段,因此查询请求的机器要求是小 CPU,大内存,详情请求的机器则是大 CPU,小内存。两类字段部署在同一引擎,各自的资源短板被放大。同时,各字段实时增量是累加的。因此,我们拆分了查询引擎与详情引擎,缓解增量压力的同时,机器资源消耗也有所降低。电商搜索里都有啥?详解闲鱼搜索系统 117 4.增量分级 做了以上两个优化后,增量延迟有所缓解,但整

171、体的增量量级并没有下降多少。因此在离线链路中,我们开发了增量 Profile 插件,用来统计字段修改的频率。其中两个字段合计占到修改量的 37%(总 400+字段),经排查,两个字段并不需要秒级的实时性。引擎的离线链路中,虽然设定了批次增量与实时增量,但事实上所有开启的增量都会走实时增量通道,挤占优先级更高的增量的吞吐量。因此,进入引擎的增量务必根据业务诉求再做一次分级。电商搜索里都有啥?详解闲鱼搜索系统 118 由此,我们在 buildService 上开发了增量分级插件,提供增量分级处理的能力,对于准实时性要求的增量仅做批次增量(1 小时进引擎),不挤占核心实时增量的通道,保障商品核心特征

172、的实时性。5.辅表写扩散问题 优化增量量级的另一个重要关注点就是辅表写扩散问题。商品以 item_id 为主键分列部署,离线阶段,当辅表有驱动增量时,跟主表 join 后增量翻倍增长。例:商品上挂载用户在线状态的特征,当用户状态变化时,该用户的所有商品都会发出实时增量消息。量级将远大于用户在线状态的实际消息量级。解决方式是将这一类字段,单独改造成在线辅表,通过在线 join 的方式查询。电商搜索里都有啥?详解闲鱼搜索系统 119 经过以上系列的优化后,我们最终把引擎延迟抹平,达到真正实时化搜索的定义(基本无延迟,红框部分为全量后追增量阶段,采用逐行 Rolling 模式,切换中机器不服务,对用

173、户体验无感知)。三、最后 电商平台的搜索是一项系统性工程,经过多年发展,已沉淀出一套通用性的框架。里面不仅包含对搜索引擎的理解,也体现服务端架构设计的可伸缩、平行扩展等概念。但仅有框架的认知还不足以支撑快节奏的互联网业务发展。在通用框架的基础上,深刻理解搜索业务,关注稳定性、研发效能,找到应用场景的痛点,有针对性的做出架构调整,才能构建出真正助力业务发展的搜索系统。QCon 直击闲鱼推荐大规模应用背后的工程实践 120 QCon 直击闲鱼推荐大规模应用背后的工程实践 作者:吴白 一、推荐在闲鱼的应用 不同于搜索的确定性,推荐场景面临的问题往往是不确定的。但是正是因为这种不确定,带来了非常大的可

174、能。所以推荐在闲鱼基本上遍地开花的状态。尽管如此,推荐在闲鱼仍然面临着非常大的挑战,而这些挑战和闲鱼 C2C 市场的定位和特性密切相关。总的来说,闲鱼有四个比较明显的 C2C 特性:浅库存。闲鱼的商品基本都是孤品单库存,售出即下架。这就需要推荐系统需要非常实时的感知到这种状态变化,对实效性要求非常高。轻发布。闲鱼中的卖家大部分都是个人卖家,为了简化发布成本,一直提倡轻发布。所以闲鱼商品的结构化信息非常少,这给推荐带来的挑战非常大。自由市场。闲鱼还是一个自由市场,在这个市场中有个人卖家,小 B 卖家,行业和专业卖家,同时也有各种黑灰产用户。这导致在闲鱼流量分发的难度非常高。快速成长。最后闲鱼是一

175、个成长型业务,快速增长带来的规模对推荐的整个基线压力非常大。QCon 直击闲鱼推荐大规模应用背后的工程实践 121 闲鱼推荐的演进历程和这四个特性密不可分,所以闲鱼推荐大致可以分成四个阶段 阶段一:圈品+离线打分。这个阶段推荐主要靠圈品+离线算分为主,无个性化,时效性天级。阶段二:少量算法。阶段二开始在上海品茶核心场景引入算法,以天级的 I2I 为主,但推荐底池时效性已经到了秒级。阶段三:扩大应用。随着业务拿到算法第一波红利,越来越多的业务开始接入算法。特征和模型时效性也从天级提升至小时级,闲鱼首次引入招选搭投,应用大规模铺开。阶段四:随着业务快速成长,规模快速扩大,底层基建迎来大规模升级。全图化

176、,模型自动压缩,通用推荐等实现从 0 到 1 的越跃变。经历了上述四个阶段的演进,形成了如下所示的闲鱼推荐 HLA。QCon 直击闲鱼推荐大规模应用背后的工程实践 122 二、算法离线面临的挑战 1.闲鱼对实时数据的强诉求-公共实时数仓建设 闲鱼有非常多的场景需要消费实时数据,比如在生态治理方面,闲鱼上很多优质供给在很短时间被黄牛扫光,但是从平台角度看肯定更希望这些优质供给能被更多的用户消费。因为这个原因,闲鱼长久以来有 3 个团队在做数据相关的建设:算法面向模型,需要非常实时的数据,所以会做很多定制化的链路;BI 面向数据分析;工程面向应用。随着不断的演进叠加,其问题也逐渐暴露出来:数据时效

177、性缺乏。除了算法,工程和 BI 对实时数据的诉求也越来越强。数据准确度问题。由于之前的数据都是烟囱式开发,其数据在使用之前无论是数据口径还是精确都需要做多轮 check。成本浪费。存在大量的数据冗余存储和计算。所以 BI,工程,算法一起打造闲鱼的公共实时数仓,作为下游众多应用的数据来源。2.特征需要被统一管控 在闲鱼,特征不仅应用广泛,而且影响非常大。无论是从沉淀服务更多场景还是成本的角度看,都需要统一管控起来。QCon 直击闲鱼推荐大规模应用背后的工程实践 123 我们期望通过特征中心的建设,实现以下几个目的:特征能以资产的形式沉淀下来。所以我们构建了特征写入和存储模块,结合特征管理模型,实

178、现特征的低成本快读接入。特征能够高效的对外输出。一方面性能足够好,另一方面能以不同的形式服务众多下游。特征生命周期管控。在这以前,闲鱼的特征是不可迭代的。因为只上不下,线上哪些特征在用,这些特征的价值如何,没有人能回答。因此我们构建了特征质量模块,通过对特征重要性分析,对特征进行统一管控。基于特征的样本构建足够高效。通过特征全埋点来自动构建在离线一致的实时样本流。3.在离线模型的差异 一般来说,离线训练得到的模型并不能直接部署线上。这里有多方面的原因:模型网络结构的差异。典型的模型离线训练流程为:输入,预测,优化。在线预测阶段则只需要输入,预测两个环节。输入的差异。一方面数据结构的差异,模型离

179、线训练阶段需要输入特征和 label共同组成样本。在线则只需要输入特征即可。另一方面数据源也存在着差异。QCon 直击闲鱼推荐大规模应用背后的工程实践 124 训练阶段的数据大多来自于存储在某个地方的数据集,在线服务的输入则来自于请求入参和在线服务。计算架构的差异。离线训练阶段由于对性能要求不高,模型可能运行在 CPU 之上。但在线阶段由于对性能要求较高,则有可能运行在更为复杂的如 cpu+gpu环境之上。1)模型自动裁剪 在模型 Offline2Online 过程中,我们首先需要对模型做一轮自动裁剪。2)模型网络压缩 压缩过程中主要有几个策略:模型计算图的拆分。将 cpu 和 gpu 拆开,

180、计算密集型的任务放在 gpu 上完成。计算逻辑 Online2Offline。部分计算逻辑从在线计算变为离线提前计算完,在线只需要 IO 读取。模型计算图压缩。这部分主要是算子的合并,减少数据传输和切换的开销。QCon 直击闲鱼推荐大规模应用背后的工程实践 125 除此之外,在 GPU 上也做了大量的优化工作,这些优化工作总结起来遵循两个原则。最后做一个总结,闲鱼推荐系统的离线架构如上所示,主要解决:数据研发效率。如何快速拿到需要的数据。这里面会涉及到数据时效性如何,数据准确度是否满足要求,数据是否散落在各地等一系列问题。特征迭代效率。这里会面临的问题包括:特征在离线一致性;样本高效回刷;特征

181、一致性解析(训练阶段和在线服务阶段,特征来源不一样)。样本生产效率。每次模型迭代机会都会涉及到样本的更改。一个比较典型问题如行存样本导致的重复计算和重复存储的问题。QCon 直击闲鱼推荐大规模应用背后的工程实践 126 模型开发效率。可复用能力:有效的模型网络是否沉淀,以便新模型快速冷启;一些和模型无关的细节能否底层做掉,对算法屏蔽掉细节,比如滑动 auc 窗口时 auc 自动清零等;常用的脚手架代码能否通过框架完成等。三、推荐在线服务部署方案 1.在线服务部署-兼顾性能和效率 闲鱼推荐整体流程采用业界通用的分阶段召回架构,主要包括召回、粗排、精排、上下文重排这几个大模块,如下图所示:在讲在线

182、部署方案的时候,我们需要回答两个问题:1)对业务来说,部署方式是否足够灵活。哪些因素会阻碍业务想要的灵活性?我们认为有以下几点:QCon 直击闲鱼推荐大规模应用背后的工程实践 127 对算法是否足够轻量,让算法专注策略本身,屏蔽工程细节(性能,部署,资源调度等)。上线周期是否足够快。能不能进行低成本,大流量分层实验。2)对算法来说,部署方式能否保证其有足够的迭代空间。比如是不是足够稳定,性能是不是足够好,资源利用率是不是足够高等等。为了回答这两个问题,闲鱼推荐在线服务采用上面的部署方案,总体包括 4 个模块:1)在线服务模块。面向业务的 serverless 层,将算法能力打包成在线服务对外输

183、出。它有几个特点:serverless 平台。对算法屏蔽了底层资源调度,部署等细节。足够轻量,一次发布只需要秒级即可完成。dag 引擎。采用全图化框架,底层运行在 dag 引擎之上。算法代码完成后天然高性能并发,业务逻辑可视化。实验平台。能够支持算法快速进行低成本,大流量的分层实验。QCon 直击闲鱼推荐大规模应用背后的工程实践 128 2)召回&粗排。召回&粗排由于需要对万级别的数据进行计算,所以进行单独部署。在内部采用多行多列部署模式,增加灵活性的同时保障有足够的性能。3)打分引擎。精排环节由于数据已经收敛,所以优先保障迭代的灵活性,所以精排单独部署,同时会把模型和特征尽可能提前部署在一块

184、儿,最大化性能表现。4)监控平台。屏蔽底层差异,无论是基于 Java 的在线服务还是基于 C/Python 的引擎层,都可以通过足够标准化的数据协议使用一套方案解决。2.业务规模快速增长带来的挑战 四、可度量的策略解决方案 1.开放平台下的生态运营-混排策略层 闲鱼为什么需要单独的策略层?这和闲鱼自身的定位和特性强相关。闲鱼是一个足够开放的平台,面临的问题非常多且复杂。足够开放的平台之下闲鱼还是一个 C2C 市场,流量的分发需要回归 C2C 本质。QCon 直击闲鱼推荐大规模应用背后的工程实践 129 比如闲鱼面临着以下命题,因此在服务之上,架了一层策略层,来实现流量分发的全局最优。2.生态治

185、理解决方案 我们把闲鱼面临的一些问题从调控对象和范围两个维度划分来六个象限。基于这六个象限,设计了一套流量调控系统。QCon 直击闲鱼推荐大规模应用背后的工程实践 130 3.分层架构下的置信实验体系 到现在,闲鱼推荐的分层架构基本就已经很清晰了,如下所示:这里面每一层都会进行独立的实验迭代,也就意味着每一层都可能对大盘产生影响,无论是正向的还是负向的。因此在闲鱼经常会面临着一些下面的灵魂拷问。为了回答这些灵魂拷问,我们对实验体系进行了重构,主要重构点包括:新增空白层。每个实验域新增一层不做任何实验的空白层。由于每一层的流量都会均匀分布在空白层,因此该层的指标可以体现所有层的实验叠加后的效果。

186、新增空桶。每个实验域新增一个贯穿每一层的空桶。空白层 vs.空桶,可以得到每个域的实验叠加后对指标的提升效果。全局空桶。新增一个全局空桶贯穿每个实验域,用来衡量业务大盘的一些指标情况。QCon 直击闲鱼推荐大规模应用背后的工程实践 131 五、总结与展望 综上,未来仍有很多优化手段可以关注:指标自动归因,推荐链路白盒化;模型可解释;大规模负样本采样等。闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 132 闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 作者:泊垚 一、问题与挑战 如何描述、存储和计算优惠并提供较好的业务可扩展性。如何保障大流量下优惠实时计算的性能。为优惠查询加速做

187、的数据同步如何实现一致性。本文的方案经过线上系统验证,对于优惠系统设计的场景和数据同步的场景可做相应的参考。二、背景 在我们日常生活中,常常会遇到下面这样的场景:在闲鱼上,针对闲鱼交易中的粉丝购买和粉丝回购的优惠促销场景,提供了一种定向一口价的优惠能力:卖家可以按商品分别面向全部粉丝、老粉、已购粉设置不同的优惠价格。买家在导购、下单等场景可以实时看到自己能够享受的最低优惠价格。闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 133 三、技术实现 我们通过三个步骤来实现:分解优惠的基本要素,实现优惠的基本表达和计算。为了保障大流量下的优惠查询下性能和业务的可扩展性,对优惠对象的判定过程进行

188、抽象和加速。在优惠对象制备的过程中,通过离线+实时的方式同步数据,保障数据一致性。1.优惠的描述、存储与计算 一个优惠主要描述了“谁对哪个商品享受什么优惠”,拆解为三个要素就是:【优惠对象】+【优惠商品】+【优惠价格】。在这个规则中,主要是要解决如何描述优惠对象:在粉丝优惠的场景下,优惠对象是指卖家的粉丝、卖家的已购粉丝等,在存储一条优惠时,一个卖家的粉丝可以被描述为“卖家 ID_all_fans”的符号(同理,已购粉丝是“卖家 ID_buy_fans”)。这样我们可以得到一个优惠规则的描述大致如下:【卖家 A_all_fans】+【商品 1234】+【18.88 元】,对应的业务语义是:卖家

189、 A 的所有粉丝,对于(卖家 A 的)商品 1234,可以以 18.88 元的优惠价格成交。以这条优惠为例,当买家 B 访问商品 1234 时,我们会执行这样的一个过程:闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 134 查询商品 1234 上的优惠规则,发现一条【卖家 A_all_fans】+【商品 1234】+【18.88 元】的规则。分析【卖家 A_all_fans】表达的含义,表示的是卖家 A 的全部粉丝可以享受优惠。确定买家 B 是否是卖家 A 的粉丝,如果是,则以 18.88 元的价格展示优惠或者成交。这样,我们就实现了优惠设置和计算的能力,这个时候,我们只需要这样一个架

190、构就可以实现:2.优惠对象判定的抽象和加速人群 但这样的架构存在两个问题:优惠计算过程需要解析【优惠对象】这个符号背后所包含的业务语义,再由系统进行判断买家是否符合条件,随着业务规则的升级,系统的会变的非常复杂,可扩展性差。每一次优惠查询,都需要访问用户的关注关系、购买关系,这整个查询过程非常长,性能低下,当面对大流量时,系统会陷入瘫痪。闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 135 为了解决这两个问题,我们希望优惠计算过程不再需要理解【优惠对象】的语义,判定过程中也不要再去查询各个业务系统。我们发现,优惠对象的判定过程,都是在回答“用户是否属于某个群体”,我们可以将这个关系进行

191、抽象,提前制备并存储起来。在我们常见的技术手段中,表达一个用户是否属于某个群体有两种实现:在用户对象上打上一个标记。创建一个“人群”对象,将用户关联到人群。一般情况下,第一种方式使用于群体较少可枚举的情况,第二种方案适用于群体较多的情况。在我们的实现中,使用了第二种方案。我们将用于描述优惠对象的符号(例如“卖家 A_all_fans”)作为人群的名称去定义一个人群,按照这个规则,我们为每个卖家的不同分组各定义这样一个人群(这里人群作为一个符号,这里不需要实际被“创建”)。人 群 和 用 户 的 关 系 存 储 可 以 通 过 redis 实 现,我 们 设 计 一 个 类 似:$user_A_

192、$crowd_B的 key 写入 redis。在查询时,查询$user_A_$crowd_B这个 key 是否存在,就可以判定 user_A 是否属于 crowd_B。(当然这是一种比较简易的实现,实际设计中需要根据数据特性进行优化)。就这样,我们定义了人群的概念,并提供了一种实现人群的技术方案,这个架构中,人群在同时充当了“协议”和“缓存”的作用。这时我们的得到的整体架构是这样的(顺带缓存了一下优惠数据):闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 136 事实上,在我们基于中台的解决方案中,从一开始面临的就是这样的架构(实际中台的架构比这个会更复杂一些)。这里我们尝试从头演进了这

193、个系统,也得到这样的一个方案。在实际落地的过程中,我们核心要解决的问题,是如何将业务系统中的关注和购买关系同步到人群中,并保证数据的一致性。3.人群同步的数据一致性 人群的同步整体上分为两个主要部分:将离线业务数据通过 T+1 的方式,同步到人群服务中。通过实时同步的方式,将当天实时产生的关注、取消关注等行为产生的变动,同步的更新到人群服务中。闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 137 这种结合的方式具有以下优点:实时消费消息进行同步,保障了数据的实时性。离线 T+1 的全量同步,保证实时同步过程中产生的数据不一致会被及时的纠正,保障了数据的最终一致。离线同步解决了数据初始化

194、过程中的全量同步问题。但上述的两个过程中,会出现两类问题:离线数据因为其数据存储的特征,只会记录存在的关注关系,如果是被删除的关注关系(取消关注),则不会出现在离线数据中。因此实时同步中,因未同步取消关注事件产生了不一致,数据无法被全量同步纠正。离线同步和实时同步在实际实施过程中,会产生一种常见的数据冲突:用户 A今天原本关注了用户 B,某天较早的时候取消关注了,如果这个时候的离线数据还没同步完成,全量同步会再次将 A 对 B 的关注关系写入到人群中,出现了与实际数据的不一致。闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 138 针对上述的两个问题,分别给出了以下两个解决方案:针对取关

195、数据误差无法通过全量同步纠正的问题,同步过程中,写入人群的时候会添加一个过期时间,这个过期时间略长于离线全量同步的间隔,这样的好处是一旦在实时同步过程中,出现了取关但未同步到人群的情况,这条记录会自动过期,从而避免了不一致的数据在系统中积累。针对同步过程中发生数据冲突的问题,通过在实时同步的过程中,取关的事件在 redis 写入一条临时记录,表示该数据近期发生过取关;在全量同步过程中,去比对 redis 中是否有取关记录,避免发生冲突。通过上述两个解决方案,我们实现了人群同步的最终一致性,最终实现的方式如图:这样的同步方案,对于搜索、推荐等大流量的导购场景,提供了充分的数据一致性保障(绝大多数

196、情况下,数据实时一致,对于小概率出现数据实时同步不一致,通过全量同步保障数据最终一致,满足导购场景的一致性要求)。此外,针对交易这样的要求强一致性但访问规模较小的场景,我们通过下单前对人群同步的数据进行核对,保障数据的实时完全一致。闲鱼如何计算实时优惠:兼顾可扩展、高并发与数据一致性 139 四、结语 本文从三个部分介绍了优惠的实现:通过对优惠要素的拆解和人群的定义,我们在描述、存储和计算优惠的同时,提供较好的业务可扩展性。通过提前制备人群数据,我们保障了大流量下的优惠查询下性能,系统能够支持几十万 QPS 下的毫秒级响应。在人群同步的过程中,通过离线+实时的方式同步数据,保障了数据的最终一致

197、性。五、思考 在优惠的实现过程中,我们直接面临了一个迭代了多年的优惠中台,需要我们通过同步人群数据的方式进行接入。可能一开始会疑惑为什么需要执行一个复杂、高成本且会引入数据一致性风险的同步过程。但当我们从业务的可扩展性、系统的性能角度从头进行推演的时候,我们发现最终会回到类似的架构上来。可以说,在特定的业务规模下,架构的演进有它历史的必然性。当然,也不是说这样的架构是适用于所有情况的,如果我们在一个较小的规模下去快速验证一个优惠能力,那么可能最开始的架构是最合适的,架构选型还是需要结合实际情况出发量身定制。基互动抽奖背后的随机性与算法实现 140 互动抽奖背后的随机性与算法实现 作者:华采 一

198、、背景 抽奖,是一种典型的互动玩法形式。无论是大 V 的粉丝抽奖,还是活动会场的参与抽奖,这种起源于彩票开奖的互动玩法,同时兼顾了高期待感和低预期的特征,让活动在成本控制之余又能有惊喜和引爆点,这样的优势让其在各种运营场景中幻化万千,大行其道。在闲鱼各种互动场与营销活动中,抽奖自然也是一个相当高频使用的互动玩法。众所周知,越是经典的玩法,业务需求就越发别出机杼,在参与条件、开奖展示、奖品规则等各方面千变万化,闲鱼内典型的就有现金夺宝、低碳双十一抽 iphone、旧衣回收抽锦鲤等,不胜枚举。本系列文章的出发点即是从技术的视角出发,讲一讲抽奖这种互动玩法自底向上的各种思考。而本文主要探讨的,就是抽

199、奖随机性的来源和常用的算法实现。二、关于随机 抽奖最需要保证的,其实是公平的产生抽奖结果,而这个公平,则来自于足够随机的抽奖算法,而抽奖算法不论怎么设计,常常依赖于计算机随机数的发生。不妨先来看一看万仞之基础随机数是怎么产生的。基互动抽奖背后的随机性与算法实现 141 1.伪随机数与其产生线性同余 为了效率和成本计,现在常用的随机数的产生方式往往是伪随机数,最广为流行的就是线性同余产生器,其本质非常直白:不难看出,其中的 a、b、p 的取值,就是是否能产出随机分布的数据根本所在。基本数论的常识告诉我们,这个同余式的取值必定在0,p-1的范围内封闭,并且拥有最大为 p 的周期,或者是多个较小但互

200、不重合的周期构成,当其周期为 p 时,其式就成为了 0 到 p-1 的一个离散排列。之所以这个看似简单的式子,能够成为随机数的生成方法,正是因为模数运算的良好特性,一来其在周期内绝不会出现重复结果,二来其分布也相对均匀。可以将f(x)/p 视为0,1)范围内的平均分布。2.参数取值 所以,我们的第一个问题,就必然是探索,在参数满足什么条件时,能将这个函数的周期尽可能的扩大,以更有效的利用其周期特性,挖掘这个式子产出的随机性。我们先从模数p开始,不论其他,光凭数学直觉就会让人下意识的想取一个大素数,以此轻易攫取优越的分布特性和天然形成的宽周期。但是,我们要注意到,伪随机数作为一个非常底层的方法,

201、其存在本身就是为了效率的,取模操作虽然不算慢,但此时就会有一个更加优越的模数跃入眼帘,那就是2n不但可以直接将取模操作退化为移位和与操作,也可以很轻松的理解随机数的取值范围。当然,这个周期比起素周期也更方便均分以转化为其他范围的随机函数。当然,模数不是素数的情况下,就对 a、b 的取值有了更大的约束。为了取得一个满周期序列的生成方法,The Art of Computer Programming中论证了其充要条件,也是现在大部分线性同余产生器的构造依据:b 与 p 互质 基互动抽奖背后的随机性与算法实现 142 c=a-1 是 p 所有素因子的倍数 若 p 是 4 的倍数,c 也是 4 的倍数

202、 我们可以看到,这其中对加数 b 的约束其实非常小,于是在 gcc 中,就比较随意的选择了个 12345,java 中干脆是个小素数 11。而对于 a 的取值,在已知我们取模数为 2n 时,就非常容易得知其约束条件:a-1 是 4 的倍数。3.实现时的考量 现在我们满怀欢喜的得到一个满周期序列的生成方法,似乎只需要按照某些特性去选一些优秀的生成参数就可以跑起来成为一个经典库了。但事情还没有这么简单。刚才我们的选择还遗留了一个问题,我们往往不是直接使用一个全模数范围的随机,而是由大周期的随机数取模转化为一个更小的周期来随机只要大范围的随机函数能保证概率均等,取模后自然也是一个均匀分布的函数:但是

203、以上方式有一个天然的缺陷,当我们的模数 m 与 2 的幂次相关时,其低位随机性并不是很好低位周期的分布也会在这个小周期上呈现周期,形式化地说,就是:也总是一个满周期序列,所以,无论怎么去改变参数分布,在模数非素的情况下,随机的分布都会呈现一个特别均匀的形式,当我们想取得范围特别小时,比如我们只需要 0-1 的整数,这个算法就会持续输出 0、1、0、1、0、1、0、1。当然,它仍然是满周期的,但是呈现出的结果完全违背了我们对于“随机”这件事的直觉,可预测性太强了。这个时候,我们重新回顾一下,就会发现,我们想要的其实不是满周期的随机性,当周期非常小的时候,我们更期待的是超越本周期的随机性分布,比如

204、,给 0-1 的 基互动抽奖背后的随机性与算法实现 143 随机安排一个 00101110 这样的周期序列,这个要求在本周期的计算比较难达成的,但是既然这个小周期是由一个更大的周期序列摘取到的,我们就能够将大周期的随机性反映到小周期当中去。很多平台的实现当中,是舍弃这些随机性不强的低比特位,换为截取高位比特位作为结果序列,这样当然会导致该序列一些很好的数列特性消失,但是从而也增强了其本身的随机性。比如在 java 的实现中:private final AtomicLong seed;private static final long multiplier=0 x5DEECE66DL;priva

205、te static final long addend=0 xBL;private static final long mask=(1L (48-bits);4.使用中的细节 其实到这里,随机数的生成问题我们基本上已经摸清楚了,既然我们知道了随机数的发生过程,其实就很容易抓住重点,那就是随机数种子才是最为重要的,随机数只是一种生成过程,甚至说理解为一种可持续的 hash 方式也无不可。随机数的随机性完全来自于你随机数种子的随机性。所以在习惯中,我们常常会使用当前毫秒时间作为种子,而在 java 里的默认种子生成如下:public Random()this(seedUniquifier()Sys

206、tem.nanoTime();基互动抽奖背后的随机性与算法实现 144 private static long seedUniquifier()/LEcuyer,Tables of Linear Congruential Generators of /Different Sizes and Good Lattice Structure,1999 for(;)long current=seedUniquifier.get();long next=current*6652981L;if(seedUpareAndSet(current,next)return next;priv

207、ate static final AtomicLong seedUniquifier =new AtomicLong(8682522807148012L);可以看到,为了避免不传种子的情况出现,java 默认提供了一个种子,这里也有把自旋锁,加上随机数生成本身的那一把,可以看到随机数发生在多线程的情况是会导致竞争的(虽然损耗很低),所以在阿里巴巴开发规约中会推荐使用ThreadLocalRandom 中的随机数来生成。如果你还记得上面的内容,还可以看出,这个种子本身也是个线性同余发生器发出的随机数,只不过特殊一点,是 b=0 情况下的乘法发生器。这种发生器的周期必定无法满周期,但是对于生成“随

208、机种子的因子”这种情况,够用。有点黑色幽默的是,虽然这里堂而皇之的标明了这里的常数来源于 Tables of Linear Congruential Generators of Different Sizes and Good Lattice Structure 这篇论文,但是实际上 6652981 这个数并不存在于论文推荐的表现最佳的因子中,看上去这里是一个 Typo数字开头少打了个 1,但是实际上大家也知道了,一个“不那么完美”的分布其实也没那么有所谓。随机数的生成原理并不复杂,整体的实现也是非常简洁直白的,但这其中又包含了很多精巧的构思,最终达到了一种效率与结果的统

209、一,技术的美感往往就分布在这些简单而不失优雅的实现当中。当然,任何算法都有自己的适用范围,伪随机数在密码学意义上并不足够安全,如果是对于随机性有着强需求的场景,我们应当使用其他的随机数生成方法。基互动抽奖背后的随机性与算法实现 145 三、关于公平的抽奖算法 1.“公平”的维度 提到抽奖,大家最先想到的,一定是,如何保证抽奖的公平性?而在开始这一部分前,笔者想先阐明一个观点,公平是多维度的,存在用户视角上的公平,客观意义上的公平,算法上的公平等等多种意义上的公平。比如我们完全可以将一种名之为“抽奖”的东西概率设为 100%,然后用库存限制抽中,这样所谓抽奖就退化为秒杀,在没有任何暗箱操作的情况

210、下,我们可以说这是完全公平的,但是客观上来说,也可以认为,对网较差、设备性能不佳或晚了一点参与的用户是相当不公平的。所以,反直觉的一点是,其实抽奖的算法也可以不那么公平,因为在任何一个拥有大量参与用户的活动中,用户的参与已经带来了大量的随机性上的因子,在没有提前看过参与用户的前提下,就算由一个值班同学一拍脑袋说今天是第 4090 位用户中奖,在某种程度上也是个相当公平的算法。所以,抽奖算法也是最容易设计的算法因为无论你产出的结果分布有多糟糕,也很难被“开盒”唾骂。实际上,在实践经验中,我们往往更要保证的是,结果的存在性和及时性,也就是说,无论你通过什么手段开出来奖,一定要在约定时间内发到有效的

211、用户手上延迟与无效是工程上能体现出的最致命的问题。2.真正应该看重的维度 不过,除了公平性之外,抽奖其实还有很多其他维度的业务考量:抽奖机会限制,是只能单次参与?还是能一次性多注参与?还是能反复追加的多次参与?开奖时机限制,是希望即时开奖?还是接受参与结束后定时开奖?奖品分布限制,是所有用户争夺唯一大奖?还是分一二三奖的奖品序列?还是瓜分红包或者积分的面额?人人有奖还是基本无奖?基互动抽奖背后的随机性与算法实现 146 并发系统性能限制,同时来参与、来领奖的用户规模和 qps 如何?是否会影响演算时间和接口延时?根据业务目标的不同,实际开发中,主要还是根据这些维度的特征实现特定的抽奖算法,以下

212、列举几种常用的抽奖算法:当抽奖的奖品粒度是个时。1)选中法 在得知所有参与用户的情况下,我们可以每次直接生成 1-n 的随机数的方式,抽出中奖用户的编号。这种方法的缺陷是有可能会造成碰撞,需要考虑如何剔除已经中过奖的用户。但是对于用户规模很大的活动来说,碰撞的概率极低,这种方法是最快速的。当然,我们上面也说过,这个方法无论使用的是怎样天花乱坠的随机函数,并不比你拍脑袋写几个固定数字更高明,还不会遇到碰撞。闲鱼内的“百币夺宝”就采用的此方案,因为每个夺宝活动都是唯一的奖品,与用户约定了选中的号码规则,就可以方便快捷的找出中奖用户。2)洗牌法 洗牌法是对整个中奖名单进行一次 shuffle,彻底乱

213、序后留在特定位置的用户成为中奖用户。一般认为这种方法造成的 I/O 次数是最多的,但还是有很多手段可以进行优化的,比如分区处理。它也存在着最佳的应用场景,比如当一个活动人人都能中基本不同的奖品时。基互动抽奖背后的随机性与算法实现 147 除此之外,它与选中法一样,实施的前提条件,也是必须获得全部的参与用户信息然后处理,对于实时想获知中奖信息或者想快速开奖的活动并不适用。3)蓄水池抽样法 抽奖本身也可以视作一种抽样,那么蓄水池抽样法就是一个非常适用的开奖法,它在参与人数未确定时就可以开始运作。我们维护一个大小为奖品数量 k 的奖池,每个用户过来都有 k/n 的机会与奖池内一个中奖者进行交换,其中

214、 n 为当前参与规模。可以证明,这样所有用户的中奖记录仍然是均等的。抽样法本质上可以视作 shuffle 的一种优化,利用参与人数与中奖规模的差异,免去了一些无用的 swap 操作。但这个优化会带来两点不同,一来,活动时刻维护着中奖列表,可以让活动终止在任何时刻并即刻开奖,无需等待至所有用户参与完毕后再演算开奖;二来,其容错性和鲁棒性会大大提升,很容易并行化,在参与数量比较巨大的情况下,前后执行产生的概率变化非常微小,而且中间任何一个用户操作出错,都不会对系统整体带来什么关键影响。闲鱼内的“回收抽锦鲤”就采用的此方案,对于各不相同的奖品列表,维护大小为k 的中奖者列表,最终留在列表内的就是中奖

215、者。基互动抽奖背后的随机性与算法实现 148 瓜分类型的算法,比如瓜分现金红包或者积分,抽奖的粒度是分。4)瓜分红包算法 最著名的自然是微信的瓜分红包算法,每个人瓜分的红包额度是这样计算的,先给每个人瓜分 0.01 元,然后加上 rand(0剩余平均红包金额的两倍),而最后一个人拿到剩余所有钱。这个算法可以保证大家瓜分的期望金额是相同的,但方差会越来越大,从而导致中最大奖的概率在各位置上是不同的。局限性在于,这个算法还是利用了一个先验信息,即参与人数 n,需要发红包者预先选择好能领取的数量。其最大的优势是不需要等到所有参与信息都获得之后进行,可以实时开奖。但同时,最大的问题是因为强依赖了“红包

216、剩余金额”这个信息,并发性会比较差,往往还是需要提前演算出结果,无法应对实时高并发的场景。闲鱼内的现金夺宝就采用了这种算法,但是由于参与规模特别巨大,往往是将奖池与参与用户划分为若干批次再进行的开奖。“不公平”但最好用的抽奖算法。5)概率法 有时候我们希望用户能即时开出奖来,这个时候我们并不知道整场活动的全貌,只能靠一些先验的经验来决定当前用户的中奖概率,在发奖时实时由概率计算用户是否中奖,比如在奖池内提前规定 A 奖品中奖概率 30%。但概率法并不公平。由于奖品数量有限,库存会消耗光的缘故,对于固定中奖概率来说,后来的中奖概率是小于原来的中奖概率的,假设我们仅有一个奖品一个库存的话,每个人过

217、来的中奖概率应当需要为,才能保证用户无论什么时候参与概率都是相等的。可以看到,这个式子不但计算麻烦,会产生并发问题,居然还有规模限制。而有多个库存的情况下,情况将会更复杂。基互动抽奖背后的随机性与算法实现 149 实际场景中上我们还有很多其他因素的干扰,特别是业务本身的决策,比如我们并不希望一开始就放出所有库存,中途才放出一点点库存。或者我们希望中奖库存是随着时间过去均匀的消耗掉的,这样我们就有更多的动机去调节这个本来就不公平的概率算法。所以说,概率法,是离客观公平最远的算法。但是,我们一开始的讨论也说明,这部分的不公平只是存在于理论上,如果我们没有人为操控使特定人中奖,在任何用户都是黑盒的情

218、况下,我们还是可以认为,这是公平的,因为用户没有机会判断得知,究竟这个活动的概率分布呈现什么样的情况,从而实现套利。用户只能通过一些经验化的谣言,比如“大额券总是在活动末期要冲量时才会放出”,或者“活动初期中奖概率更高”,来控制参与时机进行博弈。所以,这里的“不公平”打上了引号,事实是概率法由于其充分的运营空间和简明的规则,反而成为应用最广的抽奖方式。值得一提的是,概率法可以采用“概率保持”策略来应对部分奖品库存耗尽或者命中限中的情况,即该部分概率由其他奖品按概率比例再分配,也可以简单做到保证必中。在工程实现上,以上的算法其实都可以在规模到一定程度时,进行分布式优化,将奖池和参与者分而治之;也

219、完全可以在方法间互相结合,来达到想要的业务效果。四、小结 本篇文章从随机数和抽奖算法两个基础的层面,浅显地探讨了一些关于互动抽奖的内容,从最底层实现层面形叙述了抽奖的基本逻辑,但万丈高楼平地起,选定了算法,只是实现十万百万级在线用户参与的抽奖互动玩法的第一步。实际上,抽奖作为在无数互动场都屡试不爽的玩法,在业务实践中有着更复杂的场景与问题,接下 基互动抽奖背后的随机性与算法实现 150 来还有关于权益发放,关于用户驱动,以及关于抽奖玩法模型本身的更多内容,敬请期待。151 技术质量主题(此页面将由下图全覆盖,此为编辑稿中的示意,将在终稿 PDF 版中做更新)这半年我做交易链路自动化回归的那些事

220、儿 152 这半年我做交易链路自动化回归的那些事儿 作者:桃珂 一、背景 闲鱼交易链路作为应用中关键链路的一环,具有多业务、多状态、多操作的特征。以订单操作举例:不同的订单类型、订单状态包含不同的操作;不同操作下触发的业务行为、领域服务的交互行为也各不相同。二、问题 交易链路质量稳定性保障的测试难点包括:改动点涉及的业务范围广、评估难度高:交易承接着 10 余种复杂多样的业务场景和交易模式,一次改动往往涉及所有业务场景的验证。更糟糕的是,一次看似不起眼的线上开关值变更,往往依赖业务经验来评估其影响范围,给业务验证和变更带来巨大风险。新老链路需要双重保障:链路上的数据结构变动,需要保障新老版本下

221、调用链路切换的问题。交易链路上订单标的正确性:一笔交易订单主订单上就有超过 100 个标;这些订单标以及根据这些标衍生出的业务场景如何快速校验?带着这些问题,闲鱼交易链路自动化回归采用接口+链路的验证,在应用交付的全生命周期内,在发布流水线中不断运行自动化测试,保障全链路,把控发布质量,成为应用真正上线的最后一道防线。三、方案说明 通过接口流量录制回放、定海神针场景链路验证的方式,形成自动化测试任务集,在交易核心应用发布过程中,新增发布流水线的测试验证节点,当应用完成安全生产环境部署后,自动化触发执行关联的测试任务。测试任务执行后,验证当前的自 这半年我做交易链路自动化回归的那些事儿 153

222、动化结果情况、应用核心测试集校验情况。根据应用预先配置的卡点阀值来判断卡点校验是否通过。如果卡点校验通过,则可以继续进行后续的发布流程。如果卡点校验未通过(即自动化验证失败),需要立即定位自动化失败的具体原因,避免将变更问题带到线上,以及发布流程的长时间阻塞。基于此,来看看闲鱼交易下的自动化体系建设思路:1.自动化测试集设计 编写并选择测试用例是实现自动化验证的核心。合理的用例设计,既保证自动化的效益和可靠性,又便于测试集的维护和扩展。对于业务场景多、操作多样化的闲鱼交易域,在自动化测试集设计上,需要确认的问题是:想要实现自动化验证最大产出,在开始实施时,应该选择哪些用例加入自动化测试任务集?

223、对于预先定义的一组或多组输入、输出数据,自动化结果具备可预测性吗?基于以上的考量,进行接口链路的编排,并借助接口测试工具来实现交易场景的自动化覆盖。借助集团的定海神针平台,进行链路自动化用例编写,包括以下两个方面:这半年我做交易链路自动化回归的那些事儿 154 1)数据预置 在用例编写前,需要准备有效的测试数据,使用例能够真正地执行起来。不同类型的商品数据、买/卖家用户身份及账号数据、交易资金等都作为生成交易订单的预置数据,需要和用例编写分开,不仅减少用例执行成本,更减少用例之间的耦合度。此方案设计中采用闲鱼测试设计的测试数据构造平台进行数据生成和获取。2)用例编写 准备好测试数据后,在编写场

224、景用例时需要注意:合理分解:拆分复杂交易场景和业务逻辑,区分原子场景,避免测试失败时阻断其他功能用例的执行,快速得到测试结果,提高用例执行稳定性。简化用例:根据交易链路节点可复用的性质进行用例简化。例如在场景分解后,验证发货场景时,不需要重复构造订单数据,复用上一节点的订单即可。复杂的执行和校验可能影响发布节奏,给理解、调试和维护带来更大的成本和挑战。多层校验:设计合理的断言,避免由于用例原因造成的随机失败。校验规则覆盖接口契约、订单数据(订单标/订单状态/订单操作)、业务规则各层次。并学会从线上问题里找反思,补充校验点。这半年我做交易链路自动化回归的那些事儿 155 体现业务特性:了解用户和

225、应用的交互,在用例编写时体现用户使用系统的实际端到端的历程,而不只是自动验收标准的集合,片面强调覆盖率。在交易场景用例中覆盖领域交互的验证,增加对交易状态流转后,买/卖家系统异步消息通知卡片的校验、资金流向校验等。下图是以闲鱼内基础 C2C 交易为例,进行业务测试用例拆分:2.发布流水线卡点 完成自动化测试用例沉淀后,将接口、链路质量验证能力与应用发布关联。基于变更管控,完成自动化回归验证、发布卡点。利用发布流水线将开发、测试、发布、验证等关键活动串接在一起,没有间断和跳过,流畅优雅。首先简单介绍:集团内开放 Aone 平台,提供完整的产品全生命周期管理和协作能力。在应用发布内,Aone 整合

226、了产品部署发布、持续集成服务和测试执行实验室,升级流水线能力,关联研发流程中的各个阶段,实现了自动化的构建、部署、测试与发布,确保让代码能够快速、安全的部署到产品生产环境中,提升整个研发体系的效率。依赖 Aone 发布流水线能力,可以在 Aone 发布流程中平稳地支持测试校验,自动触发和运行测试任务:在交易核心应用变更代码部署完成后,自动执行指定的测试任务校验测试,更新用例回归结果并自动决策研发流程的执行,直观体现质量信息。这半年我做交易链路自动化回归的那些事儿 156 在自动化验证失败时阻断发布,进行 100%通过率强卡点,即卡点校验项未通过时,无法继续进行后续的发布流程,若想继续需进行特殊

227、审批。四、总结及展望 目前交易链路自动化回归已经覆盖了交易内核心应用的接口质量验证,同时覆盖了基础 C2C 业务下场景链路质量验证能力。基础 C2C 交易回归验证由原先的手工耗时半小时、频繁账号切换和点击操作,缩短至一分钟内自动完成,极大减少手工验证的重复性,提供更优的质量保障能力和执行效率。本着没有适当的测试策略,不给予自动化测试的基本原则,闲鱼交易域内的自动化体系建设,是建立在基建完善的基础上,比如解决了数据构造问题、应用环境隔离、发布流水线引擎的基建统一等,进而助推质量保障、降低发布风险。后续我们将持续推进基建,沉淀更多核心业务场景下的自动化测试任务集,最终实现向用户持续高效地交付价值。

228、关于闲鱼测试数据构造,我有几条心得 157 关于闲鱼测试数据构造,我有几条心得 作者:羲竹 一、背景 随着闲鱼业务的高速发展,其商品类型、交易模板以及互动玩法日趋丰富。造数常常需要耗费测试同学大量的时间,其根本问题归纳为以下几点:人工成本高:商品、订单的类型与状态笛卡尔乘积后多达上百种,数据种类丰富且构造流程长,测试过程费时又费力。造数门槛高:商品数据构造往往和账户类型、人群等有强耦合关系,无论是测试验收还是跨部门协作时,都需要测试同学投入很多额外的时间辅助数据构造。测试工具无数据支撑:在自动化测试、性能测试过程时,需要丰富的数据类型作为驱动。为了解决以上问题,闲鱼测试设计了一套各业务可快速接

229、入,并在 PC、闲鱼 APP 内和钉钉上均可使用的测试数据构造解决方案,旨在提升测试效率的同时,更好地推进测试左移。二、方案设计 如下图所示为闲鱼业务的整体架构图,造数平台需要触达多条业务线,支撑商品、订单、优惠等业务的数据构造,并为测试自动化工具提供数据支持。此外,我们期望合作方在进行产品验收时,也能以便捷的方式获取到数据。关于闲鱼测试数据构造,我有几条心得 158 业务架构图 基于以上愿景,造数平台系统内部设计考虑到了可扩展性、易用性两大方面,其整体架构图如下图所示,一是提供了用户进行模板化管理的入口,通过可插拔的配置来自定义搭建自己所需的造数场景;二是和各平台打通,发挥各平台的优势,达到

230、敏捷高效造数的目的。造数架构设计图 关于闲鱼测试数据构造,我有几条心得 159 1.支持动态化配置 为了方便后续不同业务的接入,并为自动化巡检、CI/CD、接口测试等提供数据支持,平台期望以一种确保数据源动态可插拔的方式来承接:不同业务可结合自身业务,配置不同接入源类型的元数据模板,并做到数据源隔离;而后再基于元数据模板进行自定义的业务模板配置,整体步骤如下:在造数平台上对业务元数据进行配置管理。基于元数据进行业务模板化配置。造数 PC 端、闲鱼 APP 以及钉钉机器人上将共享一份配置。2.打通多端造数入口 为了兼顾不同用户人群的使用体验,平台上层入口支持了三种:PC 工作台、闲鱼 APP内和

231、钉钉内交互机器人。三种渠道各有优势。1)闲鱼 APP 内 闲鱼 APP 内的优势在于可以自动获取设备环境信息。以商品域测试为例,闲鱼不同商品类型的发布入口不同,其中部分商品发布流程有一定时间成本。我们基于 JS Bridge,拿到闲鱼 app 当前用户的登录态,一键发布宝贝,并获取到商品的 schema信息跳转至商品详情页,方便测试同学进行快速验证。闲鱼 APP 内发布商品,原文为 gif 关于闲鱼测试数据构造,我有几条心得 160 2)钉钉内交互机器人 钉钉交互机器人的方式进行造数的优势在于便捷、通用、简洁,可以和日常工作无缝衔接。举个栗子:验货宝业务是 C2S2C 的模式,中间部分订单节点

232、的推进是需要联系开发或对应的服务商的。遇到问题都需要拉群进行处理,如下图所示,我们将验货宝推单的功能做在钉钉内,支持机器人交互的方式进行推单,一是省去了联系开发和服务商推单的时间成本,二是如遇订单推进的相关问题,我们也可直接将错误信息反馈到群内,省去了换端的成本。钉钉机器人交互 3)PC 工作台 PC 工作台操作的优势在于方便管理。在 PC 端,我们可以进行商品发布和订单模板的配置和自定义修改,其操作流程可参加上文中提及的动态化配置步骤。此外在 PC端,我们还支持了模板克隆、以及批量造数等功能。关于闲鱼测试数据构造,我有几条心得 161 PC 端批量造数 3.提升数据覆盖度 目前平台主要覆盖了

233、商品、交易、营销优惠三大业务线,支持构造商品、订单、交易履约以及营销优惠的数据构造。如下图所示为造数工厂目标覆盖的主要数据类型。数据覆盖度 关于闲鱼测试数据构造,我有几条心得 162 其中商品支持了诸如优品、营销以及一些基础商品类型的构造,目前全部类型都已覆盖完成。交易已支持 C2C 不同状态类型的订单构造,其他订单类型也已在持续接入中。三、效果及展望 造数工厂未上线之前,无论是业务测试、产品设计验收还是跨部门合作,都需要牺牲测试同学的大量时间构造数据。现如今我们以最小的建设成本,搭建了一套具有可配置、可扩展能力的造数工具平台,支持大家自主获取数据,工作效率得以大幅度提升。根据目前效果来看,商

234、品的获取速度由原本的分级提升至秒级。至于订单的构造和履约推进流程复杂,如下图所示,测试同学往往需要准备买卖家两个账号,分别发布和购买商品,后续履约推进还需联系开发,中间的等待过程往往总是漫长。现在通过造数工厂即可自助造单和履约推进,单笔交易流程回归耗时由 1h 下降到分钟级别。交易测试流程 自年初上线以来,造数平台已接入商品、交易、优惠三大业务线,覆盖核心商品类型 20+,通过平台发布商品 60000+,造单 100+。此外还支撑了商品合规、商详升级等多个重构需求的数据准备工作,测试效率得到显著提升。后续,我们将从几个方面对平台进行持续优化:持续提升数据覆盖度,承接诸如订单诊断、商品诊断、用户资产等更多的数据构造工作。目前平台的业务接入不支持自定义插件化扩展,降低接入成本将是我们持续努力的方向。丰富“测钉一体化”的交互模式,让平台所有的数据获取,做到一个群就够了。关于闲鱼测试数据构造,我有几条心得 163 平台希望通过快速的数据构造能力赋能业务测试,推进测试左移。让更多的闲鱼小二解放双手,从重复性的劳动中跳脱出来。感谢本电子书共同作者:宗心、新宿、光酒、岑彧、意境、三莅、吉丰、叶遥(已离职)、颂晨、云钟、吴白、泊垚、华采、桃柯、羲竹 闲鱼技术 2022 年度白皮书 阿里闲鱼 2022 技术干货

友情提示

1、下载报告失败解决办法
2、PDF文件下载后,可能会被浏览器默认打开,此种情况可以点击浏览器菜单,保存网页到桌面,就可以正常下载了。
3、本站不支持迅雷下载,请使用电脑自带的IE浏览器,或者360浏览器、谷歌浏览器下载即可。
4、本站报告下载后的文档和图纸-无水印,预览文档经过压缩,下载后原文更清晰。

本文(阿里云开发者社区:2022闲鱼技术年度白皮书(163页).pdf)为本站 (数据大神) 主动上传,三个皮匠报告文库仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知三个皮匠报告文库(点击联系客服),我们立即给予删除!

温馨提示:如果因为网速或其他原因下载失败请重新下载,重复下载不扣分。
会员购买
客服

专属顾问

商务合作

机构入驻、侵权投诉、商务合作

服务号

三个皮匠报告官方公众号

回到顶部