本文正在参加星光计划3.0–夏日挑战赛

作者:曹昌言
​ 在面向全场景、全连接、全智能时代背景下,OpenHarmony必然会受到越来越多开发者的支持,在不同场景下,会根据实际需求裁剪某些非必要的子系统或组件,也会增加新的子系统或者组件。如果你想添加子系统或者添加服务/组件的话,希望本文能够给你带来一些启示。

1 基本概念

​ 介绍自定义服务之前,先简单介绍几个概念:

​ ①在鸿蒙系统中有三个基本概念,它们是子系统(subsystems),组件(components),功能(features).

​ OpenHarmony整体遵从分层设计,从下向上依次为:内核层、系统服务层、框架层和应用层

1.png

​ 系统功能按照“系统 > 子系统 > 组件”逐级展开。子系统是一个逻辑概念,它具体由对应的组件构成。组件是对子系统的进一步拆分,可复用的软件单元,它包含源码、配置文件、资源文件和编译脚本;能独立构建,以二进制方式集成,是具备独立验证能力的二进制单元。

​ 举例来说,鸿蒙(系统) -->多媒体(子系统) -->音频(组件)-->采集(功能)

​ ②IPC(Inter-Process Communication)机制:使用Binder驱动,用于设备内的跨进程通信。IPC通常采用客户端-服务器(Client-Server)模型,服务请求方(Client)可获取提供服务提供方(Server)的代理 (Proxy),并通过此代理读写数据来实现进程间的数据通信。

​ 通常,Server会先注册系统能力(System Ability)到系统能力管理者(System Ability Manager,缩写saMgr)中,saMgr负责管理这些SA并向Client提供相关的接口。Client要和某个具体的SA通信,必须先从saMgr中获取该SA的代理,然后使用代理和SA通信。一般使用Proxy表示服务请求方,Stub表示服务提供方。

image20220519134146061.png

2 预期目标

  • [x] 目标一:新服务如何配置,编译

  • [x] 目标二:如何整合一个新服务到OHOS中

  • [x] 目标三:如何和新服务进行通信

3 实现效果

3.1.编译成功

​ 配置完成之后,代码编译成功

​ 以产品rk3568-khdvk为例,img文件生成,如下图

image20220614193500377.png

3.2.新服务运行

​ 烧录img文件到开发板,新服务hello以独立进程在开发板后台运行,如下图

image20220519103004896.png

3.3.和新服务进行通信

​ 可以通过命令行方式或者应用程序启动触发方式和服务端进行通信。

3.3.1 命令行进行通信

​ 客户端操作:执行myhello可执行程序,输入命令send,发送字符串"Hello,World"。

image20220519134734307.png

​ 服务端响应:在service层收到了发送的字符串"Hello,World"。

image20220615170150521.png

3.3.2 应用程序触发进行通信

​ 应用程序启动触发:

image20220615170712728.png

​ 服务端响应:返回字符串打印到应用程序窗体内。

image20220615170525142.png

4 代码实现目录结构

​ 整体位于目录foundation下,即foundation/mytest/hello。

mytest
    └─hello
    │  ohos.build // 管理mytest子系统各层级BUILD.gn
    │  
    ├─etc
    │      BUILD.gn  // 预编译配置管理
    │      hello.cfg
    │      hello.rc
    │      
    ├─interface
    │  │  BUILD.gn 
    │  │  
    │  ├─include
    │  │      hello_client.h
    │  │      hello_logs.h // 日志头文件
    │  │      hello_proxy.h
    │  │      ihello.h 
    │  │      
    │  ├─src
    │  │      hello_client.cpp // 接收应用层接口调用入口
    │  │      hello_proxy.cpp  // IPC通信代理
    │  │      
    │  └─test
    │      │  BUILD.gn // 对cli工具进行管理
    │      │  
    │      ├─include
    │      │      cli_tool.h 
    │      │      
    │      └─src
    │             cli_tool.cpp // 客户端测试入口
    │              
    ├─sa_profile
    │      9999.xml // 以sa ID命名
    │      BUILD.gn // 对xml进行管理
    │      
    └─service
        │  BUILD.gn // 服务端功能管理
        │  
        ├─include
        │      hello_service.h
        │      hello_stub.h
        │      
        └─src
                hello_service.cpp  // 服务程序
                hello_stub.cpp     // IPC通信桩      

