一开始我以为在 OpenHarmony 平板上让 React Native 页面支持横竖屏切换,也就是写几行样式、监听一下方向变化的事。毕竟 React Native 在 Android 和 iOS 上的横竖屏适配已经很成熟了,社区里要什么现成方案都有。但真正在 OpenHarmony 设备上跑起来之后,我发现这套经验一半都派不上用场——容器线程怎么重建、布局引擎怎么把宽高传递给 JS 层、设备树选错导致屏幕方向事件压根不触发,这些都是 Android 上根本没见过的坑。
这篇内容写给正在做 React Native for OpenHarmony 适配、或者准备把现有 RN 应用迁移到鸿蒙设备上的同学。我会从渲染链路讲起,然后依次拆解监听方案、布局适配、状态保持,最后把 RK3568 设备树选型和白屏排查这些实战中一定会遇到的坑讲清楚。文章里的代码都是我在真实项目里验证过的,你可以直接照着改。
1. 先摸清虚实:RN for OpenHarmony 的渲染链路与旋转机制
横竖屏切换看似是前端问题,但在 OpenHarmony 上更像一个"宿主环境"问题。不搞明白 RN 在鸿蒙上是怎么把 JS 页面画出来的,后面遇到方向变化时的闪烁、错位、甚至直接白屏,你连排查方向都没有。
1.1 RN 三驾马车:JS 引擎、C++ 桥接与 ArkUI 容器
React Native for OpenHarmony 的架构延续了 RN 经典的"三段式"设计:
- JS 层:你的业务代码、React 组件树、状态管理都跑在 JS 引擎里。OpenHarmony 社区用的是自研的 ArkTS 运行时来承载 JS 逻辑,虽然和 V8/Hermes 不同,但对 RN 应用来说,JS 层面的 API 基本是兼容的。
- C++ 桥接层:负责 JS 层与原生层之间的通信。横竖屏切换时,屏幕宽高的变化就通过这一层从原生传递到 JS 层。
- UI 层:RN 在 OpenHarmony 上最终通过 ArkUI 的
XComponent或CustomComponent来承载渲染内容。ArkUI 的布局引擎负责把 RN 计算出来的布局结果绘制到屏幕上。
方向旋转对这套架构的影响是连锁的:系统屏幕方向变化 → 应用窗口尺寸改变 → ArkUI 容器重新布局 → 宽高数据通过桥接层传递给 JS → Dimensions 触发事件 → React 组件重新渲染。
1.2 为什么 OpenHarmony 上的旋转比 Android 更"伤筋动骨"
Android 上,即使屏幕旋转,Activity 帮你扛住了大部分生命周期问题,RN 的视图树可以原地刷新。但 OpenHarmony 的 UIAbility 生命周期模型不一样,屏幕方向变化在某些场景下会触发 UIAbility 的配置更新,如果 manifest 里没有处理好 orientation 相关配置,整个 RN 实例可能会被重建。
我在项目里遇到的情况是:旋转屏幕后,页面重新加载,JS bundle 重新执行,之前通过 Redux 存在内存里的数据全丢了,页面回到初始状态。这不是代码 bug,是 UIAbility 重新走了一遍 onCreate。
所以在动手写横竖屏适配代码之前,先确认两件事:
- 模块配置文件
module.json5里,UIAbility 的orientation属性是否声明了可旋转的取值(如"auto_rotation"或"landscape")。 - 在
abilities配置中是否设置了"orientationChanged"相关的配置项,让系统知道这个 UIAbility 允许旋转重建。
如果这两步没做对,后面 JS 层写再多判断代码都是白搭,因为根本收不到方向变更的事件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 监听方向变化的四条技术路径与应用取舍
在 OpenHarmony 上监听横竖屏切换,有四种主要方式,但每一种的适用场景和坑都不太一样。我把它们全部列出来,方便你对号入座。
2.1 最优先选择:useWindowDimensions Hook
useWindowDimensions 是 RN 默认提供的 Hook,它会在窗口尺寸变化时自动触发组件渲染。在 OpenHarmony 上,这个 Hook 由 Dimensions 模块驱动,旋转屏幕后,宽高数据更新,Hook 自动返回新值。
typescript复制import { useWindowDimensions } from 'react-native';
function useOrientation() {
const { width, height } = useWindowDimensions();
return width > height ? 'LANDSCAPE' : 'PORTRAIT';
}
function GamePage() {
const orientation = useOrientation();
return (
<View style={{ flex: 1, backgroundColor: '#141414' }}>
{orientation === 'LANDSCAPE' ? (
<GameBoardHorizontal />
) : (
<GameBoardVertical />
)}
</View>
);
}
这个方案最省心,因为它是"声明式"的:你告诉 React 当前是横屏还是竖屏,React 自动帮你切换视图。但我实测发现一个细节:useWindowDimensions 在旋转过程中可能连续触发多次渲染(表现为宽高中间的过渡态),如果页面里有重组件,会明显卡顿。
优化办法是引入防抖,或者在宽高变化较大时才认为是真正旋转了:
typescript复制import { useEffect, useState } from 'react';
import { useWindowDimensions } from 'react-native';
function useOrientationWithThreshold() {
const { width, height } = useWindowDimensions();
const [orientation, setOrientation] = useState<'PORTRAIT' | 'LANDSCAPE'>(
width > height ? 'LANDSCAPE' : 'PORTRAIT'
);
useEffect(() => {
const isLandscape = width > height;
// 用 50 的阈值过滤旋转过程中的中间态
const threshold = Math.abs(width - height) > 50;
if (threshold) {
setOrientation(isLandscape ? 'LANDSCAPE' : 'PORTRAIT');
}
}, [width, height]);
return orientation;
}
2.2 手动订阅 Dimensions 的 change 事件
如果你的页面用 useMemo 做了大量缓存,或者你想手动控制渲染时机,可以直接订阅事件:
typescript复制import { useEffect, useState } from 'react';
import { Dimensions } from 'react-native';
function useManualOrientation() {
const [orientation, setOrientation] = useState<'PORTRAIT' | 'LANDSCAPE'>();
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => {
const next = window.width > window.height ? 'LANDSCAPE' : 'PORTRAIT';
setOrientation(next);
});
return () => subscription.remove();
}, []);
return orientation;
}
注意:Dimensions.addEventListener 在 OpenHarmony 上的行为与 Android 略有差异,某些旧版本可能触发重复注册问题。我在项目里用了一个全局单例来确保只有一个监听器:
typescript复制class OrientationListener {
private static instance: OrientationListener;
private subscribers = new Set<(orientation: string) => void>();
private subscription: any;
static getInstance() {
if (!OrientationListener.instance) {
OrientationListener.instance = new OrientationListener();
}
return OrientationListener.instance;
}
subscribe(callback: (orientation: string) => void) {
this.subscribers.add(callback);
if (!this.subscription) {
this.subscription = Dimensions.addEventListener('change', ({ window }) => {
const orientation = window.width > window.height ? 'LANDSCAPE' : 'PORTRAIT';
this.subscribers.forEach((cb) => cb(orientation));
});
}
return () => {
this.subscribers.delete(callback);
};
}
}
2.3 用 onLayout 兜底
有一种特殊情况:OpenHarmony 的某些系统弹窗(如输入法弹出、分屏拖拽),不会触发 Dimensions 的 change 事件,但会改变容器组件的实际宽高。如果你只依赖全局的 Dimensions,可能会出现 UI 错位。
我的做法是在根 View 上挂一个 onLayout 作为兜底,保证无论什么原因导致布局变化,React 侧都能感知到:
jsx复制function App() {
const [layout, setLayout] = useState({ width: 0, height: 0 });
return (
<View
style={{ flex: 1 }}
onLayout={({ nativeEvent }) => {
const { width, height } = nativeEvent.layout;
setLayout({ width, height });
}}
>
<MainContent width={layout.width} height={layout.height} />
</View>
);
}
2.4 四套方案的适用场景对比
| 监听方式 | 适用场景 | 优点 | 潜在坑 |
|---|---|---|---|
| useWindowDimensions | 大多数页面组件 | 声明式、自动联动 | 旋转过程中多次渲染 |
| Dimensions 的 change 事件 | 需要手动控制渲染时机 | 灵活、可集中管理 | 需手动清理、版本差异 |
| onLayout | 分屏、输入法弹出等复杂场景 | 最底层、最可靠 | 触发频繁、需处理性能 |
| 原生监听 orientation 事件 | 需要获取系统角度值 | 最精准 | 需要写原生桥接代码 |
除非你的页面有特别复杂的场景(比如视频播放器旋转、3D 地图渲染),否则我建议优先用 useWindowDimensions,配合 onLayout 兜底,两者组合基本能覆盖 OpenHarmony 上 95% 的情况。
3. 布局适配:动态宽高、安全区与键盘避让
横竖屏切换最终要落实到视觉上:同一套内容,在横竖屏下该以什么方式展示。这比监听方向更考验实现细节,因为一不小心就会全军覆没。
3.1 Flexbox 布局的局限性:宽高频繁变化时的性能处理
RN 的布局核心是 Yoga 引擎,它会把 JS 侧的 Flexbox 布局转换成 ArkUI 的原生布局指令。横竖屏切换时,Yoga 需要重新计算整个组件树的布局。如果页面层级深、组件多,这个计算过程会导致明显的视觉卡顿。
我在一个列表页里测过:页面有 200 多个子组件的时候,旋转屏幕的布局计算耗时大概是 120ms,肉眼可见的卡。优化方案有两个:
方案一:层级扁平化。 减少不必要的嵌套 View。比如列表项用单一容器,避免每层都用自定义组件包裹。
方案二:关键路径拆分。 旋转时只重新渲染变化的区域,用 React.memo 包裹不需要随方向变化的子组件:
tsx复制const MemoizedHeader = React.memo(function Header() {
return <HeaderContent />;
});
function ListPage() {
const orientation = useOrientation();
return (
<View style={{ flex: 1 }}>
<MemoizedHeader />
{orientation === 'LANDSCAPE' ? (
<ListHorizontal />
) : (
<ListVertical />
)}
</View>
);
}
特别注意:用
React.memo时确保子组件不依赖方向变化的 props,否则 memo 就没意义了。
3.2 安全区与状态栏高度:OpenHarmony 设备上的适配细节
OpenHarmony 设备形态复杂,从手机到平板再到 IPC(工业计算设备),每一类屏幕的安全区逻辑都不一样。在横竖屏切换时,状态栏高度、底部导航栏位置都会变化。Android 上有 SafeAreaView 和 StatusBar.currentHeight,OpenHarmony 上这些 API 的取值逻辑略有不同。
实测结果是:SafeAreaView 在 OpenHarmony 上基本可用,但底部安全区计算不如 Android 准确,尤其是平板设备开启了手势导航后。我建议改用显式计算 padding:
typescript复制import { StatusBar, Platform, Dimensions } from 'react-native';
function getStatusBarHeight() {
if (Platform.OS === 'harmony') {
// OpenHarmony 上通过各能力接口获取状态栏高度,
// 向下取整避免旋转动画过程中的高度异常
return Math.round(StatusBar.currentHeight || 24);
}
return StatusBar.currentHeight;
}
竖屏时状态栏高度可能是 24,横屏时变成 0(大多数设备横屏会隐藏状态栏)。如果你在横屏时依然给顶部留了竖屏的 padding,页面就会出现一段空白。所以安全区的计算要放在方向判断函数里,而不是全局一次性完成。
3.3 键盘避让与屏幕方向联动:RN 默认行为的补充
横竖屏切换时,如果页面里有输入框,键盘的避让逻辑特别容易出问题。RN 的 KeyboardAvoidingView 在 Android 上依赖 adjustResize 窗口模式,在 OpenHarmony 上这个机制的实现方式不同,实测经常出现键盘把输入框完全遮住,或者键盘关闭后布局不回弹的问题。
我的处理方式是:
- 在 UIAbility 里配置窗口避免模式,让窗口尺寸随键盘弹出而调整(类似 Android 的
adjustResize)。 - JS 侧用
Keyboard模块监听键盘状态,在键盘弹起时手动调整关键元素的位置。
typescript复制import { useEffect, useState } from 'react';
import {
Keyboard,
KeyboardAvoidingView,
Platform,
View,
TextInput,
} from 'react-native';
function ChatInput() {
const [keyboardHeight, setKeyboardHeight] = useState(0);
const isHarmony = Platform.OS === 'harmony';
useEffect(() => {
if (!isHarmony) return;
const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
setKeyboardHeight(e.endCoordinates.height);
});
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardHeight(0);
});
return () => {
showSub.remove();
hideSub.remove();
};
}, [isHarmony]);
if (isHarmony) {
// 在 OpenHarmony 上用手动调整 padding 代替 KeyboardAvoidingView
return (
<View style={{ paddingBottom: keyboardHeight }}>
<TextInput style={{ height: 44, backgroundColor: '#fff' }} />
</View>
);
}
return (
<KeyboardAvoidingView behavior="padding">
<TextInput style={{ height: 44, backgroundColor: '#fff' }} />
</KeyboardAvoidingView>
);
}
这个方法在 OpenHarmony 的 API 9 和 API 10 上验证过都能正常工作,但要注意 keyboardDidShow 的事件名称,某些系统版本上可能不触发,需要改用 keyboardWillShow。
4. 组件重建与状态保持:如何避免旋转后数据丢失
横竖屏切换不仅是布局问题,还涉及页面生命周期。这个话题是很多 RN 开发者拿捏不准的地方:到底旋转时组件要不要重建?状态怎么保留?我直接把 OpenHarmony 上的行为逻辑拆给你看。
4.1 UIAbility 重建场景下的 JS 上下文恢复策略
前面提过,OpenHarmony 的 UIAbility 在屏幕方向变化时可能重建(具体取决于模块配置)。UIAbility 重建意味着 RN 环境重新初始化,JS 全局变量、内存缓存、Redux store 全部清空。用户旋转一下屏幕,应用就回到了首页,这体验没法接受。
我的应对方案是把关键状态持久化到本地存储,方向变更后恢复:
typescript复制import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = 'app_orientation_state';
async function saveOrientationState(orientation: string, pageName: string) {
try {
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify({ orientation, pageName, timestamp: Date.now() })
);
} catch (error) {
// AsyncStorage 写入失败不要阻断业务
console.warn('saveOrientationState error:', error);
}
}
async function restoreOrientationState() {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw);
}
每次方向变化时把当前页面信息和关键参数写进去,重建后读取恢复。注意恢复逻辑要放在路由注册之前执行,否则页面会先跳到初始路由,再跳回目标页,多一次无意义的渲染。
4.2 避免组件重复挂载的机制
OpenHarmony 上 RN 组件树在旋转时有时会整树卸载再挂载,表现是页面闪烁或进入 loading 状态。要避免这个问题,可以通过捕获模块 module.json5 中的 orientation 配置,让系统非重建模式对齐:
常见配置:
json5复制{
"module": {
"abilities": [
{
"name": "EntryAbility",
"orientation": "auto_rotation",
"supportWindowMode": ["fullscreen", "split", "floating"]
}
]
}
}
auto_rotation 表示跟随用户开启的自动旋转开关。对于不需要重建的页面,使用 "orientation": "unspecified" 配合动态窗口调整,让 UIAbility 保持存活,只更新窗口尺寸,这样组件树就不会被卸载。
但要注意:
unspecified并不是所有机型都能原生处理,实测部分 OpenHarmony 开发板依然会回调重建流程,所以不要指望配置一改就万事大吉。稳妥打法是把状态持久化逻辑做好,而不是单纯依赖原生配置。
4.3 网络请求、定时器与动画的方向切换处理
方向切换时如果页面上有正在进行的网络请求或定时器,一旦组件被卸载,这些异步任务就会变成"野指针",回调时触发 setState 警告甚至直接崩溃。我在项目里统一封装了一个 useAsyncTask Hook 来管理这些任务:
typescript复制import { useEffect, useRef } from 'react';
function useAsyncTask(task: () => Promise<void>, deps: any[]) {
const mounted = useRef(true);
const taskRef = useRef(task);
taskRef.current = task;
useEffect(() => {
mounted.current = true;
taskRef.current().then(
() => {
if (!mounted.current) return;
// 正常完成后的 UI 更新放在这里
},
(err) => {
if (!mounted.current) {
console.warn('组件已卸载,忽略异步错误:', err);
return;
}
// 错误处理
}
);
return () => {
mounted.current = false;
};
}, deps);
}
方向切换导致组件卸载时,mounted.current 变为 false,异步回调就不再触发 UI 更新。定时器同理,在卸载逻辑里统一 clearTimeout 和 cancelAnimationFrame。
5. 实测中必须拿下的三座大山:设备树选型、白屏与旋转闪动
最后这部分不是写业务代码,但踩过的人都知道,这三件事如果没处理好,前面的所有努力都会被埋葬。尤其是 RK3568 的设备树选型,网上资料少,找错文件就是几个小时的时间成本。
5.1 RK3568 系列开发板的设备树选型:别一上来就乱选
OpenHarmony 的热门硬件平台中,RK3568 使用率很高。DAYU200、润和 HiHope、软通动力等多款开发板都用这颗 SoC。但很多人在适配 RN 时会忽略一个前置问题:当前系统的设备树文件选没选对。
RK3568 家族包含 RK3568J、RK3568B2、RK3566 等不同型号,每个型号的引脚复用、显示接口、内存布局都有细微差异。系统编译时如果加载了错误的设备树,轻则触摸屏驱动不工作,重则系统根本起不来,更谈不上测试横竖屏切换。
我的选型经验分成三步:
- 先看板卡背面的丝印或官方说明,确认具体芯片型号。DAYU200 用的是 RK3568J,不是 RK3568 通用型号,选错直接开不了机。
- 到 kernel 目录下查看
arch/arm64/boot/dts/rockchip/目录,里面有大量rk3568-*.dts文件。不要随便选一个,找与你板卡名称完全匹配的文件,比如rk3568-dayu200-demo.dts。 - 用
dmesg或串口日志核对驱动的加载情况。刷机后如果触摸框、屏幕背光都能正常,说明设备树选对了。
在 OpenHarmony 编译框架中,不同产品形态的 config 里 .dts 引用路径也不一样。以 DAYU200 为例,在 vendor/hihope/rk3568/config.json 里会指定 "device_tree": "rk3568-dayu200",确认这一项与板卡对应。
一句话总结:设备树选型的坑不在"选哪个",而在"你根本不知道有多个选择"。多留意这个细节,能帮你省下大量定位问题的时间。
5.2 React Native 启动白屏:从加载链路逐层排查
"react native 启动白屏"是热搜常客,OpenHarmony 上也不例外。RN 启动过程中,JS Bundle 需要从本地文件(或者 Metro 开发服务器)加载并交给 JS 引擎执行,执行完毕才能渲染出第一帧。这个时间窗口内页面全是白屏,体验很差。
我在 OpenHarmony 上的排查链路如下:
- 确认 bundle 是否打进了 HAP 包。如果 bundle 用的是 assets 路径,但打包时漏配了资源目录,运行时找不到 bundle,会一直白屏。
- 确认 Metro 配置的端口和 IP。开发模式下 RN 需要通过 Metro 获取 bundle,本机调试时要保证
metro.config.js的端口与设备网络能连通。127.0.0.1在模拟器上往往不行,需要局域网 IP。 - 加载完成后的首帧回调。在入口调
AppRegistry.runApplication后,OpenHarmony 侧会有onFirstFrame回调。如果回调一直不触发,大概率是 JS 侧渲染有问题,而不是加载问题。
针对白屏,我的最终方案是两层配合:
- 原生启动页:在 UIAbility 展示阶段先显示一张与 App 主色一致的底图,避免纯白闪屏。等 RN 首帧渲染完成后再移除。
- JS 侧骨架屏:RN 加载完成后不要直接渲染具体页面,先渲染一个简单的 Loading 组件,等数据就绪再进入正式页面。
typescript复制// index.js 入口
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
function Bootstrap() {
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
// 替代:从本地读取缓存、初始化 SDK 等
setTimeout(() => setReady(true), 100);
}, []);
if (!ready) {
return <SplashScreen />;
}
return <App />;
}
AppRegistry.registerComponent(appName, () => Bootstrap);
这种方案不改变业务代码结构,也方便后续接入更多启动阶段的初始化逻辑。
5.3 旋转瞬间的页面闪烁与错位:终极定位法
最后说一个很隐蔽的问题:旋转过程中页面会"闪一下",甚至短暂出现布局错位,然后才恢复正常。这个问题的根源通常是两个:
- Dimensions 事件在 UIAbility 窗口尺寸 commit 之前就传递给 JS 层了,导致 JS 拿到的是旧值 + 新布局的错配。2. 原生容器在新旧宽高切换动画期间,RN 视图的 bounds 更新不及时。
我的定位思路是:在原生侧打点记录时间线,确认 JS 收到事件是在窗口 resize 之前还是之后。
具体做法是改造 HarmonyOS 侧的 RN 容器模块,在 onConfigurationUpdated 回调里打印当前窗口宽高,同时在 JS 侧 Dimensions 监听器里打印宽高。两边日志一比,就能看出顺序。
如果发现 JS 比原生早,就需要在原生侧延迟发送维度变更事件,通常延迟 16ms 到 32ms(一到两帧)就能解决问题。这是我在项目里用的折中方案,不建议延迟太久,否则旋转动画会显得迟滞。
另外,错位问题往往与状态栏高度有关。横竖屏切换时状态栏先隐藏或显示,布局引擎还没来得及重新计算。可以在方向变化时先隐藏所有依赖状态栏高度的元素,等宽高稳定后再显示:
typescript复制function useStableLayout() {
const [stable, setStable] = useState(false);
const { width, height } = useWindowDimensions();
useEffect(() => {
setStable(false);
// 这里给布局引擎一个刷新周期的时间
const timer = setTimeout(() => setStable(true), 100);
return () => clearTimeout(timer);
}, [width, height]);
return stable;
}
这个方法不优雅,但非常有效。在 OpenHarmony 的早期版本上,很多闪烁问题靠这种"强制延迟显示"解决了,后续会继续跟踪是否有更原生的修复方案。
6. 还有两个绕不开的细节:横屏下的触控坐标与多窗口适配
这部分内容在需求列表里往往不会写得那么细,但不处理一定出问题。
6.1 旋转后 onPress 热区偏移的处置
OpenHarmony 平板上出现过横屏后点按按钮没反应、但视觉位置和按钮位置差一截的问题。这通常是因为设备树的触摸屏分辨率配置和当前显示分辨率不一致,或者横屏后坐标系没有同步切换。
排查时先在系统设置里打开"指针位置"看触摸坐标,如果触摸点的系统坐标在横屏下确实偏移,说明是底层触控校准问题,不是 RN 层面。
RN 侧的规避方式是使用 onPress 的固定热区,而不是依赖全动态布局。在按钮外层包一层固定尺寸的容器,确保热区不随安全区变化而漂移。这治标不治本,但在某些板卡驱动不完善时是唯一可用的方案。
6.2 分屏与悬浮窗下的 RN 横竖屏策略
OpenHarmony 系统支持分屏和悬浮窗,窗口尺寸可以随意变化,不一定是完整横屏或竖屏。此时如果应用按"横竖屏二选一"的逻辑渲染,会出现大量内容被截断。
我的经验是:尽量不用二元的 orientation 判断,而是直接以宽高比作为布局依据。比如 isLandscape = width > height 在方形分屏下就变得没有意义,这种情况下应该给出第三种紧凑布局。
typescript复制interface WindowDimensions {
width: number;
height: number;
}
type Breakpoint = 'COMPACT' | 'MEDIUM' | 'EXPANDED';
function getBreakpoint({ width, height }: WindowDimensions): Breakpoint {
const ratio = width / Math.max(height, 1);
if (width < 400 || ratio < 1.2) return 'COMPACT';
if (width < 800 || ratio < 1.7) return 'MEDIUM';
return 'EXPANDED';
}
宽高比判断比单纯的方向判断覆盖的场景更广。分屏时即使 width 和 height 都在变化,系统也能根据比例给出稳定布局。
按照我个人的习惯,在 OpenHarmony 上做 RN 横竖屏适配,我会把"方向监听 + 宽高比断点 + 持久化存储 + 原生配置"四件事一起做。这不是过度设计,而是为了在低性能的 RK3568 设备、未及时更新的内核驱动、以及各种奇怪的窗口模式叠加下,依然能让页面正常展示。项目已经上线跑了两三个月,没有比这更稳定的组合了。
