标签 HarmonyOS 下的文章

写在前面

在万物互联的全场景时代,智能设备的形态正从手机、平板向车载、穿戴、智能家居等多端快速延伸,用户对交互体验的要求早已突破 “能用” 的基础阈值,转而追求 “自然、无感、高效” 的跨设备操作体验。手势交互作为一种摆脱物理按键束缚的自然交互方式,凭借其直观性和沉浸感,已成为 HarmonyOS 构建全场景生态的核心交互语言。HarmonyOS作为新一代面向万物互联的操作系统,不仅重构了多设备协同的底层逻辑,更在手势识别能力上完成了升级,它提供了一套轻量化、高适配的手势开发框架,让开发者仅通过几行代码就能实现丰富的手势交互,从而在不同设备上打造统一且流畅的用户体验。单一手势是 HarmonyOS 手势体系的基础单元,所有复杂的组合手势都由它演化而来。本文将从技术原理入手,系统拆解单一手势的类型、实现逻辑与设计原则,并结合真实场景代码案例,帮助开发者快速掌握这一核心能力,为打造全场景交互体验筑牢基础。

image.png

单一手势什么是?

在 HarmonyOS 的交互框架中,系统将通用输入事件划分为触屏、键鼠、焦点及拖拽等核心类型。而手势交互则是通过 “手势绑定方法 + 具体手势实例” 的组合来实现,根据交互复杂度又可细分为单一手势与组合手势两大类别。
单一手势是构成所有复杂交互的 “原子单元”,它仅通过单次、独立的触摸动作(如点击、滑动、按压)触发功能,是组合手势的基础组件,也是开发者入门 HarmonyOS 交互开发的第一课。

手势操作的类型及实现

关于单一手势操作的类型有点击、长按、拖动、捏合、旋转、滑动六大类型,具体实现如下所示。

1、点击手势(TapGesture):交互的起点

点击手势是最基本的手势操作,支持单次点击和多次点击。通过TapGesture可以轻松实现按钮点击、菜单打开等功能,以下代码展示了如何实现一个双击操作:

@Entry
@Component
struct Index {
  @State value: string = "";