5 实现过程

5.1 新服务如何配置及编译

5.1.1 新服务如何配置

​ 鸿蒙操作系统一个子系统的配置文件主要有如下四个:

5.1.1.1 模块目录中BUILD.gn文件

​ 在模块目录下配置BUILD.gn,根据类型选择对应的模板。

支持的模板类型:

ohos_executable // 指定 target 是个可执行文件
ohos_shared_library // 声明一个动态(win=.dll、linux=.so)
ohos_static_library // 静态库(win=.lib、linux=.a)
ohos_source_set // 定义源码集,会逐一对应生成 .o 文件,即尚未链接(link)的文件

# 预编译模板:
ohos_prebuilt_executable // 拷贝可执行文件
ohos_prebuilt_shared_library // 拷贝so文件
ohos_prebuilt_etc // 拷贝其他格式的文件

例子:

​ ohos_shared_library示例

import("//build/ohos.gni")

ohos_shared_library("hello_native") { // 动态库,会生成libhello_native.z.so
    sources = [ // 使用的源文件
        "src/hello_proxy.cpp",
        "src/hello_client.cpp"
    ]

    include_dirs = [ // 头文件包含位置
        "include",
        "//utils/native/base/include",
        "//utils/system/safwk/native/include",
        "//foundation/distributedschedule/samgr/interfaces/innerkits/samgr_proxy/include"
    ]

    deps = [ // 依赖模块
        "//utils/native/base:utils",
    ]
  # 跨部件模块依赖定义,
  # 定义格式为 "部件名:模块名称"
  # 这里依赖的模块必须是依赖的部件声明在inner_kits中的模块
    external_deps = [ // 外部依赖
        "hisysevent_native:libhisysevent",
        "hiviewdfx_hilog_native:libhilog",
        "ipc:ipc_core",
        "safwk:system_ability_fwk",
        "samgr_standard:samgr_proxy",
    ]

    part_name = "hello" // 部件名
    subsystem_name = "mytest" // 子系统名
}

​ ohos_executable示例:

​ ohos_executable模板属性和ohos_shared_library基本一致

import("//build/ohos.gni")

ohos_executable("myhello") { // 生成可执行文件myhello
  install_enable = true // true表示安装的意思
  sources = [
    "src/cli_tool.cpp",
  ]

  include_dirs = [
    "//foundation/mytest/hello/interface/test/include",
    "//foundation/mytest/hello/interface/include",
  ]

  deps = [
    "//base/hiviewdfx/hilog/interfaces/native/innerkits:libhilog",
    "//foundation/mytest/hello/interface:hello_native",
    "//foundation/distributedschedule/safwk/interfaces/innerkits/safwk:system_ability_fwk",
    "//utils/native/base:utils",
  ]

  external_deps = [ "ipc:ipc_core" ]

  cflags_cc = [
    "-std=c++17",
  ]

  part_name = "hello"
  subsystem_name = "mytest"
}

说明

​ 可执行模块(即ohos_executable模板定义的)默认是不安装的,如果要安装,需要指定 install_enable = true

​ ohos_prebuilt_etc示例:

import("//build/ohos.gni")
ohos_prebuilt_etc("etc_file") {
  source = "file"
  deps = []                 # 部件内模块依赖
  module_install_dir = ""   # 可选,模块安装路径,从system/,vendor/后开始指定
  relative_install_dir = "" # 可选,模块安装相对路径,相对于system/etc;如果有module_install_dir配置时,该配置不生效
  part_name = ""            # 必选,所属部件名称
}
import("//build/ohos.gni")

ohos_prebuilt_etc("hello_sa_rc") {
  source = "hello.cfg"
  relative_install_dir = "init"
  part_name = "hello"
  subsystem_name = "mytest"
}

