在这里插入图片描述
在这里插入图片描述

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名)

大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。

我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括 前端工程化、小程序、React / RN、Flutter、跨端方案
在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。

技术方向:前端 / 跨端 / 小程序 / 移动端工程化
内容平台:
掘金、知乎、CSDN、简书
创作特点:
实战导向、源码拆解、少空谈多落地
文章状态:
长期稳定更新,大量原创输出

我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、真实项目中如何取舍,希望能帮你在实际工作中少走弯路。

子玥酱 · 前端成长记录官 ✨
👋 如果你正在做前端,或准备长期走前端这条路
📚 关注我,第一时间获取前端行业趋势与实践总结
🎁 可领取 11 类前端进阶学习资源(工程化 / 框架 / 跨端 / 面试 / 架构)
💡 一起把技术学“明白”,也用“到位”

持续写作,持续进阶。
愿我们都能在代码和生活里,走得更稳一点 🌱

先把主线程的“合法职责”划清楚

在 HarmonyOS 游戏里(无论你用 ArkTS + 自研引擎,还是三方引擎封装),主线程本质上只该做三件事

1. 接收输入
2. 驱动一帧的调度
3. 提交渲染结果

用伪代码表示就是:

gameLoop(deltaTime) {
  handleInput()        // 轻
  updateSchedule()     // 极轻
  submitRender()       // 不阻塞
}

凡是不属于这三类的逻辑,你都要下意识问一句:

这东西,为什么要在主线程?

下面我们直接列绝对不能放主线程的几类逻辑

一、任何「不确定耗时」的逻辑

典型误区:我觉得它“很快”

// 主线程
const data = JSON.parse(bigConfig)
applyConfig(data)

你心里想的是:

配置文件嘛,一次而已

但在游戏里,这种代码的风险在于:

  • JSON 大小不可控
  • GC 压力不可控
  • 不同设备性能差异极大

主线程最怕的不是“慢”,而是:

你不知道它会慢到什么程度

正确做法

TaskPool.execute(() => {
  const data = JSON.parse(bigConfig)
  return data
}).then((data) => {
  applyConfig(data) // 主线程只做赋值
})

原则一句话:

主线程只能处理“结果”,不能参与“过程”。

二、资源加载与解码逻辑(重灾区)

常见错误写法

// 主线程
const texture = loadTexture("enemy.png")
sprite.setTexture(texture)

哪怕 loadTexture 内部是 async,你也别高兴太早。

很多所谓的“异步”实际上是:

  • IO 在后台
  • 解码 / GPU upload 在主线程

结果就是:

帧时间被偷偷拉长

更安全的结构

// 后台线程
TaskPool.execute(() => {
  const raw = loadFile("enemy.png")
  const bitmap = decodeImage(raw)
  return bitmap
}).then((bitmap) => {
  // 主线程
  const texture = uploadToGPU(bitmap)
  sprite.setTexture(texture)
})

即便如此,你也要注意:

  • uploadToGPU 是否阻塞
  • 是否集中在同一帧

否则依然会「一卡一卡」。

三、物理 / 碰撞的完整计算

很多人会说:

不放主线程,逻辑不同步怎么办?

这是典型的 App 思维

错误直觉

// 主线程
physics.step(deltaTime)
resolveCollision()

问题不在“对不对”,而在于:

  • 碰撞复杂度随对象数量指数增长
  • 某一帧可能突然爆炸

你永远无法保证:

这一帧的物理计算 = 这一帧的预算

更合理的做法

// 后台物理线程
physicsWorker.step(fixedDelta)

// 主线程
const snapshot = physicsWorker.getSnapshot()
applyTransform(snapshot)

关键点:

  • 主线程不关心“怎么算”
  • 只关心“这一帧拿到什么结果”

四、任何“遍历整个世界”的逻辑

典型例子

// 主线程
entities.forEach(e => {
  if (e.hp <= 0) remove(e)
})

问题在于:

  • entities 数量不稳定
  • remove 可能触发结构变化
  • Debug / Release 表现差异极大

正确拆法

// 后台
const deadList = []
for (const e of entitiesSnapshot) {
  if (e.hp <= 0) deadList.push(e.id)
}
return deadList
// 主线程
deadList.forEach(id => removeEntity(id))

主线程只做“批量执行”,不做“逐个判断”。

五、你以为是异步,其实还是主线程的逻辑

表面异步,实则同步

async function loadSkill() {
  const data = await fetchConfig()
  parseSkill(data) // ⚠️ 主线程
  buildSkillTree() // ⚠️ 主线程
}

await 不是免死金牌。

只要:

  • 没有切线程
  • 没有 TaskPool / Worker

后面的逻辑,依然跑在主线程。

正确心智模型

await TaskPool.execute(() => {
  return parseAndBuild(data)
})

一个非常实用的判断标准

你可以用这个问题来审视每一段代码:

这段逻辑,会不会让某一帧变得“不可预测”?

如果答案是“可能会”:它就不该在主线程。

总结一句工程结论

HarmonyOS 游戏里,主线程的地位非常清晰:

它是调度者,不是劳工。

你一旦把:

  • 计算
  • 解码
  • 遍历
  • 决策

塞进主线程,本质上就是在赌:

这一帧刚好没事。

而游戏卡顿,几乎都是赌输的结果

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