☰
OpenHarmony自定义子系统与部件实践指南
2026/10/23 3:20:55 网站建设 项目流程

1. 为什么需要在 OpenHarmony 中自定义子系统与部件

做 OpenHarmony 系统开发的人,早晚都会碰到一个需求:系统默认的子系统、部件划分不满足当前项目的定制要求,需要往里面加自己的东西。比如某厂商要做一个带特殊安全模块的设备,或者某团队在移植 OpenHarmony 到一款新的开发板上,需要让构建系统识别自家的硬件抽象层。这类场景下,“自定义子系统 + 部件 + 模块”就是绕不开的基础操作。

我最初接触这个需求时,第一反应是翻官方文档找“如何添加一个部件”,结果发现 OpenHarmony 的构建系统基于一个叫hb的工具,配置逻辑藏在build目录的配置框架里。官方文档大多在讲“如何使用现有子系统”,对“如何新增一套完整的自定义子系统”讲得比较碎片化。所以这篇文章把我实际踩通的路整理出来,包括目录结构怎么摆、配置文件怎么写、部件如何挂载到产品定义中,以及最终如何用hb命令编译验证。这套流程适合系统移植工程师、ROM 定制开发者和所有需要在 OpenHarmony 源码树里增加私有代码的开发者参考。

在 OpenHarmony 的构建体系里,“子系统”是逻辑上的功能集合,比如内核、图形、分布式软总线;“部件”是子系统下的可编译单元,类似 Android 里的模块(Module);而“模块”落实到具体就是一个BUILD.gn里定义的 target。理解这三层关系很重要,后续所有配置都围绕这个层次展开。

2. 整体设计思路与方案选型

2.1 构建系统的基础认知

OpenHarmony 的构建框架选用的是 GN + Ninja,配合hb(HarmonyOS Build)工具做入口。GN 负责生成 Ninja 文件,Ninja 负责真正执行编译。整个构建系统通过读取产品定义的 JSON 配置文件,决定最终编译哪些子系统、哪些部件。

在动手之前,先把下面两个核心概念摸清,否则后续很容易绕晕:

  • vendor/<厂商>/<产品>:产品级别的配置目录,决定这个产品“要什么”。
  • device/<厂商>/<产品>:设备级别的配置目录,决定“跑在什么硬件上”,包含内核、驱动、编译脚本等。

而自定义子系统一般放在vendor或device下一层,也可以放到独立的domain目录中,但最常用的做法是放在vendor/<厂商>/<自定义子系统名>/下,这样便于和产品配置捆绑管理。

2.2 为什么选择“自建子系统目录”而不是“塞进已有子系统”

一开始我图省事,想过把自定义模块直接加进arkui或者distributedschedule这种已有子系统里。后来发现这会带来三个问题:

  • 代码归属混乱,后续同步上游代码时容易产生冲突。
  • 部件的component名称要全局唯一,硬塞进已有子系统容易重名。
  • 产品级配置无法做精细裁剪,定制性差。

所以正确的做法是:新建一个独立子系统,再去产品定义里按需挂载。这样既不污染原生代码,又能做到按产品特性精确裁剪。

2.3 自定义子系统的目录布局

我的推荐目录结构如下(以厂商名acme、子系统名custom_sys为例):

vendor/acme/ ├── custom_sys/ │ ├── BUILD.gn │ ├── bundle.json │ ├── modules/ │ │ ├── hello/ │ │ │ ├── BUILD.gn │ │ │ ├── include/ │ │ │ │ └── hello.h │ │ │ └── src/ │ │ │ └── hello.c │ │ └── feature_demo/ │ │ ├── BUILD.gn │ │ └── src/ │ │ └── demo.cpp │ └── ...

其中bundle.json是这个子系统的“身份证”,BUILD.gn用于定义编译的入口和依赖关系。每个子模块(比如hello)内部有自己的BUILD.gn,描述该模块的源文件、头文件路径、依赖的静态库或动态库。这种结构把模块之间的依赖关系表达得非常清晰,编译时 GN 能自动解析出正确的构建顺序。

提示:vendor/acme/custom_sys只是其中一个放置位置。如果某个子系统是强硬件相关的,也可以放到device/acme/<产品>/目录下。具体取决于该子系统的复用范围——跨产品复用就放 vendor,单一产品专用就放 device。

3. 实际操作:添加子系统的完整流程

3.1 第一步:创建子系统的 bundle.json

bundle.json是子系统定义的关键文件。它告诉构建系统:这个子系统叫什么名字、包含哪些部件、每个部件依赖哪些其他部件。下面是一个最小可用的示例:

{ "name": "@vendor/custom_sys", "description": "Acme custom subsystem for demo", "version": "1.0.0", "subsystems": [ { "name": "custom_sys", "components": [ { "name": "hello", "description": "Hello world component", "dir": "vendor/acme/custom_sys/modules/hello", "deps": [] }, { "name": "feature_demo", "description": "A feature demo component", "dir": "vendor/acme/custom_sys/modules/feature_demo", "deps": ["hello"] } ] } ] }