说明:

​ 要添加一个模块到已有部件中去,只需要在该部件的module_list中添加新加模块的gn编译目标;假如该模块提供给其它模块接口,需要在inner_kits中添加对应的配置;如果有该模块的测试用例,需要添加到test_list中去。

5.1.1.2 创建ohos.build文件

​ 每个子系统有一个ohos.build配置文件(或者有bundle.json配置文件),在子系统的根目录下。在新建的子系统目录下每个部件对应的文件夹下创建ohos.build文件,定义部件信息。

{
  "subsystem": "子系统名",
  "parts": {
    "新建部件名": {
      "module_list": [
        "部件包含模块的gn目标"
      ],
      "inner_kits": [
      ],
      "test_list": [
        "测试用例",
      ]
    }
  }
}

说明:

​ subsystem定义了子系统的名称;parts定义了子系统包含的部件。

​ 一个部件包含部件名,部件包含的模块module_list,部件提供给其它部件的接口inner_kits,部件的测试用例test_list。

{
  "subsystem": "mytest",
  "parts": {
    "hello": {
        "module_list": [
          "//mytest/hello/service:hello_service",
          "//mytest/hello/sa_profile:hello_sa_profiles",
          "//mytest/hello/etc:hello_sa_rc",
          "//mytest/hello/interface:hello_native"
        ],
        "test_list": [
        ]
    }
  }
}

​ 在已有子系统中添加一个新的部件,有两种方法:

​ a)在该子系统原有的ohos.build文件中添加该部件

​ b)新建一个ohos.build文件

**说明**:

​ 无论哪种方式该ohos.build文件均在对应子系统所在文件夹下

​ ohos.build文件包含两个部分,第一部分subsystem说明了子系统的名称,parts定义了该子系统包含的部件,要添加一个部件,需要把该部件对应的内容添加进parts中去。添加的时候需要指明该部件包含的模块module_list,假如有提供给其它部件的接口,需要在inner_kits中说明,假如有测试用例,需要在test_list中说明,inner_kits与test_list没有也可以不添加。

5.1.1.3 subsystem_config.json文件

​ 修改系统build目录下的subsystem_config.json文件

{
  "子系统名": {
    "path": "子系统目录",
    "name": "子系统名",
    ...
  }
}

​ 该文件定义了有哪些子系统以及这些子系统所在文件夹路径,添加子系统时需要说明子系统path与name,分别表示子系统路径和子系统名。

image20220614172419368.png

5.1.1.4 产品配置文件{product_name}.json

​ 在productdefine/common/products目录下的产品配置如RK3568-KHDVK.json中添加对应的部件,直接添加到原有部件后面即可。

{
    ...
    "parts":{
        "部件所属子系统名:部件名":{}
    }
}
{
  "product_name": "RK3568-KHDVK",
  "product_company": "kaihong",
  "product_device": "rk3568-khdvk",
  "version": "2.0",
  "type": "standard",
  "product_build_path": "device/kaihong/build",
  "parts":{
    ......
    "multimedia:multimedia_histreamer":{},
    "multimedia:multimedia_media_standard":{},
    "multimedia:multimedia_audio_standard":{},
    "multimedia:multimedia_camera_standard":{},
    "multimedia:multimedia_image_standard":{},
    "multimedia:multimedia_media_library_standard":{},
    "mytest:hello":{}, // 添加自己的部件(注意前后逗号,保持文件格式正确)
    ......
  }
}

​ 指明了产品名,产品厂商,产品设备,版本,要编译的系统类型,以及产品包含的部件。

image20220518203050105.png

5.1.2 新服务如何编译

​ 编译整个开源鸿蒙系统,命令如下:

./build.sh --product-name {product_name}

​ 此处{product_name}在实际操作时变更为产品名,例如,rk3566,rk3568等。编译所生成的文件都归档在out/{device_name}/目录下,结果镜像输出在 out/{device_name}/packages/phone/images/ 目录下。

​ 编译之后,至于如何烧录请参考官网或者其他文章。

5.2 整合新服务到ohos中

​ 从代码结构上可以看出,除了5.1配置之外,还需要:

5.2.1 创建sa_profile目录及相关文件

​ 在子系统根目录创建sa_profile目录,创建服务ID为前缀的xml文件及BUILD.gn。

说明:

​ 服务ID值定义在 foundation/distributedschedule/samgr/interfaces/innerkits/samgr_proxy/include/system_ability_definition.h 中,若没有则新建一个。

image20220518215427964.png

​ sa_profile目录示例:

image20220518215514547.png

​ 9999.xml文件示例:

<info>
    <process>hello</process>  // 进程名称hello
    <systemability>
        <name>9999</name> <!-- Declare the id of system ability. Must be same with //utils/system/safwk/native/include/system_ability_definition.h -->
        <libpath>libhello_service.z.so</libpath> <!--加载路径-->
        <run-on-create>true</run-on-create> <!--true: 进程启动后即向samgr组件注册该SystemAbility; false:按需启动,即在其他模块访问到该SystemAbility时启动-->
        <distributed>false</distributed> <!--true:该SystemAbility为分布式,支持跨设备访问; false:本地跨IPC访问-->
        <dump-level>1</dump-level>
    </systemability>
</info>

​ BUILD.gn示例:

import("//build/ohos/sa_profile/sa_profile.gni")

ohos_sa_profile("hello_sa_profiles") {
  sources = [ "9999.xml" ]
  part_name = "hello"
}

5.2.2 创建etc目录及相关文件

​ 在子系统根目录创建etc目录,创建服务进程对应的.rc文件。

​ etc目录示例:

image20220518215722634.png

​ hello.cfg配置示例:

{
    "services" : [{
            "name" : "hello",
            "path" : ["/system/bin/sa_main", "/system/profile/hello.xml"], // 说明使用sa拉起来,配置文件时hello.xml
            "uid" : "system",
            "gid" : ["system", "shell"]
        }
    ]
}

5.3 如何和新服务进行通信

5.3.1 通过命令行和server端进行通信

​ 建立main函数进行测试,代码如下:

int main()
{
    std::shared_ptr<CliTool> tool = getCliTool();

    if (tool == nullptr) {
        cerr << "Internal error!" << endl;
        return -1;
    }

    cout << tool->getName() << std::endl;

    while (true) {
        char cmd[MAX_LINE_SIZE] = "";
        cin.getline(cmd, MAX_LINE_SIZE - 1);
        cout << "Input is " << cmd << endl;

        if (strcasecmp("quit", cmd) == 0) {
            cout << "Quit" << endl;
            break;
        }

        if (strcasecmp("help", cmd) == 0) {
            tool->usage();
            continue;
        }

        string scmd(cmd);
        tool->execute(scmd);
    }

    return 0;
}

​ 定义AbilityCliTool类,使用send绑定了函数cmdSendMessage,通过main中execute函数调用AbilityCliTool::cmdSendMessage,代码如下:

class AbilityCliTool: public std::enable_shared_from_this<AbilityCliTool>, public CliTool {
public:
    AbilityCliTool() {
        std::function<void(std::vector<std::string>&)> f;

        f = std::bind(&AbilityCliTool::cmdSendMessage, this, std::placeholders::_1);
        commands_.emplace("send", f); // 这里使用send绑定了函数cmdSendMessage

        spProxy_ = std::make_shared<HelloClient>(); // 实例化HelloClient对象spProxy_
        if (spProxy_) {
            spProxy_->InitService();
        }
    }

    virtual void usage() {
        std::cout << "AbilityCliTool usage" << std::endl;
    }

    virtual std::string getName() {
        return "AbilityCliTool";
    }

    virtual void execute(std::string &cmd) {
        std::cout << "AbilityCliTool::execute(" << cmd << ")" << std::endl;
        auto args = StringSplit(cmd, " "); 
        auto func = commands_[args[0]];
        if (func) {
            func(args);
        }
    }