  build() {
    Column() {
      Text('双击').fontSize(28)
        .gesture(
          TapGesture({ count: 2 }) // 绑定双击事件,绑定count为2的TapGesture
            .onAction((event: GestureEvent|undefined) => {
              this.value = JSON.stringify(event.fingerList[0]);
            }))
      Text(this.value)
    }
    .height(200)
    .width(250)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

2、长按手势(LongPressGesture):触发深度操作

长按手势常用于触发 “二次确认” 类功能,如长按复制、弹出操作菜单等,支持重复触发模式以实现连续交互。长按手势用于触发长按手势事件,通过LongPressGesture可以轻松实现长按复制等功能,以下代码展示了如何实现在Text组件上绑定可以重复触发的长按手势:

@Entry
@Component
struct Index {
  @State count: number = 0;

  build() {
    Column() {
      Text('长按').fontSize(28)
        .gesture(
          // 绑定可以重复触发的LongPressGesture
          LongPressGesture({ repeat: true })
           .onAction((event: GestureEvent|undefined) => {
              if(event){
                if (event.repeat) {
                  this.count++;
                }
              }
            })
            .onActionEnd(() => {
              this.count = 0;
            })
        )
    }
    .height(200)
    .width(250)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

3、拖动手势(PanGesture):实现元素自由位移

拖动手势在滑动距离超过系统阈值(默认 5vp)时触发,常用于实现组件拖拽、位置调整等交互,核心是通过回调实时更新布局参数。拖动手势用于触发拖动手势事件,以下代码展示了在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动:

@Entry
@Component
struct Index {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;

  build() {
    Column() {
      Text('拖动')
        .fontSize(28)
        .height(200)
        .width(300)
        .padding(20)
        .border({ width: 3 })
          // 在组件上绑定布局位置信息
        .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
        .gesture(
          // 绑定拖动手势
          PanGesture()
           .onActionStart((event: GestureEvent|undefined) => {
            })
              // 当触发拖动手势时,根据回调函数修改组件的布局位置信息
            .onActionUpdate((event: GestureEvent|undefined) => {
              if(event){
                this.offsetX = this.positionX + event.offsetX;
                this.offsetY = this.positionY + event.offsetY;
              }
            })
            .onActionEnd(() => {
              this.positionX = this.offsetX;
              this.positionY = this.offsetY;
            })
        )
    }
    .height(200)
    .width(250)
  }
}

4、捏合手势(PinchGesture):控制元素缩放

捏合手势通过识别多指(支持 2-5 指)的距离变化计算缩放比例,是图片浏览、地图缩放等场景的核心交互方式。捏合手势用于触发捏合手势事件,常用于图片和地图的缩放操作,这里举例以在Column组件上绑定三指捏合手势为例,可以通过在捏合手势的函数回调中获取缩放比例,实现对组件的缩小或放大:

@Entry
@Component
struct Index {
  @State scaleValue: number = 1;
  @State pinchValue: number = 1;
  @State pinchX: number = 0;
  @State pinchY: number = 0;

  build() {
    Column() {
      Column() {
        Text('捏合')
      }
      .height(200)
      .width(300)
      .border({ width: 3 })
      .margin({ top: 100 })
      // 在组件上绑定缩放比例,可以通过修改缩放比例来实现组件的缩小或者放大
      .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
      .gesture(
        // 在组件上绑定三指触发的捏合手势
        PinchGesture({ fingers: 3 })
          .onActionStart((event: GestureEvent|undefined) => {
          })
            // 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例
          .onActionUpdate((event: GestureEvent|undefined) => {
            if(event){
              this.scaleValue = this.pinchValue * event.scale;
              this.pinchX = event.pinchCenterX;
              this.pinchY = event.pinchCenterY;
            }
          })
          .onActionEnd(() => {
            this.pinchValue = this.scaleValue;
          })
      )
    }
  }
}

5、旋转手势(RotationGesture):实现元素角度调整

旋转手势通过识别触摸点的角度变化计算旋转值,常用于图片编辑、表盘调整等需要角度控制的场景。旋转手势用于触发旋转手势事件,这里以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转:

@Entry
@Component
struct Index {
  @State angle: number = 0;
  @State rotateValue: number = 0;

  build() {
    Column() {
      Text('旋转').fontSize(28)
        // 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转
        .rotate({ angle: this.angle })
        .gesture(
          RotationGesture()
           .onActionStart((event: GestureEvent|undefined) => {
            })
              // 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度
            .onActionUpdate((event: GestureEvent|undefined) => {
              if(event){
                this.angle = this.rotateValue + event.angle;
              }
            })
              // 当旋转结束抬手时,固定组件在旋转结束时的角度
            .onActionEnd(() => {
              this.rotateValue = this.angle;
            })
            .onActionCancel(() => {
            })
        )
        .height(200)
        .width(300)
        .padding(20)
        .border({ width: 3 })
        .margin(100)
    }
  }
}

6、滑动手势(SwipeGesture):触发页面级交互

滑动手势在滑动速度超过 100vp/s 时触发,支持上下左右四个方向,是列表滚动、页面切换等场景的核心交互。滑动手势用于触发滑动事件,可以实现上下左右滑动操作,常用于列表滚动和页面切换,以在Column组件上绑定滑动手势实现组件的旋转为例:

@Entry
@Component
struct Index {
  @State rotateAngle: number = 0;
  @State speed: number = 1;

  build() {
    Column() {
      Column() {
        Text('滑动')
      }
      .border({ width: 3 })
      .width(300)
      .height(200)
      .margin(100)
      // 在Column组件上绑定旋转,通过滑动手势的滑动速度和角度修改旋转的角度
      .rotate({ angle: this.rotateAngle })
      .gesture(
        // 绑定滑动手势且限制仅在竖直方向滑动时触发
        SwipeGesture({ direction: SwipeDirection.Vertical })
          // 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改
          .onAction((event: GestureEvent|undefined) => {
            if(event){
              this.speed = event.speed;
              this.rotateAngle = event.angle;
            }
          })
      )
    }
  }
}

手势操作的设计原则

优质的手势交互不仅需要技术实现,更需要遵循用户体验设计的底层逻辑,核心原则可归纳为两点:自然直观和一致性。

1、自然直观

手势设计应模拟现实世界中的操作习惯,让用户能够凭借直觉进行交互,比如从屏幕底部边缘向上滑动返回主屏幕,这一操作类似于翻开书页的动作。

2、一致性

在整个HarmonyOS生态系统中,手势操作的含义和效果应保持一致,比如双指缩放手势在不同应用中都应实现放大或缩小的功能。

实战落地:单一手势在场景化开发中的典型应用

这里举两个简单的实用示例,方便各位学习使用。

1、音乐播放器应用

在音乐播放器应用中,用户可以通过左右滑动切换歌曲,双指缩放调整封面大小,以下代码展示了如何实现左右滑动切换歌曲:

@Entry
@Component
struct MusicPlayerSwipe {
  @State currentSongIndex: number = 0;
  @State songs: string[] = ['Song 1', 'Song 2', 'Song 3'];

  build() {
    Column() {
      Text(this.songs[this.currentSongIndex])
    }
    .swipe({
      start: SwipeDirection.Left,
      onSwipe: () => {
        if (this.currentSongIndex < this.songs.length - 1) {
          this.currentSongIndex++;
        }
      }
    })
    .swipe({
      start: SwipeDirection.Right,
      onSwipe: () => {
        if (this.currentSongIndex > 0) {
          this.currentSongIndex--;
        }
      }
    })
  }
}

2、手势截屏

手势截屏是另一个实用的功能,用户可以通过下滑手势调用全屏截图功能,通过双击手势调用区域截图功能,以下代码展示了如何实现全屏截图:

Stack() {
  Column() {
    ...
  }
  .gesture(
    PanGesture({
      fingers: 1,
      direction: PanDirection.Down,
      distance: CommonConstants.MINIMUM_FINGER_DISTANCE
    })
      .onActionStart(() => {
        let screenshotOptions: screenshot.ScreenshotOptions = {
          rotation: 0
        };
        screenshot.save(screenshotOptions, (err: Error, data: image.PixelMap) => {
          if (err) {
            Logger.error(`Failed to save the screenshot. Error:${JSON.stringify(err)}`);
          }
          if (this.pixelMap !== undefined) {
            this.pixelMap.release();
          }
          this.pixelMap = data;
          this.dialogController.open();
        });
      })
  )
}

最后

随着 HarmonyOS 6 的发布,全场景交互的边界正在被重新定义,单一手势作为交互体系的 “原子单元”,不仅是开发者构建基础交互的必备技能,更是打造复杂组合手势、实现多设备协同交互的核心基石。本文从技术原理、类型实现、设计原则到场景实战,系统拆解了 HarmonyOS 单一手势的开发逻辑。这些看似基础的交互能力,恰恰是构建 “自然、无感、高效” 全场景体验的关键:无论是手机端的流畅操作,还是车载、穿戴设备的极简交互,都离不开对单一手势的精准把控。对于 HarmonyOS 开发者而言,掌握单一手势的底层逻辑与实践技巧,不仅能提升应用的用户体验,更能为未来探索多设备协同手势、AI 增强手势等进阶能力筑牢基础。在万物互联的浪潮中,唯有以用户体验为核心,以技术实力为支撑,才能打造出真正适配全场景的优质应用。希望本文能成为你探索 HarmonyOS 交互开发的起点,在构建智能生态的道路上持续进阶。

《鸿蒙架构师修炼之道》已于近日上市,该书由北京大学出版社出版。该书主要介绍如何培养鸿蒙架构师,内容涉及HarmonyOS架构设计思维/原理/模式、工具、编程语言、UI设计、线程模型设计、通信设计、持久化设计、安全性、测试、调优调测等多方面。

本文希望与读者朋友们分享下这本书里面的大致内容。

封面部分

首先是介绍封面部分。

《鸿蒙架构师修炼之道》封面右上角是本书的书名,清晰凸显出“鸿蒙”及“HarmonyOS”字眼。

封面整体色调是青色,小清新、富有活力。

右下角貌似是一只蜂鸟。蜂鸟寓意着坚韧与勇气‌:蜂鸟体型虽小,却拥有惊人的飞行能力,能悬停、倒飞,象征着以微小之躯挑战巨大困难的精神。本书封面配以蜂鸟,体现了在鸿蒙架构师修炼道路上,需要极大的勇气与自我价值的肯定。‌

封面左下角体现了本书的一些特色,比如:

  • 本书附赠完整的源代码和习题,所有代码均经过严格测试验证,确保能够顺利运行并达到预期效果。这对于大中院校的师生来说非常友好,直接可以将这本书作为学习鸿蒙的上课教材。
  • 本书介绍专家级架构师的思维方式与工作方法。
  • 本书介绍HarmonyOS架构设计思维/原理/模式、工具、编程语言、UI设计、线程模型设计、通信设计、持久化设计、安全性、测试、调优调测等多方面。

封面底部是出版社“北京大学出版社”字样。

封底部分

介绍封底部分。

封底部分较为简介,跟封面内容相似。

全书400页,较为丰富,定价为119元,也不算贵,非常极具有性价比。

内容简介

所有程序员都有成为架构师的潜力,只要掌握了架构师的思维方式和工作方法,你也能成长为架构师。 鸿蒙操作系统是华为自研的、面向万物互联的全场景分布式操作系统,支持手机、平板、PC、智能穿戴、智慧屏等多种终端设备运行,是提供应用开发、设备开发的一站式服务的平台。随着 HarmonyOS NEXT 正式 发布,市面上对于鸿蒙架构设计方面的需求呈井喷之势。 本书以最新的 HarmonyOS 版本为基石,详细介绍成为鸿蒙架构师应具备和掌握的核心能力和工 作方法,包括架构设计思维、架构设计原理、架构设计模式、工具、编程语言、UI 设计、线程模型设计、通信设计、持久化设计、安全性、测试、调优调测等多个主题。 本书不但通过真实案例讲解架构设计流程和经验,还总结了丰富的鸿蒙架构师工作原则和技巧,尤其适合广大鸿蒙程序员进阶学习。同时,学习本书也有助于产品经理、测试人员、运维人员和其他行业从业者理解鸿蒙软件架构设计工作。

全书总共包含13章,包括:

  • 第1章 成为鸿蒙架构师
  • 第2章 架构设计思维
  • 第3章 架构设计原理
  • 第4章 架构设计模式
  • 第5章 工具
  • 第6章 编程语言
  • 第7章 UI设计
  • 第8章 线程模型设计
  • 第9章 通信设计
  • 第10章 持久化设计
  • 第11章 安全性
  • 第12章 测试
  • 第13章 调优调测

更多介绍,详见“参考引用”。

写作背景

自HarmonyOS面世之时,笔者便已经开始关注HarmonyOS的发展。笔者在各大论坛也对HarmonyOS进行过非常多的文章介绍以及技术布道。本书所选用HarmonyOS版本的也是市面上能看到的最新正式版本。

由于笔者长期混迹于鸿蒙开发与推广,出版过多本关于鸿蒙的专著,包括《鸿蒙HarmonyOS手机应用开发实战》《鸿蒙HarmonyOS应用开发从入门到精通》《鸿蒙之光HarmonyOS NEXT原生应用开发入门》《鸿蒙之光HarmonyOS 6应用开发入门》等等,并在长期维护一本开源书《跟老卫学HarmonyOS开发》,但这些书籍都是介绍如何入门鸿蒙生态,如何进行HarmonyOS应用开发。《鸿蒙架构师修炼之道》不同点在于,这是一本专注于培养鸿蒙架构师的教程,是一名鸿蒙开发老兵的经验升华,在业界尚属首例。

本书的内容聚焦于告诉读者鸿蒙架构师是如何修炼的,成为鸿蒙架构师应具备怎么样的核心能力和工作方法,包括架构设计思维、架构设计原理、架构设计模式、工具、编程语言、UI设计、线程模型设计、通信设计、持久化设计、安全性、测试、调优调测等。本书不但通过真实案例讲解架构设计流程和经验,还总结了丰富的鸿蒙架构师工作原则和技巧,尤其适合广大鸿蒙开发人员进阶学习。

源代码

本书提供的素材和源代码可从以下网址下载:
https://github.com/waylau/harmonyos-tutorial

勘误和交流

本书如有勘误,会在以下网址发布:
https://github.com/waylau/harmonyos-tutorial/issues

参考引用

背景介绍

随着HarmonyOS 的发展,很多开发者将鸿蒙作为重要开发平台,尤其是在华为激励计划的加持下,涌入大量开发者贡献了大量应用,将大量创意带个了鸿蒙生态。

但随着时间推移,许多开发者发现,鸿蒙的应用审核似乎异常“严格”,很多开发者上架提审时被卡在了《审核指南》3.5和3.7项:

  • 3.5项的规则是:应用需具备实用价值,能为用户提供实质功能/服务,且需具备创意,不得为纯信息展示,包括但不限于单一图片、单一页面、单一影视剧集类、单一图书单行本类、单一非官方游戏攻略类等。应用不得是简单打包的网站页面或套用模板、内容聚合、罗列链接、广告推广等,或为手机系统自带的简易功能。
  • 3.7项的规则是:请避免继续在已有较多类似应用的类别下进行开发,如敲木鱼、随机选择、计算器、手电筒、记事本、记账、天气、数字大小写转换、日历、指南针、智能遥控、镜子、助眠睡眠、证件照、色彩助手、手持弹幕、播放器、万能遥控器、外卖跑腿聚合平台、生鲜买菜服务聚合平台、计时类、Wi-Fi管理类、Wi-Fi搜索连接类、Wi-Fi检测提速类等类别的应用,除非您的应用能够提供独特、高质量的体验,为用户提供多样、优质的功能和服务,否则您的应用可能会被拒绝或移除。
    还有不少开发者反馈,被3.5或3.7规则拒审后,又增加了不少页面和功能还是被以同样的原因拒审,甚至有人再传只要被3.5或3.7基本死刑了,需要重新想创意开发了。小编正好之前被3.5拒审后面通过迭代成功上架打破传言,本文就通过复盘3.5后迭代的经历分享打破3.5魔咒的经验。
    image.png

应用功能介绍

小编开发的应用叫”智能带办“,踩中了个人开发者最常开发的应用清单,是个清单类应用。创意来源于日常生活中自己的痛点,每次出差出远门或者从帝都回老家,都要拉一个单子把所有要带的东西都列出来,大部分情况带的东西都差不多,一般都记录在备忘录中,列清单的时候很耗费精力,想到AI能力越来越强大,可不可以让AI给生成?在AI工具中虽然可以生成清单,但是又没法做勾选等操作,融合操作和AI能力就想到做一个智能生成带办的应用,应用的亮点就是专注解决出行携带难题,通过AI智能生成场景清单,让你告别遗忘,轻松应对每一次出差、旅行、露营与日常外出。

智能带办,让你每一次出发,都底气十足。
告别“忘带焦虑”,从容开始每一段行程。
image.png

3.5拒审版本功能盘点

提审被拒绝的版本主要包含四个页面:Chat、历史、我的、详情。在Chat页面输入要办的事情自动生成要带物品清单,勾选物品确认后生成带办清单并自动跳转到详情页,页面效果如下:
Chat页面:
image.png
清单页面:
image.png

清单展开详情页:
image.png

详情页:
image.png

新迭代功能

在重新提审的版本对整个代码工程做了重构,UI也进行了优化,包含功能:
推荐:
image.png

清单页:
image.png

Chat页:
image.png
详情页:
image.png

碰一碰页:
image.png

语音输入:
image.png

对比拒审前和拒审后版本功能区别如下:
1、UI美化
2、增加了推荐功能
3、增加了HarmonyOS 系统碰一碰分享能力
4、增加了语音输入功能
5、Chat页输入框上方增加了推荐问题

复盘总结

通过对比被拒版本与最终上架版本,我们可以清晰地看到一个核心转变:从“一个不错的功能点子”进化为“一个完整、独特且有深度的产品”。这不仅是一次功能的叠加,更是对审核规则内涵的深刻理解与主动契合。下面,我将逐点拆解迭代背后的逻辑,还原打破“3.5魔咒”的真实路径。

  1. 从“单薄的功能演示”到“完整的用户体验闭环”

    • 原版本痛点:应用流程始于Chat输入,终于清单生成与勾选。这更像是一个AI工具的“功能演示”,用户使用路径短,用完即走,缺乏留存价值和持续使用场景,恰好落入规则3.5所述“功能单薄”的范畴。
    • 迭代策略与效果:

      • 增加“推荐”页:这是本次迭代的“棋眼”。它不再是空白的起点,而是提供了“出差”、“露营”、“健身”等丰富的预设场景。这带来了三大好处:其一,直观证明了应用的“实用价值”和解决多种场景问题的能力,直接回应了审核对“实质功能”的要求;其二,降低了用户冷启动门槛,提升了易用性;其三,构建了内容厚度,让应用看起来像一个精心策划的工具集,而非一个简单的输入框。
      • 结果:应用从一个“AI清单生成器”变成了一个“出行准备助手”,用户体验形成了“浏览场景-选择/自定义-生成-管理”的完整闭环。
  2. 从“通用AI套壳”到“彰显HarmonyOS独特性”

    • 原版本痛点:功能完全依赖AI接口,在任何平台均可实现,未能体现鸿蒙生态的独特优势。这容易让审核认为应用是“简单打包”或“套用模板”,缺乏不可替代性。
    • 迭代策略与效果:

      • 深度集成“碰一碰”能力:此功能是彰显“鸿蒙基因”的关键。它不再是简单的文本分享,而是通过系统能力实现了跨设备的无缝清单流转。这充分展示了开发者对HarmonyOS系统级能力的钻研与应用,证明了应用是为鸿蒙原生体验而设计,提供了其他平台难以复制的“独特、高质量的体验”(这也恰好回应了规则3.7的精神)。
      • 结果:应用的核心竞争力从“能生成清单”升级为“能在鸿蒙生态中优雅、便捷地生成和协同处理清单”,差异性豁然开朗。
  3. 从“基础交互”到“丰富且人性化的交互维度”

    • 原版本痛点:交互方式仅有文字输入和点击勾选,较为单一。
    • 迭代策略与效果:

      • 增加“语音输入”:这不仅仅是增加一个功能,更是提升了应用的易用性、包容性和现代化程度。在出行准备等双手可能不便的场景下,语音输入尤为实用。它展现了开发者在打磨用户体验上的深度思考。
      • 增加“推荐问题”:在Chat页输入框上方添加推荐问题(如“周末露营带什么?”),极大地引导了用户,丰富了交互的启发性和探索性,让AI工具变得更“聪明”和友好。
      • 结果:应用提供了文字、语音、预设场景选择、碰一碰分享等多种交互路径,功能层次变得更加立体和丰满,彻底摆脱了“单一页面”、“简单操作”的观感。
  4. UI美化:不仅是“面子”,更是“里子”的体现

    • UI重构与美化:这常常被开发者视为“表面功夫”,但在审核视角中,精致的UI是应用“高质量”和“完成度”最直观的外在表现。一个粗糙的界面会强化“敷衍”、“模板化”的印象;而一个设计精良、符合鸿蒙设计规范的界面,则传递出开发者认真打磨产品、尊重用户的积极信号。本次的UI优化,与功能深化同步,共同塑造了一款成熟应用的质感。

核心经验提炼:给开发者的避坑指南

  1. 超越功能点,思考用户旅程:不要只满足于实现核心功能。问自己:用户从哪里来(入口引导)?核心功能之后还能做什么(场景延伸/分享/管理)?如何让他下次还想用(留存价值)?构建闭环。
  2. 拥抱系统能力,打造生态差异化:在鸿蒙上开发,务必主动探索并集成Kit能力(如碰一碰、原子化服务、卡片等)。这是证明你为鸿蒙而来、并能为鸿蒙生态增色的最强证据。
  3. 叠加交互维度,展现思考深度:在主流程上,思考是否能提供更便捷(如语音)、更引导(如推荐)、更趣味(如动效)的交互方式。丰富的交互是“功能深度”的体现。
  4. 用视觉品质为产品背书:将UI/UX视为产品不可或缺的一部分。高质量的设计能无形中提升审核对应用整体质量的评价。

结论

“智能带办”通过审核的经历证明,规则3.5并非“死刑判决”,而是一道清晰的“产品成熟度”分水岭。被拒不是创意的终结,而是产品打磨的开始。关键在于,开发者必须跳出“我明明有这个功能”的委屈心态,转而以审核规则为镜,以更高标准审视自己的应用:它是否构成了完整服务?是否具备生态特色?交互是否丰满精致?当你的应用能从这些维度展现出独特价值和用心之处时,“3.5魔咒”自然不攻自破。

目录

  • 前言
  • 关于跨设备文件访问和拷贝
  • 实现跨设备文件访问
  • 实现跨设备文件拷贝
  • 结束语

    前言

    在HarmonyOS构建的全场景智能生态体系中,“万物互联”不再是抽象的概念,而是通过一系列核心技术落地到用户日常操作中的实用体验,其中跨设备文件访问与拷贝功能,更是衔接多终端、提升用户操作效率的关键支撑,也是鸿蒙原生应用开发中不可或缺的核心技能模块。随着鸿蒙原生生态的持续完善,越来越多的开发者开始聚焦多设备协同场景的应用开发,而跨设备文件操作能力,直接决定了应用在全场景生态中的适配性与竞争力,它能够让用户在手机、平板、智慧屏、手表等不同鸿蒙设备之间,无缝共享、传输各类文件,无需依赖第三方传输工具,也无需进行复杂的设备配对与设置,真正实现“一次操作,多端同步”的便捷体验。为了帮助广大鸿蒙原生开发者快速掌握这一核心技术,规避开发误区,本文将以实战为导向,在保留可直接复用示例代码的基础上,详细拆解跨设备文件访问与拷贝的实现逻辑、技术选型、操作步骤,从理论解析到实操落地,全方位讲解如何在HarmonyOS应用中高效实现这两项关键功能,助力开发者轻松适配全场景协同开发需求。

image.png

关于跨设备文件访问和拷贝

HarmonyOS搭载的分布式技术体系,为跨设备文件访问与拷贝功能的实现提供了坚实的底层支撑,其中分布式文件系统与分布式任务调度两大核心能力,共同打破了不同设备之间的文件壁垒,让应用能够像操作本地文件一样,轻松访问和操控远程设备上的文件资源,大幅降低了跨设备开发的难度。
先为大家解析跨设备文件访问功能,分布式文件系统为鸿蒙应用提供了原生的跨设备文件访问能力,当开发者在两台不同的设备上安装了同一应用后,通过系统提供的基础文件接口,即可跨设备读写另一台设备上该应用分布式文件路径(/data/storage/el2/distributedfiles/)下的所有文件。典型的应用场景就是多设备数据流转,当两台设备完成组网互联后,设备A上的目标应用,能够直接访问设备B上同一应用分布式路径下存储的文件;如果开发者希望应用中的某个文件能够被其他设备访问,只需将该文件移动至上述分布式文件路径即可,操作便捷且无需额外配置。
然后再来讲解跨设备文件拷贝功能,同样基于分布式文件系统的支撑,鸿蒙应用具备了跨设备、跨应用的文件拷贝能力,开发者可通过系统基础文件接口,实现不同设备、不同应用之间的文件拷贝操作。以多设备数据流转场景为例,当两台设备完成组网互联后,设备A上的应用在执行复制操作时,会先将自身沙箱文件拷贝至设备A的分布式文件路径中;而设备B在执行粘贴操作时,则会从自身的分布式文件路径中,将对应的文件拷贝至目标沙箱路径下,整个过程由系统底层自动完成设备间的协同,开发者无需关注底层通信细节。

实现跨设备文件访问

1、完成分布式组网

首先需将需要实现跨设备访问的两台设备,登录同一鸿蒙账号完成身份认证,同时确保两台设备的蓝牙与Wi-Fi功能处于开启状态(无需手动完成蓝牙互连,Wi-Fi也无需接入同一局域网,系统会自动完成设备组网匹配)。

2、访问跨设备文件

同一应用在不同设备之间实现跨设备文件访问,核心操作是将需要共享的文件放置在应用沙箱的分布式文件路径下,下面先展示设备A在分布式路径下创建测试文件并写入内容的具体操作,具体的示例代码如下所示:

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
let pathDir: string = context.distributedFilesDir;
// 获取分布式目录的文件路径
let filePath: string = pathDir + '/test.txt';

try {
  // 在分布式目录下创建文件
  let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  // 向文件中写入内容
  fs.writeSync(file.fd, 'content');
  // 关闭文件
  fs.closeSync(file.fd);
} catch (error) {
  let err: BusinessError = error as BusinessError;

} 

设备B需主动向设备A发起链路建立,待建链成功后,即可在自身的分布式文件路径下读取设备A创建的测试文件。需要特别说明的是,此处需通过分布式设备管理接口获取设备A的networkId,以此实现设备间的精准关联,具体示例代码如下所示:

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
import { distributedDeviceManager } from '@kit.DistributedServiceKit'

// 通过分布式设备管理的接口获取设备A的networkId信息
let dmInstance = distributedDeviceManager.createDeviceManager("com.example.hap");
let deviceInfoList: Array<distributedDeviceManager.DeviceBasicInfo> = dmInstance.getAvailableDeviceListSync();
let networkId = deviceInfoList[0].networkId;

// 定义访问公共文件目录的回调
let listeners : fs.DfsListeners = {
  onStatus: (networkId: string, status: number): void => {
    console.info('Failed to access public directory');
  }
}

// 访问并挂载公共文件目录
fs.connectDfs(networkId, listeners).then(() => {
  console.info("Success to connectDfs");
  let context = getContext(); // 获取设备B的UIAbilityContext信息
  let pathDir: string = context.distributedFilesDir;
  // 获取分布式目录的文件路径
  let filePath: string = pathDir + '/test.txt';

  try {
    // 打开分布式目录下的文件
    let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
    // 定义接收读取数据的缓存
    let arrayBuffer = new ArrayBuffer(4096);
    // 读取文件的内容,返回值是读取到的字节个数
    class Option {
        public offset: number = 0;
        public length: number = 0;
    }
    let option = new Option();
    option.length = arrayBuffer.byteLength;
    let num = fs.readSync(file.fd, arrayBuffer, option);
    // 打印读取到的文件数据
    let buf = buffer.from(arrayBuffer, 0, num);
    console.info('read result: ' + buf.toString());
  } catch (error) {
    let err: BusinessError = error as BusinessError;

  }
}).catch((error: BusinessError) => {
  let err: BusinessError = error as BusinessError;
  
});

3、断开链路

当设备B完成跨设备文件访问操作后,需及时断开设备间的链路,避免资源占用,具体实现代码如下所示:

import { BusinessError } from '@kit.BasicServicesKit';
import { distributedDeviceManager } from '@kit.DistributedServiceKit'
import { fileIo as fs } from '@kit.CoreFileKit';

// 获取设备A的networkId
let dmInstance = distributedDeviceManager.createDeviceManager("com.example.hap");
let deviceInfoList: Array<distributedDeviceManager.DeviceBasicInfo> = dmInstance.getAvailableDeviceListSync();
let networkId = deviceInfoList[0].networkId;

// 取消公共文件目录挂载
fs.disconnectDfs(networkId).then(() => {
  console.info("Success to disconnectDfs");
}).catch((error: BusinessError) => {
  let err: BusinessError = error as BusinessError;
  
})

实现跨设备文件拷贝

1、完成分布式组网

首先,将需要执行跨设备文件拷贝操作的所有设备接入同一局域网环境,同时完成同一鸿蒙账号的认证登录,确保设备间能够实现正常的分布式通信,完成组网准备。

2、拷贝跨设备文件

然后拷贝跨设备文件。同一应用在不同设备之间实现跨设备文件拷贝,核心逻辑与跨设备文件访问类似,需先将待拷贝文件移动至应用的分布式文件路径下。下面先展示设备A将自身沙箱文件拷贝至分布式路径下的具体操作,示例代码如下所示:

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileUri } from '@kit.CoreFileKit';

let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
let pathDir: string = context.filesDir;
let distributedPathDir: string = context.distributedFilesDir;
// 待拷贝文件沙箱路径
let filePath: string = pathDir + '/src.txt';

try {
 // 文件不存在时,需要创建文件并写入内容
 let file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
 fs.writeSync(file.fd, 'Create file success');
 fs.closeSync(file);
} catch (error) {
}

// 获取待拷贝文件uri
let srcUri = fileUri.getUriFromPath(filePath);

// 将待拷贝的沙箱文件,拷贝到分布式目录下
let destUri: string = fileUri.getUriFromPath(distributedPathDir + '/src.txt');

try {
 // 将沙箱路径下的文件拷贝到分布式路径下
 fs.copy(srcUri, destUri).then(()=>{


 }).catch((error: BusinessError)=>{
   let err: BusinessError = error as BusinessError;

 })
} catch (error) {

}

接着当设备B需要获取设备A的沙箱文件时,只需从自身的分布式文件路径下,将对应的文件拷贝至目标沙箱路径即可,以此完成整个跨设备文件拷贝流程,具体操作代码如下所示:

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileUri } from '@kit.CoreFileKit';

let context = getContext(this) as common.UIAbilityContext; // 获取设备B的UIAbilityContext信息
let pathDir: string = context.filesDir;
let distributedPathDir: string = context.distributedFilesDir;
// 待拷贝文件的目标沙箱路径
let filePath: string = pathDir + '/dest.txt';

// 获取目标路径uri
let destUri = fileUri.getUriFromPath(filePath);

// 获取分布式路径下的源文件
let srcUri: string = fileUri.getUriFromPath(distributedPathDir + '/src.txt');

// 定义拷贝回调
let progressListener: fs.ProgressListener = (progress: fs.Progress) => {

};
let options: fs.CopyOptions = {
  "progressListener" : progressListener
}

try {
 // 将分布式路径下的文件拷贝到其他沙箱路径下
 fs.copy(srcUri, destUri, options).then(()=>{


 }).catch((error: BusinessError)=>{
   let err: BusinessError = error as BusinessError;

 })
} catch (error) {

}

结束语

通过本文的详细解析与实战代码演示,相信各位鸿蒙原生开发者已经清晰掌握了跨设备文件访问与拷贝的核心实现逻辑、操作步骤以及关键注意事项。在HarmonyOS全场景智能生态飞速发展的当下,跨设备文件操作能力早已不是“加分项”,而是鸿蒙原生应用适配多终端场景、提升用户体验的“必备项”,它不仅能够为用户提供无缝、高效的文件共享与传输体验,打破不同设备之间的资源壁垒,更能帮助开发者拓宽应用的适配场景,提升应用在鸿蒙生态中的核心竞争力。
随着HarmonyOS生态的不断迭代升级,分布式技术的应用场景也将更加广泛,跨设备文件操作的功能也将更加完善。未来,期待各位开发者能够将本文所学灵活运用到实际开发中,不断探索分布式技术的更多可能,开发出更多适配全场景需求、兼具实用性与创新性的鸿蒙原生应用,共同助力HarmonyOS原生生态的繁荣发展。同时也希望本文能够成为各位开发者的实用参考工具,在后续的跨设备开发工作中提供有力支撑,若在实操过程中遇到相关问题,可结合本文的步骤与代码进一步排查调试,稳步提升自身的鸿蒙原生开发能力。

在HarmonyOS应用开发中,性能问题直接决定用户体验——滑动卡顿、启动缓慢、内存泄漏等问题,往往成为应用上线的“拦路虎”。DevEco Profiler作为官方性能分析利器,提供了实时监控、深度录制、多场景专项分析能力,能精准定位从底层资源到上层UI的各类性能瓶颈。

本文将以“理论+实操+专项”三维视角,拆解基于DevEco Profiler的性能优化闭环流程,重点覆盖Frame(卡顿丢帧)与ArkUI(组件/状态)两大高频场景,提供可直接落地的分析方法与避坑指南,助力开发者高效解决性能难题。

一、核心认知:性能优化的闭环逻辑与指标基准

性能优化并非“头痛医头”,而是一套“识别-定界-定位-优化-验证”的闭环流程。在动手分析前,需先明确性能指标基准与工具分工,避免无方向调优。

1.1 关键性能指标基准

以用户可感知体验为核心,结合HarmonyOS应用特性,核心指标参考如下(可根据业务场景微调):

  • 流畅度:页面滑动、动画播放帧率稳定在60fps以上,无掉帧、卡顿;60fps对应Vsync周期16.6ms,单帧耗时需控制在该阈值内。
  • 启动速度:冷启动耗时≤2秒,热启动耗时≤500ms;启动阶段需重点监控初始化链路耗时。
  • 资源占用:无高负载操作时,CPU占用率≤30%;内存无持续上涨(排除泄漏);GPU使用率适配场景,无无效渲染。
  • 稳定性:无因性能过载导致的崩溃、闪退,正常使用无异常发烫。

1.2 DevEco Profiler核心工具分工

工具能力与优化流程深度绑定,核心分工如下,避免重复操作或无效录制:

工具模块

核心作用

适用阶段

Realtime Monitor(实时监控)

快速识别资源异常,定界问题类型与场景

识别-定界、验证阶段

场景化模板(Frame/ArkUI/Launch等)

深度录制数据,精准定位问题根因(代码级)

定位阶段

离线符号解析、源码跳转

还原Native函数栈,定位具体代码行

定位阶段(底层问题)

二、性能优化全流程实操(闭环落地)

本流程适用于所有性能问题场景,核心是“先快速定界,再精准定位”,避免盲目深度录制浪费资源。

步骤1:实时监控定界——快速锁定异常场景

核心目标:10分钟内排查是否存在性能问题、明确问题类型与触发场景,不深入底层细节。

实操步骤(零基础可照做):

  1. 环境准备:USB连接真机(不支持模拟器),开启开发者模式与USB调试;确保macOS 12+,DevEco Studio版本匹配(建议5.1.0+)。
  2. 启动工具与选目标:通过菜单栏(View→Tool Windows→Profiler)、底部工具栏“Profiler”或搜索启动工具,在左侧会话区依次选择“设备—应用—进程”。
  3. 复现场景并监控:会话列表默认加载Realtime Monitor,操作应用复现核心场景(冷启动、列表滑动、动画播放等),观察数据区泳道的CPU、内存、帧率、GPU数据。
  4. 标记异常并定界:用快捷键M标记异常时间点,记录核心信息——如“列表滑动时帧率降至40fps(卡顿)”“内存多次操作后只增不减(泄漏)”,明确问题类型与场景。

干货技巧:实时监控仅用于“筛问题”,无需长时间录制;重点关注帧率、CPU占用两大指标,可快速锁定80%的表层性能问题。

步骤2:深度录制定位——精准找到代码根因

核心目标:针对定界的问题,用场景化模板录制精细化数据,从宏观指标拆解至具体代码行,找到根本原因。

实操核心步骤:

  1. 选对场景化模板(关键!):模板选错会导致数据无效,匹配关系如下:

问题类型

推荐模板

核心分析维度

页面滑动/动画卡顿

Frame/ArkUI

帧率丢帧、组件绘制、状态更新

应用启动慢

Launch

启动各阶段耗时、热点函数

ArkTS层内存泄漏

Snapshot

对象持有关系、内存分配节点

Native层问题

Allocation/CPU

Native内存分配、CPU热点函数

  1. 深度录制场景:选中模板后点击“Create Session”,点击录制按钮(▶),完整复现异常场景(如滑动卡顿需滑动3次以上),结束录制后等待数据解析。
  2. Top-Down逐层分析(高效方法):从宏观到微观拆解数据,以卡顿问题为例:

  • 顶层:Frame泳道查看丢帧时间点与类型(App侧/Render侧);
  • 中层:CPU/Callstack泳道查看耗时函数;
  • 底层:双击函数栈帧跳转至源码,定位耗时代码行。

干货技巧:用Alt+框选聚焦异常时段,可快速过滤无关数据;涉及Native层问题需导入离线符号表(工具控制栏按钮),还原函数名才能定位代码。

步骤3:代码优化+验证——形成闭环

核心原则:围绕“降负载”优化,分为永久降负载(彻底解决)与临时降负载(缓解体验),避免过度优化。

高频优化场景与方案:

  • 卡顿优化:简化UI层级(减少嵌套)、耗时计算移至子线程、避免滑动时执行复杂渲染。
  • 冗余刷新:拆分大型Object为小对象、避免子组件重复绑定同一状态变量。
  • 内存泄漏:释放无用对象引用、避免全局变量滥用、正确使用@Prop/@Link装饰器。

验证步骤:优化后重新用Realtime Monitor复现场景,对比指标——如卡顿场景帧率恢复至60fps、启动耗时缩短50%,即说明优化有效;未达标则重复“定位-优化”流程。

三、专项分析:Frame卡顿丢帧深度拆解

Frame模板是分析卡顿的核心工具,可覆盖GPU渲染、帧链路、异常操作等多维度,精准定位掉帧根源。

3.1 核心泳道解读(必懂)

展开Frame泳道后,重点关注以下子泳道,覆盖帧渲染全链路:

  • RS Frame/App Frame:分别对应Render Service侧与App侧帧数据,绿色为正常帧,红色为卡顿帧(耗时超16.6ms)。
  • Lost Frames/Hitch Time:直观展示丢帧数与卡顿时长,点选可查看具体时段数据。
  • Anomaly:检测图片解码超时(超8.3ms告警)、序列化/反序列化超时(默认8ms阈值),仅支持非上架应用。
  • User Events:查看用户操作(如点击)的处理耗时,定位交互卡顿原因。

3.2 实操分析流程(卡顿场景)

  1. 框选卡顿时段,查看RS Frame/App Frame泳道,判断卡顿来自App侧还是Render侧;
  2. 若为App侧卡顿:切换至ArkTS Callstack泳道,定位耗时最长的组件绘制或状态更新函数;
  3. 若为Render侧卡顿:查看GPU使用率,排查是否因硬件合成渲染过载;
  4. 通过“Statistics”区域统计卡顿率、次数,验证优化后的数据改善情况。

3.3 快捷键高效操作(提升50%效率)

  • 时间轴:W/S放大/缩小,A/D左右移动(需激活泳道区);
  • 标记:M添加单点标记,Shift+M添加时间段标记;
  • 标记切换:Ctrl+,/Ctrl+. 前后切换单点标记,Ctrl+[/Ctrl+] 切换时间段标记。

四、专项分析:ArkUI组件与状态卡顿定位

ArkUI层卡顿多源于组件布局、状态管理不当,通过ArkUI模板的专属泳道,可精准定位这类上层问题。

4.1 典型问题场景(高频踩坑点)

  1. 布局嵌套过多:组件层级超过5层,导致绘制链路冗长;
  2. 冗余刷新:更新大型Object部分属性,触发全对象刷新;
  3. 状态绑定异常:子组件重复绑定同一状态变量,更新时多次刷新;
  4. 装饰器误用:@Prop传递大型对象,引发不必要的深度拷贝。

4.2 核心泳道实操

4.2.1 ArkUI Component泳道(组件绘制分析)

  1. 框选时段后,“Summary”列表展示组件绘制统计(次数、总耗时、最大耗时),快速锁定绘制耗时最长的组件;
  2. 点选泳道条块,“More”区域展示组件树,直观查看布局嵌套层级,优化冗余组件。

4.2.2 ArkUI State泳道(状态更新分析)

  1. 录制状态更新场景(如点击按钮更新数据),“Summary”区域展示状态变量的变化次数、所属组件;
  2. 选中状态变量变化记录,开启“Delivery Chain”开关,图形化查看状态影响的组件链路,定位冗余刷新组件;
  3. 关联ArkUI Component泳道,验证状态更新是否触发组件过度刷新。

注意事项

因隐私政策,已上架应用不支持录制ArkUI Component/State泳道,需在开发测试阶段完成全量性能验证。

五、实战避坑与优化建议(干货总结)

结合大量项目实践,整理以下高频避坑点与优化技巧,帮你少走弯路:

  • 录制时务必完整复现场景:如卡顿需重复触发3次以上,避免数据碎片化导致定位失败;
  • 优先优化“耗时占比最高”的函数:这类函数往往是性能瓶颈的核心,优化后收益最明显;
  • 版本适配:页面布局查看、Component Animation等能力需DevEco Studio 5.1.0+,提前升级避免功能缺失;
  • 避免过度优化:如为简化布局牺牲功能扩展性,需平衡性能与代码可维护性;
  • 数据备份:解析完成后导出会话数据,便于团队共享分析或后续回溯问题。

六、总结

DevEco Profiler的核心价值的是“让性能问题可量化、可定位”,其优化流程的本质是“用数据驱动决策”——而非凭经验猜测。通过“实时监控定界→深度录制定位→优化验证闭环”的标准化流程,结合Frame与ArkUI专项分析,可高效解决HarmonyOS应用的各类性能问题。

建议在开发阶段就融入性能测试,每完成一个核心功能就用Realtime Monitor排查,避免上线前集中“救火”。

image.png

💡 鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞 💡
★ 更多内容,一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板
★ 一键直达 HarmonyOS 行业解决方案

image.png

模版

模板名称更新内容
综合商城应用模板能力接入:预加载、开屏广告、华为推送、数字收银台
综合商城应用模板新增组件:地址管理、应用设置、登录、支付、分享、个人信息、意见反馈、会员组件
美食菜谱应用模板能力接入:数字收银台、华为广告
美食菜谱应用模板功能增强:新增下拉刷新和饮食计划、折叠屏适配
美食菜谱应用模板新增组件:登录、设置、个人信息、会员组件
美业元服务模板能力接入:智能填充、一多适配、华为地图
美业元服务模板功能增强:UI改版
美业元服务模板新增组件:基础组件、个人信息组件
点餐元服务模板能力接入:一多适配、服务卡片
点餐元服务模板功能增强:首页新增城市选择、扫一扫、预约订座和排队取号;新增24卡片、适配一多折叠屏、优化堂食和外带场景、优化钱包和积分功能、组件元服务胶囊、兼容平板设备弹窗、兼容模拟器充值功能
点餐元服务模板新增组件:选择店铺、搜索组件*
汽车驾考应用模板能力接入:开屏广告、华为分享、数字收银台、华为推送等功能
汽车驾考应用模板新增组件: 应用设置、会员、分享、个人信息、反馈组件
综合新闻应用模板功能增强:新增视频直播支持退后台小窗播放、数字报纸功能、广播功能、地方电视功能
综合新闻应用模板新增组件:登录、分享、意见反馈、隐私弹窗、应用设置、朗读、个人信息、网络请求模拟库axios-mock-adapter、日志库util-log组件
综合工具应用模板能力接入:华为推送、华为账号一键登录、开屏广告
综合工具应用模板新增组件:会员、登录、意见反馈、个人信息组件
电子书阅读应用模板功能增强:折叠屏和平板适配

组件

组件归属组件名称更新内容
美食菜谱应用模板菜谱瀑布流组件瀑布流增加下拉加载和上拉刷新;新增折叠屏适配
综合新闻应用模板短视频滑动组件修改视频横竖屏切换问题;修改互动内的视频在页面退出后,会在持续播放的问题;修改滑动进度条时不展示总时间的问题
综合新闻应用模板数字报纸组件首次发布
综合工具应用模板吉他调音器组件新增会员功能
综合工具应用模板修图神器组件新增会员功能
综合工具应用模板视频剪辑组件新增会员功能
综合工具应用模板解压缩组件新增会员功能;修复大文件压缩问题
美业元服务模板个人信息编辑组件接入智能填充服务;一多适配

更多模板&组件上新,敬请关注!
欢迎下载使用模板&组件“点击下载”,若您有体验和开发问题,或者相关心愿单
欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板开发者社群”,精彩上新&活动不错过!
image.png

【相关推荐】
👉HarmonyOS官方模板优秀案例系列持续更新,点击查看 往期案例汇总贴,欢迎收藏,方便查找!
👉【组件征集】HarmonyOS组件开发征集活动,点击参加
👉【HarmonyOS行业解决方案】为各行业鸿蒙应用提供全流程技术方案。点击查看

image.png

💡 鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞 💡
★ 更多内容,一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板
一键直达 HarmonyOS 行业解决方案

image.png

模板 | 求职招聘应用模板(点击下载

本模板为招聘类应用提供了常用功能的开发样例,模板主要分职位、消息、个人中心三大模块。本模板已集成华为账号、即时通讯、推送、分享等服务,支持深色模式、适老化、无障碍等特性,提供完整的招聘应用解决方案,只需做少量配置和定制即可快速实现招聘应用的核心功能。
image.png

模板 | 购物(回收)应用模板(点击下载

本模板为回收类应用提供了常用功能的开发样例,模板主要分首页和我的两大模块。本模板已集成回收服务、华为账号、订单流程、地址管理、银行卡等服务,支持创建订单、编辑地址、我的设置等特性,提供完整的回收服务应用解决方案,只需做少量配置和定制即可快速实现回收服务应用的核心功能。
image.png

模板 | 旅游攻略应用模板(点击下载

本模板为旅游类应用提供了常用功能的开发样例,模板主要分首页、行程、消息和个人中心四大模块。本模板已集成华为账号、微信登录、消息管理、应用更新检查、意见反馈、实名认证等服务,采用模块化架构设计,支持多设备适配,只需做少量配置和定制即可快速实现旅游攻略应用的核心功能。
image.png

组件 | 可分可合组件

归属模块名称简介
公交地铁应用模板公交地铁站点地图组件提供了展示当前位置信息、附近站点、路线规划、导航功能。
公交地铁应用模板站点线路组件提供公交地铁站点和线路的综合信息展示功能,支持导航、收藏、到站提醒及地图轨迹展示,还可查看同站线路。
求职招聘应用模板求职意向组件提供了求职意向管理功能,支持添加、编辑、删除求职意向信息,包括工作性质、意向职位、工作城市、行业类型、期望薪资和求职状态等信息的填写和管理。
回收应用模板回收估价表单组件提供手机/电脑回收估价表单能力。估价表单:通过 BuildValuationInfoPageBuilder 构建页面,支持型号、设备类型、选项选择与价格计算,并通过回调返回估价结果。
旅游攻略应用模板旅行足迹组件提供了旅行足迹管理的相关功能,支持足迹地图展示、总里程统计、足迹列表浏览、添加新足迹等能力。
旅游攻略应用模板发票编辑组件提供了发票管理的相关功能,支持发票列表展示、添加新发票、编辑现有发票、删除发票等能力。
旅游攻略应用模板路线规划组件提供了旅游路线规划功能,支持用户选择出发地、目的地和游玩天数,并根据选择智能推荐旅游路线。
壁纸应用模板壁纸详情组件提供了壁纸详情展示功能,其中包含:作者名称、作者头像、壁纸分类、壁纸关键词、以及收藏、取消收藏、预览、下载、设为壁纸、左右切换壁纸等功能。

更多模板&组件上新,敬请关注!
欢迎下载使用模板&组件“点击下载”,若您有体验和开发问题,或者相关心愿单
欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板开发者社群”,精彩上新&活动不错过!

image.png

【相关推荐】

👉 HarmonyOS官方模板优秀案例系列持续更新, 点击查看 往期案例汇总贴,欢迎收藏,方便查找!
👉【组件征集】HarmonyOS组件开发征集活动,点击参加
👉【HarmonyOS行业解决方案】为各行业鸿蒙应用提供全流程技术方案。点击查看

在这里插入图片描述

摘要

随着鸿蒙应用逐步走向国际化,应用不再只面对中文和英文用户。
中东、北非 等地区,阿拉伯语、希伯来语 这类 从右到左(RTL)语言 是主流,如果应用在这些语言环境下:

  • 布局顺序是反的
  • 返回按钮方向不对
  • 文字对齐看着很别扭

那基本可以直接劝退用户。

好消息是:
鸿蒙系统对 RTL 是原生支持的,而且大部分情况下是“自动完成”的。
坏消息是:
一旦你写了不该写的代码,系统也救不了你。

这篇文章就从真实开发角度,聊清楚鸿蒙里 RTL 适配到底该怎么做、哪些地方最容易踩坑,以及在真实页面里该怎么写。

引言

在早期做 Android / Web 国际化时,RTL 基本属于“高级需求”,很多项目甚至直接忽略。
但在鸿蒙生态里,国际化是默认要考虑的事情,尤其是:

  • 智能设备出海
  • 海外 ROM
  • 多语言系统级应用

在这些场景下,RTL 不再是“锦上添花”,而是基础能力

鸿蒙的设计理念其实很明确:

系统帮你做方向适配,你只要别把方向写死。

问题就在于:
很多开发者在不知不觉中,把方向写死了。

鸿蒙对 RTL 的整体支持机制

系统层是自动感知的

当系统语言切换为 RTL 语言时,鸿蒙会自动做这些事情:

  • 整体布局方向切换为 RTL
  • 文本阅读方向切换
  • Row / Flex 子组件顺序镜像
  • 列表、导航组件交互方向变化

前提只有一个:
你的代码要写得“语义化”。

布局方向适配的核心原则

永远不要写死 left / right

这是 RTL 适配里最常见、也是最致命的问题

错误示例(真实项目里经常看到)

Text('返回')
  .margin({ left: 16 })

这段代码在中文、英文环境下完全正常,
但在 RTL 环境下:

  • 系统已经整体翻转
  • 你又强行加了 left
  • 结果就是布局看起来“很怪”

正确示例(推荐写法)

Text('返回')
  .margin({ start: 16 })

这里的 start 是一个语义方向

  • LTR 语言下等价于 left
  • RTL 语言下等价于 right

你不用管语言,系统会帮你算。

Demo:基础 RTL 自适应 Row

下面是一个可以直接运行的 Demo,你只需要切换系统语言就能看到效果。

@Entry
@Component
struct RtlBaseDemo {
  build() {
    Row() {
      Image($r('app.media.arrow'))
        .width(24)
        .height(24)

      Text('返回')
        .margin({ start: 8 })
    }
    .padding({ start: 16, end: 16 })
  }
}

这个 Demo 的特点:

  • 没有写 left / right
  • 没有强制方向
  • 图标和文字顺序会自动镜像

在阿拉伯语系统下,你会发现:

  • 箭头跑到了右侧
  • 文本在左
  • 间距依然正确

文本方向与对齐的正确方式

文本不要写 Left / Right 对齐

很多人习惯性这样写:

Text('مرحبا')
  .textAlign(TextAlign.Left)

问题是:
Left 在 RTL 里并不是“阅读起点”。

正确的写法是:

Text('مرحبا')
  .textAlign(TextAlign.Start)

系统会自动判断:

  • 英文 → 左对齐
  • 阿拉伯语 → 右对齐

Demo:多语言文本展示

@Entry
@Component
struct TextAlignDemo {
  build() {
    Column() {
      Text('Hello HarmonyOS')
        .textAlign(TextAlign.Start)
        .fontSize(18)

      Text('مرحبا هارموني')
        .textAlign(TextAlign.Start)
        .fontSize(18)
    }
    .padding(16)
  }
}

这个 Demo 非常适合用来自测
切换系统语言,你能直观看到对齐方向变化。

结合真实业务场景的 RTL 适配实践

场景一:应用顶部导航栏

这是 RTL 最容易翻车的地方。

典型需求

  • 返回按钮
  • 页面标题

正确实现方式

@Component
struct TitleBar {
  build() {
    Row() {
      Image($r('app.media.back'))
        .width(24)
        .height(24)

      Text('设置')
        .margin({ start: 12 })
        .fontSize(20)
    }
    .padding(16)
  }
}

这里的关键点:

  • 不指定 FlexDirection
  • 使用 start 间距
  • 图标自动镜像

系统语言一换,整个标题栏方向自然就对了。

场景二:设置页列表项

设置页通常是左右结构,比如:

  • 左边是标题
  • 右边是开关或箭头

推荐写法

@Component
struct SettingItem {
  build() {
    Row() {
      Text('通知')
        .layoutWeight(1)

      Image($r('app.media.arrow'))
        .width(16)
    }
    .padding({ start: 16, end: 16, top: 12, bottom: 12 })
  }
}

在 RTL 下:

  • 文本会靠右
  • 箭头会跑到左侧
  • 整体阅读顺序符合习惯

你不需要为 RTL 单独写一套 UI。

场景三:列表页面与滑动方向

鸿蒙的 List 在 RTL 下:

  • 排列顺序自动调整
  • 滑动方向符合阅读习惯

示例代码

@Entry
@Component
struct ListDemo {
  build() {
    List() {
      ForEach(['Item A', 'Item B', 'Item C'], (item: string) => {
        ListItem() {
          Text(item)
            .padding(16)
            .textAlign(TextAlign.Start)
        }
      })
    }
  }
}

只要你不去强制对齐方向,列表在 RTL 下基本是“零成本适配”。

QA:开发中常见问题

Q1:需要手动判断当前是不是 RTL 吗?

一般不需要。
90% 的页面交给系统就够了。

只有在:

  • 自定义绘制
  • 特殊动画
  • 非标准交互

这些场景下,才需要手动处理。

Q2:图片什么时候需要手动镜像?

  • 返回箭头
  • 方向性极强的图标

可以使用:

Image($r('app.media.arrow'))
  .mirror(true)

普通装饰性图片不建议镜像。

Q3:为什么我写了 start / end 还是不生效?

通常是因为:

  • 强制写了 FlexDirection.Row
  • 写死了 Alignment.Left
  • 在父容器里破坏了方向规则

RTL 出问题,优先回头检查是不是哪一层写死了方向

总结

鸿蒙里的 RTL 适配,其实不是“多写代码”,而是“少犯错误”。

一句话经验总结:

  • start / end
  • TextAlign.Start
  • 不强制方向
  • 相信系统

只要遵守这几条规则,
绝大多数 RTL 问题都会在你“什么都没做”的情况下自动解决。

在这里插入图片描述

摘要(背景与现状)

随着鸿蒙系统在手机、平板、穿戴设备以及 IoT 场景中的逐步落地,同一套应用需要面向不同国家、不同地区、不同语言和政策环境已经成为常态。
在实际项目中,我们经常会遇到这些问题:

  • 不同地区展示的文案不一样
  • 某些功能在特定地区不能上线
  • 活动内容、公告、支付方式存在地区差异

如果地区适配逻辑处理得不好,就很容易出现代码混乱、维护成本高、后期改动困难的问题。

本文结合鸿蒙系统(HarmonyOS / OpenHarmony)的实际开发方式,从系统能力、资源机制和业务逻辑三个层面,总结一套可落地、好维护的地区特定内容实现方案。

引言(发展情况与应用场景)

从早期 Android / iOS 开发经验来看,地区适配往往依赖大量 if-else 判断,代码里到处是国家缩写,后期维护非常痛苦。
鸿蒙在设计之初,就在国际化与地区适配方面做了比较完整的能力封装,比如:

  • 系统级语言和地区识别
  • 资源文件按地区自动匹配
  • ArkUI 对多语言、多地区资源的天然支持

在真实项目中,大多数地区定制需求并不复杂,核心思路其实只有一句话:

先交给系统做资源适配,实在不行再写判断逻辑。

下面我们一步一步来看具体实现方式。

鸿蒙地区特定内容的整体实现思路

在鸿蒙系统中,地区定制通常可以拆分为三个层次:

  1. 系统层:获取当前设备的语言和地区信息
  2. 资源层:根据地区自动加载不同资源
  3. 业务层:在运行时根据地区控制功能和内容

这三层并不是互斥的,而是经常组合使用。

通过系统语言和地区识别用户环境

获取系统地区信息

鸿蒙提供了 i18n 模块用于国际化相关能力,获取系统地区非常简单。

import i18n from '@ohos.i18n';

const locale: string = i18n.getSystemLocale();
console.info(`当前系统地区为: ${locale}`);

常见返回值包括:

  • zh-CN:中国大陆
  • zh-HK:香港地区
  • en-US:美国
  • ja-JP:日本

这个值通常在应用启动时获取一次即可。

基于地区进行基础内容控制

let isChinaRegion: boolean = false;

if (locale.startsWith('zh-CN')) {
  isChinaRegion = true;
}

在 ArkUI 页面中直接使用:

if (isChinaRegion) {
  Text('中国地区专属内容')
    .fontSize(16)
}

这种方式比较直观,适合少量差异控制,但不建议大量使用在文案层面。

通过资源文件实现地区内容自动适配

资源目录结构设计

这是鸿蒙中最推荐、维护成本最低的方式。

resources/
 ├─ base/
 │   └─ element/
 │       └─ string.json
 ├─ zh_CN/
 │   └─ element/
 │       └─ string.json
 ├─ en_US/
 │   └─ element/
 │       └─ string.json

不同地区资源内容示例

base 目录作为兜底资源:

{
  "welcome_text": "Welcome"
}

中国地区资源:

{
  "welcome_text": "欢迎使用(中国地区)"
}

美国地区资源:

{
  "welcome_text": "Welcome (US Version)"
}

ArkUI 中直接使用资源

Text($r('app.string.welcome_text'))
  .fontSize(18)

系统会根据当前设备地区自动匹配资源,不需要任何额外判断。

如果没有对应地区资源,就自动回退到 base。

结合运行时逻辑实现地区功能差异

在真实项目中,地区差异不仅体现在文案上,功能层面的限制更常见。

地区功能开关示例

let enablePayment: boolean = true;

if (!locale.startsWith('zh-CN')) {
  enablePayment = false;
}

ArkUI 中控制按钮展示:

if (enablePayment) {
  Button('立即支付')
    .width(200)
}

代码逻辑说明

  • 地区判断逻辑集中在一个地方
  • UI 只关心布尔状态,不直接判断地区
  • 后期调整地区规则只改一处代码

这种写法在中大型项目中特别重要。

结合实际业务场景的应用示例

场景一:地区公告与活动内容展示

不同地区活动内容变化频繁,适合服务端下发。

let requestParam = {
  locale: locale
};

服务器返回内容:

{
  "notice": "日本地区限定活动"
}

客户端展示:

Text(serverData.notice)

这种方式运营改内容不需要重新发版。

场景二:支付方式地区限制

function isPaymentSupported(locale: string): boolean {
  return locale.startsWith('zh-CN');
}
if (isPaymentSupported(locale)) {
  Button('使用本地支付')
}

清晰区分业务规则和 UI。

场景三:隐私协议与合规文案差异

通过资源文件区分不同地区隐私条款:

Text($r('app.string.privacy_policy'))

不同地区加载不同内容,避免代码层面处理复杂文本。

常见问题 QA

Q1:可以只用代码判断不做资源适配吗?

可以,但不推荐。
代码判断适合控制功能,不适合承载大量文案。

Q2:地区和语言一定是一一对应的吗?

不一定。
比如香港地区可能使用中文或英文,建议优先按语言,再结合地区判断。

Q3:地区变化时需要重启应用吗?

一般不需要,重新加载页面即可。
资源匹配通常在页面创建时生效。

总结

在鸿蒙系统中实现地区特定内容,其实并不复杂,关键在于合理分层

  • 文案和静态内容优先使用资源适配
  • 功能和业务规则使用少量逻辑判断
  • 活动和运营内容交给服务端

一句话概括就是:

资源适配解决大部分问题,代码只处理真正的差异逻辑。

在这里插入图片描述

摘要

在鸿蒙(HarmonyOS / OpenHarmony)应用和系统开发中,IO 操作几乎无处不在,比如文件读写、配置加载、日志输出、数据库访问以及 OTA 升级等。很多性能问题表面上看是应用卡顿、启动慢、耗电高,实际上根源都指向 IO 使用不当。本文结合当前鸿蒙系统的实际开发现状,从应用层和系统层两个角度,系统梳理 IO 性能优化的常见思路,并通过可运行的 Demo 代码,讲清楚这些优化在真实项目中该怎么落地。

文章整体偏向实战,语言尽量贴近日常开发交流,适合正在做鸿蒙应用、系统服务或设备升级相关开发的同学参考。

引言

随着鸿蒙生态逐渐完善,应用形态从早期的简单页面,发展到现在的多端协同、分布式能力、设备级应用,IO 压力明显变大。一方面,应用启动阶段要加载更多配置和资源;另一方面,系统服务、后台任务、设备升级都会产生大量读写操作。

在实际项目中,经常能看到下面这些情况:

  • 页面一打开就卡,结果发现主线程在读文件
  • 日志一多,设备开始明显发热
  • OTA 升级时间很长,写盘阶段占了一大半
  • 分布式数据一同步,前台体验明显下降

这些问题并不是鸿蒙系统本身性能不行,而是 IO 的使用方式不够合理。下面我们就从最常见、也最容易优化的地方开始讲。

鸿蒙 IO 性能瓶颈从哪来

在多数项目中,IO 性能问题通常集中在下面几个点:

  • 频繁进行小文件读写
  • 同步 IO 放在主线程执行
  • 每次用文件都重新 open 和 close
  • 没有任何缓存策略
  • 用文件存 KV 数据
  • 日志输出不受控制

只要命中其中一两条,性能基本都会出问题。

应用层 IO 优化(最常用)

IO 一定不要放在主线程

这是最基础,也是最容易踩坑的一点。ArkTS 中如果直接使用同步文件接口,UI 线程就会被直接卡住。

错误示例

import fs from '@ohos.file.fs';

let text = fs.readTextSync('/data/storage/test.txt');

这种写法在数据量稍微大一点时,页面就会出现明显卡顿。

推荐写法(异步 IO Demo)

import fs from '@ohos.file.fs';

export async function readFileAsync(path: string): Promise<string> {
  let file = await fs.open(path, fs.OpenMode.READ_ONLY);
  let buffer = new ArrayBuffer(4096);
  let result = '';

  let readLen = await fs.read(file.fd, buffer);
  if (readLen > 0) {
    result = String.fromCharCode(...new Uint8Array(buffer, 0, readLen));
  }

  await fs.close(file);
  return result;
}

代码说明

  • 使用 async/await,把 IO 操作放到异步任务中
  • 读取完成后再返回结果,不阻塞 UI
  • 真实项目中可以配合 taskpool 使用

合并小 IO,减少系统调用

很多性能问题不是数据量大,而是 IO 次数太多。

不推荐的写法

for (let i = 0; i < list.length; i++) {
  fs.writeSync(fd, list[i]);
}

推荐写法

let content = list.join('');
fs.writeSync(fd, content);

实际效果

  • 系统调用次数明显减少
  • 写盘效率更高
  • 对 Flash 存储更友好

引入内存缓存,避免重复读文件

配置文件、初始化数据非常适合放进内存缓存。

let configCache: string | null = null;

export async function getConfig(path: string): Promise<string> {
  if (configCache !== null) {
    return configCache;
  }
  configCache = await readFileAsync(path);
  return configCache;
}

使用场景

  • 应用启动配置
  • JSON 静态数据
  • 权限或状态信息

能用 Preferences 就别用文件

对于少量 KV 数据,文件 IO 的性价比非常低。

Preferences Demo

import preferences from '@ohos.data.preferences';

export async function saveUserInfo(context, userId: string) {
  let pref = await preferences.getPreferences(context, 'user_config');
  await pref.put('userId', userId);
  await pref.flush();
}

优点

  • 内部自带缓存
  • 自动批量落盘
  • 使用简单,性能稳定

系统层 IO 优化(Native / 服务侧)

使用缓冲 IO

在系统服务或 Native 模块中,直接写裸 IO 往往效率不高。

#include <stdio.h>

void writeFile(const char* path, const char* data, size_t len) {
    FILE* fp = fopen(path, "w");
    if (!fp) return;

    setvbuf(fp, nullptr, _IOFBF, 8 * 1024);
    fwrite(data, 1, len, fp);
    fclose(fp);
}

说明

  • 设置 8KB 缓冲区
  • 减少实际写盘次数
  • 适合大量顺序写场景

顺序 IO 优于随机 IO

off_t offset = 0;
pread(fd, buffer, size, offset);
offset += size;

尽量避免频繁 seek 和交叉读写多个文件。

控制日志 IO

日志在调试阶段很有用,但在正式环境中是 IO 隐形杀手。

if (__DEV__) {
  console.info('debug log');
}

建议:

  • 发布版本关闭 debug 和 info
  • 避免循环内打印日志
  • 合并日志输出

典型应用场景分析

场景一:应用启动阶段加载配置

问题

启动慢,页面白屏时间长。

解决方案

  • 异步读取配置
  • 内存缓存
await getConfig('/data/storage/app_config.json');

场景二:OTA 升级文件写入

问题

升级包大,写盘耗时长。

优化思路

  • 分块下载
  • 分块写入
  • 写完再统一校验
async function writeChunk(fd: number, data: Uint8Array) {
  await fs.write(fd, data.buffer);
}

场景三:日志过多导致设备发热

问题

设备运行一段时间后发热、掉帧。

解决方案

  • 控制日志级别
  • 关闭非必要日志

常见问题 QA

Q:异步 IO 一定比同步快吗?
A:不一定,但一定不会卡 UI。

Q:缓存会不会导致数据不一致?
A:需要设计好更新策略,配置类数据问题不大。

Q:文件和 RDB 怎么选?
A:结构化数据选 RDB,大文件选文件。

总结

IO 性能优化并不复杂,关键在于使用方式是否合理。大多数性能问题,并不是因为设备性能不足,而是 IO 用得太随意。

简单总结几句话:

  • IO 不要放主线程
  • 少做小 IO,多做批量 IO
  • 能缓存就缓存
  • 能不用文件就不用文件
  • 日志一定要克制

这些原则在应用层、系统层、OTA 场景中都是通用的。如果你正在做鸿蒙系统相关开发,把 IO 优化当成基本功,会少踩很多坑。

在这里插入图片描述

摘要

在 HarmonyOS 的 ArkUI 开发中,经常会遇到这样一种交互需求:
用户按下某个组件,拖动它,然后在松手的一瞬间触发一个“释放”动作,比如飞出去、回弹、投放到某个区域,或者触发业务逻辑。

很多同学在一开始都会问一个问题:
ArkUI 里有没有现成的“施放 API”?

答案是:没有。
但 ArkUI 提供的 手势系统、状态管理和动画能力,已经足够我们组合出各种“施放效果”。

这篇文章就从一个最基础的拖拽开始,一步一步讲清楚:
ArkUI 中的“施放功能”到底是怎么实现的,以及在真实项目中该怎么用。

引言

随着 HarmonyOS 应用交互越来越偏向“自然操作”,像拖拽、投放、抛出这类交互,在实际项目中出现得非常多,比如:

  • 卡片拖到指定区域触发操作
  • 图标长按后丢进回收区
  • 功能模块拖拽排序
  • 智能设备管理中,把设备“丢”进分组

在 ArkUI 里,这些效果并不是某一个组件单独完成的,而是多种能力的组合
理解这一点之后,你会发现实现起来并不复杂,而且扩展性非常强。

ArkUI 中“施放”的本质是什么

从技术角度来看,所谓“施放”,本质就是三步:

  1. 用手势感知用户操作
  2. 用状态驱动组件位置变化
  3. 在松手时,通过动画完成“释放效果”

换句话说就是:
手势负责输入,状态负责位置,动画负责感觉。

最基础的施放实现:拖拽 + 松手回弹

实现思路

这个 Demo 不考虑目标区域,只关注三件事:

  • 手指拖动时,组件跟着动
  • 松手后触发动画
  • 动画结束后回到原位

可运行 Demo 示例

@Entry
@Component
struct CastBasicDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Column() {
      Text('拖拽组件,松手后施放')
        .fontSize(18)
        .margin(20)

      Box()
        .width(80)
        .height(80)
        .backgroundColor(Color.Blue)
        .translate({ x: this.offsetX, y: this.offsetY })
        .gesture(
          PanGesture()
            .onUpdate((event) => {
              // 拖动过程中,组件位置实时更新
              this.offsetX = event.offsetX
              this.offsetY = event.offsetY
            })
            .onEnd(() => {
              // 松手瞬间,触发“施放”动画
              animateTo({
                duration: 300,
                curve: Curve.EaseOut
              }, () => {
                this.offsetX = 0
                this.offsetY = 0
              })
            })
        )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

代码讲解(重点)

这里其实就三行是核心:

this.offsetX = event.offsetX
this.offsetY = event.offsetY

组件的位置完全由 @State 控制,手势只是不断修改状态。

而“施放”的感觉来自这里:

animateTo({}, () => {
  this.offsetX = 0
  this.offsetY = 0
})

只要状态变化发生在动画作用域内,就会自动过渡。

带目标区域的“施放”:成功 or 回弹

在真实项目中,施放通常不是随便松手就算成功,而是:

  • 拖到某个区域才成功
  • 没拖到就回弹

思路拆解

  • 拖拽过程中,持续记录位移
  • 松手时判断最终位置
  • 根据结果执行不同动画

示例代码

@Entry
@Component
struct CastTargetDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0

  build() {
    Stack() {
      // 目标区域
      Box()
        .width(120)
        .height(120)
        .backgroundColor(Color.Grey)
        .position({ x: 200, y: 300 })

      // 可施放组件
      Box()
        .width(80)
        .height(80)
        .backgroundColor(Color.Green)
        .translate({ x: this.offsetX, y: this.offsetY })
        .gesture(
          PanGesture()
            .onUpdate((event) => {
              this.offsetX = event.offsetX
              this.offsetY = event.offsetY
            })
            .onEnd(() => {
              if (this.offsetX > 150 && this.offsetY > 250) {
                // 施放成功,吸附到目标
                animateTo({ duration: 200 }, () => {
                  this.offsetX = 200
                  this.offsetY = 300
                })
              } else {
                // 失败,回弹
                animateTo({ duration: 300 }, () => {
                  this.offsetX = 0
                  this.offsetY = 0
                })
              }
            })
        )
    }
    .width('100%')
    .height('100%')
  }
}

这里在做什么判断

if (this.offsetX > 150 && this.offsetY > 250)

这本质上是一个区域命中判断
在正式项目中,你可以:

  • 根据组件尺寸动态计算
  • 封装成工具函数
  • 甚至引入碰撞检测逻辑

真实应用场景示例

场景一:卡片拖拽投放到功能区

典型应用
首页卡片管理、模块编辑模式。

示例核心代码

.onEnd(() => {
  if (this.offsetX > 180) {
    animateTo({ duration: 200 }, () => {
      this.offsetX = 220
      this.offsetY = 0
    })
    // 这里可以触发业务逻辑,比如加入列表
  } else {
    animateTo({ duration: 300 }, () => {
      this.offsetX = 0
      this.offsetY = 0
    })
  }
})

逻辑上非常清晰:
UI 动画和业务逻辑是分开的,不会互相影响。

场景二:图标拖进回收站

这种交互非常常见,关键点是:

  • 松手瞬间让组件消失
  • 而不是回弹
.onEnd(() => {
  if (this.offsetY > 400) {
    animateTo({ duration: 200 }, () => {
      this.offsetY = 600
    })
  } else {
    animateTo({ duration: 300 }, () => {
      this.offsetX = 0
      this.offsetY = 0
    })
  }
})

你也可以配合透明度一起做:

.opacity(this.isRemoved ? 0 : 1)

场景三:设备管理中的“拖拽分组”

结合你后续可能做的鸿蒙设备管理场景:

  • 左侧设备列表
  • 右侧分组区域
  • 拖拽设备到分组完成绑定

这时就可以升级到 Drag & Drop,实现跨组件投放。

Box()
  .draggable(true)
  .onDragStart(() => {
    return { data: 'device-id-001' }
  })

目标区域:

Column()
  .onDrop((event) => {
    console.log('接收到设备:', event.data)
  })

这种方式更适合复杂业务。

QA 常见问题

Q1:为什么不用绝对定位?

绝对定位是死的,而 translate 是基于状态的,动画过渡更自然,也更安全。

Q2:施放动画卡顿怎么办?

  • 确保只操作必要的状态
  • 避免在 onUpdate 里写复杂逻辑
  • 动画时间不要太长

Q3:PanGesture 和 Drag 怎么选?

  • 单组件内部效果:PanGesture
  • 跨组件、跨区域:Drag & Drop

总结

在 ArkUI 中,“施放功能”并不是某一个 API,而是一种交互设计模式

  • 手势负责感知用户行为
  • 状态决定组件位置
  • 动画塑造最终体验

只要你理解了这个组合思路,就可以根据项目需求,灵活实现各种拖拽、投放、释放效果,而且代码非常干净、可维护性也很好。

在这里插入图片描述

摘要

这两年,跨屏协作在鸿蒙生态里出现得越来越频繁。
从最早的文件互传、多屏办公,到现在的教育课堂、车机联动,设备之间已经不再是“各干各的”。

在游戏领域,这个变化更明显:

  • 一块屏幕已经不够玩
  • 玩家希望多设备一起参与
  • 大屏负责画面,小屏负责操作

但很多开发者一提“跨屏游戏”,第一反应还是投屏、远程控制、镜像显示。
实际上,鸿蒙给的不是投屏方案,而是一整套分布式游戏协作能力

这篇文章就从游戏开发者的真实视角,讲清楚鸿蒙是如何把多设备变成“一个游戏系统”的。

引言

在传统系统里,如果你想做多设备协作游戏,通常意味着:

  • 自己写网络协议
  • 自己做设备发现
  • 自己处理数据一致性
  • 自己兜底各种异常情况

而在 HarmonyOS 里,这些事情被系统层直接兜住了:

  • 设备发现靠软总线
  • 状态同步靠分布式数据
  • UI 跨屏靠 Ability 调度

你要做的事情更偏向游戏逻辑设计本身,而不是重复造轮子。

接下来我们一步一步拆。

什么是鸿蒙里的跨屏游戏协作

跨屏不是投屏

先说一个很重要的点:

鸿蒙的跨屏游戏 ≠ 投屏

投屏的特点是:

  • 一端渲染
  • 另一端只是显示
  • 没有真正的协作逻辑

而鸿蒙的跨屏游戏,更像是:

  • 多设备同时运行
  • 各自承担不同功能
  • 通过系统级分布式能力协同

比如:

  • 手机只负责操作和技能
  • 平板或智慧屏负责主战场渲染
  • 游戏状态在多设备之间自动同步

一个最常见的跨屏游戏形态

手机(控制器)
  │
  │ 操作指令
  ▼
平板 / 智慧屏(主画面)
  │
  │ 游戏状态同步
  ▼
分布式数据中心

支撑跨屏游戏的三大核心能力

分布式软总线:设备能“找到彼此”

在游戏里,你最关心的不是网络协议,而是:

  • 能不能快速发现附近设备
  • 延迟够不够低
  • 掉线能不能感知

鸿蒙的分布式软总线解决的正是这些问题。

你不需要关心设备是:

  • Wi-Fi
  • 蓝牙
  • 局域网
  • 点对点

系统会自动选最优链路。

分布式数据管理:状态天然同步

跨屏游戏最怕的几个问题:

  • 状态不一致
  • 数据打架
  • 玩家看到的画面不同步

鸿蒙提供的分布式 KV 数据,天生适合游戏里的:

  • 玩家位置
  • 血量
  • 技能状态
  • 回合阶段

而且是系统级同步,不是你自己发包。

分布式 UI:屏幕不是绑死的

在鸿蒙里:

  • Ability 可以被拉起到其他设备
  • 游戏不用重新启动
  • 状态不需要你手动迁移

这对游戏来说很重要,因为你可以自由设计:

  • 哪个屏幕显示什么
  • 玩家如何参与
  • 随时切换设备角色

跨屏游戏的整体架构设计

一个可落地的结构示例

┌────────────┐
│ 手机端     │
│ 操作输入   │
│ 技能按钮   │
└─────┬──────┘
      │
      │ 分布式 KV 数据
      ▼
┌────────────┐
│ 平板端     │
│ 游戏主画面 │
│ 渲染逻辑   │
└────────────┘

手机不负责画面,平板不负责输入,各司其职。

实战核心:跨屏游戏状态同步 Demo

创建分布式 KV Store

import distributedData from '@ohos.data.distributedData';

const kvManager = distributedData.createKVManager({
  bundleName: 'com.example.crossgame',
  context: getContext()
});

const store = await kvManager.getKVStore('gameStore', {
  kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
  securityLevel: distributedData.SecurityLevel.S1
});

这个 store 在多设备之间是共享的。

手机端发送操作指令

// 模拟摇杆方向
async function sendMove(x: number, y: number) {
  await store.put('player_move', JSON.stringify({
    x,
    y,
    time: Date.now()
  }));
}

这里同步的是“操作”,而不是最终坐标。

平板端监听并更新角色

store.on('dataChange', (data) => {
  data.insertedEntries.forEach(entry => {
    if (entry.key === 'player_move') {
      const move = JSON.parse(entry.value as string);
      updatePlayer(move.x, move.y);
    }
  });
});

跨屏 UI:把主画面拉到大屏

从手机拉起平板的游戏界面

import featureAbility from '@ohos.ability.featureAbility';

featureAbility.startAbility({
  want: {
    bundleName: 'com.example.crossgame',
    abilityName: 'GameMainAbility',
    deviceId: 'remoteDeviceId'
  }
});

前提是:

  • 游戏状态已经存在分布式数据中
  • 新设备启动后直接读取即可

为什么这个能力对游戏很重要

你不需要:

  • 手动传进度
  • 重新初始化状态
  • 处理复杂的恢复逻辑

系统已经帮你兜底。

真实应用场景拆解

场景一:手机当手柄,大屏玩游戏

适合类型

  • 派对游戏
  • 本地多人
  • 家庭娱乐

逻辑示例

// 手机端:技能释放
await store.put('skill_cast', {
  skillId: 2,
  playerId: 'p1'
});
// 大屏端:技能响应
store.on('dataChange', (data) => {
  data.insertedEntries.forEach(e => {
    if (e.key === 'skill_cast') {
      castSkill(e.value);
    }
  });
});

场景二:非对称协作游戏

比如:

  • 一个人当指挥
  • 一个人实际操作
// 指挥端下达命令
await store.put('command', {
  type: 'attack',
  target: 'boss'
});

操作端只负责执行,不做决策。

场景三:教育 + 游戏化互动

老师平板控制节奏,学生手机参与。

// 教师端切换关卡
await store.put('game_stage', 'level_2');

学生端监听并同步切换界面。

常见问题 QA

Q1:分布式 KV 会不会太慢?

不会。
它适合的是:

  • 低频状态
  • 操作指令
  • 游戏阶段

高频帧同步需要更底层方案。

Q2:能不能用在竞技类游戏?

可以,但不建议直接用 KV 同步帧数据。
更适合:

  • 操作同步
  • 客户端预测
  • 状态校正

Q3:设备掉线怎么办?

KV 会自动触发变更事件,你可以监听:

  • 玩家退出
  • 状态回收
  • AI 接管

总结

从游戏开发角度看,鸿蒙的跨屏协作并不是噱头,而是一套真正能落地的系统能力

核心就一句话:

多设备在鸿蒙里,不是多个客户端,而是一个分布式游戏系统。

  • 软总线解决连接
  • 分布式数据解决同步
  • Ability 解决跨屏 UI
  • ArkTS 足够把 Demo 跑起来

在这里插入图片描述

摘要

随着智能终端越来越多,应用早就不再只运行在一台设备上。手机、平板、智慧屏、手表之间的协作,已经成了很常见的需求。在这种背景下,多设备任务该怎么分、分到哪台设备执行,就成了开发中绕不开的问题。

在鸿蒙系统中,这个问题并不是靠开发者“手动指定设备”来解决的,而是通过 设备能力感知 + 分布式调度机制 来完成。开发者更多关心的是:
这个任务适合干什么,而不是非要在哪台设备干。

本文会结合鸿蒙系统的分布式能力,介绍多设备任务分配的整体思路,并通过可运行的 Demo 代码,把这个过程完整跑一遍,最后再结合几个真实场景,聊聊它在实际项目中该怎么用。

引言

如果放在以前,一个应用基本只跑在一台手机上,最多考虑前后台切换。但现在不一样了:

  • 手机在你手里
  • 平板在桌子上
  • 智慧屏在客厅
  • 手表戴在手上

用户希望的是:
设备不同,但体验是连着的。

鸿蒙系统的分布式能力,正是为这种场景设计的。它不是简单的“跨设备通信”,而是把 任务、数据、能力 都变成可以在多设备之间流动的资源。

而多设备任务分配,本质上就是一句话:

把合适的任务,交给合适的设备去做。

鸿蒙多设备任务分配的整体思路

先发现设备,再谈分配

在鸿蒙系统中,只要设备在同一个分布式网络里,系统就能自动发现它们。
开发者不需要自己维护“设备表”,也不用关心设备什么时候上线、下线。

系统会帮你感知这些信息:

  • 设备类型(手机、平板、智慧屏)
  • 基本性能情况
  • 是否可信
  • 当前是否可用

你只需要在合适的时机拿到设备列表即可。

任务一定要能拆

多设备任务分配的前提是:
你的业务本身是能拆开的。

比如:

  • 页面展示是一块
  • 数据采集是一块
  • 计算处理是一块

如果一个任务从头到尾全写死在一个 Ability 里,那基本就没法分配了。

系统负责“怎么选设备”

在鸿蒙里,真正“选哪台设备执行”的逻辑,大部分是系统完成的:

  • 当前设备忙不忙
  • 网络情况好不好
  • 设备能力是否匹配
  • 是否更适合本地执行

开发者更多是通过 Ability 启动方式、Service 类型、数据同步方式 来间接影响分配结果。

核心实现方式一:跨设备启动 Ability

适合什么场景

这种方式最常见,适合:

  • 页面展示
  • 功能模块整体迁移
  • 用户可感知的交互任务

比如:
手机负责控制,平板负责显示大屏内容。

Demo:在平板上启动远程 Ability

import distributedDeviceManager from '@ohos.distributedDeviceManager';
import featureAbility from '@ohos.ability.featureAbility';

const BUNDLE_NAME = 'com.example.distributeddemo';

let deviceManager = distributedDeviceManager.createDeviceManager(BUNDLE_NAME);

function startRemotePage() {
  let devices = deviceManager.getTrustedDeviceListSync();

  devices.forEach(device => {
    if (device.deviceType === 2) { // 假设 2 表示平板
      let want = {
        bundleName: BUNDLE_NAME,
        abilityName: 'RemotePageAbility',
        deviceId: device.deviceId
      };
      featureAbility.startAbility(want);
    }
  });
}

代码说明

  • createDeviceManager:创建设备管理器
  • getTrustedDeviceListSync:获取可信设备列表
  • deviceType:用于简单区分设备类型
  • startAbility:指定 deviceId 后,Ability 会在远端设备启动

整个过程不需要你关心远端设备的进程、生命周期,系统会处理。

核心实现方式二:分布式 Service 执行任务

适合什么场景

这种方式更适合:

  • 计算密集型任务
  • 后台处理
  • 不需要 UI 的逻辑

比如:
手机采集数据,交给性能更强的设备做分析。

Demo:连接远端计算 Service

import featureAbility from '@ohos.ability.featureAbility';

function connectRemoteService(remoteDeviceId: string) {
  let want = {
    bundleName: 'com.example.distributeddemo',
    abilityName: 'ComputeServiceAbility',
    deviceId: remoteDeviceId
  };

  featureAbility.connectAbility(want, {
    onConnect(elementName, remote) {
      console.log('远程 Service 已连接');
      remote.sendMessage({
        command: 'startCompute',
        data: [1, 2, 3, 4]
      });
    },
    onDisconnect() {
      console.log('远程 Service 已断开');
    }
  });
}

代码说明

  • Service 在远端设备运行
  • 本地通过 IPC 的方式和远端通信
  • 计算逻辑完全在远端执行
  • 本地只负责发请求、收结果

这种方式非常适合“重计算、轻交互”的任务。

典型应用场景分析与示例

场景一:手机 + 平板的学习展示系统

场景说明

  • 手机负责控制、翻页
  • 平板负责展示课件内容

实现思路

  • 手机发现平板
  • 在平板启动展示 Ability
  • 通过分布式数据同步当前页码
import distributedData from '@ohos.data.distributedData';

async function syncPage(page: number) {
  let kvManager = distributedData.createKVManager();
  let store = await kvManager.getKVStore('pageStore');
  await store.put('current_page', page);
}

平板端监听数据变化,自动刷新页面。

场景二:多设备健康数据分析

场景说明

  • 手表采集心率
  • 手机做基础处理
  • 平板做数据可视化

实现思路

  • 手表同步原始数据
  • 手机过滤、预处理
  • 平板负责展示图表

核心在于:
任务不是“复制”,而是“分工”。

场景三:家庭智慧屏协同控制

场景说明

  • 手机是遥控器
  • 智慧屏负责 UI 展示
  • 计算逻辑放在智慧屏

实现思路

  • 手机只负责发指令
  • 智慧屏 Service 处理业务逻辑
  • 结果同步回手机

这种模式下,手机压力很小,体验反而更流畅。

常见问题 QA

Q1:我能不能指定“一定要某台设备执行”?

不推荐。
鸿蒙的设计思想是 声明需求,而不是指定设备
你可以通过能力需求去“引导”,但不建议写死。

Q2:设备突然下线怎么办?

系统会通知连接断开,
你需要做的只有一件事:
支持本地降级执行或重试。

Q3:分布式任务一定比本地慢吗?

不一定。
当任务本身就不适合本地执行时,
分布式反而更快、更省电。

总结

在鸿蒙系统中,多设备任务分配并不是一套复杂、难以理解的机制,它的核心思想其实很简单:

  • 把任务拆清楚
  • 描述好任务需求
  • 把调度交给系统

只要你在设计阶段考虑好“哪些任务适合分出去”,鸿蒙的分布式能力就能自然地帮你把事情做好。

一句话总结就是:

多设备任务分配,不是设备协作有多复杂,而是你有没有把任务设计清楚。

在这里插入图片描述

摘要

随着 HarmonyOS / OpenHarmony 在手机、平板、智慧屏、车机等多设备上的落地,应用的复杂度正在明显提升。页面不再只是简单展示,而是伴随着网络请求、数据计算、设备协同等大量逻辑。如果这些逻辑处理不当,很容易出现页面卡顿、点击无响应,甚至 Ability 被系统回收的问题。

线程阻塞,已经成为鸿蒙应用开发中最容易踩坑、也最影响体验的问题之一。本文将结合实际开发场景,用尽量口语化的方式,聊一聊在鸿蒙系统中如何系统性地避免线程阻塞,并给出可以直接运行的 Demo 代码。

引言

在早期的应用开发中,很多开发者习惯把逻辑直接写在点击事件里,或者在页面加载时同步读取数据。这种写法在简单页面中问题不大,但在 HarmonyOS 这种强调流畅体验和多设备协同的系统中,很容易暴露问题。

鸿蒙的 UI 是声明式的,系统对主线程(UI 线程)非常敏感。一旦主线程被占用,页面掉帧、动画卡住、操作延迟都会立刻出现。因此,理解哪些操作会阻塞线程,以及如何把这些操作合理地“挪走”,是每个鸿蒙开发者绕不开的一课。

下面我们从原理、工具、代码和真实场景几个角度,完整地拆解这个问题。

为什么线程阻塞在鸿蒙中这么致命

UI 线程到底在忙什么

在 HarmonyOS 中,UI 线程主要负责三件事:

  • ArkUI 页面渲染
  • 用户事件分发(点击、滑动等)
  • Ability 生命周期回调

简单理解就是:只要和“看得见、点得动”有关的事情,几乎都在 UI 线程上完成

一旦你在这里做了耗时操作,比如计算、IO、网络等待,页面就会立刻表现出“卡”的感觉。

常见的阻塞来源

在实际项目中,最容易导致阻塞的操作通常包括:

  • 同步网络请求
  • 文件读写
  • 数据库查询
  • 大量 for 循环计算
  • 人为 sleep 或死循环

这些操作本身不一定是错的,问题在于它们被放在了不该放的线程上

鸿蒙中避免线程阻塞的核心思路

一个总原则

可以把鸿蒙里的线程使用总结成一句话:

UI 线程只处理 UI,其他事情交给异步、线程池或 Worker。

围绕这个原则,系统也提供了多种工具,帮助开发者把任务“分流”。

异步编程是第一道防线

使用 async / await 处理耗时逻辑

在 ArkTS 中,官方推荐优先使用 Promise 和 async / await。它的好处是代码结构清晰,而且不会阻塞 UI 线程。

示例:页面加载网络数据

@Entry
@Component
struct AsyncDemo {
  @State message: string = '加载中...'

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
        .margin(20)

      Button('重新加载')
        .onClick(() => {
          this.loadData()
        })
    }
  }

  async loadData() {
    this.message = '请求中...'
    let response = await fetch('https://example.com/data')
    let result = await response.text()
    this.message = result
  }
}

代码说明

  • loadData 使用 async 声明,不会阻塞 UI
  • await 只是暂停当前函数执行,不会卡住页面
  • UI 更新完全由状态变化驱动

这是最基础、也是最常用的一种防阻塞方式。

TaskPool:处理计算和 IO 的利器

什么时候该用 TaskPool

当你遇到下面这些情况时,TaskPool 几乎是必选项:

  • 大量计算
  • 批量数据处理
  • 文件压缩、解析

可运行 Demo 示例

import taskpool from '@ohos.taskpool'

@Concurrent
function calculateSum(count: number): number {
  let sum = 0
  for (let i = 0; i < count; i++) {
    sum += i
  }
  return sum
}

@Entry
@Component
struct TaskPoolDemo {
  @State result: string = '等待计算'

  build() {
    Column() {
      Text(this.result)
        .fontSize(18)
        .margin(20)

      Button('开始计算')
        .onClick(() => {
          this.startTask()
        })
    }
  }

  startTask() {
    this.result = '计算中...'
    taskpool.execute(calculateSum, 1000000).then(res => {
      this.result = `结果是:${res}`
    })
  }
}

代码说明

  • @Concurrent 表示该函数可以并发执行
  • TaskPool 自动管理线程,不需要开发者手动创建线程
  • UI 线程只负责接收结果和更新状态

在真实项目中,使用 TaskPool 往往能立刻解决页面卡顿问题。

Worker:长期后台任务的选择

Worker 的使用场景

如果任务具有下面这些特点,就更适合使用 Worker:

  • 长时间运行
  • 需要持续处理数据
  • 与 UI 强隔离

比如日志分析、音视频处理、复杂解析等。

示例:使用 Worker 处理数据

主线程代码

let worker = new Worker('workers/data_worker.ts')

worker.postMessage({ action: 'start' })

worker.onmessage = (e) => {
  console.log('收到结果:', e.data)
}

Worker 线程代码

onmessage = function (e) {
  if (e.data.action === 'start') {
    let result = 0
    for (let i = 0; i < 500000; i++) {
      result += i
    }
    postMessage(result)
  }
}

代码说明

  • Worker 与 UI 线程完全独立
  • 即使计算时间较长,也不会影响页面交互
  • 通过消息机制进行通信

结合实际场景的应用示例

场景一:列表页面加载大量数据

问题:

  • 首次进入页面时一次性处理全部数据
  • 页面明显卡顿

解决思路:

  • 网络请求使用 async
  • 数据整理放入 TaskPool
async loadList() {
  let data = await fetchData()
  taskpool.execute(processData, data).then(list => {
    this.list = list
  })
}

场景二:文件导入与解析

问题:

  • 文件较大
  • 解析过程耗时

解决思路:

  • Worker 负责解析
  • UI 只显示进度
worker.postMessage({ filePath })

场景三:复杂计算驱动 UI 更新

问题:

  • 计算逻辑和 UI 耦合

解决思路:

  • 计算完全放到 TaskPool
  • UI 只订阅结果

QA 环节

Q:async / await 会不会阻塞线程?
A:不会,它只是让出执行权,不会卡住 UI 线程。

Q:TaskPool 和 Worker 怎么选?
A:短期、一次性的任务优先 TaskPool,长期或持续任务用 Worker。

Q:能不能在生命周期里做耗时操作?
A:不建议,生命周期函数应尽量轻量。

总结

线程阻塞并不是某一个 API 的问题,而是设计问题。在 HarmonyOS 中,系统已经为我们准备好了异步模型、TaskPool 和 Worker,只要遵循“UI 线程只做 UI”的原则,大多数卡顿问题都可以提前避免。

在真实项目中,提前做好任务拆分、线程规划,比后期排查卡顿要省心得多。这也是鸿蒙开发从“能跑”到“跑得顺”的一个重要分水岭。

在这里插入图片描述

摘要

这两年,跨屏协作在鸿蒙生态里出现得越来越频繁。
从最早的文件互传、多屏办公,到现在的教育课堂、车机联动,设备之间已经不再是“各干各的”。

在游戏领域,这个变化更明显:

  • 一块屏幕已经不够玩
  • 玩家希望多设备一起参与
  • 大屏负责画面,小屏负责操作

但很多开发者一提“跨屏游戏”,第一反应还是投屏、远程控制、镜像显示。
实际上,鸿蒙给的不是投屏方案,而是一整套分布式游戏协作能力

这篇文章就从游戏开发者的真实视角,讲清楚鸿蒙是如何把多设备变成“一个游戏系统”的。

引言

在传统系统里,如果你想做多设备协作游戏,通常意味着:

  • 自己写网络协议
  • 自己做设备发现
  • 自己处理数据一致性
  • 自己兜底各种异常情况

而在 HarmonyOS 里,这些事情被系统层直接兜住了:

  • 设备发现靠软总线
  • 状态同步靠分布式数据
  • UI 跨屏靠 Ability 调度

你要做的事情更偏向游戏逻辑设计本身,而不是重复造轮子。

接下来我们一步一步拆。

什么是鸿蒙里的跨屏游戏协作

跨屏不是投屏

先说一个很重要的点:

鸿蒙的跨屏游戏 ≠ 投屏

投屏的特点是:

  • 一端渲染
  • 另一端只是显示
  • 没有真正的协作逻辑

而鸿蒙的跨屏游戏,更像是:

  • 多设备同时运行
  • 各自承担不同功能
  • 通过系统级分布式能力协同

比如:

  • 手机只负责操作和技能
  • 平板或智慧屏负责主战场渲染
  • 游戏状态在多设备之间自动同步

一个最常见的跨屏游戏形态

手机(控制器)
  │
  │ 操作指令
  ▼
平板 / 智慧屏(主画面)
  │
  │ 游戏状态同步
  ▼
分布式数据中心

支撑跨屏游戏的三大核心能力

分布式软总线:设备能“找到彼此”

在游戏里,你最关心的不是网络协议,而是:

  • 能不能快速发现附近设备
  • 延迟够不够低
  • 掉线能不能感知

鸿蒙的分布式软总线解决的正是这些问题。

你不需要关心设备是:

  • Wi-Fi
  • 蓝牙
  • 局域网
  • 点对点

系统会自动选最优链路。

分布式数据管理:状态天然同步

跨屏游戏最怕的几个问题:

  • 状态不一致
  • 数据打架
  • 玩家看到的画面不同步

鸿蒙提供的分布式 KV 数据,天生适合游戏里的:

  • 玩家位置
  • 血量
  • 技能状态
  • 回合阶段

而且是系统级同步,不是你自己发包。

分布式 UI:屏幕不是绑死的

在鸿蒙里:

  • Ability 可以被拉起到其他设备
  • 游戏不用重新启动
  • 状态不需要你手动迁移

这对游戏来说很重要,因为你可以自由设计:

  • 哪个屏幕显示什么
  • 玩家如何参与
  • 随时切换设备角色

跨屏游戏的整体架构设计

一个可落地的结构示例

┌────────────┐
│ 手机端     │
│ 操作输入   │
│ 技能按钮   │
└─────┬──────┘
      │
      │ 分布式 KV 数据
      ▼
┌────────────┐
│ 平板端     │
│ 游戏主画面 │
│ 渲染逻辑   │
└────────────┘

手机不负责画面,平板不负责输入,各司其职。

实战核心:跨屏游戏状态同步 Demo

创建分布式 KV Store

import distributedData from '@ohos.data.distributedData';

const kvManager = distributedData.createKVManager({
  bundleName: 'com.example.crossgame',
  context: getContext()
});

const store = await kvManager.getKVStore('gameStore', {
  kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
  securityLevel: distributedData.SecurityLevel.S1
});

这个 store 在多设备之间是共享的。

手机端发送操作指令

// 模拟摇杆方向
async function sendMove(x: number, y: number) {
  await store.put('player_move', JSON.stringify({
    x,
    y,
    time: Date.now()
  }));
}

这里同步的是“操作”,而不是最终坐标。

平板端监听并更新角色

store.on('dataChange', (data) => {
  data.insertedEntries.forEach(entry => {
    if (entry.key === 'player_move') {
      const move = JSON.parse(entry.value as string);
      updatePlayer(move.x, move.y);
    }
  });
});

跨屏 UI:把主画面拉到大屏

从手机拉起平板的游戏界面

import featureAbility from '@ohos.ability.featureAbility';

featureAbility.startAbility({
  want: {
    bundleName: 'com.example.crossgame',
    abilityName: 'GameMainAbility',
    deviceId: 'remoteDeviceId'
  }
});

前提是:

  • 游戏状态已经存在分布式数据中
  • 新设备启动后直接读取即可

为什么这个能力对游戏很重要

你不需要:

  • 手动传进度
  • 重新初始化状态
  • 处理复杂的恢复逻辑

系统已经帮你兜底。

真实应用场景拆解

场景一:手机当手柄,大屏玩游戏

适合类型

  • 派对游戏
  • 本地多人
  • 家庭娱乐

逻辑示例

// 手机端:技能释放
await store.put('skill_cast', {
  skillId: 2,
  playerId: 'p1'
});
// 大屏端:技能响应
store.on('dataChange', (data) => {
  data.insertedEntries.forEach(e => {
    if (e.key === 'skill_cast') {
      castSkill(e.value);
    }
  });
});

场景二:非对称协作游戏

比如:

  • 一个人当指挥
  • 一个人实际操作
// 指挥端下达命令
await store.put('command', {
  type: 'attack',
  target: 'boss'
});

操作端只负责执行,不做决策。

场景三:教育 + 游戏化互动

老师平板控制节奏,学生手机参与。

// 教师端切换关卡
await store.put('game_stage', 'level_2');

学生端监听并同步切换界面。

常见问题 QA

Q1:分布式 KV 会不会太慢?

不会。
它适合的是:

  • 低频状态
  • 操作指令
  • 游戏阶段

高频帧同步需要更底层方案。

Q2:能不能用在竞技类游戏?

可以,但不建议直接用 KV 同步帧数据。
更适合:

  • 操作同步
  • 客户端预测
  • 状态校正

Q3:设备掉线怎么办?

KV 会自动触发变更事件,你可以监听:

  • 玩家退出
  • 状态回收
  • AI 接管

总结

从游戏开发角度看,鸿蒙的跨屏协作并不是噱头,而是一套真正能落地的系统能力

核心就一句话:

多设备在鸿蒙里,不是多个客户端,而是一个分布式游戏系统。

  • 软总线解决连接
  • 分布式数据解决同步
  • Ability 解决跨屏 UI
  • ArkTS 足够把 Demo 跑起来

在这里插入图片描述

摘要

随着智能终端越来越多,应用早就不再只运行在一台设备上。手机、平板、智慧屏、手表之间的协作,已经成了很常见的需求。在这种背景下,多设备任务该怎么分、分到哪台设备执行,就成了开发中绕不开的问题。

在鸿蒙系统中,这个问题并不是靠开发者“手动指定设备”来解决的,而是通过 设备能力感知 + 分布式调度机制 来完成。开发者更多关心的是:
这个任务适合干什么,而不是非要在哪台设备干。

本文会结合鸿蒙系统的分布式能力,介绍多设备任务分配的整体思路,并通过可运行的 Demo 代码,把这个过程完整跑一遍,最后再结合几个真实场景,聊聊它在实际项目中该怎么用。

引言

如果放在以前,一个应用基本只跑在一台手机上,最多考虑前后台切换。但现在不一样了:

  • 手机在你手里
  • 平板在桌子上
  • 智慧屏在客厅
  • 手表戴在手上

用户希望的是:
设备不同,但体验是连着的。

鸿蒙系统的分布式能力,正是为这种场景设计的。它不是简单的“跨设备通信”,而是把 任务、数据、能力 都变成可以在多设备之间流动的资源。

而多设备任务分配,本质上就是一句话:

把合适的任务,交给合适的设备去做。

鸿蒙多设备任务分配的整体思路

先发现设备,再谈分配

在鸿蒙系统中,只要设备在同一个分布式网络里,系统就能自动发现它们。
开发者不需要自己维护“设备表”,也不用关心设备什么时候上线、下线。

系统会帮你感知这些信息:

  • 设备类型(手机、平板、智慧屏)
  • 基本性能情况
  • 是否可信
  • 当前是否可用

你只需要在合适的时机拿到设备列表即可。

任务一定要能拆

多设备任务分配的前提是:
你的业务本身是能拆开的。

比如:

  • 页面展示是一块
  • 数据采集是一块
  • 计算处理是一块

如果一个任务从头到尾全写死在一个 Ability 里,那基本就没法分配了。

系统负责“怎么选设备”

在鸿蒙里,真正“选哪台设备执行”的逻辑,大部分是系统完成的:

  • 当前设备忙不忙
  • 网络情况好不好
  • 设备能力是否匹配
  • 是否更适合本地执行

开发者更多是通过 Ability 启动方式、Service 类型、数据同步方式 来间接影响分配结果。

核心实现方式一:跨设备启动 Ability

适合什么场景

这种方式最常见,适合:

  • 页面展示
  • 功能模块整体迁移
  • 用户可感知的交互任务

比如:
手机负责控制,平板负责显示大屏内容。

Demo:在平板上启动远程 Ability

import distributedDeviceManager from '@ohos.distributedDeviceManager';
import featureAbility from '@ohos.ability.featureAbility';

const BUNDLE_NAME = 'com.example.distributeddemo';

let deviceManager = distributedDeviceManager.createDeviceManager(BUNDLE_NAME);

function startRemotePage() {
  let devices = deviceManager.getTrustedDeviceListSync();

  devices.forEach(device => {
    if (device.deviceType === 2) { // 假设 2 表示平板
      let want = {
        bundleName: BUNDLE_NAME,
        abilityName: 'RemotePageAbility',
        deviceId: device.deviceId
      };
      featureAbility.startAbility(want);
    }
  });
}

代码说明

  • createDeviceManager:创建设备管理器
  • getTrustedDeviceListSync:获取可信设备列表
  • deviceType:用于简单区分设备类型
  • startAbility:指定 deviceId 后,Ability 会在远端设备启动

整个过程不需要你关心远端设备的进程、生命周期,系统会处理。

核心实现方式二:分布式 Service 执行任务

适合什么场景

这种方式更适合:

  • 计算密集型任务
  • 后台处理
  • 不需要 UI 的逻辑

比如:
手机采集数据,交给性能更强的设备做分析。

Demo:连接远端计算 Service

import featureAbility from '@ohos.ability.featureAbility';

function connectRemoteService(remoteDeviceId: string) {
  let want = {
    bundleName: 'com.example.distributeddemo',
    abilityName: 'ComputeServiceAbility',
    deviceId: remoteDeviceId
  };

  featureAbility.connectAbility(want, {
    onConnect(elementName, remote) {
      console.log('远程 Service 已连接');
      remote.sendMessage({
        command: 'startCompute',
        data: [1, 2, 3, 4]
      });
    },
    onDisconnect() {
      console.log('远程 Service 已断开');
    }
  });
}

代码说明

  • Service 在远端设备运行
  • 本地通过 IPC 的方式和远端通信
  • 计算逻辑完全在远端执行
  • 本地只负责发请求、收结果

这种方式非常适合“重计算、轻交互”的任务。

典型应用场景分析与示例

场景一:手机 + 平板的学习展示系统

场景说明

  • 手机负责控制、翻页
  • 平板负责展示课件内容

实现思路

  • 手机发现平板
  • 在平板启动展示 Ability
  • 通过分布式数据同步当前页码
import distributedData from '@ohos.data.distributedData';

async function syncPage(page: number) {
  let kvManager = distributedData.createKVManager();
  let store = await kvManager.getKVStore('pageStore');
  await store.put('current_page', page);
}

平板端监听数据变化,自动刷新页面。

场景二:多设备健康数据分析

场景说明

  • 手表采集心率
  • 手机做基础处理
  • 平板做数据可视化

实现思路

  • 手表同步原始数据
  • 手机过滤、预处理
  • 平板负责展示图表

核心在于:
任务不是“复制”,而是“分工”。

场景三:家庭智慧屏协同控制

场景说明

  • 手机是遥控器
  • 智慧屏负责 UI 展示
  • 计算逻辑放在智慧屏

实现思路

  • 手机只负责发指令
  • 智慧屏 Service 处理业务逻辑
  • 结果同步回手机

这种模式下,手机压力很小,体验反而更流畅。

常见问题 QA

Q1:我能不能指定“一定要某台设备执行”?

不推荐。
鸿蒙的设计思想是 声明需求,而不是指定设备
你可以通过能力需求去“引导”,但不建议写死。

Q2:设备突然下线怎么办?

系统会通知连接断开,
你需要做的只有一件事:
支持本地降级执行或重试。

Q3:分布式任务一定比本地慢吗?

不一定。
当任务本身就不适合本地执行时,
分布式反而更快、更省电。

总结

在鸿蒙系统中,多设备任务分配并不是一套复杂、难以理解的机制,它的核心思想其实很简单:

  • 把任务拆清楚
  • 描述好任务需求
  • 把调度交给系统

只要你在设计阶段考虑好“哪些任务适合分出去”,鸿蒙的分布式能力就能自然地帮你把事情做好。

一句话总结就是:

多设备任务分配,不是设备协作有多复杂,而是你有没有把任务设计清楚。

在这里插入图片描述

摘要

随着 HarmonyOS / OpenHarmony 在手机、平板、智慧屏、车机等多设备上的落地,应用的复杂度正在明显提升。页面不再只是简单展示,而是伴随着网络请求、数据计算、设备协同等大量逻辑。如果这些逻辑处理不当,很容易出现页面卡顿、点击无响应,甚至 Ability 被系统回收的问题。

线程阻塞,已经成为鸿蒙应用开发中最容易踩坑、也最影响体验的问题之一。本文将结合实际开发场景,用尽量口语化的方式,聊一聊在鸿蒙系统中如何系统性地避免线程阻塞,并给出可以直接运行的 Demo 代码。

引言

在早期的应用开发中,很多开发者习惯把逻辑直接写在点击事件里,或者在页面加载时同步读取数据。这种写法在简单页面中问题不大,但在 HarmonyOS 这种强调流畅体验和多设备协同的系统中,很容易暴露问题。

鸿蒙的 UI 是声明式的,系统对主线程(UI 线程)非常敏感。一旦主线程被占用,页面掉帧、动画卡住、操作延迟都会立刻出现。因此,理解哪些操作会阻塞线程,以及如何把这些操作合理地“挪走”,是每个鸿蒙开发者绕不开的一课。

下面我们从原理、工具、代码和真实场景几个角度,完整地拆解这个问题。

为什么线程阻塞在鸿蒙中这么致命

UI 线程到底在忙什么

在 HarmonyOS 中,UI 线程主要负责三件事:

  • ArkUI 页面渲染
  • 用户事件分发(点击、滑动等)
  • Ability 生命周期回调

简单理解就是:只要和“看得见、点得动”有关的事情,几乎都在 UI 线程上完成

一旦你在这里做了耗时操作,比如计算、IO、网络等待,页面就会立刻表现出“卡”的感觉。

常见的阻塞来源

在实际项目中,最容易导致阻塞的操作通常包括:

  • 同步网络请求
  • 文件读写
  • 数据库查询
  • 大量 for 循环计算
  • 人为 sleep 或死循环

这些操作本身不一定是错的,问题在于它们被放在了不该放的线程上

鸿蒙中避免线程阻塞的核心思路

一个总原则

可以把鸿蒙里的线程使用总结成一句话:

UI 线程只处理 UI,其他事情交给异步、线程池或 Worker。

围绕这个原则,系统也提供了多种工具,帮助开发者把任务“分流”。

异步编程是第一道防线

使用 async / await 处理耗时逻辑

在 ArkTS 中,官方推荐优先使用 Promise 和 async / await。它的好处是代码结构清晰,而且不会阻塞 UI 线程。

示例:页面加载网络数据

@Entry
@Component
struct AsyncDemo {
  @State message: string = '加载中...'

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
        .margin(20)

      Button('重新加载')
        .onClick(() => {
          this.loadData()
        })
    }
  }

  async loadData() {
    this.message = '请求中...'
    let response = await fetch('https://example.com/data')
    let result = await response.text()
    this.message = result
  }
}

代码说明

