6

OHOS构建自定义服务实战

 1 year ago
source link: https://os.51cto.com/article/712290.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0311a7172e6eb3da137397f49222c5e4e1d616.png

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

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

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

一、基本概念

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

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

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

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

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

②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表示服务提供方。

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

二、预期目标

  • 目标一:新服务如何配置,编译。
  • 目标二:如何整合一个新服务到OHOS中。
  • 目标三:如何和新服务进行通信。

三、实现效果

1、编译成功

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

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

2、新服务运行

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

3、和新服务进行通信

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

(1)命令行进行通信

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

(2)应用程序触发进行通信

应用程序启动触发:

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

四、代码实现目录结构

整体位于目录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通信桩

五、实现过程

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中去。

创建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没有也可以不添加。

subsystem_config.json文件

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

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

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

产品配置文件{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":{}, // 添加自己的部件(注意前后逗号,保持文件格式正确)
    ......
  }
}

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

(2)新服务如何编译

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

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

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

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

2、整合新服务到ohos中

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

(1)创建sa_profile目录及相关文件

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

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

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

sa_profile目录示例:

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

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"
}

(2)创建etc目录及相关文件

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

etc目录示例:

#夏日挑战赛#OHOS构建自定义服务实战-开源基础软件社区

hello.cfg配置示例:

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

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__);
}

(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"在应用程序中显示。

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

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

​51CTO 开源基础软件社区​

​https://ost.51cto.com​​。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK