- 快召唤伙伴们来围观吧
- 微博 QQ QQ空间 贴吧
- 文档嵌入链接
- 复制
- 微信扫一扫分享
- 已成功复制到剪贴板
美团点评2018前端篇
展开查看详情
1 .
2 . 序 春节已近,年味渐浓。 又到了我们献上技术年货的时候。 不久前,我们已经给大家分享了技术沙龙大套餐,汇集了过去一年我们线上线下技术沙龙 99位讲师,85 个演讲,70+小时 分享。 今天出场的,同样重磅——技术博客全年大合集。 2018年,是美团技术团队官方博客第5个年头, 博客网站 全年独立访问用户累计超过300万,微信公众 号(meituantech)的关注数也超过了15万。 由衷地感谢大家一直以来对我们的鼓励和陪伴! 在2019年春节到来之际,我们再次精选了114篇技术干货,制作成一本厚达1200多页的电子书呈送给大 家。 这本电子书主要包括前端、后台、系统、算法、测试、运维、工程师成长等7个板块。疑义相与析,大家 在阅读中如果发现Bug、问题,欢迎扫描文末二维码,通过微信公众号与我们交流。 也欢迎大家转给有相同兴趣的同事、朋友,一起切磋,共同成长。 最后祝大家,新春快乐,阖家幸福。
3 . 目录 - 前端篇 用Vue.js开发微信小程序:开源框架mpvue解析 ...................................................................... 5 Flutter原理与实践 ...................................................................... 12 Picasso 开启大前端的未来 ...................................................................... 33 美团客户端响应式框架 EasyReact 开源啦 ...................................................................... 44 Logan:美团点评的开源移动端基础日志库 ...................................................................... 57 美团点评移动端基础日志库——Logan ...................................................................... 65 MCI:移动持续集成在大众点评的实践 ...................................................................... 74 美团外卖Android Crash治理之路 ...................................................................... 91 美团外卖Android平台化的复用实践 ...................................................................... 106 美团外卖Android平台化架构演进实践 ...................................................................... 116 美团外卖Android Lint代码检查实践 ...................................................................... 132 Android动态日志系统Holmes ...................................................................... 143 Android消息总线的演进之路:用LiveDataBus替代 ...................................................................... 152 RxBus、EventBus Android组件化方案及组件消息总线modular-event实战 ...................................................................... 163 Android自动化页面测速在美团的实践 ...................................................................... 175 Kotlin代码检查在美团的探索与实践 ...................................................................... 191 WMRouter:美团外卖Android开源路由框架 ...................................................................... 201 美团外卖客户端高可用建设体系 ...................................................................... 217 iOS 覆盖率检测原理与增量代码测试覆盖率工具实现 ...................................................................... 226 iOS系统中导航栏的转场解决方案与最佳实践 ...................................................................... 238 Category 特性在 iOS 组件化中的应用与管控 ...................................................................... 259 美团开源Graver框架:用“雕刻”诠释iOS端UI界面的高效 ...................................................................... 277 渲染 美团外卖iOS App冷启动治理 ...................................................................... 285 美团外卖iOS多端复用的推动、支撑与思考 ...................................................................... 302 【基本功】深入剖析Swift性能优化 ...................................................................... 317 前端安全系列(一):如何防止XSS攻击? ...................................................................... 337 前端安全系列(二):如何防止CSRF攻击? ...................................................................... 345
4 .Hades:移动端静态分析框架 ...................................................................... 360 Jenkins的Pipeline脚本在美团餐饮SaaS中的实践 ...................................................................... 376 MSON,让JSON序列化更快 ...................................................................... 387 Toast与Snackbar的那点事 ...................................................................... 394 WWDC案例解读:大众点评相机直接扫描支付是怎么实现 ...................................................................... 405 的 beeshell —— 开源的 React Native 组件库 ...................................................................... 410 前端遇上Go: 静态资源增量更新的新实践 ...................................................................... 445 深入理解JSCore ...................................................................... 455 深度学习及AR在移动端打车场景下的应用 ...................................................................... 473 美团点评金融平台Web前端技术体系 ...................................................................... 486 插件化、热补丁中绕不开的Proguard的坑 ...................................................................... 503 美团扫码付小程序的优化实践 ...................................................................... 513 用微前端的方式搭建类单页应用 ...................................................................... 519 构建时预渲染:网页首帧优化实践 ...................................................................... 528 美团扫码付的前端可用性保障实践 ...................................................................... 540 ARKit:增强现实技术在美团到餐业务的实践 ...................................................................... 550
5 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 用Vue.js开发微信小程序:开源框架mpvue解析 作者: 成全 前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架。使用此框架,开发者将得到完整的 Vue.js 开发 体验,同时为 H5 和小程序提供了代码复用的能力。如果想将 H5 项目改造为小程序,或开发小程序后希 望将其转换为 H5,mpvue 将是十分契合的一种解决方案。 目前, mpvue 已经在美团点评多个实际业务项目中得到了验证,因此我们决定将其开源,希望更多技术 同行一起开发,应用到更广泛的场景里去。github 项目地址请参见 mpvue 。使用文档请参见 http://mpvue.com/ 。 为了帮助大家更好的理解 mpvue 的架构,接下来我们来解析框架的设计和实现思路。文中主要内容已经 发表在《程序员》杂志2017年第9期小程序专题封面报道,内容略有修改。 小程序开发特点 微信小程序推荐简洁的开发方式,通过多页面聚合完成轻量的产品功能。小程序以离线包方式下载到本 地,通过微信客户端载入和启动,开发规范简洁,技术封装彻底,自成开发体系,有 Native 和 H5 的影 子,但又绝不雷同。 小程序本身定位为一个简单的逻辑视图层框架,官方并不推荐用来开发复杂应用,但业务需求却难以做到 精简。复杂的应用对开发方式有较高的要求,如组件和模块化、自动构建和集成、代码复用和开发效率 等,但小程序开发规范较大的限制了这部分能力。为了解决上述问题,提供更好的开发体验,我们创造了 mpvue,通过使用 Vue.js 来开发微信小程序。 mpvue是什么 mpvue 是一套定位于开发小程序的前端开发框架,其核心目标是提高开发效率,增强开发体验。使用该 框架,开发者只需初步了解小程序开发规范、熟悉 Vue.js 基本语法即可上手。框架提供了完整的 Vue.js 开发体验,开发者编写 Vue.js 代码,mpvue 将其解析转换为小程序并确保其正确运行。此外,框架还通 过 vue-cli 工具向开发者提供 quick start 示例代码,开发者只需执行一条简单命令,即可获得可运行的 项目。 为什么做mpvue 在小程序内测之初,我们计划快速迭代出一款对标 H5 的产品实现,核心诉求是:快速实现、代码复用、 低成本和高效率… 随后经历了多个小程序建设,结合业务场景、技术选型和小程序开发方式,我们整理 汇总出了开发阶段面临的主要问题: 组件化机制不够完善
6 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 代码多端复用能力欠缺 小程序框架和团队技术栈无法有机结合 小程序学习成本不够低 组件机制:小程序逻辑和视图层代码彼此分离,公共组件提取后无法聚合为单文件入口,组件需分别在视 图层和逻辑层引入,维护性差;组件无命名空间机制,事件回调必须设置为全局函数,组件设计有命名冲 突的风险,数据封装不强。开发者需要友好的代码组织方式,通过 ES 模块一次性导入;组件数据有良好 的封装。成熟的组件机制,对工程化开发至关重要。 多端复用:常见的业务场景有两类,通过已有 H5 产品改造为小程序应用或反之。从效率角度出发,开发 者希望通过复用代码完成开发,但小程序开发框架却无法做到。我们尝试过通过静态代码分析将 H5 代码 转换为小程序,但只做了视图层转换,无法带来更多收益。多端代码复用需要更成熟的解决方案。 引入 Vue.js:小程序开发方式与 H5 近似,因此我们考虑和 H5 做代码复用。沿袭团队技术栈选型,我 们将 Vue.js 确定为小程序开发规范。使用 Vue.js 开发小程序,将直接带来如下开发效率提升: H5 代码可以通过最小修改复用到小程序 使用 Vue.js 组件机制开发小程序,可实现小程序和 H5 组件复用 技术栈统一后小程序学习成本降低,开发者从 H5 转换到小程序不需要更多学习 Vue.js 代码可以让所有前端直接参与开发维护 为什么是 Vue.js?这取决于团队技术栈选型,引入新的选型与统一技术栈和提高开发效率相悖,有违开 发工具服务业务的初衷。 mpvue 的演进 mpvue的形成,来源于业务场景和需求,最终方案的确定,经历了三个阶段。 第一阶段:我们实现了一个视图层代码转换工具,旨在提高代码首次开发效率。通过将H5视图层代码转 换为小程序代码,包括 HTML 标签映射、Vue.js 模板和样式转换,在此目标代码上进行二次开发。我们 做到了有限的代码复用,但组件化开发和小程序学习成本并未得到有效改善。 第二阶段:我们着眼于完善代码组件化机制。参照 Vue.js 组件规范设计了代码组织形式,通过代码转换 工具将代码解析为小程序。转换工具主要解决组件间数据同步、生命周期关联和命名空间问题。最终我们 实现了一个 Vue.js 语法子集,但想要实现更多特性或跟随 Vue.js 版本迭代,工作量变得难以估计,有永 无止境之感。 第三阶段:我们的目标是实现对 Vue.js 语法全集的支持,达到使用 Vue.js 开发小程序的目的。并通过引 入 Vue.js runtime 实现了对 Vue.js 语法的支持,从而避免了人肉语法适配。至此,我们完成了使用 Vue.js 开发小程序的目的。较好地实现了技术栈统一、组件化开发、多端代码复用、降低学习成本和提 高开发效率的目标。 mpvue设计思路 Vue.js 和小程序都是典型的逻辑视图层框架,逻辑层和视图层之间的工作方式为:数据变更驱动视图更 新;视图交互触发事件,事件响应函数修改数据再次触发视图更新,如图1所示。
7 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 图1: 小程序实现原理 鉴于 Vue.js 和小程序一致的工作原理,我们思考将小程序的功能托管给 Vue.js,在正确的时机将数据变 更同步到小程序,从而达到开发小程序的目的。这样,我们可以将精力聚焦在 Vue.js 上,参照 Vue.js 编 写与之对应的小程序代码,小程序负责视图层展示,所有业务逻辑收敛到 Vue.js 中,Vue.js 数据变更后 同步到小程序,如图2所示。如此一来,我们就获得了以 Vue.js 的方式开发小程序的能力。为此,我们设 计的方案如下: 图2:mpvue 实现原理 Vue代码 将小程序页面编写为 Vue.js 实现 以 Vue.js 开发规范实现父子组件关联 小程序代码
8 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 以小程序开发规范编写视图层模板 配置生命周期函数,关联数据更新调用 将 Vue.js 数据映射为小程序数据模型 并在此基础上,附加如下机制 Vue.js 实例与小程序 Page 实例建立关联 小程序和 Vue.js 生命周期建立映射关系,能在小程序生命周期中触发 Vue.js 生命周期 小程序事件建立代理机制,在事件代理函数中触发与之对应的 Vue.js 组件事件响应 这套机制总结起来非常简单,但实现却相当复杂。在揭秘具体实现之前,读者可能会有这样一些疑问: 要同时维护 Vue.js 和小程序,是否需要写两个版本的代码实现? 小程序负责视图层展现,Vue.js的视图层是否还需要,如果不需要应该如何处理? 生命周期如何打通,数据同步更新如何实现? 上述问题包含了 mpvue 框架的核心内容,下文将仔细为你道来。首先,mpvue 为提高效率而生,本身 提供了自动生成小程序代码的能力,小程序代码根据 Vue.js 代码构建得到,并不需要同时开发两套代 码。 Vue.js 视图层渲染由 render 方法完成,同时在内存中维护着一份虚拟 DOM,mpvue 无需使用 Vue.js 完成视图层渲染,因此我们改造了 render 方法,禁止视图层渲染。熟悉源代码的读者,都知道 Vue runtime 有多个平台的实现,除了我们常见的 Web 平台,还有 Weex。从现在开始,我们增加了新的平 台 mpvue。 生命周期关联:生命周期和数据同步是 mpvue 框架的灵魂,Vue.js 和小程序的数据彼此隔离,各自有不 同的更新机制。mpvue 从生命周期和事件回调函数切入,在 Vue.js 触发数据更新时实现数据同步。小程 序通过视图层呈现给用户、通过事件响应用户交互,Vue.js 在后台维护着数据变更和逻辑。可以看到, 数据更新发端于小程序,处理自 Vue.js,Vue.js 数据变更后再同步到小程序。为实现数据同步,mpvue 修改了 Vue.js runtime 实现,在 Vue.js 的生命周期中增加了更新小程序数据的逻辑。 事件代理机制:用户交互触发的数据更新通过事件代理机制完成。在 Vue.js 代码中,事件响应函数对应 到组件的 method, Vue.js 自动维护了上下文环境。然而在小程序中并没有类似的机制,又因为 Vue.js 执行环境中维护着一份实时的虚拟 DOM,这与小程序的视图层完全对应,我们思考,在小程序组件节点 上触发事件后,只要找到虚拟 DOM 上对应的节点,触发对应的事件不就完成了么;另一方面,Vue.js 事件响应如果触发了数据更新,其生命周期函数更新将自动触发,在此函数上同步更新小程序数据,数据 同步也就实现了。 mpvue如何使用 mpvue框架本身由多个npm模块构成,入口模块已经处理好依赖关系,开发者只需要执行如下代码即可完 成本地项目创建。 #安装 vue-cli $ npm install --global vue-cli #根据模板项目创建本地项目,目前为内网地址 $ vue init mpvue/mpvue-quickstart my-project #安装依赖和启动自动构建 $ cd my-project
9 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 $ npm install $ npm run dev 执行完上述命令,在当前项目的 dist 子目录将构建出小程序目标代码,使用小程序开发者工具载入 dist 目录即可启动本地调试和预览。示例项目遵循 Vue.js 模板项目规范,通过Vue.js 命令行工具vue-cli创 建。代码组织形式与 Vue.js 官方实例保持一致,我们为小程序定制了 Vue.js runtime 和 webpack 加载 器,此部分依赖也已经内置到项目中。 针对小程序开发中常见的两类代码复用场景,mpvue 框架为开发者提供了解决思路和技术支持,开发者 只需要在此指导下进行项目配置和改造。我们内部实践了一个将 H5 转换为小程序的项目,下图为使用 mpvue 框架的转换效果: 图3:H5和小程序转换效果 将小程序转换为H5:直接使用 Vue.js 规范开发小程序,代码本身与H5并无不同,具体代码差异会集中 在平台 Api 部分。此外并不需明显改动,改造主要分如下几部分: 将小程序平台的 Vue.js 框架替换为标准 Vue.js 将小程序平台的 vue-loader 加载器替换为标准 vue-loader 适配和改造小程序与 H5 的底层 Api 差异 将H5转换为小程序:已经使用 Vue.js 开发完 H5,我们需要做的事情如下: 将标准 Vue.js 替换为小程序平台的 Vue.js 框架 将标准 vue-loader 加载器替换为小程序平台的 vue-loader 适配和改造小程序与 H5 的底层 Api 差异
10 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 根据小程序开发平台提供的能力,我们最大程度的支持了 Vue.js 语法特性,但部分功能现阶段暂时尚未 实现。 表1:mpvue暂不支持的语法特性 项目转换注意事项:框架的目标是将小程序和 H5 的开发方式通过 Vue.js 建立关联,达到最大程度的代 码复用。但由于平台差异的客观存在(主要集中在实现机制、底层Api 能力差异),我们无法做到代码 100% 复用,平台差异部分的改造成本无法避免。对于代码复用的场景,开发者需要重点思考如下问题并 做好准备: 尽量使用平台无的语法特性,这部分特性无需转换和适配成本 避免使用不支持的语法特性,譬如 slot, filter 等,降低改造成本 如果使用特定平台 Api ,考虑抽象好适配层接口,通过切换底层实现完成平台转换 mpvue 最佳实践 在表2中,我们对微信小程序、mpvue、WePY 这三个开发框架的主要能力和特点做了横向对比,帮助大 家了解不同框架的侧重点,结合业务场景和开发习惯,确定技术方案。对于如何更好地使用 mpvue 进行 小程序开发,我们总结了一些最佳实践。 使用 vue-cli 命令行工具创建项目,使用Vue 2.x 的语法规范进行开发 避免使用框架不支持的语法特性,部分 Vue.js语法在小程序中无法使用,尽量使用 mpvue 和 Vue.js 共有特性 合理设计数据模型,对数据的更新和操作做到细粒度控制,避免性能问题 合理使用组件化开发小程序,提高代码复用率
11 . 用Vue.js开发微信小程序:开源框架mpvue解析 - 美团技术团队 表2:框架使用特点对比 结语 mpvue 框架已经在业务项目中得到实践和验证,目前正在美团点评内部大范围使用。mpvue 来源于开源 社区,饮水思源,我们也希望为开源社区贡献一份力量,为广大小程序开发者提供一套技术方案。mpvue 的初衷是让 Vue.js 的开发者以低成本接入小程序开发,做到代码的低成本迁移和复用,我们未来会继续 扩展现有能力、解决开发者的诉求、优化使用体验、完善周边生态建设,帮助到更多的开发者。 最后,mpvue 基于 Vue.js 源码进行二次开发,新增加了小程序平台的实现,我们保留了跟随 Vue.js 版 本升级的能力,由衷的感谢 Vue.js 框架和微信小程序给业界带来的便利。 作者简介 成全:美团点评酒旅事业群资深前端工程师,目前主要从事移动端和小程序技术方向,致力于小程序的工程化开发和 业务级应用。 招聘时间 美团点评酒旅业务研发中心诚招中、高级前端工程师、技术专家,欢迎投递简历到 huchengquan#meituan.com。
12 . Flutter原理与实践 - 美团技术团队 Flutter原理与实践 作者: 少杰 Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia 的默认开发套件。自从2017年5月发布 第一个版本 以来,目前Flutter已经发布了近60个版本,并且在2018年5月发布 了第一个 “Ready for Production Apps” 的Beta 3版本,6月20日发布了第一个 “Release Preview” 版本。 初识Flutter Flutter的目标是使同一套代码同时运行在Android和iOS系统上,并且拥有媲美原生应用的性能,Flutter甚至提供了两套 控件来适配Android和iOS(滚动效果、字体和控件图标等等)为了让App在细节处看起来更像原生应用。 在Flutter诞生之前,已经有许多跨平台UI框架的方案,比如基于WebView的Cordova、AppCan等,还有使用 HTML+JavaScript渲染成原生控件的React Native、Weex等。 基于WebView的框架优点很明显,它们几乎可以完全继承现代Web开发的所有成果(丰富得多的控件库、满足各种需求 的页面框架、完全的动态化、自动化测试工具等等),当然也包括Web开发人员,不需要太多的学习和迁移成本就可以 开发一个App。同时WebView框架也有一个致命(在对体验&性能有较高要求的情况下)的缺点,那就是WebView的渲 染效率和JavaScript执行性能太差。再加上Android各个系统版本和设备厂商的定制,很难保证所在所有设备上都能提供 一致的体验。 为了解决WebView性能差的问题,以React Native为代表的一类框架将最终渲染工作交还给了系统,虽然同样使用类 HTML+JS的UI构建逻辑,但是最终会生成对应的自定义原生控件,以充分利用原生控件相对于WebView的较高的绘制效 率。与此同时这种策略也将框架本身和App开发者绑在了系统的控件系统上,不仅框架本身需要处理大量平台相关的逻 辑,随着系统版本变化和API的变化,开发者可能也需要处理不同平台的差异,甚至有些特性只能在部分平台上实现,这 样框架的跨平台特性就会大打折扣。 Flutter则开辟了一种全新的思路,从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。渲染引擎依 靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体 验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。 Flutter同时支持Windows、Linux和macOS操作系统作为开发环境,并且在Android Studio和VS Code两个IDE上都提供 了全功能的支持。Flutter所使用的Dart语言同时支持AOT和JIT运行方式,JIT模式下还有一个备受欢迎的开发利器“热刷 新”(Hot Reload),即在Android Studio中编辑Dart代码后,只需要点击保存或者“Hot Reload”按钮,就可以立即更新 到正在运行的设备上,不需要重新编译App,甚至不需要重启App,立即就可以看到更新后的样式。 在Flutter中,所有功能都可以通过组合多个Widget来实现,包括对齐方式、按行排列、按列排列、网格排列甚至事件处 理等等。Flutter控件主要分为两大类,StatelessWidget和StatefulWidget,StatelessWidget用来展示静态的文本或者 图片,如果控件需要根据外部数据或者用户操作来改变的话,就需要使用StatefulWidget。State的概念也是来源于 Facebook的流行Web框架 React ,React风格的框架中使用控件树和各自的状态来构建界面,当某个控件的状态发生 变化时由框架负责对比前后状态差异并且采取最小代价来更新渲染结果。 Hot Reload 在Dart代码文件中修改字符串“Hello, World”,添加一个惊叹号,点击保存或者热刷新按钮就可以立即更新到界面上,仅 需几百毫秒:
13 . Flutter原理与实践 - 美团技术团队 Flutter通过将新的代码注入到正在运行的DartVM中,来实现Hot Reload这种神奇的效果,在DartVM将程序中的类结构 更新完成后,Flutter会立即重建整个控件树,从而更新界面。但是热刷新也有一些限制,并不是所有的代码改动都可以 通过热刷新来更新: 1. 编译错误,如果修改后的Dart代码无法通过编译,Flutter会在控制台报错,这时需要修改对应的代码。 2. 控件类型从 StatelessWidget 到 StatefulWidget 的转换,因为Flutter在执行热刷新时会保留程序原来的state,而某个控件从 stageless→stateful后会导致Flutter重新创建控件时报错“myWidget is not a subtype of StatelessWidget”,而从stateful→stateless 会报错“type ‘myWidget’ is not a subtype of type ‘StatefulWidget’ of ‘newWidget’”。 3. 全局变量和静态成员变量,这些变量不会在热刷新时更新。 4. 修改了main函数中创建的根控件节点,Flutter在热刷新后只会根据原来的根节点重新创建控件树,不会修改根节点。 5. 某个类从普通类型转换成枚举类型,或者类型的泛型参数列表变化,都会使热刷新失败。 热刷新无法实现更新时,执行一次热重启(Hot Restart)就可以全量更新所有代码,同样不需要重启App,区别是 restart会将所有Dart代码打包同步到设备上,并且所有状态都会重置。 Flutter插件 Flutter使用的Dart语言无法直接调用Android系统提供的Java接口,这时就需要使用插件来实现中转。Flutter官方提供了 丰富的原生接口封装: android_alarm_manager ,访问Android系统的 AlertManager 。 android_intent ,构造Android的Intent对象。 battery ,获取和监听系统电量变化。 connectivity ,获取和监听系统网络连接状态。 device info ,获取设备型号等信息。 image_picker ,从设备中选取或者拍摄照片。 package_info ,获取App安装包的版本等信息。 path_provider ,获取常用文件路径。 quick_actions ,App图标添加快捷方式,iOS的eponymous concept 和Android的App Shortcuts 。
14 . Flutter原理与实践 - 美团技术团队 sensors ,访问设备的加速度和陀螺仪传感器。 shared_preferences ,App KV存储功能。 url_launcher ,启动URL,包括打电话、发短信和浏览网页等功能。 video_player ,播放视频文件或者网络流的控件。 在Flutter中,依赖包由 Pub 仓库管理,项目依赖配置在pubspec.yaml文件中声明即可(类似于NPM的版本声明 Pub Versioning Philosophy ),对于未发布在Pub仓库的插件可以使用git仓库地址或文件路径: dependencies: url_launcher: ">=0.1.2 <0.2.0" collection: "^0.1.2" plugin1: git: url: "git://github.com/flutter/plugin1.git" plugin2: path: ../plugin2/ 以shared_preferences为例,在pubspec中添加代码: dependencies: flutter: sdk: flutter shared_preferences: "^0.4.1" 脱字号“^”开头的版本表示 和当前版本接口保持兼容 的最新版, ^1.2.3 等效于 >=1.2.3 <2.0.0 而 ^0.1.2 等效 于 >=0.1.2 <0.2.0 ,添加依赖后点击“Packages get”按钮即可下载插件到本地,在代码中添加import语句就可以使用 插件提供的接口: import 'package:shared_preferences/shared_preferences.Dart'; class _MyAppState extends State<MyAppCounter> { int _count = 0; static const String COUNTER_KEY = 'counter'; _MyAppState() { init(); } init() async { var pref = await SharedPreferences.getInstance(); _count = pref.getInt(COUNTER_KEY) ?? 0; setState(() {}); } increaseCounter() async { SharedPreferences pref = await SharedPreferences.getInstance(); pref.setInt(COUNTER_KEY, ++_count); setState(() {}); } ... Dart Dart 是一种强类型、跨平台的客户端开发语言。具有专门为客户端优化、高生产力、快速高效、可移植(兼容 ARM/x86)、易学的OO编程风格和原生支持响应式编程(Stream & Future)等优秀特性。Dart主要由Google负责开发 和维护,在 2011年10启动项目 ,2017年9月发布第一个2.0-dev版本。 Dart本身提供了三种运行方式: 1. 使用Dart2js编译成JavaScript代码,运行在常规浏览器中(Dart Web )。 2. 使用DartVM直接在命令行中运行Dart代码(DartVM )。 3. AOT方式编译成机器码,例如Flutter App框架(Flutter )。 Flutter在筛选了20多种语言后,最终选择Dart作为开发语言主要有几个原因: 1. 健全的类型系统,同时支持静态类型检查和运行时类型检查。 2. 代码体积优化(Tree Shaking),编译时只保留运行时需要调用的代码(不允许反射这样的隐式引用),所以庞大的Widgets库不会造 成发布体积过大。
15 . Flutter原理与实践 - 美团技术团队 3. 丰富的底层库,Dart自身提供了非常多的库。 4. 多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化。 5. 跨平台,iOS和Android共用一套代码。 6. JIT & AOT运行模式,支持开发时的快速迭代和正式发布后最大程度发挥硬件性能。 在Dart中,有一些重要的基本概念需要了解: 所有变量的值都是对象,也就是类的实例。甚至数字、函数和 null 也都是对象,都继承自Object 类。 虽然Dart是强类型语言,但是显式变量类型声明是可选的,Dart支持类型推断。如果不想使用类型推断,可以用dynamic 类型。 Dart支持泛型, List<int> 表示包含int类型的列表, List<dynamic> 则表示包含任意类型的列表。 Dart支持顶层(top-level)函数和类成员函数,也支持嵌套函数和本地函数。 Dart支持顶层变量和类成员变量。 Dart没有public、protected和private这些关键字,使用下划线“_”开头的变量或者函数,表示只在库内可见。参考库和可见性 。 DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内 存段的过程: Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速 分配。 Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间 中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存: 整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种算法也非常适合Flutter框 架中大量Widget重建的场景。 Flutter Framework Flutter的框架部分完全使用Dart语言实现,并且有着清晰的分层架构。分层架构使得我们可以在调用Flutter提供的便捷 开发功能(预定义的一套高质量Material控件)之外,还可以直接调用甚至修改每一层实现(因为整个框架都属于“用户 空间”的代码),这给我们提供了最大程度的自定义能力。Framework底层是Flutter引擎,引擎主要负责图形绘制 (Skia)、文字排版(libtxt)和提供Dart运行时,引擎全部使用C++实现,Framework层使我们可以用Dart语言调用引 擎的强大能力。 分层架构
16 . Flutter原理与实践 - 美团技术团队 Framework的最底层叫做Foundation,其中定义的大都是非常基础的、提供给其他所有层使用的工具类和方法。绘制库 (Painting)封装了Flutter Engine提供的绘制接口,主要是为了在绘制控件等固定样式的图形时提供更直观、更方便的 接口,比如绘制缩放后的位图、绘制文本、插值生成阴影以及在盒子周围绘制边框等等。Animation是动画相关的类,提 供了类似Android系统的ValueAnimator的功能,并且提供了丰富的内置插值器。Gesture提供了手势识别相关的功能, 包括触摸事件类定义和多种内置的手势识别器。GestureBinding类是Flutter中处理手势的抽象服务类,继承自 BindingBase类。Binding系列的类在Flutter中充当着类似于Android中的SystemService系列(ActivityManager、 PackageManager)功能,每个Binding类都提供一个服务的单例对象,App最顶层的Binding会包含所有相关的Bingding 抽象类。如果使用Flutter提供的控件进行开发,则需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控 件,而直接调用Render层,则需要使用RenderingFlutterBinding。 Flutter本身支持Android和iOS两个平台,除了性能和开发语言上的“native”化之外,它还提供了两套设计语言的控件实 现Material & Cupertino,可以帮助App更好地在不同平台上提供原生的用户体验。 渲染库(Rendering) Flutter的控件树在实际显示时会转换成对应的渲染对象( RenderObject )树来实现布局和绘制操作。一般情况下,我 们只会在调试布局,或者需要使用自定义控件来实现某些特殊效果的时候,才需要考虑渲染对象树的细节。渲染库主要 提供的功能类有: abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, HitTestable { ... } abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget { abstract class RenderBox extends RenderObject { ... } class RenderParagraph extends RenderBox { ... } class RenderImage extends RenderBox { ... } class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData>, RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData>, DebugOverflowIndicatorMixin { ... } RendererBinding 是渲染树和Flutter引擎的胶水层,负责管理帧重绘、窗口尺寸和渲染相关参数变化的监听。 RenderObject 渲染树中所有节点的基类,定义了布局、绘制和合成相关的接口。 RenderBox 和其三个常用的子类 RenderParagraph 、 RenderImage 、 RenderFlex 则是具体布局和绘制逻辑的实现类。 在Flutter界面渲染过程分为三个阶段:布局、绘制、合成,布局和绘制在Flutter框架中完成,合成则交由引擎负责。
17 . Flutter原理与实践 - 美团技术团队 控件树中的每个控件通过实现 RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject 方法来创建对应的不同类型的 RenderObject 对象,组成渲染对象树。因为Flutter极大地简化了布局的逻辑,所以整个 布局过程中只需要深度遍历一次: 渲染对象树中的每个对象都会在布局过程中接受父对象的 Constraints 参数,决定自己的大小,然后父对象就可以按照 自己的逻辑决定各个子对象的位置,完成布局过程。子对象不存储自己在容器中的位置,所以在它的位置发生改变时并 不需要重新布局或者绘制。子对象的位置信息存储在它自己的 parentData 字段中,但是该字段由它的父对象负责维 护,自身并不关心该字段的内容。同时也因为这种简单的布局逻辑,Flutter可以在某些节点设置布局边界(Relayout boundary),即当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然:
18 . Flutter原理与实践 - 美团技术团队 布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置,Flutter会把所有对象绘制到不同的图层上: 因为绘制节点时也是深度遍历,可以看到第二个节点在绘制它的背景和前景不得不绘制在不同的图层上,因为第四个节 点切换了图层(因为“4”节点是一个需要独占一个图层的内容,比如视频),而第六个节点也一起绘制到了红色图层。这 样会导致第二个节点的前景(也就是“5”)部分需要重绘时,和它在逻辑上毫不相干但是处于同一图层的第六个节点也必 须重绘。为了避免这种情况,Flutter提供了另外一个“重绘边界”的概念:
19 . Flutter原理与实践 - 美团技术团队 在进入和走出重绘边界时,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响。典型的应用场景就是 ScrollView,当滚动内容重绘时,一般情况下其他内容是不需要重绘的。虽然重绘边界可以在任何节点手动设置,但是一 般不需要我们来实现,Flutter提供的控件默认会在需要设置的地方自动设置。 控件库(Widgets) Flutter的控件库提供了非常丰富的控件,包括最基本的文本、图片、容器、输入框和动画等等。在Flutter中“一切皆是控 件”,通过组合、嵌套不同类型的控件,就可以构建出任意功能、任意复杂度的界面。它包含的最主要的几个类有: class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding { ... } abstract class Widget extends DiagnosticableTree { ... } abstract class StatelessWidget extends Widget { ... } abstract class StatefulWidget extends Widget { ... } abstract class RenderObjectWidget extends Widget { ... } abstract class Element extends DiagnosticableTree implements BuildContext { ... } class StatelessElement extends ComponentElement { ... } class StatefulElement extends ComponentElement { ... } abstract class RenderObjectElement extends Element { ... } ... 基于Flutter控件系统开发的程序都需要使用 WidgetsFlutterBinding ,它是Flutter的控件框架和Flutter引擎的胶水 层。 Widget 就是所有控件的基类,它本身所有的属性都是只读的。 RenderObjectWidget 所有的实现类则负责提供配 置信息并创建具体的 RenderObjectElement 。 Element 是Flutter用来分离控件树和真正的渲染对象的中间层,控件用 来描述对应的element属性,控件重建后可能会复用同一个element。 RenderObjectElement 持有真正负责布局、绘制 和碰撞测试(hit test)的 RenderObject 对象。 StatelessWidget 和 StatefulWidget 并不会直接影响 RenderObject 的创建,它们只负责创建对应的 RenderObjectWidget , StatelessElement 和 StatefulElement 也是类似的功能。 它们之间的关系如下图:
20 . Flutter原理与实践 - 美团技术团队 如果控件的属性发生了变化(因为控件的属性是只读的,所以变化也就意味着重新创建了新的控件树),但是其树上每 个节点的类型没有变化时,element树和render树可以完全重用原来的对象(因为element和render object的属性都是可 变的): 但是,如果控件树种某个节点的类型发生了变化,则element树和render树中的对应节点也需要重新创建: 外卖全品类页面实践 在调研了Flutter的各项特性和实现原理之后,外卖计划灰度上线Flutter版的全品类页面。对于将Flutter页面作为App的一 部分这种集成模式,官方并没有提供 完善的支持 ,所以我们首先需要了解Flutter是如何编译、打包并且运行起来的。 Flutter App构建过程 最简单的Flutter工程至少包含两个文件: 运行Flutter程序时需要对应平台的宿主工程,在Android上Flutter通过自动创建一个Gradle项目来生成宿主,在项目目录 下执行 flutter create . ,Flutter会创建ios和android两个目录,分别构建对应平台的宿主项目,android目录内容如 下:
21 . Flutter原理与实践 - 美团技术团队 此Gradle项目中只有一个app module,构建产物即是宿主APK。Flutter在本地运行时默认采用Debug模式,在项目目录 执行 flutter run 即可安装到设备中并自动运行,Debug模式下Flutter使用JIT方式来执行Dart代码,所有的Dart代码都 会打包到APK文件中assets目录下,由libflutter.so中提供的DartVM读取并执行: kernel_blob.bin是Flutter引擎的底层接口和Dart语言基本功能部分代码: third_party/dart/runtime/bin/*.dart third_party/dart/runtime/lib/*.dart third_party/dart/sdk/lib/_http/*.dart third_party/dart/sdk/lib/async/*.dart third_party/dart/sdk/lib/collection/*.dart third_party/dart/sdk/lib/convert/*.dart third_party/dart/sdk/lib/core/*.dart third_party/dart/sdk/lib/developer/*.dart third_party/dart/sdk/lib/html/*.dart third_party/dart/sdk/lib/internal/*.dart third_party/dart/sdk/lib/io/*.dart third_party/dart/sdk/lib/isolate/*.dart third_party/dart/sdk/lib/math/*.dart third_party/dart/sdk/lib/mirrors/*.dart third_party/dart/sdk/lib/profiler/*.dart third_party/dart/sdk/lib/typed_data/*.dart third_party/dart/sdk/lib/vmservice/*.dart flutter/lib/ui/*.dart
22 . Flutter原理与实践 - 美团技术团队 platform.dill则是实现了页面逻辑的代码,也包括Flutter Framework和其他由pub依赖的库代码: flutter_tutorial_2/lib/main.dart flutter/packages/flutter/lib/src/widgets/*.dart flutter/packages/flutter/lib/src/services/*.dart flutter/packages/flutter/lib/src/semantics/*.dart flutter/packages/flutter/lib/src/scheduler/*.dart flutter/packages/flutter/lib/src/rendering/*.dart flutter/packages/flutter/lib/src/physics/*.dart flutter/packages/flutter/lib/src/painting/*.dart flutter/packages/flutter/lib/src/gestures/*.dart flutter/packages/flutter/lib/src/foundation/*.dart flutter/packages/flutter/lib/src/animation/*.dart .pub-cache/hosted/pub.flutter-io.cn/collection-1.14.6/lib/*.dart .pub-cache/hosted/pub.flutter-io.cn/meta-1.1.5/lib/*.dart .pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.4.2/*.dart kernel_blob.bin和platform.dill都是由flutter_tools中的 bundle.dart 中调用 KernelCompiler 生成。 在Release模式( flutter run release )下,Flutter会使用Dart的AOT运行模式,编译时将Dart代码转换成ARM指 令: kernel_blob.bin和platform.dill都不在打包后的APK中,取代其功能的是(isolate/vm)_snapshot_(data/instr)四个文件。 snapshot文件由Flutter SDK中的 flutter/bin/cache/artifacts/engine/androidarmrelease/darwin x64/gen_snapshot 命令生成,vm_snapshot_*是Dart虚拟机运行所需要的数据和代码指令,isolate_snapshot_*则是每 个isolate运行所需要的数据和代码指令。 Flutter App运行机制 Flutter构建出的APK在运行时会将所有assets目录下的资源文件解压到App私有文件目录中的flutter目录下,主要包括处 理字符编码的icudtl.dat,还有Debug模式的kernel_blob.bin、platform.dill和Release模式下的4个snapshot文件。默认 情况下Flutter在 Application#onCreate 时调用 FlutterMain#startInitialization 来启动解压任务,然后在 FlutterActivityDelegate#onCreate 中调用 FlutterMain#ensureInitializationComplete 来等待解压任务结束。 Flutter在Debug模式下使用JIT执行方式,主要是为了支持广受欢迎的热刷新功能:
23 . Flutter原理与实践 - 美团技术团队 触发热刷新时Flutter会检测发生改变的Dart文件,将其同步到App私有缓存目录下,DartVM加载并且修改对应的类或者 方法,重建控件树后立即可以在设备上看到效果。 在Release模式下Flutter会直接将snapshot文件映射到内存中执行其中的指令: 在Release模式下, FlutterActivityDelegate#onCreate 中调用 FlutterMain#ensureInitializationComplete 方法 中会将AndroidManifest中设置的snapshot(没有设置则使用上面提到的默认值)文件名等运行参数设置到对应的C++同 名类对象中,构造 FlutterNativeView 实例时调用 nativeAttach 来初始化DartVM,运行编译好的Dart代码。 打包Android Library 了解Flutter项目的构建和运行机制后,我们就可以按照其需求打包成AAR然后集成到现有原生App中了。首先在 andorid/app/build.gradle中修改: APK AAR 修改android插件类型 apply plugin: ‘com.android.application’ apply plugin: ‘com.android.library’ 删除applicationId字段 applicationId “com.example.fluttertutorial” applicationId “com.example.fluttertutorial” 建议添加发布所有配置 - defaultPublishConfig ‘release’ 功能,方便调试 publishNonDefault true 简单修改后我们就可以使用Android Studio或者Gradle命令行工具将Flutter代码打包到aar中了。Flutter运行时所需要的 资源都会包含在aar中,将其发布到maven服务器或者本地maven仓库后,就可以在原生App项目中引用。 但这只是集成的第一步,为了让Flutter页面无缝衔接到外卖App中,我们需要做的还有很多。
24 . Flutter原理与实践 - 美团技术团队 图片资源复用 Flutter默认将所有的图片资源文件打包到assets目录下,但是我们并不是用Flutter开发全新的页面,图片资源原来都会 按照Android的规范放在各个drawable目录,即使是全新的页面也会有很多图片资源复用的场景,所以在assets目录下新 增图片资源并不合适。 Flutter官方并没有提供直接调用drawable目录下的图片资源的途径,毕竟drawable这类文件的处理会涉及大量的 Android平台相关的逻辑(屏幕密度、系统版本、语言等等),assets目录文件的读取操作也在引擎内部使用C++实现, 在Dart层面实现读取drawable文件的功能比较困难。Flutter在处理assets目录中的文件时也支持添加 多倍率的图片资源 ,并能够在使用时 自动选择 ,但是Flutter要求每个图片必须提供1x图,然后才会识别到对应的其他倍率目录下的图 片: flutter: assets: - images/cat.png - images/2x/cat.png - images/3.5x/cat.png new Image.asset('images/cat.png'); 这样配置后,才能正确地在不同分辨率的设备上使用对应密度的图片。但是为了减小APK包体积我们的位图资源一般只 提供常用的2x分辨率,其他分辨率的设备会在运行时自动缩放到对应大小。针对这种特殊的情况,我们在不增加包体积 的前提下,同样提供了和原生App一样的能力: 1. 在调用Flutter页面之前将指定的图片资源按照设备屏幕密度缩放,并存储在App私有目录下。 2. Flutter中使用时通过自定义的 WMImage 控件来加载,实际是通过转换成FileImage并自动设置scale为devicePixelRatio来加载。 这样就可以同时解决APK包大小和图片资源缺失1x图的问题。 Flutter和原生代码的通信 我们只用Flutter实现了一个页面,现有的大量逻辑都是用Java实现,在运行时会有许多场景必须使用原生应用中的逻辑 和功能,例如网络请求,我们统一的网络库会在每个网络请求中添加许多通用参数,也会负责成功率等指标的监控,还 有异常上报,我们需要在捕获到关键异常时将其堆栈和环境信息上报到服务器。这些功能不太可能立即使用Dart实现一 套出来,所以我们需要使用Dart提供的Platform Channel功能来实现Dart→Java之间的互相调用。 以网络请求为例,我们在Dart中定义一个 MethodChannel 对象: import 'dart:async'; import 'package:flutter/services.dart'; const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network'); Future<Map<String, dynamic>> post(String path, [Map<String, dynamic> form]) async { return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) { return new Map<String, dynamic>.from(result); }).catchError((_) => null); } 然后在Java端实现相同名称的MethodChannel: public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler { private static final String CHANNEL_NAME = "com.sankuai.waimai/network"; @Override public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) { switch (methodCall.method) { case "post": RetrofitManager.performRequest(post((String) methodCall.argument("path"), (Map) methodCall.argument("body")), new DefaultSubscriber<Map>() { @Override public void onError(Throwable e) { result.error(e.getClass().getCanonicalName(), e.getMessage(), null); } @Override public void onNext(Map stringBaseResponse) { result.success(stringBaseResponse);
25 . Flutter原理与实践 - 美团技术团队 } }, tag); break; default: result.notImplemented(); break; } } } 在Flutter页面中注册后,调用post方法就可以调用对应的Java实现: loadData: (callback) async { Map<String, dynamic> data = await post("home/groups"); if (data == null) { callback(false); return; } _data = AllCategoryResponse.fromJson(data); if (_data == null || _data.code != 0) { callback(false); return; } callback(true); }), SO库兼容性 Flutter官方只提供了四种CPU架构的SO库:armeabi-v7a、arm64-v8a、x86和x86-64,其中x86系列只支持Debug模 式,但是外卖使用的大量SDK都只提供了armeabi架构的库。虽然我们可以通过修改引擎 src 根目录和 third_party/dart 目录下 build/config/arm.gni , third_party/skia 目录下的 BUILD.gn 等配置文件来编译出 armeabi版本的Flutter引擎,但是实际上市面上绝大部分设备都已经支持armeabi-v7a,其提供的硬件加速浮点运算指令 可以大大提高Flutter的运行速度,在灰度阶段我们可以主动屏蔽掉不支持armeabi-v7a的设备,直接使用armeabi-v7a版 本的引擎。做到这点我们首先需要修改Flutter提供的引擎,在Flutter安装目录下的 bin/cache/artifacts/engine 下有 Flutter下载的所有平台的引擎: 我们只需要修改android-arm、android-arm-profile和android-arm-release下的flutter.jar,将其中的lib/armeabi- v7a/libflutter.so移动到lib/armeabi/libflutter.so即可: cd $FLUTTER_ROOT/bin/cache/artifacts/engine for arch in android-arm android-arm-profile android-arm-release; do pushd $arch
26 . Flutter原理与实践 - 美团技术团队 cp flutter.jar flutter-armeabi-v7a.jar # 备份 unzip flutter.jar lib/armeabi-v7a/libflutter.so mv lib/armeabi-v7a lib/armeabi zip -d flutter.jar lib/armeabi-v7a/libflutter.so zip flutter.jar lib/armeabi/libflutter.so popd done 这样在打包后Flutter的SO库就会打到APK的lib/armeabi目录中。在运行时如果设备不支持armeabi-v7a可能会崩溃,所 以我们需要主动识别并屏蔽掉这类设备,在Android上判断设备是否支持armeabi-v7a也很简单: public static boolean isARMv7Compatible() { try { if (SDK_INT >= LOLLIPOP) { for (String abi : Build.SUPPORTED_32_BIT_ABIS) { if (abi.equals("armeabi-v7a")) { return true; } } } else { if (CPU_ABI.equals("armeabi-v7a") || CPU_ABI.equals("arm64-v8a")) { return true; } } } catch (Throwable e) { L.wtf(e); } return false; } 灰度和自动降级策略 Horn是一个美团内部的跨平台配置下发SDK,使用Horn可以很方便地指定灰度开关: 在条件配置页面定义一系列条件,然后在参数配置页面添加新的字段flutter即可: 因为在客户端做了ABI兜底策略,所以这里定义的ABI规则并没有启用。 Flutter目前仍然处于Beta阶段,灰度过程中难免发生崩溃现象,观察到崩溃后再针对机型或者设备ID来做降级虽然可以 尽量降低影响,但是我们可以做到更迅速。外卖的Crash采集SDK同时也支持JNI Crash的收集,我们专门为Flutter注册 了崩溃监听器,一旦采集到Flutter相关的JNI Crash就立即停止该设备的Flutter功能,启动Flutter之前会先判断 FLUTTER_NATIVE_CRASH_FLAG 文件是否存在,如果存在则表示该设备发生过Flutter相关的崩溃,很有可能是不兼容导致 的问题,当前版本周期内在该设备上就不再使用Flutter功能。 除了崩溃以外,Flutter页面中的Dart代码也可能发生异常,例如服务器下发数据格式错误导致解析失败等等,Dart也提 供了全局的异常捕获功能:
27 . Flutter原理与实践 - 美团技术团队 import 'package:wm_app/plugins/wm_metrics.dart'; void main() { runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) { uploadException("$obj\n$stack"); }); } 这样我们就可以实现全方位的异常监控和完善的降级策略,最大程度减少灰度时可能对用户带来的影响。 分析崩溃堆栈和异常数据 Flutter的引擎部分全部使用C/C++实现,为了减少包大小,所有的SO库在发布时都会去除符号表信息。和其他的JNI崩 溃堆栈一样,我们上报的堆栈信息中只能看到内存地址偏移量等信息: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys' Revision: '0' Author: collect by 'libunwind' ABI: 'arm64-v8a' pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<< signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 backtrace: r0 00000000 r1 ffffffff r2 c0e7cb2c r3 c15affcc r4 c15aff88 r5 c0e7cb2c r6 c15aff90 r7 bf567800 r8 c0e7cc58 r9 00000000 sl c15aff0c fp 00000001 ip 80000000 sp c0e7cb28 lr c11a03f9 pc c1254088 cpsr 200c0030 #00 pc 002d7088 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #01 pc 002d5a23 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #02 pc 002d95b5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #03 pc 002d9f33 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #04 pc 00068e6d /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #05 pc 00067da5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so #09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr 单纯这些信息很难定位问题,所以我们需要使用NDK提供的ndk-stack来解析出具体的代码位置: ndk-stack -sym PATH [-dump PATH] Symbolizes the stack trace from an Android native crash. -sym PATH sets the root directory for symbols -dump PATH sets the file containing the crash dump (default stdin) 如果使用了定制过的引擎,必须使用 engine/src/out/androidrelease 下编译出的libflutter.so文件。一般情况下我们 使用的是官方版本的引擎,可以在 flutter_infra 页面直接下载带有符号表的SO文件,根据打包时使用的Flutter工具版 本下载对应的文件即可。比如0.4.4 beta版本: $ flutter --version # version 命令可以看到 Engine 对应的版本 06afdfe54e Flutter 0.4.4 • channel beta • https://github.com/flutter/flutter.git Framework • revision f9bb4289e9 (5 weeks ago) • 2018-05-11 21:44:54 -0700 Engine • revision 06afdfe54e Tools • Dart 2.0.0-dev.54.0.flutter-46ab040e58 $ cat flutter/bin/internal/engine.version # flutter 安装目录下的 engine.version 文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa 06afdfe54ebef9168a90ca00a6721c2d36e6aafa 拿到引擎版本号后在 https://console.cloud.google.com/storage/browser/flutter_infra/flutter/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/ 看到该版本对应的所有构建产物,下载android-arm-release、android-arm64-release和android-x86目录下的 symbols.zip,并存放到对应目录: 执行ndk-stack即可看到实际发生崩溃的代码和具体行数信息:
28 . Flutter原理与实践 - 美团技术团队 ndk-stack -sym flutter-production-syms/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/armeabi-v7a -dump flutter_jni_crash.txt ********** Crash dump: ********** Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys' pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<< signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 Stack frame #00 pc 002d7088 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::WordBreaker::setText(unsigned sh ort const*, unsigned int) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/WordBreaker.cp p:55 Stack frame #01 pc 002d5a23 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine minikin::LineBreaker::setText() at /b/bui ld/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/minikin/LineBreaker.cpp:74 Stack frame #02 pc 002d95b5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::ComputeLineBreaks() at / b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:273 Stack frame #03 pc 002d9f33 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine txt::Paragraph::Layout(double, bool) at / b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/third_party/txt/src/txt/paragraph.cc:428 Stack frame #04 pc 00068e6d /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine blink::ParagraphImplTxt::layout(double) a t /b/build/slave/Linux_Engine/build/src/out/android_release/../../flutter/lib/ui/text/paragraph_impl_txt.cc:54 Stack frame #05 pc 00067da5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine tonic::DartDispatcher<tonic::IndicesHolde r<0u>, void (blink::Paragraph::*)(double)>::Dispatch(void (blink::Paragraph::*)(double)) at /b/build/slave/Linux_Engine/build/src/out/android_re lease/../../topaz/lib/tonic/dart_args.h:150 Stack frame #06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine void tonic::DartCall<void (blink::Paragra ph::*)(double)>(void (blink::Paragraph::*)(double), _Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../t opaz/lib/tonic/dart_args.h:198 Stack frame #07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::AutoScopeNativeCallWra pperNoStackCheck(_Dart_NativeArguments*, void (*)(_Dart_NativeArguments*)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../th ird_party/dart/runtime/vm/native_entry.cc:198 Stack frame #08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::LinkNativeCall(_Dart_N ativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:348 Stack frame #09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr Dart异常则比较简单,默认情况下Dart代码在编译成机器码时并没有去除符号表信息,所以Dart的异常堆栈本身就可以 标识真实发生异常的代码文件和行数信息: FlutterException: type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'num' in type cast #0 _$CategoryGroupFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:29) #1 new CategoryGroup.fromJson (package:wm_app/all_category/model/category_model.dart:51) #2 _$CategoryListDataFromJson.<anonymous closure> (package:wm_app/lib/all_category/model/category_model.g.dart:5) #3 MappedListIterable.elementAt (dart:_internal/iterable.dart:414) #4 ListIterable.toList (dart:_internal/iterable.dart:219) #5 _$CategoryListDataFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:6) #6 new CategoryListData.fromJson (package:wm_app/all_category/model/category_model.dart:19) #7 _$AllCategoryResponseFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:19) #8 new AllCategoryResponse.fromJson (package:wm_app/all_category/model/category_model.dart:29) #9 AllCategoryPage.build.<anonymous closure> (package:wm_app/all_category/category_page.dart:46) <asynchronous suspension> #10 _WaimaiLoadingState.build (package:wm_app/all_category/widgets/progressive_loading_page.dart:51) #11 StatefulElement.build (package:flutter/src/widgets/framework.dart:3730) #12 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3642) #13 Element.rebuild (package:flutter/src/widgets/framework.dart:3495) #14 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2242) #15 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding&WidgetsBinding.drawFr ame (package:flutter/src/widgets/binding.dart:626) #16 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding._handlePersistentFram eCallback (package:flutter/src/rendering/binding.dart:208) #17 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/b inding.dart:990) #18 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/bindin g.dart:930) #19 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/bindi ng.dart:842) #20 _rootRun (dart:async/zone.dart:1126) #21 _CustomZone.run (dart:async/zone.dart:1023) #22 _CustomZone.runGuarded (dart:async/zone.dart:925) #23 _invoke (dart:ui/hooks.dart:122) #24 _drawFrame (dart:ui/hooks.dart:109) Flutter和原生性能对比 虽然使用原生实现(左)和Flutter实现(右)的全品类页面在实际使用过程中几乎分辨不出来:
29 . Flutter原理与实践 - 美团技术团队 但是我们还需要在性能方面有一个比较明确的数据对比。 我们最关心的两个页面性能指标就是页面加载时间和页面渲染速度。测试页面加载速度可以直接使用美团内部的Metrics 性能测试工具,我们将页面Activity对象创建作为页面加载的开始时间,页面API数据返回作为页面加载结束时间。从两 个实现的页面分别启动400多次的数据中可以看到,原生实现(AllCategoryActivity)的加载时间中位数为210ms, Flutter实现(FlutterCategoryActivity)的加载时间中位数为231ms。考虑到目前我们还没有针对FlutterView做缓存和重 用,FlutterView每次创建都需要初始化整个Flutter环境并加载相关代码,多出的20ms还在预期范围内: