跨平台桌面架构:平台抽象层(QPA / QNativeInterface)、原生集成与发布矩阵

写一次代码,跑在 Windows / macOS / Linux 上——这件事在 Web 里由浏览器无偿替你完成,在桌面客户端里没有人替你完成。今天讲清楚:抽象层应该切在哪一层、原生能力(托盘/通知/深色模式/HiDPI)怎么接、以及为什么「发布矩阵」必须在写第一行代码前就定下来。

01

今日主题:为什么跨平台是桌面架构的第一个硬约束

在 Web 里,你几乎不需要为「跨平台」付费:浏览器的渲染引擎抽象掉了窗口系统、字体、输入法、DPI,你只面对一组标准接口(DOM / CSS / Web API),平台差异被收敛成几个媒体查询和 UA 判断。转到 C++/Qt 桌面客户端后,这层「免费的抽象」消失了——Qt 提供了大量,但不是全部,剩下的部分会以最粗暴的方式暴露给你:用户报「macOS 上快捷键不对」「Windows 上中文乱码」「4K 屏上字太小」「Linux 装完起不来」。

更关键的是,跨平台不是一项「功能」,而是一条贯穿全工程的约束:它决定你的模块边界(哪些代码允许碰原生 API)、构建系统(CMake 里按平台筛源文件)、CI(需要几台构建机)、证书与发布流程(签名、公证、安装包)、甚至是招聘与排期。这件事最贵的地方在于它是「架构级决策」——可以后期补日志系统、后期换构建系统,但很难在三十万行代码写完后再去清理散落在业务逻辑里的 #ifdef Q_OS_WIN。所以它值得在你转型桌面架构的方向上,被单独拿出来当作一门功课学透。

02

核心知识:三层抽象、两种隔离策略与原生集成的正道

第一层:Qt 已经替你做完的(QPA)。Qt 的跨平台地基是 QPA(Qt Platform Abstraction):它是一个插件化的平台后端层,Windows 上是 qwindows 插件,macOS 上是 qcocoa,Linux/X11 上是 qxcb,Wayland 上是 qwayland。层次关系是 QtCore(无 GUI)→ QtGui(QGuiApplication、QWindow、QPA 接口 QPlatformIntegration)→ QtWidgets(QWidget 建立在 QWindow 的 backing store 之上)。理解这一点能解释一个常见困惑:QWidget 不是操作系统的原生控件,它由 Qt 自己绘制(真正接近"原生控件"的是 QML 的 Qt Quick Controls 的某些样式和 macOS 上被 Qt 提升为原生菜单的 QMenuBar)。这正是 Qt 能在三个平台保持像素级一致观感的原因,也是它「看起来不够 Mac 味」的根源——这是一笔明确的取舍,不是 bug。

第二层:编译期隔离 vs 运行期隔离。平台差异的治理只有两种基本手段。其一是编译期隔离:用条件编译(#ifdef Q_OS_WIN / Q_OS_MACOS / Q_OS_LINUX)或在 CMake 里按平台筛选源文件;其二是运行期隔离:定义纯虚接口 + 工厂/依赖注入,在启动时装配对应平台的实现。两条铁律:①编译期差异必须"文件级隔离"而不是"语句级散落"——同一个接口的三个实现分别放在 platform/platform_win.cpp、platform/platform_mac.mm、platform/platform_linux.cpp,由 CMake 决定编译哪一个,业务层零 #ifdef;②接口粒度按"能力"划分,不按"API"划分——你应该抽象出 showNotification()、enableAutoStart()、registerFileAssociation(),而不是 callWin32Api()。

第三层:必须碰原生时,走 QNativeInterface。Qt 6 提供了官方的逃生舱 QNativeInterface:Windows 上 QNativeInterface::QWindowsWindow::handle() 拿到 HWND,macOS 上从 QCocoaWindow 拿到 NSView*/NSWindow*,X11 上拿到 xcb_connection_t*。这是官方认可的方式,比过去用 QWindow::winId() 强转(reinterpret_cast<HWND>(winId()),不可移植且容易被 Qt 内部实现变更打碎)要安全得多。注意 macOS 的原生代码必须写成 Objective-C++(.mm 文件,CMake 需要启用 OBJCXX 语言),这本身就是一个必须在构建系统里提前处理的工程问题。

第四层:Qt 6 里必须自己处理的真实差异清单。①HiDPI:Qt 6 默认开启高 DPI 缩放(Qt 5 需要 AA_EnableHighDpiScaling),你可以用 QT_SCALE_FACTOR / QT_SCREEN_SCALE_FACTORS 覆盖,用 QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough) 避免 150% 缩放被取整成 100%/200%;尺寸一律用逻辑像素,位图资源按 @2x 约定提供,或者直接上 SVG。②深浅色:Qt 6.5 起 QStyleHints::colorScheme() 返回 Qt::ColorScheme,并提供 colorSchemeChanged 信号,你可以据此切换主题;在此之前大家都是靠读注册表或 defaults read 硬猜。③编码与路径:QString 内部是 UTF-16,Windows 文件 API 走宽字符、Unix 走 UTF-8,Qt 在 QFile/QDir 里都替你做了转换——所以业务代码里不应该出现 toLocal8Bit() 处理路径。真正会咬人的是 MSVC 源码字符集:MSVC 默认按系统区域代码页(简体中文 Windows 上是 GBK/936)解释没有 BOM 的源文件,导致 "中文" 字面量直接乱码;解法是编译选项 /utf-8(CMake 里 add_compile_options("$<$<COMPILE_LANG_AND_ID:CXX,MSVC>:/utf-8>"))或给源文件加 BOM。④QtWinExtras 已在 Qt 6 中移除:Windows 任务栏进度、跳转列表(Jump List)、缩略图工具栏这类原生集成不再有官方封装,需要你自己调用 Win32/COM——这正是"按能力抽象"要提前留好接缝的地方。

第五层:发布矩阵是架构的一部分。Windows 侧要处理 windeployqt 收集 Qt DLL、代码签名证书(没有签名+积累 SmartScreen 信誉,用户会看到"未知发布者"警告);macOS 侧是 .app bundle + Info.plist + Hardened Runtime + 公证(notarization,用 notarytool),否则 Gatekeeper 会直接拦住你的安装包;Linux 侧要面对发行版碎片化,实务上优先选 AppImage(便携)或 Flatpak(沙箱、依赖统一)。还有一条法务红线:选择 LGPLv3 版 Qt 意味着动态链接 + 提供替换 Qt 的能力(静态链接要提供可重链接的目标文件或商业授权)——这是团队 Leader 必须知道、不能靠工程师"回头再说"的事。

03

实际案例:五个真实客户端如何在同一条选择题上做出不同答案

Chromium / Chrome:自绘一切。Google 没有用任何平台原生控件,而是自建了 Views 工具包,所有按钮、菜单、滚动条都是自己画的。原因有三:一是渲染与行为完全可控(同一份代码三个平台像素级一致,安全策略和渲染管线不被系统主题干扰);二是避开各平台控件的缺陷与版本差异;三是 Chrome 本来就有一个跨平台渲染引擎可复用。代价同样清晰:无障碍(Accessibility)要逐平台对接系统 API,观感不会"跟系统长在一起",团队要长期供养一个 UI 工具包。

VSCode / Electron 系:用 Web 技术买下跨平台。VSCode、Slack、早期的 Teams 都选择 Electron——本质是把跨平台问题外包给 Chromium + Node。收益是 Web 团队的技能直接复用、迭代速度极快;代价是安装包 100MB+、常驻内存高、原生集成(通知、菜单栏、剪贴板特殊格式、输入法)要靠 Electron 的桥接层一个个补。微软更新的 Teams 2.0 从 Electron 换到 Edge WebView2,正是因为可以复用系统里已有的运行时,把体积和内存压下来——这是"跨平台运行时的成本"被真实记账的例子。

JetBrains IDE:Java 之上再叠一层平台适配。IntelliJ 系列跑在 JVM 上,用自研的 Swing/Skija 渲染管线绘制界面,但为 macOS 单独做了 native Look and Feel、原生菜单栏与快捷键映射,让"看起来像 Mac 应用"。它的架构启示是:抽象层和平台适配层可以是一对多的——一个自绘 UI 内核 + 若干薄薄的平台适配器,而不是要么全原生要么全自绘。

微信桌面版与 QQ NT:务实主义的两条路。微信 Windows 版长期以 MFC + 自绘 UI 为主,macOS 版则大量使用原生 AppKit,团队接受"两套 UI 代码"以换取各平台的原生观感与稳定性;而 QQ NT 版则整体转向 Electron + 原生混合(渲染交给 Chromium,音视频、系统能力走原生模块)。两者的选择差异恰恰说明:没有最优解,只有"你愿意为哪一项付出代价"——是维护成本、观感、包体,还是迭代速度。

Telegram Desktop:全 Qt Widgets 的现实范本。它是一个用 Qt Widgets 构建、自绘程度很高、同时支持 Windows/macOS/Linux 的大型客户端,在 macOS 上接入原生托盘与通知、适配了深浅色。对正在用 Qt6 做客户端的你来说,它是最贴近"我这条路线能走多远"的参考样本:Qt 路线完全可以做出商业级产品,但你必须在原生集成、安装包、签名分发上自己补齐功课。

04

常见错误:跨平台项目里最贵的五个坑

①语句级 #ifdef 散落在业务代码里。典型症状:一个 800 行的业务函数里出现十几处 #ifdef Q_OS_WIN,Windows 分支里还有 #else 兜底。原因是没有把"平台能力"建模成接口,工程师图省事就地打补丁。后果是三个平台的编译组合成倍增长、测试无法覆盖、新人不敢改。避免方式:平台差异只允许出现在 platform/ 目录下的实现文件里,CMake 按平台挑选源文件;Code Review 里看到业务层出现 Q_OS_* 一律要求重构。

②硬编码路径、分隔符与盘符。写死 "C:\\Users\\Public\\data" 或用 QString::split("\\") 解析路径,在 macOS/Linux 上必然炸;用 toLocal8Bit() 把路径转成 char* 再交给原生 API,在中文用户目录下会产生乱码文件名。避免方式:一律用 QDir 拼接、QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) 取可写目录(Windows 落到 %APPDATA%、macOS 落到 ~/Library/Application Support、Linux 落到 ~/.local/share),需要原生路径时用 QFile::encodeName()/nativePath();写配置用 QSaveFile 保证原子落盘。

③MSVC 下的中文乱码。症状各平台表现不一:Windows 上界面文案变成"锟斤拷"或十六进制方块,Linux/macOS 完全正常。根因是 MSVC 以系统代码页解释无 BOM 源文件,而 GCC/Clang 默认按 UTF-8。避免方式:CMake 中对 MSVC 目标统一加 /utf-8,并在 CI 里加一条"Windows 编译 + 启动截图"的冒烟检查;控制台输出乱码则单独处理(Windows 控制台默认非 UTF-8,需要设置代码页或用调试输出通道)。

④把 HiDPI 当成"高分辨率用户才遇到的问题"。真实情况是:150% 缩放会触发取整策略问题,200% 下按物理像素硬编码的控件会缩成一半,图标用 32×32 PNG 在高 DPI 下模糊,用 QPixmap 手动加载的图片没有设置 devicePixelRatio 导致被放大后虚化。避免方式:只用逻辑像素思考尺寸;图标优先 SVG 或按 @2x 提供;给自绘代码用 QPainter 前显式信任 devicePixelRatio;把"150% 缩放 + 4K 屏"写进测试用例。

⑤"在我机器上能跑"就发包,把签名/公证/依赖当收尾工作。最典型的翻车链条:Linux 上忘了打包 xcb 相关库导致装完打不开;macOS 包没公证,用户下载后被 Gatekeeper 提示"已损坏";Windows 包没签名 + 未跑 windeployqt,用户报缺 Qt6Core.dll。避免方式:把打包与冒烟测试放进 CI 的第一条流水线,在干净的虚拟机/容器里启动一次产物;签名证书、公证凭据、更新服务器凭据要有明确的 owner 与轮换计划,而不是藏在某个人笔记本的密钥串里。

05

最佳实践:五条能落进 Code Review 的规则

①平台差异只允许"文件级隔离"。约定一个 platform/ 目录,每个平台一个实现文件(.cpp / .mm),由 CMake 的 target_sources 按平台挑选。业务层只 include 接口头文件。何时用:只要有第二个真实平台就必须这样做。何时不用:项目确定永远只在 Windows 上跑——此时引入抽象层是纯粹的负担,直接写 Win32 代码更快也更省事。Trade-off:多了一层间接调用与一个接口文件,换来的是「业务代码零条件编译」这个巨大的可维护性收益。

②按"能力"抽象,不按 API 抽象。接口应该是 showNotification()、enableAutoStart(bool)、setBadgeCount(int)、registerFileAssociation()、moveWindowToMacMenuBar() 这样的业务语义;而不是 getHWND()、callObjC() 这样的技术动词。原因:能力接口天然可 mock、可在单测里替换、可以在某个平台上返回「不支持」;而 API 级抽象会把平台细节泄漏到调用点,等于没抽象。Trade-off:能力接口粒度偏粗,有时一个能力在不同平台上语义不完全一致(比如通知在 macOS 上支持操作按钮、Linux 上可能不支持),需要谨慎设计返回值(用 bool supported() 或 std::optional 显式表达"不支持"),而不是静默失败。

③能用 QNativeInterface 就别用 winId() 强转。reinterpret_cast<HWND>(window->winId()) 这类写法依赖 Qt 内部实现细节,Qt 6 官方明确推荐用 QNativeInterface 获取 HWND / NSView* / xcb_connection_t*。同时:把原生调用封装成独立的 target(如 platform_win),让 Core 层不依赖任何平台 API,这样核心逻辑可以在任意平台上编译与单测。何时不用:QML/Qt Quick 的渲染窗口有自己的原生句柄获取路径,混用 Widgets 与 Quick 时会踩到两个窗口对象不匹配的坑,务必先读官方对应章节。

④先定发布矩阵,再写第一行代码。需要明确:支持哪些 OS 版本(Windows 10 是否还要支持?macOS 最低 12 还是 13?)、哪些架构(x64 还是 arm64,需要通用二进制吗)、哪种安装形态(MSI / DMG / AppImage / Flatpak)、更新机制(Qt Installer Framework、自研增量更新,还是先手动升级)。原因:这些问题倒推着影响技术选型——例如只要 macOS arm64 通用二进制,就必须考虑第三方库的交叉编译;只要走沙箱分发,就必须提前接受文件系统访问限制。Trade-off:过早定死矩阵会限制早期灵活性,所以做法是「明确首批支持范围 + 明确不在范围内的声明」,而不是模糊地"尽量都支持"。

⑤把"跟随系统深浅色 + HiDPI 正确"当作验收项,而不是优化项。Qt 6.5 起用 QStyleHints::colorScheme() 与 colorSchemeChanged 跟随系统;尺寸一律逻辑像素,位图按 @2x 提供或用矢量图。何时不用:如果你的产品有强品牌视觉规范、明确要求"不受系统主题影响",那就不跟随,但要显式提供手动主题切换并在文档里说明——最糟的是"半跟随",即部分控件跟随、自绘控件不跟随,用户在深色模式下看到一片刺眼的白色面板。

06

与 Web 技术的联系:你其实已经做过一轮跨平台架构

浏览器就是终极的跨平台抽象层。你写 addEventListener('click') 时,Chrome 在 macOS 上把它映射成 Cocoa 的鼠标事件,在 Windows 上映射成 Win32 消息,在 Linux 上映射成 X11/Wayland 事件——这层"标准接口 + 各平台实现"的映射,与 Qt 的 QPA 是同一个设计模式:QPlatformIntegration 之于 Qt,就是 Blink 的平台层之于 Chrome。差别在于 Web 把这条边界做成了公开标准(W3C/WHATWG),所有厂商被动对齐;Qt 把它做成了可替换插件,你被动接受它的取舍。

条件编译 ≈ 打包器的平台切换。你在 webpack/vite 里用 resolve.alias 或 process.env.PLATFORM 按构建目标切换实现文件,本质上就是 CMake 里 if(WIN32) target_sources(...);而 React Native 的 Foo.ios.js / Foo.android.js 文件后缀约定,正是我上面说的"文件级隔离"——RN 团队用工程规范强制了这件事,而 C++ 世界往往靠架构师的口头和 Code Review 来强制,所以更容易腐化。

