跨端技术概述 | 青训营笔记

153 阅读18分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

跨端技术概述

跨端是什么

跨端背景

随着业务的发展,产生了越来越多的业务场景,同时随着技术的发展,产生了越来越多的端, PC 端(Windows、Mac),移动端(安卓、iOS)、web 端、IoT 设备(车载设备、手表)等。

常见痛点

  1. 各端功能几乎一致
  2. 开发、维护成本高
  3. 安卓、iOS 发版周期长

跨端技术方案目标

  1. 研发效率高

    • 学习成本低
    • 多端一致性高
  2. 用户体验好

    • 稳定性高
    • 性能体验好
  3. 动态化

    • 支持动态化下发,满足日益增长的业务需求

跨端技术方案介绍

Hybrid 方案

基于 WebView 渲染,通过 JS Bridge 把一部分系统能力开放给 JS 调用

HybridAPP.png

Hybrid方案.png

WebView 容器的工作原理是基于 Web 技术来实现界面和功能,通过将原生的接口封装、暴露给 JavaScript 调用,JavaScript 编写的页面可以运行在系统自带的 WebView 中。 这样做的优势是,对于前端开发者比较友好,可以很快地实现页面跨端,同时保留调用原生的能力,通过搭建桥接层和原生能力打通。但这种设计跨端的能力受限于桥接层,当调用之前没有的原生能力时,就需要增加桥。另外,浏览器内核的渲染独立于系统组件,无法保证原生体验,渲染的效果会差不少。

原生渲染方案

使用 JS 开发,通过中间层桥接后使用原生组件来渲染 UI 界面。

React Native

React Native 是一个由 FaceBook 于 2015 年 9 月发布的一款开源的 JavaScript 框架, 它可以让开发者使用 JavaScript 和 React 来开发跨平台的应用。

ReactNative.png

React Native 的思路是最大化地复用前端的生态和 Native 的生态,和 WebView 容器的最大区别在于 View 的渲染体系。React Native 抛弃了低效的浏览器内核渲染,转而使用自己的 DSL 生成中间格式,然后映射到对应的平台,渲染成平台的组件。 相对于 WebView 容器,体验会有一定的提升。不过,渲染时需要 JavaScript 和原生之间的通信,在有些场景可能会导致卡顿。另外就是,渲染还是在 Native 层,要求开发人员对 Native 有一定的熟悉度。

React Native 主要由 JSI、Fabric、TurboModules 组成。

JSI

JSI 是 JavaScript Interface 的缩写,一个用 C++ 写成的轻量级框架,它的作用就是通过 JSI,JS 对象可以直接获得 C++ 对象(Host Object)引用,并调用对应方法

JSI 是整个架构的核心和基石,所有的一切都是建立在它上面。

  • JSI 将支持其他 JS 引擎;
  • JSI 允许线程之间的同步相互执行,不需要 JSON 序列化等耗费性能的操作;
  • JSI 是用 C++ 编写的,以后如果针对电视、手表等其他系统,也可以很方便地移植;

Fabric

Fabric 是新的渲染系统,它将取代当前的 UI Manager。

UI Manager:当 APP 运行时React,React 会执行你的代码并在 JS 中创建一个 ReactElementTree,基于这棵树渲染器会在 C++ 中创建一个 ReactShadowTree。UI Manager 会使用 Shadow Tree来计算 UI 元素的位置,而且一旦 Layout 完成,Shadow Tree 就会被转换为由 Native Element 组成的 HostViewTree(例如:RN 里的 会变成 Android 中的 ViewGroup 和 iOS 中的 UIView)。

之前线程之间的通信都发生在 Bridge 上,这就意味着需要在传输和数据复制上耗费时间。通过 JSON 格式来传递消息,每次都要经历序列化和反序列化。

得益于前面的 JSI,JS 可以直接调用 Native 方法,其实就包括了 UI 方法,所以 JS 和 UI 线程可以同步执行从而提高列表、跳转、手势处理等等方面的性能。

Turbo Modules

在之前的框架中 JS 使用的所有 Native Modules(例如蓝牙、地理位置、文件存储等)都必须在应用程序打开之前进行初始化,这意味着用户不需要某些模块,但是它仍然必须在启动时进行初始化。

Turbo Modules 基本上是对这些旧的 Native 模块的增强,正如前面介绍的那样,现在 JS 将能够持有这些模块的引用,所以 JS 代码可以仅在需要时才加载对于模块,这样可以显著缩短 RN 应用的启动时间。

自渲染方案

利用 Skia 重新实现渲染管线,不依赖原生组件

Flutter

