Obi Madu 的博客
返回所有文章
MobileSystem DesignInfrastructure

移动应用如何工作

构建移动应用的三种方式,每一种底下实际跑的是什么,以及 React Native、Flutter 和 Tauri Mobile 背后共享的架构模式。

移动应用如何工作

当你开始研究移动开发时,框架和方法的数量很快就让人迷惑。你听到原生应用、React Native、Flutter、Expo、Capacitor、Cordova、Ionic、Tauri Mobile,不太清楚它们到底有什么不同。它们是不是用不同方式做同一件事,还是其中一些在根子上就是不同的架构?

答案是两者都是。构建移动应用主要有三种方式,你听到的大多数框架都属于其中一种。区别在于:什么渲染 UI、什么运行逻辑、两者之间如何对话。

三种方法

这是整体格局:

方法UI 渲染用逻辑运行在例子
原生平台 UI 组件平台语言Swift/Kotlin 应用
Web 包装WebView(浏览器)JavaScriptCapacitor、Cordova、Ionic
跨平台各异(原生、画布或 WebView)JS、Dart 或 RustReact Native、Flutter、Tauri

首先要注意的是,"跨平台"不是一种方法。它是一个类别,里面每个框架的 UI 渲染策略都不一样。大部分迷惑就是从这来的。人们听到"跨平台"就以为它指的是一种东西。不是。

一个个来看。

原生应用

这是基线。你用 Swift 或 Objective-C 写 iOS 应用,用 UIKit 或 SwiftUI。你用 Kotlin 或 Java 写 Android 应用,用 Android Views 或 Jetpack Compose。代码编译成平台的原生二进制,UI 是平台自己的 UI 组件,逻辑直接跑在平台运行时上。

iOS:    Swift/SwiftUI  →  编译  →  UIKit/SwiftUI 渲染到屏幕
Android: Kotlin/Compose →  编译  →  Android Views/Compose 渲染到屏幕

好处是能完整访问平台提供的一切。每个 API、每个 UI 组件、每个平台特有的功能、每个性能优化。没有桥接,没有翻译层,没有抽象。你写的代码就是跑的代码。

代价是你要写两遍应用。一个 iOS 应用和一个 Android 应用是两份完全独立的代码库,用两种不同的语言,两套不同的 UI 框架,两个不同的构建系统,两个不同的部署目标。你要一个功能在两个平台上都有,就要构建两次。

另外两种方法就是从这儿来的。Web 包装和跨平台这两个类别都存在,因为所有东西都写两遍代价太高。

Web 包装

Web 包装方法把一个 Web 应用放进一个原生外壳里。Capacitor(来自 Ionic)和 Cordova 是这里的主要框架。

HTML/CSS/JS  →  WebView  →  原生外壳  →  OS

UI 是一个 WebView。那是一个浏览器引擎,跟你在应用里打开链接时系统用的同一个。在 iOS 上是 WKWebView,在 Android 上是 Android WebView。你的 JavaScript 跑在那个 WebView 里,就像它跑在一个浏览器标签页里一样。外层的原生外壳提供一个桥接,让你的 JavaScript 能调用平台 API、访问摄像头、读文件系统、显示原生通知等等。

如果你读过 Tauri 介绍,这个结构应该看起来眼熟。Tauri 在桌面上用同样的模式:原生窗口里的 Web UI,带一个通往 OS 的桥接。区别在后端。Capacitor 和 Cordova 桥接到平台的原生语言(Swift 或 Kotlin),每个平台分别写。Tauri 桥接到 Rust,为每个移动目标原生编译。

Web 包装的吸引力很明显。如果你已经有了一个 Web 应用,你可以把它作为移动应用发布,不用重写 UI。在浏览器里跑的同一份 HTML、CSS 和 JavaScript 在 WebView 里跑。你写一个前端,到处都能用。

代价是性能和原生感觉。WebView 是一个浏览器。它把 HTML 渲染到屏幕上,跟浏览器一样。原生 UI 组件更快、更顺滑、感觉像平台,WebView 不总能做到。滚动物理、手势处理、过渡、输入行为在 WebView 里跟在原生 UI 里都不太一样。对有些应用来说无所谓。对另一些来说,这就是全部。

跨平台框架

这里开始有意思了,因为"跨平台"对不同框架意味着不同的事。每个都选了不同的 UI 渲染策略和不同的桥接机制。

React Native

React Native 不渲染到 WebView。你的 React 组件描述的是原生 UI,React Native 为你创建真正的原生 iOS 和 Android 视图。一个 View 在 iOS 上变成 UIView,在 Android 上变成 android.view.View。没有浏览器,没有 HTML,没有 DOM。

React 组件  →  JSI 桥接  →  原生 UI 组件  →  屏幕

逻辑跑在 JavaScript 里,跑在嵌入应用内部的一个 JavaScript 引擎上。JavaScript 和原生之间的桥接叫 JSI(JavaScript 接口)。它让 JavaScript 能直接调用原生 C++ 对象,这比把所有东西序列化成 JSON 的旧桥接快。

如果你想要对 React Native 和 Expo 的完整深入分析,我写了这里。这篇帖子的简短版本是:UI 是原生的,逻辑是 JavaScript,桥接是 JSI。

Flutter

Flutter 对 UI 用了不同的方法。它不用原生 UI 组件,也不用 WebView。Flutter 有自己的渲染引擎,叫 Impeller(以前叫 Skia),直接画到画布上。屏幕上的每一个像素都是 Flutter 自己的引擎画的。

Flutter 组件  →  Dart 运行时  →  Impeller 渲染到画布  →  屏幕

逻辑跑在 Dart 里,Dart 编译成目标平台的原生 ARM 代码。Dart 和平台 API 之间的桥接叫 Platform Channels。当 Flutter 需要调用一个平台 API,比如摄像头或文件系统,它通过一个 Platform Channel 发消息给原生代码,由原生代码处理请求。

吸引力是一致性。因为 Flutter 画自己的 UI,一个 Flutter 应用在 iOS 和 Android 上外观和行为完全一样。没有"这在 iOS 上看起来像 Android 应用"的问题,因为 UI 用的不是任何一个平台的组件。代价是 Flutter 应用不会自动采用平台的外观和感觉。你想要它感觉原生,得自己构建。

Tauri Mobile

Tauri Mobile 是三者中最新的,它把 Web 包装的 UI 方法和 Rust 后端结合起来,而不是用原生语言后端。

HTML/CSS/JS  →  WebView  →  Tauri IPC  →  Rust  →  OS

UI 是 WebView,跟 Capacitor 和 Cordova 一样。区别在后端。它不是桥接到每个平台分别写的 Swift 或 Kotlin 代码,Tauri 桥接到 Rust,Rust 为每个移动目标原生编译。一份 Rust 代码库处理 iOS 和 Android 两边的原生部分。

这就是单一代码库模式有用的地方。让 Tauri 桌面应用跟 Web 前端共享代码的同一套接口和实现策略也适用于移动。你的服务接口保持不变,你在 Web 和桌面实现旁边加一个移动实现。

共享的模式

这是让整个格局在我脑子里串起来的部分。三个跨平台框架都遵循同一个架构:

   UI
    |
  桥接
    |
  引擎

UI 是展示层。桥接是通信机制。引擎是真正干活的运行时。技术变了,形状不变。

框架UI桥接引擎
React Native原生组件JSIJavaScript 引擎
FlutterImpeller 画布Platform ChannelsDart 运行时
Tauri MobileWebViewIPCRust

一旦你看到这个模式,框架之间的区别就不再令人困惑了。它们都是同一个形状的变体。React Native 选原生 UI 和 JSI。Flutter 选自己的画布和 Platform Channels。Tauri 选 WebView 和到 Rust 的 IPC。选择在于你为 UI 层接受哪些权衡,以及你想用哪个桥接机制来触达平台。

如何选择

问题不是哪个框架最好。问题是哪些权衡适合你的应用。

原生(Swift/Kotlin) 如果你需要完整的平台访问、最高的性能,或者一个在每个平台上都完全感觉原生的 UI。代价是维护两份代码库。

React Native 如果你想要由 JavaScript 驱动的原生 UI 组件,而且你已经有 React 团队或想跟 React Web 应用共享逻辑。UI 是原生的,逻辑是 JavaScript,你写一份代码库,同时针对两个平台。

Flutter 如果你想要跨平台像素级完美的一致性,而且你愿意自己构建原生外观和感觉。UI 是 Flutter 自己的渲染,逻辑是 Dart,应用在 iOS 和 Android 上默认看起来一样。

Tauri Mobile 如果你已经有 Web 前端和 Rust 后端,或者你想把 Tauri 桌面应用扩展到移动端而不换技术栈。UI 是 WebView,后端是 Rust,同一份代码库可以同时针对桌面和移动。

Web 包装(Capacitor/Cordova) 如果你已经有 Web 应用,想以最小改动发布到移动端,而且你能接受 WebView 在性能和原生感上的权衡。

更大的图景

大多数跨平台框架,以及大多数现代应用架构总体上,都遵循 UI、桥接、引擎模式。UI 是用户看到的。桥接是 UI 跟引擎对话的方式。引擎是干活的东西。不管你在构建移动应用、桌面应用还是 Web 应用,这个形状都会出现。

技术变了。架构不变。一旦你能看到这个形状,你就能通过问三个问题来评估任何新框架:什么渲染 UI、什么运行逻辑、它们如何通信。其他都是细节。