DPI 是同一个概念。window.devicePixelRatio 与 QWindow::devicePixelRatio() 完全对应;CSS 的逻辑像素与 Qt 的逻辑像素一样,都会在 200% 缩放下变成 2 个物理像素。你在 Web 里已经习惯的"用 rem 而不是物理像素思考尺寸""给图片提供 2x 资源",迁移到 Qt 就是"只用逻辑像素 + @2x 资源或 SVG",不需要重新学,只需要别丢掉。prefers-color-scheme 媒体查询对应 QStyleHints::colorScheme();Node 的 os.platform() / path.join() 对应 Q_OS_* 宏与 QDir;Node 原生模块(.node 二进制、node-gyp)跨平台编译的痛苦,本质上就是 C++ 世界 ABI 与工具链的痛苦——你当年为了让一个原生模块在 CI 上编过而花的那些时间,就是你未来会为 Qt 依赖交叉编译花的时间。

最后一条认知迁移:Electron 用"我帮你把浏览器打包进来"换走了跨平台问题,代价是体积与原生集成;Qt 用"我帮你把窗口系统抽象掉"换走了大半跨平台问题,代价是原生观感与部分原生能力要自己补。两条路的边界形状是一样的——被抽象掉的,都是"窗口系统 + 输入 + 渲染";没被抽象掉的,都是"系统集成 + 分发 + 签名"。认清这条分界线,你就知道哪些工作永远躲不掉。

07

管理者视角:把跨平台当作排期与验收的一部分

作为 Team Leader,跨平台最容易出问题的地方不在代码,而在责任与验收。第一,把"平台"变成有主的资产:每个平台指定一个 owner(哪怕是兼职),负责该平台的构建机、依赖、签名与冒烟测试;否则三个平台都是"大家的",实际就是没人的。第二,把构建机当作项目基础设施而不是个人机器:CI 至少要有 Windows / macOS / Linux 三个 runner,"我本地能编"永远不能作为验收依据。第三,Code Review 的检查点要非常具体:业务代码里出现 #ifdef Q_OS_* 一律质疑;新增第三方依赖要问"它在三个平台上有预编译包吗,没有的话我们能不能自己编";新增原生调用要问"它有没有落在平台抽象层里,其他平台怎么降级"。第四,排期上要显式留出"签名、公证、安装包、更新链路"的时间——这部分在过往项目里最常被默认为零成本,却是最容易阻塞发布的部分。最后,把"释放一个三平台可安装产物"定义成一条能跑的流水线,而不是一份文档里的 checklist;能自动化的证据,比任何承诺都可靠。

08

延伸阅读

①Qt 官方文档《Qt Platform Abstraction》与 qpa/ 源码目录——理解 qwindows/qcocoa/qxcb 插件的职责边界,是看懂 Qt 跨平台机制的入口。②Qt 6 文档中的《Native Interfaces》/ QNativeInterface 章节——官方推荐的原生句柄获取方式,读完可以直接替换掉 winId() 强转这类历史包袱。③Qt 官方《High DPI》文档——QT_SCALE_FACTOR、HighDpiScaleFactorRoundingPolicy、@2x 资源约定的权威说明,HiDPI 问题一次读完。④Chromium 文档《UI Development》/ Views 相关章节——Google 为什么坚持自绘控件,是理解"原生控件 vs 自绘"取舍的最佳一手材料。⑤Apple notarytool 与 Microsoft Azure Trusted Signing 的官方文档——发布签名与公证的实操入口;另外推荐翻一翻 Telegram Desktop(tdesktop) 源码仓库,它是一个用 Qt Widgets 做到商业级多平台客户端的现实样本,工程结构非常有参考价值。

09

今日思考题

Q1. 如果你的客户端 80% 用户只在 Windows 上,剩下 20% 分散在 macOS/Linux,"平台抽象层"应该先建还是后建?请给出一个可被验证的判断依据,而不是"看情况"。

Q2. 把通知抽象成 notify(title, body) 之后,Linux 上没有通知服务(D-Bus 不可用)时这个接口应该怎么表现才算"不撒谎"?返回 bool、抛异常、还是提供 capabilities() 查询?各自的调用点负担是什么?

