收藏 分销(赏)

闲鱼技术2022年度白皮书.pdf

上传人:Stan****Shan 文档编号:1240997 上传时间:2024-04-19 格式:PDF 页数:163 大小:33.33MB
下载 相关 举报
闲鱼技术2022年度白皮书.pdf_第1页
第1页 / 共163页
闲鱼技术2022年度白皮书.pdf_第2页
第2页 / 共163页
闲鱼技术2022年度白皮书.pdf_第3页
第3页 / 共163页
闲鱼技术2022年度白皮书.pdf_第4页
第4页 / 共163页
闲鱼技术2022年度白皮书.pdf_第5页
第5页 / 共163页
点击查看更多>>
资源描述

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 节点。另外一个需要处理的问题就是移动光标的处

展开阅读全文
相似文档                                   自信AI助手自信AI助手
猜你喜欢                                   自信AI导航自信AI导航
搜索标签

当前位置:首页 > 研究报告 > 其他

移动网页_全站_页脚广告1

关于我们      便捷服务       自信AI       AI导航        获赠5币

©2010-2024 宁波自信网络信息技术有限公司  版权所有

客服电话:4008-655-100  投诉/维权电话:4009-655-100

gongan.png浙公网安备33021202000488号   

icp.png浙ICP备2021020529号-1  |  浙B2-20240490  

关注我们 :gzh.png    weibo.png    LOFTER.png 

客服