背景
最近在开发鸿蒙的IM项目,既然是IM项目必然存在网络通信,数据库存储等相关知识,这篇文章先不提及一些IM的核心内容,而是先说一下如何使用Protobuf库。
题外话
好久没有输出文章了,原因是24年10月份工作发生了变化,去了新公司做IM项目,临近过年终于有时间总结一下输出一下,在此也给大家拜个晚年了!祝大家在新的一年,身体健康,升职加薪!
针对IM通信层,为什么选择使用Protobuf作为数据包格式?
- 高效的序列化与反序列化
Protobuf 在数据序列化时使用二进制格式,比 JSON、XML 等文本格式更加紧凑,占用更少的带宽,并且解析速度更快。 - 跨平台兼容性
Protobuf 提供多种语言的官方支持,包括 Java、Kotlin、C++、Go、Python 等,使得客户端和服务器可以较为容易实现跨平台通信。 - 灵活的可扩展性
Protobuf 允许定义消息时保持向后兼容和向前兼容,这使得新增字段不会影响已有的功能,适合快速迭代的 IM 应用开发。 - 易于定义和维护
通过 .proto 文件定义数据结构,开发者可以清晰地管理和维护通信协议,减少接口变更的成本。 - 安全性更高
由于 Protobuf 采用二进制编码,不易被人直接阅读,有助于在一定程度上提升通信数据的安全性。 - 自动生成代码
Protobuf 支持通过编译自动生成不同编程语言的数据类,减少手写解析代码的工作量,提高开发效率。
在鸿蒙napi通信过程,使用protobuf字节数组进行数据传输
我们的场景是这样的,实现一套类似企业微信的能力,核心的一块就是数据库,但在比较中,我们Android,iOS都采用了腾讯开源的WCDB数据库,它在基于Sqlite的基础上进行了修改与优化,因此在鸿蒙侧也希望可以使用,针对数据库后续会单独总结一篇文章记录。
由于在业务中,需要频繁对数据库操作,而且需要传递数据量较为大的表结构数据集,而WCDB在鸿蒙侧只能在native侧使用,这就需要考虑ets与native通信性能了,经过查阅资料,最终选择使用PB格式进行与native侧数据通信。
通过上图,我们就需要存在两部分解析,一部分是在ets侧实现PB编解码,同样在native侧实现实现PB编解码。
ETS 侧使用 Protobuf
鸿蒙官方文档中并未给出Protobuf如何使用,毕竟这个协议是Google制定的,Protobuf是数据编码规范而非完整的通信协议,因此所有的编程语言都可以支持。
花了一段时间检索,在鸿蒙三方库中有三个针对PB的编解码库,看到一个热度最高的protobufjs,它可以自动执行.proto解析脚本,并生成.ts文件 & .js文件。
如何使用就不过多阐述了,三方库有详细的使用,注意一点就是:最好不要在.proto文件中写太多的注释,会导致生成的文件不对,而且编译器也不能在编译期发现,也没有任何报错。
示例
import { conversation } from 'pb_table/conversation'
private async saveToConversationTable(convs: Conversation[]): Promise<number> {
try {
console.error('PB GENERATE START:', "START");
// 将 `convs` 数组映射为 Protobuf 格式的 Conversation 实例
const convsPBArray = convs.map(conv =>
// 创建PB对象的会话数据信息
conversation.Conversation.create({
url: conv.faceUrl,
conversationId: conv.conversationId,
name: conv.showName,
isTop: false,
})
);
// 创建 Protobuf 的 ConversationList
let convsPB = conversation.ConversationList.create({
conversations: convsPBArray
});
console.error('PB GENERATE START:', "END");
// 执行插入操作并等待完成
await ConversationOp.getInstance().insertConversations(convsPB);
console.error('WCDB INSERT START:', "END");
return 0;
} catch (error) {
console.error('Error saving conversations:', error);
return -1;
}
}
执行插入会话表,这里会调用 encode() 方法转成字节数组,再调用napi将字节数组传递到native侧执行数据库操作。
insertConversations(convs: conversation.ConversationList): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
let arrayBuffer: Uint8Array = conversation.ConversationList.encode(convs).finish(); // 调用 finish() 方法获取字节数组
const result = databaseNapi.insertConversations(arrayBuffer);
if (result >= 0) {
resolve(true);
} else {
reject(new Error('Failed to insert conversations into the database.'));
}
} catch (error) {
console.error('Error in insertConversations:', error);
reject(error);
}
});
}
综上在ETS侧使用Protobuf还是比较容易的,但是现在存在一个问题在native侧也同样需要使用Protobuf。
HarmonyOS Native(C层编译 Protobuf库)
由于没有编译Protobuf的经验而又是鸿蒙系统,不知道是否存在兼容性问题。在Github上的Protobuf库中我们可以拿到clone地址。
我这里没有使用最新的release包编译,因为之前本机上的protobuf版本是29.1版本,由于鸿蒙Native侧暂时需要使用命令将.proto文件转成C++代码,因此需要确保编译库的版本与本机protobuf版本一致。
git clone https://github.com/protocolbuffers/protobuf.git
# 进入项目目录
cd protobuf
# 切换到 29.1 版本的 tag
git checkout v29.1
# 注意这里 项目依赖了子仓库需要执行下方命令去依赖
git submodule update --init --recursive
此时v29.1代码已经克隆下来了,并要确保有cmake环境。
cmake
-DCMAKE_CXX_STANDARD=17 \
-Dprotobuf_BUILD_TESTS=OFF \
-DBUILD_SHARED_LIBS=OFF \
-Dprotobuf_PROTOC_EXE=/opt/homebrew/bin/protoc \
-DCMAKE_INSTALL_INCLUDEDIR=include \
-DCMAKE_TOOLCHAIN_FILE=/Users/EdisonLi/Library/OpenHarmony/Sdk/13/native/build/cmake/ohos.toolchain.cmake \
-DOHOS_ARCH=arm64-v8a \
-DCMAKE_INSTALL_PREFIX=/Users/EdisonLi/Downloads/protobuf/install \
-S /Users/EdisonLi/Downloads/protobuf \
-B /Users/EdisonLi/Downloads/protobuf/build
之后就会生成build配置文件,如下。
然后执行如下命令进行编译。
cmake --build /Users/EdisonLi/Downloads/protobuf/build --target install
成功后则生成install & bin & include 文件夹,
.proto 文件转 C++ 代码
我们就可以使用bin中的 protc命令将 .proto 文件转 C++ 代码了。
export PATH=/Users/EdisonLi/Downloads/protobuf/install/bin:$PATH
protoc --version
protoc --cpp_out=out conversation.proto
鸿蒙项目中编译protobuf库
转C++代码流程已完成,但将C文件在项目中编译是报错的,需要引入编译v29.1的头文件。
我们将编译好的 inclule 文件夹全部复制到项目中。
并将libs中的静态库一并复制,由于方便后续排查问题,以及不想单独编译成一个protobuf.so,所以当时我配置 cmake -DBUILD_SHARED_LIBS=OFF,如果需要打动态库则可设置为cmake -DBUILD_SHARED_LIBS=ON
之后我们需要单独在cmake配置中声明如下:
# cmake/thirdparty.cmake
# WCDB 配置
add_library(WCDB SHARED IMPORTED)
set_target_properties(WCDB
PROPERTIES
IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/third_party/wcdb/libs/${OHOS_ARCH}/libWCDB.so
)
# Protobuf 及 Abseil 静态库列表
set(PROTOBUF_LIBS
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libprotobuf-lite.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libprotobuf.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libprotoc.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_bad_any_cast_impl.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_bad_optional_access.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_bad_variant_access.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_base.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_city.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_civil_time.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_cord.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_cord_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_cordz_functions.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_cordz_handle.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_cordz_info.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_cordz_sample_token.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_crc32c.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_crc_cord_state.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_crc_cpu_detect.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_crc_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_debugging_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_demangle_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_die_if_null.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_examine_stack.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_exponential_biased.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_failure_signal_handler.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_commandlineflag.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_commandlineflag_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_config.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_marshalling.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_parse.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_private_handle_accessor.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_program_name.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_reflection.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_usage.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_flags_usage_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_graphcycles_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_hash.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_hashtablez_sampler.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_int128.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_kernel_timeout_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_leak_check.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_entry.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_flags.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_globals.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_initialize.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_check_op.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_conditions.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_fnmatch.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_format.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_globals.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_log_sink_set.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_message.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_nullguard.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_internal_proto.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_severity.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_log_sink.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_low_level_hash.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_malloc_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_periodic_sampler.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_distributions.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_distribution_test_util.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_platform.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_pool_urbg.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_randen.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_randen_hwaes.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_randen_hwaes_impl.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_randen_slow.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_internal_seed_material.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_seed_gen_exception.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_random_seed_sequences.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_raw_hash_set.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_raw_logging_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_scoped_set_env.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_spinlock_wait.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_stacktrace.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_status.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_statusor.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_str_format_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_strerror.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_string_view.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_strings.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_strings_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_symbolize.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_synchronization.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_throw_delegate.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_time.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_time_zone.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libabsl_vlog_config_internal.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libupb.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libutf8_range.a
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/libs/${OHOS_ARCH}/libutf8_validity.a
)
这里需要把所有的.a文件都编译进来,由于protobuf使用了abseil,所以大部分都是这个库的引用。由于abseil提供非常稳定实用的基础库,所以后续也可用上。
此时编译就成功了。
举个解析的例子
上述代码是通过这个命令生成
protoc --cpp_out=out conversation.proto
napi_value insertConversations(napi_env env, napi_callback_info info) {
std::vector<uint8_t> num_array = {};
uint8ArrayPassingTs2Napi(env, info, num_array);
conversation::ConversationList conversationListPb;
// 先解析字节数组,转换成pb对象
if (!conversationListPb.ParseFromArray(num_array.data(), num_array.size())) {
napi_throw_error(env, nullptr, "Failed to parse protobuf data");
return nullptr;
}
std::unique_ptr<ConversationOp> convOp = std::make_unique<ConversationOp>();
const int count = conversationListPb.conversations_size();
napi_value result;
if (count > 0) {
std::vector<Conversation> convContainer;
for (int i = 0; i < count; i++) {
const conversation::Conversation &pbConv = conversationListPb.conversations(i); // 转 C++ pb对象模型。
Conversation wcdbConv;
// string to int64
wcdbConv.conversationId = std::stoll(pbConv.conversation_id());
switch (pbConv.not_disturb_type()) {
case conversation::Conversation_NotDisturbType_NONE:
wcdbConv.notDisturbType = "NONE";
break;
case conversation::Conversation_NotDisturbType_MUTE:
wcdbConv.notDisturbType = "MUTE";
break;
case conversation::Conversation_NotDisturbType_CUSTOM:
wcdbConv.notDisturbType = "CUSTOM";
break;
default:
wcdbConv.notDisturbType = "UNKNOWN";
}
// 使用获取pb数据。
wcdbConv.isGroup = pbConv.is_group();
wcdbConv.url = pbConv.url();
wcdbConv.name = pbConv.name();
wcdbConv.lastMessage = pbConv.last_message();
wcdbConv.lastServerMsgId = pbConv.last_server_msg_id();
wcdbConv.lastTime = pbConv.last_time();
wcdbConv.unreadCount = pbConv.unread_count();
wcdbConv.readServMsgID = pbConv.read_serv_msg_id();
wcdbConv.hide = pbConv.hide() ? 1 : 0; // bool转int
wcdbConv.sender = pbConv.sender();
wcdbConv.hasAtYou = pbConv.has_at_you();
wcdbConv.hasAtAll = pbConv.has_at_all();
wcdbConv.isTop = pbConv.is_top();
wcdbConv.topId = pbConv.top_id();
wcdbConv.disturbId = pbConv.disturb_id();
wcdbConv.isStar = pbConv.is_star();
wcdbConv.starId = pbConv.star_id();
wcdbConv.relation = pbConv.relation();
wcdbConv.relationId = pbConv.relation_id();
wcdbConv.lastNameTime = pbConv.last_name_time();
const std::string& draftStr = pbConv.draft_info();
wcdbConv.draftInfo.assign(draftStr.begin(), draftStr.end());
convContainer.push_back(wcdbConv);
}
int32_t insertCounts = !convOp->insertConversations(convContainer);
if (insertCounts >= 0) {
OH_LOG_Print(LOG_APP, LOG_ERROR, 0, "insertConv", "Failed to insert conversation: %d", insertCounts);
}
napi_create_int32(env, insertCounts, &result);
} else {
napi_create_int32(env, -1, &result);
}
return result;
}
此时native侧就能获取到ets侧传递过来的大数据了,然后再进行后续的数据库写入操作。
总结
至此鸿蒙的native能力的开发,实际上和Android都一样,后续会继续探讨WCDB数据库在鸿蒙中的应用。