Q3. QPA 选择运行期插件而非编译期三套实现,额外付出了什么代价(启动开销、部署体积、调试复杂度)?换来了什么?如果让你为自家产品设计,你会选哪条?

Q4. 团队里有人提出"用 Electron 三个月就能上线,Qt 要六个月"。作为架构负责人,你的评估清单里必须包含哪三项量化对比,才能让这次讨论不变成信仰之争?

Q5. 若最终决定静态链接 Qt,LGPLv3 的合规要求(可重链接、声明、源码提供)会怎样改变你的发布流程和法务参与时机?提前多长时间介入才不算晚?

10

今日实践任务(约 45 分钟):搭一个最小平台抽象层

目标:用 CMake 条件编译 + 文件级隔离,让 main.cpp 里不出现任何平台宏,仍能输出当前平台名、可写数据目录,并调用一个平台相关的通知能力。请在同一个 CMake 工程里额外放一个业务文件,故意写一处 #ifdef Q_OS_WIN,然后重构掉它,体会两种写法的差别。

// iplatform_host.h —— 业务层唯一可见的头文件,零平台细节
#pragma once
#include <QString>
#include <memory>

class IPlatformHost {
public:
    virtual ~IPlatformHost() = default;
    virtual QString platformName() const = 0;
    virtual bool notify(const QString &title, const QString &body) = 0; // 不支持时返回 false
};
std::unique_ptr<IPlatformHost> makePlatformHost(); // 由平台实现文件提供

// main.cpp —— 注意:这里没有任何 Q_OS_* 宏
#include <QCoreApplication>
#include <QStandardPaths>
#include <QDebug>
#include "iplatform_host.h"

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);
    QCoreApplication::setApplicationName("platdemo");
    auto host = makePlatformHost();
    qInfo() << "platform:" << host->platformName();
    qInfo() << "data dir:" << QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    qInfo() << "notify ok:" << host->notify(QStringLiteral("启动完成"), QStringLiteral("平台抽象层工作正常"));
    return 0;
}

// platform/platform_win.cpp —— 与 platform_linux.cpp 结构完全对称
#include "iplatform_host.h"
#include <QDebug>

class WindowsHost final : public IPlatformHost {
public:
    QString platformName() const override { return QStringLiteral("Windows"); }
    bool notify(const QString &title, const QString &body) override {
        // 真实项目:此处通过 QNativeInterface 取 HWND 后调用 Win32/COM 通知接口
        qInfo() << "[win-notify]" << title << body;
        return true;
    }
};
std::unique_ptr<IPlatformHost> makePlatformHost() { return std::make_unique<WindowsHost>(); }
# CMakeLists.txt —— 抽象层由构建系统接线,源码里不出现平台分支
cmake_minimum_required(VERSION 3.21)
project(platdemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core)
qt_standard_project_setup()

add_executable(platdemo main.cpp iplatform_host.h)
if(WIN32)
  target_sources(platdemo PRIVATE platform/platform_win.cpp)
elseif(APPLE)
  enable_language(OBJCXX) # .mm 原生文件需要显式启用
  target_sources(platdemo PRIVATE platform/platform_mac.mm)
else()
  target_sources(platdemo PRIVATE platform/platform_linux.cpp)
endif()
target_link_libraries(platdemo PRIVATE Qt6::Core)
if(MSVC)
  target_compile_options(platdemo PRIVATE /utf-8) # 防中文乱码,务必加上
endif()

进阶挑战(选做):①在 platform_linux.cpp 里检查 QDBusConnection::sessionBus().isConnected(),不可用时让 notify() 返回 false,并让调用方打印一条明确的降级日志;②给接口加一个 capabilities() 方法返回位掩码,让调用方能提前知道哪些能力可用;③用 ctest 加一条测试:在任意平台上启动 platdemo 并断言退出码为 0,把它接到 CI 的三平台 runner 上——这就是"用可执行的证据代替口头承诺"。

一句话总结:跨平台的本质不是"写一套代码三处运行",而是把平台差异收进有边界的实现文件、把原生能力抽象成业务语义的能力接口、并让发布矩阵(签名、公证、安装包、更新)从第一周起就成为工程的一部分——Web 里浏览器替你做完的那三件事,在桌面客户端里必须由你的架构亲手做完。