开篇
本篇是鸿蒙开发专栏的第一篇,在这个专栏中,我们将以Android开发的视角,实现将Android开发中遇到的知识,尽可能的迁移到鸿蒙开发当中。当然,我也作为一个学习者,也希望能够留下更多的学习记录,分享给大家。
如何看待Harmony OS
有很多小伙伴都私下问过我,怎么看待鸿蒙。其实作为Android开发的我,直到鸿蒙next的消息出现前,都没怎么关注过鸿蒙生态,因为Android应用完全可以跑在鸿蒙当中。最近这几个月关于种种鸿蒙的消息,包括鸿蒙next移除AOSP等,也不让我重新审视这个新的操作系统。
当然,next版本还没正式发布前,我其实不怎么好评论。当然,作为一名国内的移动开发者,能让开发者们"折腾起来",其实是一件好事,因为新的事物出现,代表着新的需求,有"活"才是真道理哈哈哈。直到今天,招聘市场上直接招聘鸿蒙相关开发的岗位,有,但是很少(头部几个大厂),但是至少证明了还是有需求的。
当然,学习鸿蒙并不代表抛弃我们原有的知识,比如Android知识,我相信未来很多年,Android系统依旧是操作系统中的不可或缺的一部分,无论是国内还是国外。我们可以利用原有的知识,去迁移到新的事物中,这也是这篇文章的核心观点之一。
迁移Crash监控
针对ArkTS层
在Android开发中,我们用Java/Kotlin去编写代码,当发生异常的时候,虚拟机会通过触发Thread的UncaughtExceptionHandler 进行处理,而Thread默认的UncaughtExceptionHandler被设置为KillApplicationHandler。在KillApplicationHandler中,会通过Process.killProcess触发进程杀死。
因此针对JVM,我们可以通过注入一个自定义的UncaughtExceptionHandler,从而去复写默认的行为,从而上报异常到后端监控平台。
那么,ArkTs中,有没有类似的机制呢?当然有,在Arkts中,异常通过广播机制,可以被合适的监听者所监听,这里的监听者就是errorManager
应用中,可以通过errorManager on方法注册一个ErrorObserver,当发生异常的时候,会被执行onUnhandledException方法
typescript
private registerErrorObserver() {
errorManager.on("error", {
onUnhandledException(error: string) {
异常处理
}
})
}
on方法定义如下:
typescript
function on(type: "error", observer: ErrorObserver): number;
其中ErrorObserver 类定义如下,我们可以通过errMsg获取到需要的堆栈信息
php
export default class ErrorObserver {
/**
* Will be called when the js runtime throws an exception which doesn't caught by user.
*
* @since 9
* @syscap SystemCapability.Ability.AbilityRuntime.Core
* @param errMsg the message and error stacktrace about the exception.
* @returns -
*/
onUnhandledException(errMsg: string): void;
}
因此,我们可以通过在应用的初始化中,注册一个自定义的ErrorObserver,去拿到异常信息,从而可以去回调给到服务器。
scala
import AbilityStage from '@ohos.app.ability.AbilityStage';
import errorManager from '@ohos.app.ability.errorManager';
export default class MainAbilityStage extends AbilityStage {
onCreate() {
this.registerErrorObserver()
}
private registerErrorObserver() {
errorManager.on("error", {
onUnhandledException(error: string) {
上报服务器
}
})
}
}
AbilityStage可以理解为Android中的Appication,这里我们需要注意的是,我们可以通过errorManager.on注册多个ErrorObserver,这些注册的ErrorObserver会被放到集合中,不会出现覆盖,因此我们如果注册多个ErrorObserver,那么异常发生时,每个ErrorObserver都会被回调。区别于Android 的UncaughtExceptionHandler,每个Thread只能持有一个UncaughtExceptionHandler,如果想要接着传递这个异常信息给下一个hander,需要我们自主维护这个传递链。这里两者处理方式都有好处与坏处。
Android:UncaughtExceptionHandler | Harmony: onUnhandledException |
---|---|
效率高,一个hander可以保证 | 简单易用,不用担心额外的维护成本 |
需要自主维护handler链传递 | 引入额外的集合保存成本 |
on方法的返回值是一个number类型,其实就是当前观测的id,当我们不再需要这个观测的时候,可以通过off方法取消注册ErrorObserver
typescript
function off(type: "error", observerId: number, callback: AsyncCallback<void>): void;
当然,对于Crash监控来说,整个生命周期内,应该是确保处于on状态的。
针对Native层
在Android开发中,我们通过JNI进行Java/Kotlin 与native层的交互,他们的关系其实可以抽象如图:
JNI 提供了Java 到Native 的数据结构,比如jint,jobject等,同时我们可以利用这些数据结构实现Java层到Native层的双向交互。
在Android中,如果想要监控NativeCrash,其实是通过Linux信号机制实现的,比如sigaction注册信号,当发生信号时dump出相应的内存数据,然后通过Java层返回。
c
static void sig_func(int sig_num, siginfo_t *info, void *ptr) {
JNI 传递堆栈信息,info信息等
}
static void registerSignlHandler() {
struct sigaction sigc;
sigc.sa_sigaction = reinterpret_cast<void (*)(int, siginfo_t *, void *)>(sig_func);
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
sigaction(11, &sigc, old_sigh);
}
我们用"便便模型"来看的话,是这样
那么我们视角拉回鸿蒙,鸿蒙系统在设计中,依旧是以Linux内核为底架(手机端)
因此我们上述通过Linux系统实现native代码,是可以完好无需改变,就能够运行在鸿蒙系统当中
c
struct sigaction* old_sigh;
static void sig_func(int sig_num, siginfo_t *info, void *ptr) {
OH_LOG_WARN(LOG_APP,"call sig_func");
}
static void registerSignlHandler() {
struct sigaction sigc;
sigc.sa_sigaction = reinterpret_cast<void (*)(int, siginfo_t *, void *)>(sig_func);
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
sigaction(11, &sigc, old_sigh);
}
有趣吧,大部分C/C++库,只要是属于标准Linux调用,比如rasie,pthread库等,其实是可以完整迁移到鸿蒙中的。然后,在数据传递层,就需要发生改变了,因为与ArkTs打交道的中间人,从JNI变成了NAPI。
HarmonyOS的应用必须用js来桥接native。需要使用ace_napi仓中提供的napi接口来处理js交互。
napi 其实就是代替JNI成为ArkTs中中间人的角色。
napi 使用
我们简单介绍一下napi的使用 :OpenHarmony NAPI 将 ECMAScript 标准中定义的 Boolean、Null、Undefined、Number、BigInt、String、Symbol和 Object 这八种数据类型以及函数对应的 Function 类型统一封装成了 napi_value 类型。也就是说,区别于我们JNI中熟知的jint,jobject等jxx中间类型,napi的中间类型其实只有napi_value。
我们就拿int类型举例子:
C/C++转napi_value
arduino
NAPI_EXTERN napi_status napi_create_int32(napi_env env,
int32_t value,
napi_value* result);
NAPI_EXTERN napi_status napi_create_uint32(napi_env env,
uint32_t value,
napi_value* result);
NAPI_EXTERN napi_status napi_create_int64(napi_env env,
int64_t value,
napi_value* result);
我们可以通过C中的int类型,比如int64_t,通过napi_create_int64转化为napi_value指针,从而传递的Arkts层使用
napi_value转C/C++ 相反的,如果我们想要从C层获取napi_value的内容,可以通过napi_get_value_xx方法获取,还是以int类型举例子,我们可以在C层通过napi_get_value_int64方法获取ArtTs中传递的int64数值
arduino
NAPI_EXTERN napi_status napi_get_value_int32(napi_env env,
napi_value value,
int32_t* result);
NAPI_EXTERN napi_status napi_get_value_uint32(napi_env env,
napi_value value,
uint32_t* result);
NAPI_EXTERN napi_status napi_get_value_int64(napi_env env,
napi_value value,
int64_t* result);
更多的转化可以参考NAPI官方。
下面我们再来介绍一下,如何注册一个本地方法,提供给ArkTs调用,就拿我们Crash监控的例子:
首先我们提供两个方法,第一个registerSignlHandler是注册一个信号处理器,第二个函数createCrash其实通过rasie调用抛出一个信号。这部分代码,其实就是上面我们"便便模型"的核心Linux处理层,这部分代码可以运行在任何一个Linux子系统中,包括Android 与Harmony,目的很简单,通过信号处理器监听信号,表示发生了一个Native Crash,还有就是制造一个crash,代表着我们业务C++代码
scss
static void registerSignlHandler() {
struct sigaction sigc;
sigc.sa_sigaction = reinterpret_cast<void (*)(int, siginfo_t *, void *)>(sig_func);
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
sigaction(11, &sigc, old_sigh);
}
static void createCrash() {
raise(11);
}
要注册一个napi函数,需要满足以下步骤:
ini
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"createCrash", nullptr, napi_create_crash, nullptr, nullptr, nullptr, napi_default, nullptr}};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
其中 attribute((constructor)) 修饰的其实是C++提供的语法糖,用于初始化,当so被加载时调用,此时我们需要通过napi_module_register注册一个napi_module类型的值
napi_module含义如下:
- nm_version:nm版本号,默认值为 1。
- nm_flags:nm标记符,默认值为 0。
- nm_filename:暂不关注,使用默认值即可。
- nm_register_func:指定nm的入口函数。
- nm_modname :指定 TS 页面导入的模块名,例如:
import testNapi from 'libentry.so'
中的 libentry.so 。 - nm_priv:暂不关注,使用默认值即可。
- reserved:暂不关注,使用默认值即可。
在nm_register_func 中通过 napi_define_properties注册好映射,类似于JNI中的动态加载,RegisterNative。
napi_property_descriptor 中定义如下:
ini
typedef struct {
// One of utf8name or name should be NULL.
const char* utf8name;
napi_value name;
napi_callback method;
napi_callback getter;
napi_callback setter;
napi_value value;
napi_property_attributes attributes;
void* data;
} napi_property_descriptor;
我们需要在name中,写入暴露给TS层的调用的函数,这里我们以createCrash作为暴露给TS层的桩函数,提供一个ts文件
method 中需要填入napi_callback函数指针,当调用ArkTs调用name这个函数的时候,其实就被派发到method函数执行
napi_callback 其实是一个函数类型,我们需要一个包装函数,满足以下函数定义才能被写入
arduino
typedef napi_value (*napi_callback)(napi_env env,
napi_callback_info info);
根据上面函数定义,我们可以写一个包装函数,用于包装registerSignlHandler与createCrash方法,每次调用就注册新号为11的信号处理器,并触发一个为11(SIGSEGV 无效内存)的信号
scss
static napi_value napi_create_crash(napi_env env, napi_callback_info info) {
registerSignlHandler();
createCrash();
return nullptr;
}
这样,我们就可以在AtkTS中,愉快的调用我们的createCrash函数了:
typescript
import hilog from '@ohos.hilog';
导入testCrashNapi代表libentry.so调用类
import testCrashNapi from 'libentry.so'
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
testCrashNapi.createCrash();
})
}
.width('100%')
}
.height('100%')
}
}
运行结果如下:
完整C代码如下:
arduino
#include "napi/native_api.h"
#include "syscall.h"
#include "pthread.h"
#include "signal.h"
#include <hilog/log.h>
#define LOG_TAG "hello"
struct sigaction *old_sigh;
static void sig_func(int sig_num, siginfo_t *info, void *ptr) {
OH_LOG_WARN(LOG_APP, "call sig_func");
}
static void registerSignlHandler() {
struct sigaction sigc;
sigc.sa_sigaction = reinterpret_cast<void (*)(int, siginfo_t *, void *)>(sig_func);
sigfillset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
sigaction(11, &sigc, old_sigh);
}
static void createCrash() {
raise(11);
}
static napi_value napi_create_crash(napi_env env, napi_callback_info info) {
registerSignlHandler();
createCrash();
return nullptr;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"createCrash", nullptr, napi_create_crash, nullptr, nullptr, nullptr, napi_default, nullptr}};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
这样,我们只要在sig_func调用相关napi函数传递信息到ts中,触发网络上报到后端即可。
总结
通过本文,我们了解了ArkTs中如何监控ts层与native层crash,这里面有相当一部分概念或者知识,都是可以从Android开发中迁移过去的,同时我们也知道,无论是现在harmony版本或者即将推出的harmony next版本,最后也离不开Linux内核,因此我们在C层中代码,只要处理好数据适配层,即可迁移。当然!我也很好奇未来移除AOSP的next版本针对信号的处理,因为在Android中,其实有针对Linux信号的SigChain机制,或许以后,我们也能看到不一样的东西。