    virtual ~AbilityCliTool() {
        std::cout << "~AbilityCliTool()" << std::endl;
    }

private:
    void cmdSendMessage(std::vector<std::string> &args) {
        std::cout << "cmdSendMessage args:";
        for (auto &arg: args) {
            std::cout << " " << arg;
        }
        std::cout << std::endl;
        if (spProxy_) {
            spProxy_->SendMessage("Hello,World\n"); // 
        }
    }

    std::map<std::string, std::function<void(std::vector<std::string>&)>> commands_;

    std::shared_ptr<HelloClient> spProxy_;
};

​ HelloClient类实现代码如下:

namespace {
    constexpr int32_t MAX_RETYE_COUNT = 30;
    constexpr uint32_t WAIT_MS = 200;
}

int32_t HelloClient::InitService()
{
    if (helloServer_ != nullptr) {
        HELLO_LOGI(HELLO_NATIVE, "[InitService]Already init");
        return ERR_OK;
    }

    auto systemAbilityManager = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager(); // 通过SystemAbilityManager的GetSystemAbility方法可获取到对应SA的代理IRemoteObject
    if (systemAbilityManager == nullptr)  {
        HELLO_LOGE(HELLO_NATIVE, "[InitService]GetSystemAbilityManager failed");
        return ERR_NO_INIT;
    }   

    int retryCount = 0;
    do {
        helloServer_ = iface_cast<IHello>(systemAbilityManager->GetSystemAbility(HELLO_SERVICE_ID)); // 相当于生成一个proxy对象,said唯一标识SA,我们这里使用HELLO_SERVICE_ID。 这里使用iface_cast宏转换成具体类型
        if (helloServer_ != nullptr) { 
            HELLO_LOGI(HELLO_NATIVE, "[InitService]InitService success.");
            return ERR_OK;
        }
        HELLO_LOGI(HELLO_NATIVE, "[InitService]Get service failed, retry again ....");
        std::this_thread::sleep_for(std::chrono::milliseconds(WAIT_MS));
        retryCount++;
    } while (retryCount < MAX_RETYE_COUNT);
    HELLO_LOGE(HELLO_NATIVE, "[InitService]InitService timeout.");
    return ERR_TIMED_OUT;
}

int32_t HelloClient::SendMessage(const std::string& msg)
{
    std::lock_guard<std::mutex> lock(mutex_);
    if (helloServer_ != nullptr) {
        return helloServer_->SendMessage(msg);
    }

    if (InitService() != ERR_OK) {
        return ERR_NO_INIT;
    }
    HELLO_LOGI(HELLO_NATIVE, "[SendMessage]Call SendMessage");
    return helloServer_->SendMessage(msg);
}

​ 这里会走到HelloProxy对象中的SendMessage函数,具体代码如下:

int32_t HelloProxy::SendMessage(const std::string& msg)
{
    MessageParcel helloData;
    MessageParcel helloReply;
    MessageOption helloOption(MessageOption::TF_SYNC);

    if (!helloData.WriteString(msg)) {
        return ERR_INVALID_VALUE;
    }

    int32_t helloRet = Remote()->SendRequest(CMD_HELLO_SEND_MESSAGE, helloData, helloReply, helloOption);
    if (helloRet != ERR_OK) {
        return helloRet;
    }

    return ERR_OK;
}

​ HelloProxy类是Proxy端实现,继承IRemoteProxy<IHello>,调用SendRequest接口向Stub端发送请求,对外暴露服务端提供的能力。HelloStub类具体代码如下:

int32_t HelloStub::OnRemoteRequest(uint32_t code, MessageParcel& data, MessageParcel& reply, MessageOption& option)
{
    switch (code) {
        case CMD_HELLO_SEND_MESSAGE:
            return HelloStubSendMessage(data, reply, option);
        case CMD_HELLO_GET_VERSION:
        default: {
            HELLO_LOGD(HELLO_SERVICE, "%{public}s: not support cmd %{public}d", __func__, code);
            return IPCObjectStub::OnRemoteRequest(code, data, reply, option);
        }
    }
}

