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机制,或许以后,我们也能看到不一样的东西。

相关推荐
狂奔solar5 分钟前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky7 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
清云随笔28 分钟前
axios 实现 无感刷新方案
前端
鑫宝Code30 分钟前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线38 分钟前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf
pink大呲花41 分钟前
关于番外篇-CSS3新增特性
前端·css·css3
少年维持着烦恼.1 小时前
第八章习题
前端·css·html
我是哈哈hh1 小时前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
田本初1 小时前
如何修改npm包
前端·npm·node.js
曾经的三心草1 小时前
Mysql之约束与事件
android·数据库·mysql·事件·约束