#夏日挑战赛#OHOS构建自定义服务实战 原创
作者:曹昌言
在面向全场景、全连接、全智能时代背景下,OpenHarmony必然会受到越来越多开发者的支持,在不同场景下,会根据实际需求裁剪某些非必要的子系统或组件,也会增加新的子系统或者组件。如果你想添加子系统或者添加服务/组件的话,希望本文能够给你带来一些启示。
1 基本概念
介绍自定义服务之前,先简单介绍几个概念:
①在鸿蒙系统中有三个基本概念,它们是子系统(subsystems
),组件(components
),功能(features
).
OpenHarmony整体遵从分层设计,从下向上依次为:内核层、系统服务层、框架层和应用层。
系统功能按照“系统 > 子系统 > 组件”逐级展开。子系统是一个逻辑概念,它具体由对应的组件构成。组件是对子系统的进一步拆分,可复用的软件单元,它包含源码、配置文件、资源文件和编译脚本;能独立构建,以二进制方式集成,是具备独立验证能力的二进制单元。
举例来说,鸿蒙(系统) -->多媒体(子系统) -->音频(组件)–>采集(功能)
②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表示服务提供方。
2 预期目标
-
[x] 目标一:新服务如何配置,编译
-
[x] 目标二:如何整合一个新服务到OHOS中
-
[x] 目标三:如何和新服务进行通信
3 实现效果
3.1.编译成功
配置完成之后,代码编译成功
以产品rk3568-khdvk为例,img文件生成,如下图
3.2.新服务运行
烧录img文件到开发板,新服务hello以独立进程在开发板后台运行,如下图
3.3.和新服务进行通信
可以通过命令行方式或者应用程序启动触发方式和服务端进行通信。
3.3.1 命令行进行通信
客户端操作:执行myhello可执行程序,输入命令send,发送字符串"Hello,World"。
服务端响应:在service层收到了发送的字符串"Hello,World"。
3.3.2 应用程序触发进行通信
应用程序启动触发:
服务端响应:返回字符串打印到应用程序窗体内。
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,分别表示子系统路径和子系统名。
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":{}, // 添加自己的部件(注意前后逗号,保持文件格式正确)
......
}
}
指明了产品名,产品厂商,产品设备,版本,要编译的系统类型,以及产品包含的部件。
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 中,若没有则新建一个。
sa_profile目录示例:
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目录示例:
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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
非常完整的讲解,学习了!
有源码吗?
请问可以提供源码吗
有源码吗?
能提供源码么?
能提供源码么?