本期视频发布于 2022 年。文中的技术状态、个人经历与观点均为当时情况。
第 2 期 T Chat 的前半场,新宿介绍了在闲鱼开发 Flutter 业务和中间件的经历,重点讲解图片库 PowerImage,并简要展示富文本编辑器。后半场,闲鱼客户端团队负责人于佳(宗心)加入,与新宿和主持人讨论团队为什么选择 Flutter、怎样推进核心业务迁移,以及工程师在这个过程中如何成长。两段录播合计约 85 分钟。宗心的身份也可在闲鱼团队公开访谈中核对。
PowerImage:让 Flutter 使用已有的原生图片能力
问题不在于能否显示图片,而在于两套体系怎样协作
新宿提到,成熟原生图片库往往已经具备按显示尺寸请求合适 CDN 资源、支持特定格式、处理静态资源等能力。混合应用如果在 Flutter 侧另建一套完整实现,就可能重复建设,也可能为同一资源保存多份内容。
他用三个很具体的场景解释这件事。图片展示区域不大时,Native 图片库可以按视图尺寸选择 CDN 资源,不必下载原图;HEIC 等格式在当时的 Flutter 版本里存在支持差异,而 Native 侧已经能处理;一张静态图片如果在原生和 Flutter 页面都要使用,分别打包又会增加安装包体积。他并不是说 Flutter 无法显示图片,而是希望已有的加载、格式和资源管理能力能直接为新页面所用。
此前团队使用的图片方案主要基于外接纹理,仍遇到一些限制:Flutter 侧无法直接取得实际的 ui.Image,部分开发环境中的展示受到版本条件影响,缓存和生命周期也与 Flutter 原生图片体系分开。相册还可能有自己的加载通道,使同一应用里的图片管理更加分散。
在旧方案里,团队曾把 Flutter 自带 ImageCache 的容量调低,但业务开发仍可能使用普通 Image。结果可能是低频图片长期占据一边的缓存,纹理图片由页面栈管理,相册图片再走自己的通道。三套策略各自工作,既难统一计量,也不容易解释一张图片究竟何时应该释放。PowerImage 想处理的正是这个工程问题。
PowerImage 因此希望同时解决能力复用与管理统一:连接 Native 图片库,保留纹理路径,再用 FFI 补充相应能力,并尽量与 Flutter 的 ImageCache 协作。
Texture 路径:注册纹理,再让 Flutter 按标识绘制
分享以 iOS 为例解释外接纹理。原生侧实现相应纹理接口,注册后得到标识,再把标识交给 Flutter。Flutter 中的 Texture 组件最终通过这一标识找到对应原生纹理,取得像素缓冲并进入渲染过程。
这一通路让原生侧已有的加载、解码能力可以参与显示,但显示一个外接纹理,不等于 Flutter 侧已经拿到了普通图片流程中的真实 ui.Image。后续缓存计量、图片处理与生命周期管理,需要针对这一差异设计。
新宿还把链路继续追到引擎:Flutter 的 Texture 组件构建出相应图层,引擎按纹理标识回到原生注册表,调用原生纹理提供像素缓冲。分享以 iOS 的像素缓冲为例说明,这一步并不是在每次绘制时另造一份完整像素数据。这个细节也解释了为什么纹理路径在某些场景下有成本优势,但仍不能替代需要 ui.Image 的全部用途。
先理解原生 Image 的加载和缓存过程
新宿随后沿着 Flutter Image 的流程展开:ImageProvider 负责描述加载来源并参与缓存查找,加载结果通知界面刷新,最后由图片数据进入绘制。缓存里又存在等待加载、已经缓存和仍被使用等不同状态。
他特别指出两个连接点。一个是界面绘制所需要的图片对象,另一个是缓存怎样根据图片信息计算占用、怎样判断资源可以释放。将 Texture 接入原生体系,不能只替换最终显示组件,而不处理这些关联行为。
以网络图片为例,ImageProvider 既是资源来源的描述,也是缓存查找的关键。没有命中时,图片先进入等待加载状态;加载完成后,缓存与仍在使用的监听者继续决定它的去留。新宿先讲这条 Flutter 原生路径,再讲 Texture 的接法,是为了让听众看到:自定义显示组件如果跳过 ImageCache,预加载、容量管理和页面释放都会变成另一套规则。
三个适配问题:组件、缓存计量与原生资源释放
第一,原有图片组件的绘制路径依赖 ui.Image,纹理方案需要提供可扩展的构建入口,允许最终生成 Texture 组件。
第二,缓存大小计算依赖图片宽高等信息。分享介绍了用占位对象衔接类型要求,同时让缓存取得实际纹理尺寸的做法。这里的占位不代表资源实际只占一个像素的内存,计量仍需反映真实内容。
这个适配发生在缓存需要宽高信息的位置,而不是要求业务把每张纹理手动登记。原有 Widget 的构建入口也向外开放,让使用者决定最终绘制普通图片还是 Texture。两个改动放在一起,才使图片库对业务维持相近的调用方式,同时在内部走不同渲染链路。
第三,原生纹理有自己的生命周期。团队对 ImageCache 作适配,观察资源从相关缓存状态中释放的时机,再通知原生侧处理。这样可以减少仅依赖页面栈管理图片资源带来的割裂。
FFI 补足真实图片对象的需求,也带来不同成本
FFI 路径把原生像素数据的地址、长度等信息传到 Flutter 侧,再生成实际的 ui.Image,以支持需要图片对象的使用场景。分享中的实现会发生内存复制,完成相应处理后再释放原生资源。
因此,Texture 与 FFI 的比较需要同时看能力和资源成本:前者适合已有纹理路径,后者更自然地接入需要图片对象的流程,但可能产生不同的内存峰值。新宿没有把两者概括为一个在所有情况下都更优的方案。
他展示的 FFI 流程也指出了复制发生的位置:原生侧提供像素数据的地址和长度,Flutter 侧据此解码生成图片对象,之后通知原生侧释放原资源。开发者得到的是可供普通图片处理流程使用的对象,代价是转换过程中可能同时保有两侧内存。仅看最终图片显示是否成功,无法判断这段峰值是否符合业务要求。
应用接口尽量熟悉,原生加载器由接入方提供
应用层使用的接口尽量接近原有 Image 使用方式,包括网络图片、原生静态资源和自定义来源。业务还可以定义图片类型,例如相册图片,并把资源标识传给对应原生加载器。
相册的例子尤其直观:Flutter 侧声明图片类型为相册,传入资源标识;Native 侧注册处理该类型的加载器,由它找到并加载设备里的照片。调用方不必为相册另做一套 Texture 组件,也不用在页面里判断最终采用 Texture 还是 FFI。网络图同样可以提供显示尺寸,交由底层结合实际组件大小选择加载参数。
PowerImage 不假设每个团队都使用同一种 Native 图片库。接入方注册不同类型的加载器,完成自己的加载与解码,再把结果交回公共框架;框架内部根据选定路径处理 Texture 或 FFI。这样,业务能复用既有原生能力,也不必自行重复组织整条跨端显示流程。
项目名称、双模式入口及自定义加载器关系,可与 PowerImage 官方仓库对照。实际接入仍应检查所用 Flutter 与扩展包版本,而不是照搬录播中的版本组合。
动图、性能实验和测试一起进入工程设计
分享中的动图能力来自团队间协作,当时主要使用纹理路径。加载器返回帧与时间信息,再按节奏更新显示;资源不再展示时,需要相应处理缓存和原生侧工作,避免无意义地持续刷新。
新宿说明,当时的动图使用并不频繁,因此设计上没有让它长期留在普通图片缓存中:组件释放时,相关动图从缓存移走,原生侧也就无需继续为没有展示的页面刷新帧。静态图与动图并没有因为共用一个入口就被强行赋予相同的缓存策略。
性能部分,新宿展示了指定设备、Flutter 版本、列表图片数量与缓存配置下的对比。FFI 中的复制可能增加内存峰值,图片大小也会影响结果;原生图片体系还有滚动时延迟加载、尺寸处理等策略,比较时应注意这些条件。录播表达了团队之后更关注 FFI 路径的倾向,不能据此跳过当前场景验证。
现场给出的测试条件包括 iPhone 11 Pro、Flutter 2.5.3,以及三列列表中的约 300 张网络图片。这一组图片并不含特别大的资源;新宿特意提醒,大图出现时,FFI 路径的峰值可能更高。彼时默认仍使用 Texture,但团队在继续评估 FFI。原生图片实现还会观察列表滚动速度,快速滚动时暂缓部分请求,并对图片做尺寸处理;这些额外策略也会影响性能比较。
他也强调可维护性。核心图片能力与版本适配部分分开,单元测试覆盖关键行为;初始化时配置缓存、默认渲染方式与异常回调,再在原生侧注册加载协议。图片库不只是让演示页面显示成功,还要支持升级、定位问题与持续维护。
这种拆分还有版本升级上的考虑:图片库的核心逻辑尽量保持稳定,直接依赖 Flutter Image 与 ImageCache 内部行为的扩展单独放置。团队为关键路径写测试,也是因为底层适配一旦变化,问题可能不在写业务页面的同事那里出现,而在不同 Flutter 版本、不同资源类型和缓存释放时机上暴露。
富文本编辑器:协议、输入与交互都需要衔接
技术分享最后简要介绍了富文本编辑器 Mural。它不只是把格式化文字画出来,还需要组织内容协议、选区与输入状态,并与原生文本输入能力衔接。新宿展示了发布、详情等使用场景,也提到放大镜和光标移动等交互细节,以及不同平台返回信息的差异。
输入链路需要双向转换:编辑器侧的内容和选区状态,要转换成系统输入协议交给原生侧;键盘等系统输入带来变化后,又要把返回的编辑状态映射回编辑器自身的操作。这样,内容协议、界面展示与系统输入才能保持同步,而不是各自维护一套脱节的状态。
新宿以光标和放大镜说明交互为何复杂:移动光标时,不同平台返回的可能是位置,也可能更接近方向指令,编辑器都要把它们变成自己能理解的选区变化。现场还展示了发布页和详情页中的使用效果,包括文字、表情、主题和详情中的点击交互。这些是当时的产品场景,不意味着录播已讲完富文本协议的所有实现细节。
这部分是能力概览,录播没有完整展开编辑器实现。团队之后的协议设计说明明确使用 Mural 名称,并介绍了参考 Slate 的操作协议;可以作为进一步阅读的原始资料,但不应把后来文章的全部内容补写成现场已经讲过的内容。
团队为什么选择 Flutter
技术氛围从好奇心和实际问题开始
新宿从一线开发者的角度描述团队氛围:主动了解新技术,从技术角度分析产品体验,再把发现反馈给产品同事。例如图片上传与展示的格式、分辨率,既是技术细节,也会影响用户感受。
团队会鼓励调研和分享,把自愿交流放进周会,不限定主题;日常也关注用户反馈中的体验问题。这让好奇心能够进入实际工作,而不只停留在业余讨论。
新宿举了一个小而具体的例子:有同事从技术视角记录竞品上传图片的分辨率、展示分辨率和格式,整理给产品同事。这和产品经理常做的体验比较不同,提供了可以直接影响加载速度与视觉质量的技术证据。他也说,团队会在日常查看用户反馈,发现体验上的小问题再追踪处理。负责人对这类发现给予鼓励,周会中的自愿分享才逐渐变成大家愿意参与的讨论。
人效与人的长期成长,需要找到结合点
宗心回顾,团队在业务增长之后开始思考:除了完成业务目标,还能给成员留下什么长期价值?技术能力、个人品牌和行业口碑,是他当时关注的方向。同时,团队规模和人员结构也带来现实的人效问题,需要寻找能够帮助多端协作的技术。
选择开源、较通用的技术,还有让个人积累能够离开单一公司继续使用的考虑。团队曾比较不同跨端路径,对性能、工具和使用体验作验证。按他的回忆,从早期调研到扩大应用经历了较长时间,并非看完一个演示就全量迁移。
宗心把这段选择放在业务发展背景中:2017 年前后,闲鱼快速增长,客户端团队人数相对少,团队一方面要提高交付效率,另一方面也在问,除了完成业务目标,还能给工程师留下什么可以长期使用的能力。他们试过其他跨端技术,但对开发工具、体验和性能仍有疑问;同时,如果押注一套只在本公司能用的封闭方案,成员积累的市场价值也会受限。按他的时间线,从 2017 年左右开始验证,到 2019 年逐步用于业务,再到 2021 年左右扩大应用,期间经历了多轮问题和调整。
创新需要判断,也需要知道哪些事情不值得重复做
宗心谈到,新的方向在早期未必已有共识,推进者需要在验证基础上形成判断,并承担持续投入、反复改善体验的工作。但这并不意味着所有自研都值得做。
如果原生基础能力在团队、集团或行业里已经成熟,他更倾向于直接使用,把有限精力放到自己的痛点和尚未解决的关键问题上。客户端容器化是他当时认可的长期探索方向,录播同时保留了对未来形态的不确定性。
这也解释了新宿前半场讲图片库时为何强调复用 Native 能力:创新不是把已有的图片加载器、基础组件再造一次,而是解决两套技术栈之间尚未打通的缓存、资源和开发体验问题。宗心认为,新的方向在一开始可能尚未成为共识,负责人仍要通过长期验证、修补体验来争取支持;如果只展示最终收益而跳过中间的成本,这段判断也就失去了现实背景。
深入 Flutter,怎样从业务走向框架和引擎
不只绕过问题,还要理解问题为什么出现
新宿此前从事 iOS 开发,进入闲鱼后持续围绕 Flutter Framework 工作。他提出的学习方法很具体:业务中遇到组件嵌套或表现异常,不要只找到一种能显示的写法就结束;继续追踪原因,阅读相关源码。
除了当前代码,他还建议查看提交历史、修改说明和关联 issue。一次变更为什么发生、此前有哪些讨论,能够帮助理解框架设计的约束。源码因此不仅是现成答案,也是观察他人怎样分析问题的材料。
学习内容需要连接到具体工程场景
关于成为 Flutter 工程师需要准备什么,新宿列出原生基础、Flutter 原理与 Framework、Dart 语言特性,以及开发工具的使用。图片库就是一个例子:业务写在 Flutter 侧,仍要理解原生资源、内存和平台能力。
他自己的入门也结合了官方示例与项目实践。主持人由此强调,找到一个实际场景,边做边学习,往往更容易让抽象知识形成连接,而不必先把所有资料看完再开始。
Flutter 3.0 的讨论,聚焦多终端与生产升级成本
访谈恰逢 Flutter 3.0 发布,几位参与者谈到桌面、多终端支持与性能改进。宗心更关心已有应用怎样走向平板、折叠设备和桌面环境:如果同一应用需要多个界面独立运行,原有混合栈与单引擎方案会遇到怎样的限制?
团队希望探索多引擎、不同 isolate 之间的协作、较低的内存占用,以及尽量减少业务层改动。这些是当时正在验证的方向,并不是已经交付的通用能力。
宗心用终端变化说明这个需求:手机上一个引擎可能足够,平板分屏和折叠屏却可能同时显示多个独立界面;未来桌面端也会遇到类似情形。如果每个界面简单地各启动一套引擎,内存水位、代码共享和 isolate 间通信又会成为新问题。他期待底层机制能够让多个界面独立运行,业务代码不必整体重写,并提到团队当时先从平板场景试验。这里讨论的是目标和实验,不是 Flutter 3.0 已自动解决了所有多屏问题。
他也提醒,生产应用升级需要验证稳定性、能力覆盖和迁移成本。录播当时团队仍使用较早版本,不能把新版本发布直接等同于适合立即在核心业务全量部署。
推进核心链路,先回答收益和能力是否成立
先判断问题,再判断 Flutter 是否合适
对于“核心业务不敢上 Flutter,该怎样推动”的提问,宗心先反问,这项技术是否确实解决当前团队的问题。闲鱼当时的人员规模、客户端技术背景和双端人员分布,是其选型的重要条件;其他团队未必相同。
如果主要矛盾来自音视频底层能力或其他高度平台相关的工作,统一页面开发未必是最关键的收益。图片、表单、复杂业务逻辑较多的场景,则可能更容易从共享实现和一致性中获益。评价应回到实际业务,而不只是证明某个框架值得用。
他把闲鱼的选型与团队结构连在一起:当时客户端工程师多于前端工程师,Android 与 iOS 人员数量也不对称,复用业务实现有直接价值。对于以音视频底层能力为主的产品,他认为瓶颈可能根本不在页面双端开发;电商、表单、图片和部分富文本较多的业务,则更值得核算收益。这个比较不是给不同公司排技术优劣,而是提醒提问者先找出自己的限制条件。
列出所需能力,先验证,再持续投入
确认有价值之后,需要摸清核心链路的要求:图片、播放器、富文本、性能等能力是否具备?可以逐项列出、形成文档和可验证的演示,避免在关键体验明显受损时急于迁移。
宗心强调,新技术最初往往会降低效率,因为要支付学习和基础设施成本,之后才可能出现收益。团队是否有条件跨过这一阶段,也是选型的一部分。不能只借用别人的最终效率数字,却忽略其前期投入和持续维护。
他用一条先下降、再上升的效率曲线描述这个过程。如果团队只投入到下降阶段,发现短期交付变慢就撤回,当然看不到后面的回报;但这也不是要求所有团队都坚持到底。事先把图片、播放器、富文本、性能等条件写成清单,逐项验证并形成可运行的闭环,才能知道投入是否值得,以及何时可以进入核心业务。
体验、基础设施与人的能力要一起建设
回顾此前一年的工作,宗心坦率承认,享受共享开发收益的同时,还要补齐社区和自身实现中的缺口。图片、富文本及其他中间件改善用户体验,测试用例和 CI 支持推广,构建工具、代码评审和质量要求则改善工程过程。
人的能力也在变化。团队最初更多在框架之上开发业务,后来逐渐有人深入构建、Framework 和引擎,参与上游修复。这些能力不是一开始就具备,而是在真实问题与持续投入中发展出来。分享提到的贡献规模属于团队当时回顾,不是对今天社区贡献排名的统计。
他还谈到来自业务和管理层的质疑:共享开发确实带来效能,但体验缺口也要由团队自己承担。图片库、富文本等中间件要能解决线上问题,测试和 CI 要补齐,代码评审与工程要求也得随推广范围提高。他将体验升级、基础设施建设、人才成长放在同一段回顾中,意思是不能只买到一套框架,却省略让它在大规模应用中稳定运行的工作。
容器化的展望,需要与已落地工作分开
宗心进一步讨论了统一终端容器的可能性,希望上层开发方式能够更少受到操作系统差异影响,并提到与行业伙伴交流、自研能力和标准建设的愿望。
这些内容属于长期判断。更直接的工程路径,仍是先解决当前混合开发、多屏、性能和基础设施问题,再通过实践判断哪些部分有机会形成更通用的能力。
工具、面试与开源贡献
先把官方调试工具用起来
被问到提效工具时,新宿推荐 Flutter DevTools。组件检查可以帮助理解界面层级、定位对应代码和观察刷新行为;性能、内存与包体分析则服务于不同阶段的问题排查。
这些能力对刚接触一块业务的新成员也有价值:从屏幕上的组件回到实现,比在陌生代码里盲目搜索更直接。工具不是替代分析,而是帮助缩小需要理解的范围。
面试看怎样解决问题,不只看是否做过同一框架
宗心说,团队招人时更关心候选人做过什么、如何分析和解决问题、怎样学习,以及日常代码习惯。此前没有使用 Flutter,并不意味着没有可迁移的工程能力。
对有 Flutter 经验的人,他更愿意讨论生产环境中的混合栈、性能、基础设施与困难问题。做出一个演示,与把方案放进持续演进的真实应用,需要面对的复杂度不同。没有正式项目经验的人,也可以通过自己的尝试和对现有问题的判断,展示好奇心与理解。
上游贡献从清楚描述问题开始
新宿介绍了自己参与 Flutter 上游的流程:先搜索已有 issue 和讨论;如果问题尚未被清楚记录,就提供环境、最小复现和必要的录屏。提出修改时,需要说明它对应的问题,并补充覆盖改动的测试。
随后是项目要求的贡献协议、自动检查和维护者评审,作者要根据意见继续调整。他特别提醒,即使改动进入主分支,后续测试仍可能发现问题并回退,需要继续修正。开源贡献不是提交一次代码便结束,而是与项目共同维护质量的过程。具体标签和流程会随项目变化,应以当前贡献说明为准。
编程方式与构建效率
声明式界面和组件组织,改变多人协作方式
谈到 Flutter 带来的编程方式变化,新宿从声明式与命令式的区别展开:声明界面结构有助于快速搭建和阅读页面,但复杂场景仍需要清楚地组织状态、逻辑与交互。
他介绍了团队当时在详情、发布和交易等业务中使用 Fish Redux 的经验,认为组件化组织有利于分工和定位改动,同时也承认概念较多、初期有学习门槛。项目名称可在 Fish Redux 官方仓库核对;这段是历史实践回顾,不是对当前状态管理方案的选型推荐。
新宿的例子是多人共同修改一个复杂页面:各自围绕组件的 effect、reducer 和界面组织负责的部分,维护者之后也能直接找到模块入口。声明式写法让页面结构易读,但复杂业务仍需要明确的数据和状态边界;Fish Redux 的概念学习成本,正是为这种大型页面协作付出的另一类成本。
共享实现的价值,需要结合业务逻辑复杂度
在适用场景的追问中,新宿再次强调复杂业务逻辑和多端一致性。两端分别理解、实现并维护同一需求,可能逐渐产生差异;共享实现能够减少其中一部分重复工作。需要同时支持手机与桌面的应用,也有进一步评估跨端方案的理由。
这种收益仍依赖实际能力和平台适配,不能把“一套代码”直接解释为不再有差异。现场关于动态化、热更新的提问没有展开,主持人将其留作后续专题,因此本文也不补写成当场已经给出的方案。
构建慢,先区分 Flutter 产物与原生宿主
最后的问题是混合工程编译时间。宗心建议先拆开两段链路:Flutter 产物构建,与原生宿主构建。前者可以使用配置与宿主一致的占位工程,避免每次都带上完整宿主代码,再将产物交给原生工程集成。
原生侧则要根据构建日志定位具体耗时,分析哪些代码、依赖和产物真正需要重新编译,清理无效引用,并在适当情况下使用预构建产物。CI 环境也应复用已经准备好的依赖和缓存,避免每次重新下载;独立产物上传等步骤可以评估并行执行。
宗心还举了更细的构建环节:iOS 工程如果大量无用引用或编译告警长期累积,构建输出与后续处理都会变慢;先按构建日志看每个产物的耗时,再决定处理谁。Flutter 产物若需要上传到内部仓库、供宿主工程下载,上传和拉取的串行等待也可能成为瓶颈。固定打包机环境、复用依赖缓存和 Flutter SDK,能减少每次从头准备环境的时间。这些建议指向不同阶段,不能只靠打开一个缓存开关解决全部构建慢的问题。
这组建议并没有给出一个适用于所有工程的加速倍数,而是把构建过程拆开,逐段识别重复工作与等待,再分别改进。