Flutter 是 Google 在 2018 年世界移动大会公布的开源应用开发框架,仅通过套代码库,就能构建精美的、原生平台编译的多平台应用。

flutter方案.png

flutter架构.png

2018年 Google 推出 Flutter,通过 Dart 语言构建一套跨平台的开发组件,所有组件基于 Skia 引擎自绘,在性能上可以和 Native 平台的 View 相媲美。Flutter 站在前人的肩膀上,参考了 React 的状态管理、Web 的自绘制 UI、React Native 的 HotReload 等特点,同时考虑了与 Native 通信的 Channel 机制、自渲染、完备的开发工具链。Flutter 与上述 Recat Native、 WebView 容器本质上都是不同的,它没有使用 WebView、JavaScript 解释器或者系统平台自带的原生控件,而是有一套自己专属的Widget,底层染使用自身的高性能 C/C++ 引擎自绘

对于底层操作系统而言,Fluter 应用程序的包装方式与其他原生应用相同。在每一个平台上,会包含一个特定的嵌入层,从而提供个程序入口,程序由此可以与底层操作系统进行协调,访问诸如 surface 渲染、辅助功能和输入等服务,并且管理事件循环队列。该嵌入层采用了适合当前平台的语言编写,例如 Android 使用的是 Java 和 C++,iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。 Flutter 本身包含了各个常见平台的嵌入层,同时也存在一些其他的嵌入层。

