Harmony OS 应用开发 - 如何迁移Crash监控

开篇

本篇是鸿蒙开发专栏的第一篇,在这个专栏中,我们将以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机制,或许以后,我们也能看到不一样的东西。

相关推荐
雪芽蓝域zzs几秒前
第三十五节:Axios 统一错误拦截、401 Token 过期处理
前端·javascript·vue.js
XIE3925 分钟前
TipKit:开源富文本编辑器套件,一套逻辑,任意风格!
前端·笔记·开源
范什么特西19 分钟前
一些常用名词
开发语言·前端·javascript
晴天1621 分钟前
前端打包工具全景解析
前端
晴天1623 分钟前
Vite vs Webpack 全方位对比
前端·webpack·node.js
体毛旺盛的猿25 分钟前
HarmonyOS开发面试题
前端·华为·harmonyos
anOnion39 分钟前
构建无障碍组件之Landmarks Pattern
前端·html·交互设计
牧瀬クリスだ41 分钟前
前端三件套基础之JS!!!!
前端·javascript
风骏时光牛马44 分钟前
提示词工程:大模型交互的核心塑造技术
前端
单线程_011 小时前
从案例分析 Vue3 Tokenizer+Parser 源码四
前端·javascript·vue.js