前言
鸿蒙中对于线程的概念有了新的定义,线程之间的数据和对象不会直接通过引用共享,这就使得我们想要线程之间进行通信,变的比较复杂。
问题
如果我们是从Android转过来做鸿蒙,我们对于Java中的线程使用,可能会经常在子线程中做一些事情,在子线程中就直接使用其他线程中定义的变量。毕竟Java线程之间对象的引用还是可以共用的,不会被JMM所影响。
Android中我们程序会和C++交互,称为JNI。在鸿蒙中则称为NAPI。
场景
所以实际工作中就存在场景了:【我们有个全局变量在线程1中赋值了,期望后续可以在其他线程中使用】。
Android实现
Android中 我们会调用C++的一个方法,C++执行逻辑过程中,可能会调用Java的方法,然后Java执行完结果再将结果通过调用C++ 方法告知JNI。流程如图:
其中绿色部分,JNI调用Java方法后,Java这边在线程2会用到线程1中的对象Label,可以直接拿来使用。线程1和2中的label对象引用是同一个,保证了对象内容不会改变。
对比鸿蒙
同样的流程放到鸿蒙中,如图:
可以看到图中对应箭头打了叉号,因为线程2 中无法直接使用线程1中定义的对象。根本原因是因为鸿蒙中线程之间的数据存在完全隔离性。
官方建议解决方法
对于这个问题,官方给出的建议是使用sendable注解来保证对象在不同线程之间进行通信,但是对于复杂的业务场景下,存在一个对象特别重,内部引用很多其他对象,逻辑层层嵌套,导致该对象无法使用sendable修饰,此时就需要想办法了。
如图,我们由于某些原因,无法给Label对象设置sendable注解,但是又要实现NAPI调用ArkTs方法可以用到该对象内部的数据和方法。
对于该情况,我们肯定要依赖鸿蒙线程或进程之间的通信机制来实现。
解决思路
我们无法直接使用其他线程的对象,那么我们是不是可以跳转到目标线程,执行结束后再告知结果给NAPI?
我们先看下鸿蒙都有哪些方法起线程或进行线程间通信。
鸿蒙线程/进程通信方法
回顾下,鸿蒙线程之间通信方法有几种
- @sendable修饰一个类
- 使用taskpool起个线程执行完数据返回结果给当前线程
- 使用worker,类似taskPool
- 使用emitter,通过广播形式实现线程间通信
- 使用CES实现进程间通信
选择合适的方法
- 方法1,我们当前业务场景中的对象无法使用sendable注解。
- 方法2和方法3,这两种方法都是线程执行完逻辑后返回数据给当前线程,都是需要新起一个线程来处理逻辑,并不符合我们这种跳转线程的场景。
- 方法4,emitter,可以实现线程之间数据的传递。我们可以利用这个特性来切换线程的调用实现上述思路
- 方法5,CES目的是实现进程间通信,在当前场景太重。
使用emitter
选择到了合适的框架,接下来,我们理一下我们的思路:
- 流程 我们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 方法调用所在线程为一个,这样就可以随心所欲的使用主线程的方法来处理逻辑了。