int32_t HelloStub::HelloStubSendMessage(MessageParcel& helloData, MessageParcel& helloReply, MessageOption& helloOption)
{
    std::string msg = helloData.ReadString();

    int32_t helloRet = SendMessage(msg);
    if (helloRet != ERR_OK) {
        HELLO_LOGD(HELLO_SERVICE, "%{public}s failed, error code is %d", __func__, helloRet);
        return helloRet;
    }

    return ERR_OK;
}

说明:

​ 该类是和IPC框架相关的实现,需要继承 IRemoteStub<ITestAbility>。Stub端作为接收请求的一端,需重写OnRemoteRequest方法用于接收客户端调用。

接下来通过HelloStubSendMessage调用了服务端业务函数具体实现类HelloService中SendMessage函数,其具体代码如下:

REGISTER_SYSTEM_ABILITY_BY_ID(HelloService, HELLO_SERVICE_ID, true);

HelloService::HelloService(int32_t sysAbilityId, bool runOnCreate) : SystemAbility(sysAbilityId, runOnCreate)
{
    HELLO_LOGI(HELLO_SERVICE, "[HelloService]%{public}p", this);
}

int32_t HelloService::SendMessage(const std::string& msg)
{
    HELLO_LOGI(HELLO_SERVICE, "[SendMessage]This is just for test. %{public}s", msg.c_str()); // 日志验证点,到此结束
    return ERR_OK;
}

void HelloService::OnStart()
{
    HELLO_LOGI(HELLO_SERVICE, "[OnStart]%{public}s start.", __func__);
    bool isPublished = SystemAbility::Publish(this); // 将自身服务发布到saMgr中
    if (!isPublished) {
        HELLO_LOGD(HELLO_SERVICE, "[OnStart]publish LocationService error");
        return;
    }
}

void HelloService::OnStop()
{
    HELLO_LOGI(HELLO_SERVICE, "[OnStop]]%{public}s stop.", __func__);
}

5.3.2 通过应用层触发和server端进行通信

​ 在工具DevEcoStudio中创建工程,在index.js文件中具体代码如下:

import hello_native from '@ohos.hello'

export default {
    data: {
        title: ""
    },
    onInit() {
        hello_native.hello();
    }
}

​ 进行编译之后生成的hap包,在开发板上进行安装,执行命令如下:

.\hdc_std.exe install -r .\entry-default-signed.hap

​ 除了在应用层修改之外,还需要在框架层进行如下修改:

#include <assert.h>
#include "napi/native_api.h"
#include "napi/native_common.h"
constexpr uint32_t STR_LEN = 13
static napi_value Method(napi_env env, napi_callback_info info) {
    HELLO_LOGI(HELLO_NATIVE, "[Method]Call SendMessage");
    napi_status status;
    napi_value world;
    status = napi_create_string_utf8(env, "Hello, world!", STR_LEN, &world);
    HelloClient::SendMessage(world);
    assert(status == napi_ok);
    return world;
}

static napi_value Init(napi_env env, napi_value exports) {
    HELLO_LOGI(HELLO_NATIVE, "[Method]Call Init");
    napi_status status;
    napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION("hello", Method),
    };
    status = napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    assert(status == napi_ok);
    return exports;
}
/*
 * Module register function
 */
NAPI_MODULE(hello_native, Init)

​ 至此,就能够从应用层在初始化应用程序时触发调用hello即SendMessage函数,同时返回字符串“Hello,World"在应用程序中显示。

6 总结

​ 通过以上步骤,在ohos系统中自定义子系统或者服务,通过配置编译并烧录到整体系统中,然后通过cli命令行或者应用程序启动触发的方式进行了功能验证,达到了预期制定的三个目标,掌握构建自定义服务的流程及架构。

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

想了解更多关于开源的内容,请访问:

51CTO 开源基础软件社区

https://ost.51cto.com/#bkwz