注意,这里的name字段要唯一。subsystems列表里可以定义多个子系统,每个子系统下挂多个部件。dir指向部件的实际目录,deps声明部件间的依赖关系。构建系统在解析时,会依据deps构造一棵依赖树,确保先编译被依赖的部件。

3.2 第二步:编写模块的 BUILD.gn

以hello模块为例,它的BUILD.gn内容如下:

import("//build/ohos.gni") ohos_source_set("hello") { sources = [ "src/hello.c", ] include_dirs = [ "include", ] install_images = [ "system" ] }

这段配置的意思是:把hello.c编译成一个 source_set(静态概念上的源码集合),头文件搜索路径为include目录,编译产物安装到 system 镜像中。如果这是一个可执行程序,则需要把ohos_source_set换成ohos_executable;如果是一个动态库,则改为ohos_shared_library。

以动态库为例,配置会变成:

ohos_shared_library("hello") { sources = [ "src/hello.c", ] include_dirs = [ "include", ] install_images = [ "system" ] public_deps = [ "//build/ohos:init" ] soname = "libhello.so" }

这里多了一个public_deps,用来声明对外可见的依赖,比如内核初始化相关的init模块。soname字段可以自定义生成的 .so 文件名。

3.3 第三步:把子系统挂载到产品定义中

打开产品级定义文件,一般位于vendor/acme/<产品>/config.json。这是 OpenHarmony 产品配置的核心,构建系统会按照这个文件的描述去组织编译目标。它的关键片段如下:

{ "product_name": "acme_demo_product", "version": "1.0.0", "subsystems": [ { "subsystem": "custom_sys", "components": [ { "component": "hello", "features": [] }, { "component": "feature_demo", "features": ["feature_demo_enable_xxx = true"] } ] } ] }

需要注意的是:这里只会列出产品真正需要的部件,没有出现在列表里的部件不会被编译。这种机制使得一套代码可以支撑多款产品,每款产品通过config.json定义自己的剪裁方案。比如标准版产品可以不挂feature_demo,而增强版产品挂上它。

3.4 第四步:编译与验证

完成上述配置后,进入源码根目录执行如下命令:

hb set

这里会弹出产品选择列表,选中配置好的acme_demo_product。然后执行:

hb build

构建过程中,观察输出日志,正常情况下能看到hello和feature_demo两个模块被编译,并最终打包到系统镜像中。如果希望只编译某个特定模块做快速验证,可以使用:

hb build hello

这条命令只编译hello目标,不会触发全量构建,调试效率提升明显。

补充说明:如果hb build提示找不到产品,请检查config.json是否放置在vendor/acme/<产品>/目录下,并确保product_name字段的值在hb set时能被识别。

4. 实操中的高频问题和排查技巧

4.1 编译报错:找不到子系统或部件

这类错误最常见的原因是bundle.json中的dir路径写错了,或者config.json中的subsystem名称和bundle.json中定义的不一致。调试方法很简单,先确认bundle.json中的name字段与config.json中的subsystem字段完全匹配,再去检查dir对应的目录是否真实存在。

4.2 编译通过但产物不在镜像中

install_images这个字段决定了编译产物最终装到哪个镜像。如果漏写了这个字段,即使模块编译成功,也不会出现在最终的系统镜像中。建议在编写BUILD.gn时,确认是否需要输出到system、vendor或chipset镜像,再决定install_images的取值。

4.3 头文件找不到

出现这种问题,九成是include_dirs配置不完整。GN 的include_dirs只对当前目标生效,如果模块 A 的头文件被模块 B 引用,B 必须在自己的include_dirs中显式声明 A 的头文件路径,或者通过public_deps传递依赖。GN 的依赖传播规则是:public_deps会传导到头文件搜索路径,而deps只在链接时传导。不熟悉的话很容易掉坑。

4.4 模块之间依赖顺序错误

在 GN 中,依赖关系通过deps、public_deps声明,构建系统会自动推导执行顺序。但曾经遇到过一个场景:模块 A 在编译时需要生成某个源文件,而模块 B 的源文件依赖这个生成的源文件。此时必须在 A 的BUILD.gn中用generated_file或类似机制显式声明生成动作,否则 Ninja 可能因为源文件尚不存在而报错。这个坑比较隐蔽,遇到诡异的编译失败可以先往“构建顺序未显式声明”的方向排查。

5. 结语与经验总结

自定义子系统、部件、模块这条路,说难不难,说简单也不简单。核心在于搞清楚bundle.json、config.json、BUILD.gn三者的关系:

  • bundle.json定义子系统下有哪些部件。
  • config.json决定产品选哪些部件。
  • BUILD.gn描述每个部件怎么编译。

在实际项目里,我习惯先把最小的 hello 模块跑通,再逐渐往里面添加真正的业务逻辑。这样做的好处是,一旦编译链路出问题,可以快速定位是框架配置的问题还是业务代码的问题,避免在一个大而全的模块里反复折腾。

最后提供一个小技巧:如果团队里有多款产品共用同一套自定义子系统,建议把公共部件和产品专属部件分目录管理,前者放vendor/acme/common/,后者放vendor/acme/<产品>/,然后在config.json中按需选择。这样代码复用性最高,后期维护时也不会在产品间互相干扰。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询