openharmony 南向开发基础:ohos自定义子系统,自定义部件,调用hilog部件,hilog日志封装
自定义单部件
关于开源鸿蒙的南向教程不多,很多都是从官方文档上抄的的例子,官网的例子不是很适合入门,写的很粗糙,不适合傻瓜阅读,毕竟对于刚入行鸿蒙的新手而言,gn语法就是第一劝退魔咒,且openharmony使用的是ohos自定义gn模板,想先自学gn语法再学鸿蒙的话那得猴年马月了,说一千道一万不如实战干一干,会不会骑马你先找匹马上了再说,摔不死就会了
下面给出官网范例以及相应的视频讲解
- 官网文档subsys-build-component.md
- 视频讲解
看完上面的视频聪明的你应该能抄出一个应用程序,一个动态库,并且使用应用程序引用动态库
上面的例子基于同一个部件,应用程序跟动态库属于同一部件的两个模块,动态库的调用属于部件内调用
上面例子的局限在于,动态库(so库)没有导出部件间接口,即动态库跟exe同属一个部件,其他部件无法使用ohos推荐的gn语法调用so库,但是可以用传统意义上的C++编译规则,获取编译生成的so库和头文件加入到你的部件中,进行手动链接
自定义多部件
为了客服上述单部件创建的so库无法被另一部件gn模板调用的问题,本章节讲解如何使用部件间依赖
首先用白话描述下几个关键名词
- 子系统:子系统是一个逻辑上的概念,是鸿蒙模块化构建的第一层,由若干个部件组成,在形式上表现为一个子系统文件夹内部有多个部件文件夹,子系统目录的位置一般为源码根目录,在//build/subsystem_config.json 中可以增删编辑你需要的子系统
- 部件:部件是一个构建上的概念,在构建的时候需要提供部件的标志性文件bundle.json,即哪个文件夹内有一个bundle.json就表示这个文件夹内定义了一个部件
- 模块:模块是更细层面的构建概念,一个部件由若干个模块组成,每个模块在gn构建语法中体现为一个构建目标target
构建流程
构建的命令形如: ./build.sh --product-name 产品名 --ccache --no-prebuilt-sdk
在使用命令进行源码构建的时候会读取//build/subsystem_config.json中的配置(//表示源码根目录),只有在该文件中配置的子系统才会参与构建,构建的命令中出现了产品名,说明会读取//vendor目录下指定产品的config.json文件,该文件中指明了产品需要的子系统中的那些部件,因此想自定义一个部件参与编译至少需要三步
- //build/subsystem_config.json中添加子系统
- config.json中添加待编译的子系统部件
- 创建自定义部件
创建部件
首先在源码根目录下创建一个文件夹作为逻辑上的子系统,如这里创建一个文件夹为sample,路径为 //sample 即跟//build目录同级
在sample文件夹下创建三个目录:hello , self_share , self_static,分别表示三个部件
hello文件夹是exe部件,self_share是so库部件,self_static是a库部件
参照上面视频教程中的目录结构,在三个目录中分别创建如下结构目录
sample
| ____hello
| ____|____include
| ____| ____|____helloworld.h
| ____| ____|____selfhilog.h
| ____|____src
| ____| ____|____helloworld.cpp
| ____|____BUILD.gn
| ____|____bundle.json
|
| ____self_share
| ____|____include
| ____| ______|____selfshare.h
| ____|
| ____|____src
| ____| _____|____selfshare.cpp
| ____|____BUILD.gn
| ____|____bundle.json
|
| ____self_static
| ____|____include
| ____| ______|____selfshare.h
| ____|
| ____|____src
| ____| ____|____selfshare.cpp
| ____|____BUILD.gn
| ____|____bundle.json
根据目录很清晰可知,hello , self_share , self_static三个目录下均有bundle.json文件,表示这三个目录分别定义了三个部件
- 为什么要定义成部件呢?
在鸿蒙南向开发中,最基本的技能就是掌握部件的构建以及部件相互之间的调用,开发中的难点是如何理解部件并调用开源代码中的系统部件,为了便于快速上手,本范例提供了系统部件hilog调用,自定义一个自动安装的系统部件,一个so库部件模板,一个a库部件模板. - a库(静态库)为什么要定义成部件呢?
从正式开发上来说,a库一般作为部件内模块,不会单独作为部件提供,因为a库的特性从本质上讲就是源码,编译链接期间会编译进宿主程序,一般会把common,utils等工具类公共类的代码编译为a库,供同部件下其他程序链接使用,但是作为练习教程来讲,C++的三板斧,exe,so,a不能不讲
hello部件
hello部件作为最基础全面的入门部件,本范例提供的hello部件拥有如下功能:
- 基本的exe部件构建
- 调用系统部件hilog,封装hilog日志打印
- 调用self_share部件so库
- 调用self_static部件a库
基本的exe部件构建
构建一个exe部件的最基础文件为一个.h,一个.cpp,一个.gn,一个.json,上述目录中的selfhilog.h是封装的hilog,本节暂且不提
.h文件如下:
#ifndef HELLOWORLD_H
#define HELLOWORLD_H
#ifdef __cplusplus
#if __cplusplus
extern "C" {
#endif
#endif
void HelloPrint();
#ifdef __cplusplus
#if __cplusplus
}
#endif
#endif
#endif // HELLOWORLD_H
.cpp文件如下:
#include <stdio.h>
#include "helloworld.h"
int main(int argc, char ** argv) {
HelloPrint();
return 0;
}
void HelloPrint() {
printf("\n\n##################################");
printf("\n\tHello G!\n");
printf("\n\n----------------------------------\n");
}
.gn文件内容如下:
import("//build/ohos.gni") #导入编译模板
ohos_executable("helloworld") { #可执行模块
sources = [ #模块源码
"src/helloworld.cpp"
]
include_dirs = [ #模块依赖头文件目录
"include"
]
cflags = []
cflags_c = []
cflags_cc = []
ldflags = []
configs = []
deps = [] #部件内部依赖
external_deps = [] #部件间依赖
part_name = "hello" #所属部件名称,必选
install_enable = true #是否默认安装(缺省默认不安装),可选
}
.json文件内容如下:
{
"name": "@ohos/hello",
"description": "Hello world example",
"version": "3.1.0",
"license": "Apache License 2.0",
"publishAs": "code-segment",
"segment": {
"destPath": "sample/hello"
},
"dirs": {},
"scripts": {},
"component": {
"name": "hello",
"subsystem": "sample",
"syscap": [],
"features": [],
"adapted_system_type": ["standard"],
"rom": "",
"ram": "",
"deps": {
"components": [],
"third_party": []
},
"build": {
"sub_component": [
"//sample/hello:helloworld"
],
"inner_kits": [],
"test": []
}
}
}
.h跟.ccp文件太基础了就不提了,.gn文件跟.json文件是重点
gn文件描述的是编译的最小target,也就是上面说的模块,gn里面使用了ohos的自定义模板ohos_executable;该函数表示构建一个exe程序,gn文件中最重要的是part_name,该字段表示整个gn文件中的模块属于哪一个部件
json文件描述的是一个部件,segment -> destPath写入你的部件源码目录, component-> name 写入你的部件名字,component-> subsystem 写入当前部件所属的子系统,上面讲了,一个部件可以拥有若干个子模块,如何表示若干个子模块呢?在build->sub_component 中加入你的部件拥有的子模块 形如 子部件目录d:模块名字name,编译的时候会去子部件目录d寻找gn文件,去gn文件中找到名为name的编译模块
需要注意的是,gn中的part_name一定要跟 json中的component-> name 部件名一致
调用系统部件hilog,封装hilog日志打印
上述目录结构中的selfhilog.h 即为封装hilog日志打印,文件内容如下:
#ifndef OHOS_SELFHILOG_LOG_H
#define OHOS_SELFHILOG_LOG_H
#include <cstring>
#include <string>
#include <unistd.h>
#include "hilog/log.h"
#ifndef SELFHILOG_LOG_TAG
#define SELFHILOG_LOG_TAG "SelfHilog"
#endif
#define FILENAME (__builtin_strrchr(__FILE__, '/') ? __builtin_strrchr(__FILE__, '/') + 1 : __FILE__)
#define SELFHILOG_LOG_DOMAIN 0xD00D521
static constexpr OHOS::HiviewDFX::HiLogLabel SELFHILOG_LOG_LABEL = {LOG_CORE, SELFHILOG_LOG_DOMAIN, SELFHILOG_LOG_TAG};
#define SELFHILOG_HILOG_PRINT(Level, fmt, ...) \
(void)OHOS::HiviewDFX::HiLog::Level(SELFHILOG_LOG_LABEL, "%{public}d [%{public}s:%{public}d] " fmt, gettid(), FILENAME, \
__LINE__, ##__VA_ARGS__)
#define SELFHILOG_LOGE(fmt, ...) SELFHILOG_HILOG_PRINT(Error, fmt, ##__VA_ARGS__)
#define SELFHILOG_LOGI(fmt, ...) SELFHILOG_HILOG_PRINT(Info, fmt, ##__VA_ARGS__)
#define SELFHILOG_LOGD(fmt, ...) SELFHILOG_HILOG_PRINT(Debug, fmt, ##__VA_ARGS__)
#endif // OHOS_SELFHILOG_LOG_H
因为篇幅原因我删除了所有空行,看起来有点紧凑了,关于hilog的原始用法善用百度,这块教程很多,关于封装部分就是SELFHILOG_HILOG_PRINT这个宏函数,如何看懂这个宏函数?
首先看宏函数参数,一共有三 Level, fmt, …
- 参数
Level
,宏函数中参数是直接替换的,这个Level会被替换为你传入的字面值,如下面SELFHILOG_LOGE的定义,传入的Level为Error,那么函数体其实就是直接调用Hilog::Error函数 - 参数
fmt, ...
这俩是表示格式化输入的不定参数,fmt表示格式, … 表示输入的参数 , SELFHILOG_HILOG_PRINT的函数体参数"%{public}d [%{public}s:%{public}d] " fmt
这是一个参数位置,C++函数中以逗号分隔一个参数,"%{public}d [%{public}s:%{public}d] " fmt
表示的是一个由两部分拼接而成字符串语句,gettid(), FILENAME, __LINE__
三个值分别填入第一部分"%{public}d [%{public}s:%{public}d] "
,##__VA_ARGS__
表示…可变参数,按格式填入第二部分fmt
因为使用了Hilog部件提供的hilog日志接口,那么在哪里依赖hilog的库以及头文件呢?
deps = [] #部件内部依赖
external_deps = [
"c_utils:utils",
"hilog:libhilog",
] #部件间依赖
part_name = "hello" #所属部件名称,必选
答案是在gn文件的external_deps中,依赖的形式为 部件名:模块名
除此之外还需要在json文件中添加如下内容:
"deps": {
"components": [
"hilog",
"c_utils"
],
"third_party": []
},
修改以上两部分即可跨部件使用另外部件中的模块了
根据C++的规则,使用第三方so库有两种方式: vs studio演示动态库的两种调用方式1 vs studio演示动态库的两种调用方式2
- so库文件 , so库头文件(显式加载,通过获取函数地址指针来调用so库中的函数)
- so库文件,so库头文件,so库函数地址导入库.a文件(隐式加载,通过so函数导入库.a文件得到so库函数地址来调用so库中的函数)
不管使用哪种方式,我们都需要使用so库头文件,在selfhilog.h文件中确实#include "hilog/log.h"
,但是有没有注意到一点,我们并没有在hello项目中手动指出hilog/log.h
的文件位置,那么编译器从哪里找到的头文件位置呢? 这个疑问先按下不表,接着看下面内容
调用self_share部件so库
调用自定义部件库的前提是你得有这个部件库,下面就先创建这个部件库
创建一个最简单的so部件库只需四个文件,跟上面创建exe一样
.h文件
#ifndef _SELFSHARE_H_
#define _SELFSHARE_H_
#ifdef __cplusplus
extern "C" {
#endif
void selfshared_helloword(void);
#ifdef __cplusplus
}
#endif
#endif //_SELFSHARE_H_
.cpp文件
#include "selfshare.h"
#include <stdio.h>
void selfshared_helloword(void)
{
printf("selfshare say : hello world \n");
}
.gn文件
import("//build/ohos.gni") #导入编译模板
config("libselfshare_helloworld_config") {
visibility = [ ":*" ]
include_dirs = [ "include" ]
}
ohos_shared_library("selfshare_helloworld") { #可执行模块
sources = [ #模块源码
"src/selfshare.cpp"
]
public_configs = [ ":libselfshare_helloworld_config" ]
#all_dependent_configs 逐层递归传递变量 查看帮助gn all_dependent_configs --help
cflags = []
cflags_c = []
cflags_cc = []
ldflags = []
configs = []
deps = [] #部件内部依赖
external_deps = [
]
part_name = "self_share" #所属部件名称,必选
}
.json文件
{
"name": "@ohos/self_share",
"description": "share lib",
"version": "3.1.0",
"license": "Apache License 2.0",
"publishAs": "code-segment",
"segment": {
"destPath": "sample/self_share"
},
"dirs": {},
"scripts": {},
"component": {
"name": "self_share",
"subsystem": "sample",
"syscap": [],
"features": [],
"adapted_system_type": ["standard"],
"rom": "",
"ram": "",
"deps": {
"components": [
],
"third_party": []
},
"build": {
"sub_component": [
"//sample/self_share:selfshare_helloworld"
],
"inner_kits": [
{
"name": "//sample/self_share:selfshare_helloworld",
"header": {
"header_files": [
"selfshare.h"
],
"header_base": "//sample/self_share/include"
}
}
],
"test": []
}
}
}
.h跟.cpp文件就不说了,这里的关键是.gn跟.json文件
- 相比于上文创建一个exe,这里用的是
ohos_shared_library()
函数模板创建一个动态库,跟上文的gn文件不同在于,这里使用了两个新函数,config()
跟public_configs()
来代替include_dirs
包含头文件目录,public_configs()
函数的作用是将当前模块的头文件目录暴露给直接链接当前模块的模块,有点绕,白话就是如果一个exe依赖链接了这个库,那么这个库的头文件目录会自动暴露给这个exe,好了到这里应该能明白为什么上面链接使用hilog库却没有手动包含hilog头文件路径了么,答案就是使用了public_configs()
函数,但是这个函数仅仅能对一个exe的直接依赖链接暴露文件路径,无法对嵌套依赖链接暴露,啥意思呢?
有一个库叫A.so,A依赖链接了hilog, 现在有个B.exe,B依赖链接了A.so,现在存在两层依赖, A直接依赖hilog,B直接依赖A,如果想在B中使用hilog的原生接口是无法找到hilog的头文件的,因为B不是直接依赖hilog库,public_configs()
函数无法嵌套暴露包含的路径,那么有没有办法嵌套暴露呢?答案是使用all_dependent_configs
函数,当然了我这里并不打算演示因为演示的篇幅太长了,再写下去就跟写书一样了,各位学会了这篇的内容可以自行验证 - 这里的json文件内容也跟上面创建exe不一样,这里多使用了build->inner_kits,这个字段用于告诉gn,inner_kits里面的内容需要对别的部件暴露,方便别的部件使用部件间依赖使用本部件的模块,name字段写模块的名字(需使用全路径),header_files写需要暴露的头文件,header_base写header_files中头文件的位置
调用本部件的方法跟上文调用hilog的方法一样,需要在gn文件的external_deps中添加 本部件:模块,在json文件的deps->components添加本部件
调用self_static部件a库
a库(静态链接库)的构建方式跟so库基本一致,唯一不一样的是ohos的创建函数模板,a库使用 ohos_static_library()
函数模板
.h
#ifndef _SELFSTATIC_H_
#define _SELFSTATIC_H_
#ifdef __cplusplus
extern "C" {
#endif
void selfstatic_helloword(void);
#ifdef __cplusplus
}
#endif
#endif //_SELFSTATIC_H_
.cpp
#include "selfstatic.h"
#include <stdio.h>
void selfstatic_helloword(void)
{
printf("selfstatic say : hello world \n");
}
.gn
import("//build/ohos.gni")
config("libselfstatic_helloworld_config") {
visibility = [ ":*" ]
include_dirs = [ "include" ]
}
ohos_static_library("selfstatic_helloworld"){
sources = [
"src/selfstatic.cpp"
]
public_configs = [ ":libselfstatic_helloworld_config" ]
deps = []
part_name = "self_static"
}
.json
{
"name": "@ohos/self_static",
"description": "static lib",
"version": "3.1.0",
"license": "Apache License 2.0",
"publishAs": "code-segment",
"segment": {
"destPath": "sample/self_static"
},
"dirs": {},
"scripts": {},
"component": {
"name": "self_static",
"subsystem": "sample",
"syscap": [],
"features": [],
"adapted_system_type": ["standard"],
"rom": "",
"ram": "",
"deps": {
"components": [
],
"third_party": []
},
"build": {
"sub_component": [
"//sample/self_static:selfstatic_helloworld"
],
"inner_kits": [
{
"name": "//sample/self_static:selfstatic_helloworld",
"header": {
"header_files": [
"selfstatic.h"
],
"header_base": "//sample/self_static/include"
}
}
],
"test": []
}
}
}
调用本部件的方法跟上文调用so库的方法一样,需要在gn文件的external_deps中添加 本部件:模块,在json文件的deps->components添加本部件
编译部件
上面的内容创建了三个部件,在链接了自定义的so库跟a库后,exe的gn文件如下:
import("//build/ohos.gni") #导入编译模板
ohos_executable("helloworld") { #可执行模块
sources = [ #模块源码
"src/helloworld.cpp"
]
include_dirs = [ #模块依赖头文件目录
"include"
]
cflags = []
cflags_c = []
cflags_cc = []
ldflags = []
configs = []
deps = [
] #部件内部依赖
external_deps = [
"c_utils:utils",
"hilog:libhilog",
"self_share:selfshare_helloworld",
"self_static:selfstatic_helloworld"
]
part_name = "hello" #所属部件名称,必选
install_enable = true #是否默认安装(缺省默认不安装),可选
}
exe的json文件如下:
{
"name": "@ohos/hello",
"description": "Hello world example",
"version": "3.1.0",
"license": "Apache License 2.0",
"publishAs": "code-segment",
"segment": {
"destPath": "sample/hello"
},
"dirs": {},
"scripts": {},
"component": {
"name": "hello",
"subsystem": "sample",
"syscap": [],
"features": [],
"adapted_system_type": ["standard"],
"rom": "",
"ram": "",
"deps": {
"components": [
"hilog",
"c_utils",
"self_share",
"self_static"
],
"third_party": []
},
"build": {
"sub_component": [
"//sample/hello:helloworld"
],
"inner_kits": [],
"test": []
}
}
}
不知道聪明的你有没有写对呢
在上面构建流程章节已经讲过想自定义一个部件参与编译至少需要三步,下面将本次创建的三个部件添加编译
- 在//build/subsystem_config.json中添加如下内容:
"sample": {
"path": "sample",
"name": "sample"
}
参照subsystem_config.json中其他子系统的添加方式,注意书写格式
该内容表示添加一个子系统sample, 路径就是根目录sample目录,子系统的名字就是sample
2. /vendor/厂家/产品名/config.json中添加如下内容:
{
"subsystem": "sample",
"components": [
{
"component": "hello",
"features": []
},
{
"component": "self_share",
"features": []
},
{
"component": "self_static",
"features": []
}
]
}
参照config.json中其他模块的添加方式,注意书写格式
该内容表示将子系统sample下的hello,self_share,self_static三个部件加入到产品构建当中
3. 编译部件
新增的部件有两种构建思路:
- 单部件构建 : ./build.sh --product-name 产品名 -T helloworld --ccache --no-prebuilt-sdk
- 全量构建 : ./build.sh --product-name 产品名 --ccache --no-prebuilt-sdk
单部件构建:直接构建helloworld模块,还记得上面说的,模块是最小编译target,可以直接进行指定编译,编译helloworld模块会把依赖项hilog,selfshare_helloworld,selfstatic_helloworld全部编译;在//out目录下查找helloworld跟libselfshare_helloworld.so
find ./ -name "helloworld"
find ./ -name "*selfshare_helloworld*"
将这两个文件上传到板子
hdc file send XXXXXXXX/helloworld /bin/
hdc file send XXXXXXXX/libselfshare_helloworld.so /lib64/
因为a库被编译到helloworld中了,所以只要拷贝俩文件就行
在板子上赋予helloworld执行权限,执行helloworld
全量构建:将整个源码编译,做成镜像烧录到开发板,通过hdc链接开发板可以直接运行helloworld,因为我们在gn文件中标记了install_enable = true默认安装