  • loadData 使用 async 声明,不会阻塞 UI
  • await 只是暂停当前函数执行,不会卡住页面
  • UI 更新完全由状态变化驱动

这是最基础、也是最常用的一种防阻塞方式。

TaskPool:处理计算和 IO 的利器

什么时候该用 TaskPool

当你遇到下面这些情况时,TaskPool 几乎是必选项:

  • 大量计算
  • 批量数据处理
  • 文件压缩、解析

可运行 Demo 示例

import taskpool from '@ohos.taskpool'

@Concurrent
function calculateSum(count: number): number {
  let sum = 0
  for (let i = 0; i < count; i++) {
    sum += i
  }
  return sum
}

@Entry
@Component
struct TaskPoolDemo {
  @State result: string = '等待计算'

  build() {
    Column() {
      Text(this.result)
        .fontSize(18)
        .margin(20)

      Button('开始计算')
        .onClick(() => {
          this.startTask()
        })
    }
  }

  startTask() {
    this.result = '计算中...'
    taskpool.execute(calculateSum, 1000000).then(res => {
      this.result = `结果是:${res}`
    })
  }
}

代码说明

  • @Concurrent 表示该函数可以并发执行
  • TaskPool 自动管理线程,不需要开发者手动创建线程
  • UI 线程只负责接收结果和更新状态

在真实项目中,使用 TaskPool 往往能立刻解决页面卡顿问题。

Worker:长期后台任务的选择

Worker 的使用场景

如果任务具有下面这些特点,就更适合使用 Worker:

  • 长时间运行
  • 需要持续处理数据
  • 与 UI 强隔离

比如日志分析、音视频处理、复杂解析等。

示例:使用 Worker 处理数据

主线程代码

let worker = new Worker('workers/data_worker.ts')

worker.postMessage({ action: 'start' })

worker.onmessage = (e) => {
  console.log('收到结果:', e.data)
}

Worker 线程代码

onmessage = function (e) {
  if (e.data.action === 'start') {
    let result = 0
    for (let i = 0; i < 500000; i++) {
      result += i
    }
    postMessage(result)
  }
}

代码说明

  • Worker 与 UI 线程完全独立
  • 即使计算时间较长,也不会影响页面交互
  • 通过消息机制进行通信

结合实际场景的应用示例

场景一:列表页面加载大量数据

问题:

  • 首次进入页面时一次性处理全部数据
  • 页面明显卡顿

解决思路:

  • 网络请求使用 async
  • 数据整理放入 TaskPool
async loadList() {
  let data = await fetchData()
  taskpool.execute(processData, data).then(list => {
    this.list = list
  })
}

场景二:文件导入与解析

问题:

  • 文件较大
  • 解析过程耗时

解决思路:

  • Worker 负责解析
  • UI 只显示进度
worker.postMessage({ filePath })

场景三:复杂计算驱动 UI 更新

问题:

  • 计算逻辑和 UI 耦合

解决思路:

  • 计算完全放到 TaskPool
  • UI 只订阅结果

QA 环节

Q:async / await 会不会阻塞线程?
A:不会,它只是让出执行权,不会卡住 UI 线程。

Q:TaskPool 和 Worker 怎么选?
A:短期、一次性的任务优先 TaskPool,长期或持续任务用 Worker。

Q:能不能在生命周期里做耗时操作?
A:不建议,生命周期函数应尽量轻量。

总结

线程阻塞并不是某一个 API 的问题,而是设计问题。在 HarmonyOS 中,系统已经为我们准备好了异步模型、TaskPool 和 Worker,只要遵循“UI 线程只做 UI”的原则,大多数卡顿问题都可以提前避免。

在真实项目中,提前做好任务拆分、线程规划,比后期排查卡顿要省心得多。这也是鸿蒙开发从“能跑”到“跑得顺”的一个重要分水岭。

背景

在开发“智能带办”应用时涉及到用户体系,开发阶段使用固定验证码形式跑通,在上线前准备接入短信服务时却遇到了难题,短信服务目前只对企业开发者开放了,个人开发者没办法再使用短信服务。为了顺利上架,退后求其次,改为了使用邮箱验证码等了。

邮箱验证码登录有两个弊端,一是不方便,很多用户进来发现是邮箱验证码登录不方便直接就退出应用了;二是合规风险,在申请安全评估报告时如果涉及到用户体系要求实名,邮箱没办法保证实名,还得再加入额外的实名体系,不仅麻烦而且很多都限制个人开发者没法使用。

其实最开始也考虑过要接入华为登录,看了一键登录文档发现也是只针对企业开发者,以为也是只有企业开发者可以使用,后面看了“华为账号登录”后发现个人开发者也可以使用,只是取不到手机号,正好不使用手机号可以规避合规方面的风险。
image.png

华为登录能力介绍

华为账号服务简介

Account Kit(华为账号服务)提供简单、快速、安全的登录功能,让用户快捷地使用华为账号登录应用。用户授权后,Account Kit可提供头像、昵称、手机号码等信息,帮助应用更了解用户。华为账号服务提供了登录、获取华为账号用户信息、未成年模式等。在开发过程中涉及下面几个概念:

  • OpenID:应用维度用户标识符,是华为账号用户在应用/元服务的唯一标识。不同应用/元服务(不管是否在同一个开发者账号下)获取到用户的OpenID不同。
  • UnionID:开发者维度用户标识符,华为账号用户同一开发者账号下的唯一标识。开发者有多个应用/元服务时,同一个开发者账号下的应用/元服务获取到用户的UnionID相同。
  • GroupUnionID:关联主体账号组维度用户标识符,是华为账号用户在关联主体账号组内的唯一标识。不同开发者账号加入同一关联主体账号组后,其组内所有开发者的应用/元服务获取到用户的GroupUnionID相同。
  • permission:数据或接口权限,通过该权限判断应用是否能获取对应数据或调用对应接口。
  • scopes:scope列表,用于获取用户数据。开发者向华为账号服务申请不同类型用户数据的标识。比如头像昵称(profile)、匿名手机号(quickLoginAnonymousPhone)等。
  • Authorization Code:授权码,用户使用华为账号登录成功之后,可通过返回的凭据解析出授权码,通过授权码可获取Access Token、Refresh Token、ID Token等。
  • Access Token:访问凭证,是访问被权限管控资源的应用级凭证。可使用Access Token调用获取用户信息接口获取用户信息。
  • ID Token:用户身份凭证,是OIDC (OpenID Connect) 协议相对于OAuth 2.0 协议扩展的一个用户身份凭证,包含用户信息。用户使用华为账号登录成功之后,可通过返回的凭据解析出Authorization Code、ID Token等数据。

在我们接口华为用户服务后,可以使用OpenId和UnionID绑定我们自己的账号体系。

华为账号服务交互流程

由于个人开发者无法使用“一键登录”,本文主要介绍 “华为账号登录”按钮登录。使用按钮登录我们可以使用Account Kit提供的华为账号登录按钮及服务端交互获取华为账号用户身份标识UnionID、OpenID,通过UnionID、OpenID完成用户登录;或者与应用账号完成绑定,绑定后用于登录或者验证。

华为账号登录按钮包含文本、标志和文本、标志三种样式,以满足应用对界面风格一致性和灵活性的要求。
image.png

账号服务开发者与华为能力交互流程如下图所示:
image.png

交互流程说明如下:
流程说明:

  1. 调用登录按钮展示登录页阶段(序号1-3):

    1. 用户打开应用进行登录,应用设置LoginType类型为LoginType.ID后拉起应用自己的登录页并展示“华为账号登录”按钮,用户点击按钮,请求华为账号授权信息。
  2. 用户点击登录阶段(序号4-6):

    1. 如华为账号未登录,将拉起华为账号登录页,用户登录后,将返回Authorization Code等数据给应用。
    2. 如华为账号已登录,将直接返回Authorization Code等数据给应用。
  3. 用户关联应用账号阶段(序号7-16):

    1. 应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。
    2. 应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。
    3. 应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID/OpenID。

华为账号服务提供了LoginWithHuaweiIDButton组件,构造中需要传入LoginWithHuaweiIDButtonParams类型和 LoginWithHuaweiIDButtonController类型的参数,LoginWithHuaweiIDButtonParams属性如下:

名称类型只读可选说明
styleStyleLoginWithHuaweiIDButton组件的样式。支持样式包括:BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、ICON_RED、ICON_WHITE、ICON_WHITE_OUTLINE、ICON_BLACK、ICON_GRAY、BUTTON_GRAY、BUTTON_CUSTOM。
borderRadiusnumber按钮边框圆角半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:height属性取值的一半。单位:vp。
iconRadiusnumberIcon类型按钮的半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:24。单位:vp。
supportDarkModeboolean表示按钮的样式是否随系统深浅色模式变化。true:按钮的样式会随着系统深浅色模式变化。false:按钮的样式不会随着系统深浅色模式变化。默认值:true。
loginTypeLoginType华为账号登录类型。默认值:LoginType.ID。一键登录请使用LoginType.QUICK_LOGIN。
textAndIconStyleboolean是否展示图文混合样式的华为账号登录按钮。true:按钮支持Icon和文字混合样式。false:按钮仅支持文本样式。默认值:false。当loginType不等于LoginType.QUICK_LOGIN且style等于BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、BUTTON_GRAY时该参数生效。起始版本:5.0.0(12)
customButtonParamsCustomButtonParamsBUTTON_CUSTOM按钮样式参数。起始版本:5.0.0(12)
verifyPhoneNumberboolean华为账号用户在过去90天内未进行短信验证,是否拉起Account Kit提供的短信验证码页面。true:拉起Account Kit提供的短信验证码页面。false:不拉起Account Kit提供的短信验证码页面。需要应用验证手机号时效性。默认值:true。起始版本:5.0.0(12)
extraStyleExtraStyle如果应用想使用华为账号提供的固定样式之外的效果,可使用此接口自定义按钮样式。起始版本:5.0.0(12)
loginButtonTextTypeLoginButtonTextType当loginType为LoginType.QUICK_LOGIN时,可传入此参数,控制按钮文本内容显示。默认值:LoginButtonTextType.QUICK_LOGIN。当该参数为LoginButtonTextType.QUICK_LOGIN时,按钮文本内容显示“华为账号一键登录”。当该参数为LoginButtonTextType.QUICK_REGISTRATION时,按钮文本内容显示“华为账号一键注册”。起始版本:5.0.0(12)
riskLevelboolean是否需要获取华为账号用户风险等级。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:需要获取用户风险等级。false:不获取用户风险等级。默认值:false。起始版本:5.1.0(18)
securityVerificationboolean用户开启华为账号一键登录增强身份验证后,应用会在登录过程中通过华为账号使用生物识别或短信进行身份验证。如果需要获取用户一键登录增强身份验证的开关状态,需设置该字段为false。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:响应结果HuaweiIDCredential将不会返回 enableSecurityVerification。false:响应结果HuaweiIDCredential将返回 enableSecurityVerification。默认值:true。起始版本:6.0.0(20)

智能带办接入过程

目前应用只支持华为登录,页面UI如下:
image.png

在页面中配置红色的LoginWithHuaweiIDButton:

LoginWithHuaweiIDButton({  
    params: {  
      // LoginWithHuaweiIDButton支持的样式  
      style: loginComponentManager.Style.BUTTON_RED,  
      // 账号登录按钮在登录过程中展示加载态  
      extraStyle: {  
        buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({  
          show: true  
        })  
      },  
      // LoginWithHuaweiIDButton的边框圆角半径  
      borderRadius: 24,  
      // LoginWithHuaweiIDButton支持的登录类型  
      loginType: loginComponentManager.LoginType.ID,  
      // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换  
      supportDarkMode: true  
    },  
    controller: this.controller  
  })  
}  
.height(40)  
.width('100%')  
.margin({top:50})  
.padding({left:25, right:25})