Flutter 引擎毫无疑问是 Flutter 的核心,它主要使用 C++ 编写,并提供了 Futter 应用所需的原语。当需要绘制新一的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter 核心 API 的底层实现,包括图形(通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。

引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层。该库暴露了最底层的原语,包括用于驱动输入、图形,和文本渲染的子系统的类。

通常,开发者可以通过 Flutter 框架层 与 Fluter 交互,该架提供了以 Dart 语言编写的现代响应式框架。它包括由一系列层组成的一组丰富的平台,布局和基础库。从下层到上层,依次有:

  • 基础的 foundational 类及一些基层之上的构建块服务,如 animation、painting 和 gestures,它们可以提供上层常用的抽象。
  • 渲染层用于提供操作布局的抽象。有了渲染层,你可以构建一棵可染对象的树。在你动态更新这些对象时,渲染树也会自动根据你的变更来更新布局。
  • widget 层是一种组合的抽象。每一个染层中的染对象,都在 widgets 层中有一个对应的类。此外,widgets 层让你可以自由组合你需要复用的各种类。响应式编程模型就在该层级中被引入。
  • Material 和 Cupertino 库提供了全面的 widgets 层的原语组合,这套组合分别实现了 Material 和 iOS 设计规范。
  • Fluter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 camera 和 webview;与平台无关的功能,例如 characters、 http 和 animations。还有一些软性包来自于更为宽泛的生态系统中,例如应用内支付、Apple 认证和 Lottie 动画。

小程序方案

使用小程序 DSL + JS 开发,通过中间层桥接后调用原生能力,使用 webview 来渲染 UI 界面。

字节小程序

字节小程序是一种全新的连接用户与服务的方式,它可以在宿主 (抖音、头条等 App) 内被便捷地获取和传播,同时具有出色的使用体验。

字节小程序方案.png

小程的运行环境分成渲染层和逻辑层,其中 ttml 模板和 ttss 样式工作在渲染层,js 脚本工作在逻辑层。小程序的渲染层和逻辑层分别由 2 个线程管理:渲染层的界面使用了 Webview 进行渲染;逻辑层采用 JSC 线程运行 JS 脚本。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程,这两个线程的通信会经由客户端(下文中也会采用 Native 来代指客户端)做中转,逻辑层发送网络请求也经由 Native 转发。

字节小程序通信模型.png

跨端技术方案对比

技术方案视图层逻辑层优点缺点
hybrid 方案webviewwebview JS thread开发成本低、CSS 全集、一致性好性能中等
原生渲染方案原生组件JS Engine性能好CSS 子集、一致性一般
自渲染方案SkiaDart VM性能最好、一致性好CSS 子集、Dart 生态一般、开发成本较高
小程序方案webview + 原生组件JS Engine开发成本低、CSS 全集、一致性好性能较好

基于小程序跨端实践

快速开发一个小程序

  1. 下载小程序开发者工具
  2. 小程序开发、调试
  3. 小程序上传

小程序性能优化

优化意义

  • 留住用户
  • 提升转化率
  • 提升用户体验

小程序性能是指小程序在字节系 APP(抖音、今日头条等客户端)中加载和呈现的速度,以及用户交互的响应程度。性能问题是多种多样的,情况好点的,有的小程序会产生一些延迟,这些延会给用产带来一些不好的交互体验,也有极其糟糕的情况,那就是小程房完全无法使用,对用户输入没有反应,或两者兼而有之。这些问题将在不同程度上影响用户体验,从而导致用户流失。 小程序平台希望用户通过小程序进行有意义的交互,如果是新闻资讯小程序,希望用户能更方便获取信息。如果是电商小程序,希望将潜在的购物者变成买家。如果它是一个社交小程序,希望访问者写帖子,上传照片,并与其他人互动。

小程序性能指标

小程序性能是指小程序在 APP 中加载和呈现的速度,以及对用产交互的响应强度。性能何题是多样的,比如有的小程序在交互时候会产生一些延迟,从面给用户带来一些不好的交与体验。更严重的情况,也可能导致小程序完全无法响应。这些问题将在不同程度上影响用户体验,从而导致用户流失。因此性能是用户体验中非常关键的一部分,性能优化也是一件重要且有价值的事情。为了能够衡量小程序性能,我们需要一系列指标来描述小程序启动过程中的关键阶段。

以懂车帝小程序为例,从小程序启动到完成分为以下几个关键环节

  • 小程序启动后会显示一个 loading view 在这个阶段会初始化小程序环境
  • 初始化完成后开始加载小程序,然后开始第二个阶段首次绘制(FP) ,这个时机开始渲染小程序的首帧
  • 第三个阶段是最大内容绘制(LCP) ,在这时已经展示页面中的最大元素,也是比较接近用户视觉感知效果
  • 最后是主要元素加载完成,并目达到可交互的状态

通过以上启动阶段,可以将用户体验数字化表达。

当然,启动环节耗时并不能完全真实反映用户体验,还需要借助实际用户行为,异常监控,作为辅助指标

这些指标包含:取消率、白屏率、LCP 到达率

  • 取消率是指用户在加载过程中点击返回或右上角关闭的占比,用于描述小程序的启动性能,用户取消率越低,启动性能越好
  • 白屏率是指从启动到退出白屏的 pv 占比,出现白屏表示页面渲染失败,我们目标是尽可能降低白屏率
  • LCP 到达率,如果较多用户在 LCP 到达前离开,也表示小程序可能出现异常,或用户体验较差

借助启动阶段指标及辅助指标,就可以较为准确的描绘出小程序的体验感知。

优化手段

小程序性能优化手段.png

优化小程序可以从启动性能体验和运行时性能体验两个方面入手。

启动性能体验

  • 减少包体积

    • 合理使用分包:使用分包加载是优化小程启动后耗时效果最明显的手段,建义开发者按照功能划分,将小程序的功能使用频率和场景拆分成分包,实现代码包的按需加载。同时需要注意控制分包数量,避免过多拆包。
    • 移除无用文件:目前小程序打包是会将工程下所有文件都打入代码包内,在开发迭代过程中,如果不及时清理无用的资源,会使得包体积越来越大,开发过程中要养成良好的习惯 —— 及时清理没有使用到的资源,防止资源冗余。
    • 控制包内静态资源:避免在代码包中包含或在 ttss 中内联过多、过大的代码包内的图片,应尽量采用网络图片。代码包内的图片一般应只包含一些体积较小的图标。声音、视频等其他类型的资源应尽量避免放到代码包中。小程序代码包在下载时会使用 Gzip 算法进行压缩,降低下载时传输的数据量。这些资源文件会占用大量代码包体积,并目通常难以进一步被压缩,对于下载耗时的影响比代码文件要大很多。
  • 减少同步逻辑

    • 优先使用异步API:在小程序启动流程中,会注入开发者代码并顺序同步执行 App.onLaunch,App.onShow,Page.onLoad,Paqe.onShow。在小程序初始化代码(Page,App 定义之外的内容)和启动相关的几个生命周期中,应避免过度使用 Sync 结尾的同步 API。
    • 避免启动时运行过多同步代码:在小程序初始化代码(Page,App 定义之外的内容)和启动相关的几个生命周期中,应避免执行复杂的计算逻辑。
  • 更早的展示首屏数据

    • 尽早调用关键 API 和请求:首屏绘制可能会依赖 API 数据和网络请求,尽早的调用相关 API,发送相关网络请求,能提前数据准备时间。
    • 接入数据预取:大部分小程序在渲染首页时,需要依赖服务端的接口数据,小程序为开发者提供了数据预取,方便开发者在小程席冷启动时提前发起请求,并缓存请求内容。
    • 避免非必要的 reLaunch:reLaunch 会先关闭所有页面,非必要的 reLaunch 会导致首页白屏时间明显增长。
  • 合理缓存数据

    • 网络数据缓存:小程序提供了 tt.qetStorage、tt.setStorage 等读写本地缓存的能力,数据存储在本地,返回的会比网络请求快。如果开发者基于某些原因无法采用数据预拉取,我们推荐优先从缓存中获取数据来渲染视图,等待网络请求返回后进行更新。
    • API数据缓存:对调用频次高的方法的结果进行缓存,例如对于 tt.getystemlnfo,tt.getSystemlnfoSync 的结果应进行缓存,避免重复调用。
  • 图片优化

    • 选择合适的图片格式:对于不需要透明格式的图片,推荐采用 jpeg 格式来代替 png 格式。如果有条件,尽可能使用 webp 格式图片,能大幅缩小图片体积。
    • 进行合理的压缩:图片尽可能压缩到 200kb 以下,压缩的同时也需要兼顾图片的质量。
    • 使用 CDN 并开启缓存:使用 CDN,能大幅减少图片资源的下载速度。开启 HTTP 缓存控制后,下一次加载同样的图片,会直接从缓存读取,大大提升图片加载速度。
  • 更多优化手段

    • 框架骨架屏:通过小程序框架提供骨架屏机制,能比业务中创建的骨架屏加载时机更靠前,使用这一机制,可以减少用户的白屏等待时长,给用户带来更好的体验。
    • 占位组件:为自定义组件配置占位组件,可以指定该组件不在小程序启动时立即注入,而是等到页面中其他元素渲染完成后才注入。通过占位组件,能减少启动耗时。

运行时性能体验

  • 合理使用 setData

    • 减少发送频率
    • 动画不使用 setData
  • 合理使用自定义组件

    • 合理的拆分组件数量:Page 中的 setData 会触发染层以页面级别进行 diff 操作,如果页面比较复杂且没有使用自定义组件,那么 diff 的成本会很高,导致体验比较差(更新卡顿、不粘手等感受)。如果页面转换为若干个组件,如果在组件中 setData,只会触发渲染层对应组件的 diff 操作,diff 成本会降低很多,使用体验也会提升很多。
    • 只注册当前使用的组件:同时在 usingComponents 建议只注册当前页面有使用到的自定义组件,在小程序框架会根据 usingComponents 中的自定义组件注测(无论开发者在运行时是否有使用)。
    • 同步修改初始 data:在 app.json 中增加配置 component2: true 后,支持在 created 生命周期中修改自定义组件初始数据,自定义组件将在 created 生命周期执行完成后开始渲染。开启后有以下优化点:1.能够很好的解决依赖计算逻辑导致的 data 频繁变更。避免初始数据依赖大量计算逻辑时,由于 data 变化导致页面渲染内容闪动或频繁变动问题;2.优化 observer 触发方式,减少因数据变更导致的通信,提升小程序性能体验。合理的使用方式能进一步提升用户体验。
  • 合理监听处理事件

    • 合理监听处理 scroll 事件:避免在 scroll 事件中高频执行耗时操作,会明显降低 FPS
    • 去掉不必要的事件绑定:去掉不必要的事件绑定 (ttml 中的 bind 和 catch) ,从而减少通信的数据量和次数。
  • 内存优化

    • 及时解绑事件监听:事件监听结束后,应及时解绑监听器
    • 及时清理定时器:开发者在开发如【秒杀倒计时】等功能时,可能会使用 setlnterval 设置定时器,页面或组件销毁前,需要调用 clearinterval 方法取消定时器。
  • 导航栏适配

    • 适当开启自定义导航栏:避免在 app.json 中全局开启动态导航栏,仅在需要的页面中配置,降低适配成本。
    • 关键信息避开状态栏和胶囊按钮:如果开启自定义导航栏,需要通过。tt.getCustomButonBoundingClientRect 获取自定义导航栏下不可改变的元素来进行导航栏的适配。
  • X 分屏适配

    • 通过 onResize 监听显示区域变化:小程序支持组件和页面的生命周期函数 onResize 用于在显示区域的尺寸发生变化的时候返回当前页面的信息。其中组件需要作为页面配置到 app.json 中触发事件。
    • 不使用 JS 设置 ScrollView 高度:scroll-view 中分屏变为全屏时需要重新设置 scrol-view 高度,可能会出现空白区域问题,建议使用 CSS (vh) 完成自适应布局。

性能评分工具

可以使用小程序开发平台调试器中的 Audits 和 Trace 工具对小程序进行评分和性能分析,对症下药优化小程序。

小程序性能评分工具1.png

小程序性能评分工具2.png

总结与展望

随着端越来越多,跨端的需求会越来越强烈,跨端技术方案也会不断迭代,核心目标仍然是 [write once,run anywhere] ,未来自渲染可能会出现更多机会。