鸿蒙NAPI到ArkTs线程自动切换器

446 阅读6分钟

前言

    鸿蒙中对于线程的概念有了新的定义,线程之间的数据和对象不会直接通过引用共享,这就使得我们想要线程之间进行通信,变的比较复杂。

问题

    如果我们是从Android转过来做鸿蒙,我们对于Java中的线程使用,可能会经常在子线程中做一些事情,在子线程中就直接使用其他线程中定义的变量。毕竟Java线程之间对象的引用还是可以共用的,不会被JMM所影响。

    Android中我们程序会和C++交互,称为JNI。在鸿蒙中则称为NAPI。

场景

    所以实际工作中就存在场景了:【我们有个全局变量在线程1中赋值了,期望后续可以在其他线程中使用】。

Android实现

    Android中 我们会调用C++的一个方法,C++执行逻辑过程中,可能会调用Java的方法,然后Java执行完结果再将结果通过调用C++ 方法告知JNI。流程如图:

image.png     其中绿色部分,JNI调用Java方法后,Java这边在线程2会用到线程1中的对象Label,可以直接拿来使用。线程1和2中的label对象引用是同一个,保证了对象内容不会改变。

对比鸿蒙

    同样的流程放到鸿蒙中,如图:

image.png

    可以看到图中对应箭头打了叉号,因为线程2 中无法直接使用线程1中定义的对象。根本原因是因为鸿蒙中线程之间的数据存在完全隔离性。

官方建议解决方法

    对于这个问题,官方给出的建议是使用sendable注解来保证对象在不同线程之间进行通信,但是对于复杂的业务场景下,存在一个对象特别重,内部引用很多其他对象,逻辑层层嵌套,导致该对象无法使用sendable修饰,此时就需要想办法了。

image.png

    如图,我们由于某些原因,无法给Label对象设置sendable注解,但是又要实现NAPI调用ArkTs方法可以用到该对象内部的数据和方法。

    对于该情况,我们肯定要依赖鸿蒙线程或进程之间的通信机制来实现。

解决思路

    我们无法直接使用其他线程的对象,那么我们是不是可以跳转到目标线程,执行结束后再告知结果给NAPI?

    我们先看下鸿蒙都有哪些方法起线程或进行线程间通信。

鸿蒙线程/进程通信方法

回顾下,鸿蒙线程之间通信方法有几种

  1. @sendable修饰一个类
  2. 使用taskpool起个线程执行完数据返回结果给当前线程
  3. 使用worker,类似taskPool
  4. 使用emitter,通过广播形式实现线程间通信
  5. 使用CES实现进程间通信
选择合适的方法
  • 方法1,我们当前业务场景中的对象无法使用sendable注解。
  • 方法2和方法3,这两种方法都是线程执行完逻辑后返回数据给当前线程,都是需要新起一个线程来处理逻辑,并不符合我们这种跳转线程的场景。
  • 方法4,emitter,可以实现线程之间数据的传递。我们可以利用这个特性来切换线程的调用实现上述思路
  • 方法5,CES目的是实现进程间通信,在当前场景太重。

使用emitter

    选择到了合适的框架,接下来,我们理一下我们的思路:

NAPI线程调度器.png
  • 流程     我们ArkTs线程中监听emitter,在NAPI线程中通过emiiter发送广播给ArkTs线程的emitter,触发调用对应的方法,得到结果后通过调用NAPI的方法来告知结果给NAPI。

    其中我们为了做到后续不修改适配器里的方法,我们想要做成自动切换线程,所以我们用到了函数映射表,后续有新增函数了,只需要更新映射表就可以,而无需更改适配器代码。

线程自动调度适配器 代码讲解

    为了实现上述这个流程,我们设计下代码。

    首先我们需要一个类用于做NAPI和ArkTs通信的接口层,可以理解为Android中的JNI接口层。我们定义一个名为TestNapi的类,该类import了目标SO,并调用里边的方法告知Http请求结果给SO,同时暴露方法给SO,SO可以调用ArkTs的方法进行Http请求。

import native from 'test1.so';

export class TestNapi {
  
  /**
   * 将Http请求结果告知NAPI
   */
  static httpResponse = native.httpResponse

  /**
   * NAPI Call TS: 进行一次网络请求
   */
  static httpRequest(cid: number, timeout: number, url: string, httpMethod: string, postData: string,
    headerData: string) {

    Log.d("zzz", "httpRequest")

    NapiDispatcher.notify({
      methodName: 'httpRequest',
      args: [cid, timeout, url, httpMethod, postData, headerData]
    })
  }
}

    可以看到上述类中,方法 httpRequest 中用到了一个类NapiDispatcher,该类中封装了emitter消息机制,可以发送消息给ArkTs线程中的emitter,从而执行Http请求。

我们看 NapiDispatcher 中 代码:

export class NapiDispatcher {

  constructor(napiCallback: NapiCallback) {
    this.napiCallback = napiCallback;
  }

  /**
   * 注册总路由分配
   */
  register() {
    // 定义一个eventId为1的事件
    let event: emitter.InnerEvent = {
      eventId: NapiDispatcher.emitterId
    };

    // 收到eventId为1的事件后执行该回调
    let callback = (eventData: emitter.EventData): void => {
      let info = this.toMethodInfo(eventData)
      this.dispatch(info, this.napiCallback)
    };

    // 订阅eventId为1的事件
    emitter.on(event, callback);
  }

  private dispatch(info: MethodInfo, callback: NapiCallback) {

    const { methodName, args } = info;

    // 从函数注册表中查找并调用方法
    const registeredFunction = functionRegistry[methodName];
    if (typeof registeredFunction === 'function') {
      registeredFunction(callback, ...args);
    }
  }

  /**
   * 事件分发
   */
  // @PrintMethodInfo
  static notify(methodInfo: MethodInfo) {
    // 定义一个eventId为1的事件,事件优先级为Low
    let event: emitter.InnerEvent = {
      eventId: NapiDispatcher.emitterId,
      priority: emitter.EventPriority.HIGH
    };

    let eventData: emitter.EventData = {
      data: {
        content: JSON.stringify(methodInfo)
      }
    };

    // 发送eventId为1的事件,事件内容为eventData
    emitter.emit(event, eventData);
  }
}

方法register 为在ArkTs中需要注册的方法,在ArkTs线程中使用,用于接收来自NAPI线程中的消息。 在emitter回调中,我们接收到了方法名字和入参信息,然后通过dispatch方法,从函数映射表中来触发调用callback中对应的方法。

在NAPI线程中,NAPI根据需要任意时刻调用方法notify发送事件,且将方法名字和入参一起发送过来。我在这里将他们直接封装成了一个对象 MethodInfo。因为对于任意其他方法,要保证通用性,肯定不能直接传当前方法名,需要有个对象作为封装。

/**
 * 函数字典映射表
 */
const functionRegistry: FunctionRegistry = {
  httpRequest: (target: NapiCallback,
    ...args: [number, number, string, string, string, string]) => target.httpRequest(...args),
}

export interface NapiCallback {

  /**
   * NAPI发起HTTP请求
   *
   */
  httpRequest: (cid: number, timeout: number, url: string, httpMethod: string, postData: string,
    headerData: string) => void
}

上述代码是函数字典映射表,利用了TS的特性,我们后续只需要将新增函数写到上述 functionRegistry 对象中即可。 NAPICallback为接口,主要供给外部实现,用于实现具体方法的具体逻辑,Napicallback里的接口被触发后,其里边逻辑执行所在线程和注册 register 方法调用所在线程为一个,这样就可以随心所欲的使用主线程的方法来处理逻辑了。

框架架构图

线程框架结构图.png