控制器controller定义如下:

controller: loginComponentManager.LoginWithHuaweiIDButtonController =  
  new loginComponentManager.LoginWithHuaweiIDButtonController()  
    .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED)  
    .onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {  
      if (error) {  
        this.dealAllError(error);  
        return;  
      }  
  
      if (response) {  
        Logger.i(TAG, 'Succeeded in getting response.');  
        const authCode = response.authorizationCode;  
        // 开发者处理authCode  
        this.getUserInfoPermission(authCode)  
      }  
    });

在controller中获取回调,如果登录成功则通过authorizationCode继续申请用户华为头像和昵称授权:

getUserInfoPermission(authCode:string){  
  // 创建授权请求,并设置参数  
  const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();  
  // 获取头像昵称需要传如下scope  
  authRequest.scopes = ['profile'];  
  // 若开发者需要进行服务端开发以获取头像昵称,则需传如下permission获取authorizationCode  
  authRequest.permissions = ['serviceauthcode'];  
  // 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面  
  authRequest.forceAuthorization = true;  
  // 用于防跨站点请求伪造  
  authRequest.state = util.generateRandomUUID();  
  // 执行授权请求  
  try {  
    const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());  
    controller.executeRequest(authRequest).then((data) => {  
      const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;  
      const state = authorizationWithHuaweiIDResponse.state;  
      if (state && authRequest.state !== state) {  
        Logger.i(TAG, `Failed to authorize. The state is different, response state: ${state}`);  
        return;  
      }  
      Logger.i(TAG,'Succeeded in authentication.');  
      const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;  
      const avatarUri = authorizationWithHuaweiIDCredential?.avatarUri;  
      const nickName = authorizationWithHuaweiIDCredential?.nickName;  
      // 开发者处理avatarUri, nickName  
      const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;  
      Logger.i(TAG, 'getUserInfoPermission:' + JsonUtils.toJSONString(authorizationWithHuaweiIDCredential))  
      this.sendLoginRequest(authorizationCode??authCode)  
      // 涉及服务端开发以获取头像昵称场景,开发者处理authorizationCode  
    }).catch((err: BusinessError) => {  
      this.dealAllError(err);  
    });  
  } catch (error) {  
    this.dealAllError(error);  
  }  
}

用户授权成功后请求服务端接口,服务端通过authorizationCode调用华为服务获取accessToken,接着获取用户信息,绑定自己的账号体系返回自己账号体系的token即可。通过下面接口获取用户级凭证:

POST /oauth2/v3/token HTTP/1.1
Host: oauth-login.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&client_id=<client_id>&client_secret=<client_secret>

接着通过下面示例获取用户昵称和头像:

POST /rest.php?nsp_svc=GOpen.User.getInfo HTTP/1.1
Host: account.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

access_token=<Access Token>

必须在手机上调起授权获取用户授权后这里才可以请求到用户头像和昵称。

总结

本次“智能带办”应用的登录体系接入实践,源于上线前短信服务仅对企业开发者开放的限制,迫使我们从固定验证码、邮箱验证码转向华为账号登录方案。初期因误判“一键登录”仅限企业开发者而忽略“华为账号登录”,后发现个人开发者虽无法获取手机号,但恰好规避了邮箱登录的用户体验差(用户因不便退出)与实名合规风险(需额外实名体系),成为关键破局点。

华为账号服务(Account Kit)通过OpenID(应用唯一标识)、UnionID(开发者唯一标识)等核心概念,为个人开发者提供了安全高效的登录能力:既支持自定义样式的登录按钮(如本文配置的红色BUTTON_RED按钮),又通过Authorization CodeAccess Token→用户信息的流程保障安全,避免身份伪造等风险。接入过程中,我们通过LoginWithHuaweiIDButton组件实现前端交互,结合服务端解析凭证绑定自有账号体系,最终完成用户登录闭环。

此次实践的核心启示在于:面对企业级服务限制时,需深度挖掘平台对个人开发者的差异化能力——华为账号登录虽不提供手机号,却以“去实名化”特性解决了合规痛点,同时依托成熟的OAuth 2.0/OIDC协议与丰富组件(如支持深色模式、自定义圆角的按钮),兼顾了开发效率与用户体验。未来,可进一步探索UnionID在多应用间的用户打通能力,或结合GroupUnionID拓展关联主体场景,持续完善登录体系的灵活性与扩展性。