Hummer 的通信机制
—— 一个跨端框架应该要怎么通信?
世界上有很多跨端框架,有自己把底层渲染的工作都包了的,那个叫 Flutter,有那种把小程序装进 App,用 Webview 搞定页面的,用小程序框架来实现跨端的,这个滴滴内部自研有一个,叫星河。
还有一种更为常见,就是在 Android 和 iOS 都实现一套 UI和基础类,然后开发者只需要编写一套业务代码就能给两端用,这种有ReactNative,Weex,还有今天我要说的 Hummer。
在开发需求的过程中,有一些问题常常萦绕在我的头脑中:
在 Native 侧需要做什么工作,才能把TypeScript 代码运行起来?
或者问得更具体点:
一个 Hello world 的文字怎么才能通过几行 TypeScript 代码,就能显示到手机屏幕上?
初始化Hummer:安装引擎

像汽车一样,想让Hummer运行,车子先要有引擎
[Hummer startEngine:nil];
这行代码做了什么?
加载所有已经导出的类
什么叫「导出的类 Exported Class」?指的是那些在 Native 侧已经写好了,TypeScript 代码可以使用的类。导出的方法,导出的属性同理。
那什么叫加载呢?就是把这些Native 类起一个objc 的类名,再起一个 js 类名,然后放入一个 dictionary 中,这样既可以通过 objc 类名获取到这个类,也能通过 js 类名获取到这个类。一类,两名。就像给我家的仆人起一个中文名玛丽,起一个英文名 Mary,那既可以通过「玛丽」呼唤她,也可以通过「Mary」呼唤她。
加载所有导出的方法和属性
确定导出类的父类关系
比如我们已经加载了 ABCD 四个导出类,那其中的 A 类可能是继承B 类的,那么就要把这个父类关系给记录下来,记录在 A 类的superClassReference属性中即可(注意这里确定的 是js组件的继承关系,比如 Text 继承自 View)
点火的结果是我们得到了一个 HMExportManager ,它里边有许许多多加载好的类、方法和属性。这时Hummer 已经有了动力来源了。
好像忘了点东西?
在开始的开始,导出类是哪里来的? 我没有写导出的代码,那一定是Hummer 自己写的,神奇的事情发生在这里(我们随便找个类)

点进去看实现:
用到了__attribute__((section(“name”))) ,这个编译属性,改变了数据的存储特性。也就是说通过魔法一般的代码,我们得以在编译链接的时候就把对应的结构体(也就是 HMExportStruct)写到可执行文件 Mach-O 中,在后续的初始化代码中能读出来。
ARM 文档在这里,有兴趣就点去看看吧。
used 参数是告诉编译器:不管我用不用都不需要优化掉这个函数。这里有一篇文章讲了怎么使用这个编译属性
于是我们的疑惑得以解开了:
举个栗子🌰给观众朋友们整明白点:
我们在 .m 文件中写下 HM_EXPORT_CLASS(Loading, HMActivityIndicatorView),
就足以让编译器把对应的 __hm_export_class_Loding__ 结构体写到 Mach-O 文件中,最后在startEngine 的时候就能获取到对应的导出类。
准备控制器:执行 JS 代码的必要条件
通过 startEngine,我们的 Hummer 项目在客观上有了上路奔驰的能力,但是,还没有一个操作系统,没有油门、方向盘、换挡杆等一系列跟车子交流的能力,我们无法(从 JS 侧)控制Hummer框架。

所以这时我们还需要初始化这个「中间者」,它就是:
HMJSContext:跨端层,负责 Native 和 JS 之间的通信。
// HMViewController.m
- (void)renderWithScript:(NSString *)script {
...
//渲染脚本之前 需要注册bridge
HMJSContext *context = [HMJSContext contextInRootView:self.hmRootView];
self.context = context;
...
//执行脚本
[context evaluateScript:script fileName:self.URL];
...
}初始化 HMJSContext ,有五件事情要做
读取 Hummer bundle 文件中的 builtin.js 文件
初始化 HMJSCExecutor(这个是 context 的核心,作用后面讲)
执行 builtin.js
把 Native 端的类信息(startEngine 的时候生成的),包装成 JS 兼容的格式
把上面包装的信息注册到 JS 侧
 先做个结论:HMJSContext 初始化之后,车子就有了方向盘,就有了点火器,就有了油门,有了一切该有的,只要来个人就能把这辆车开起来了。
然后我们再细看这五件事情。虽然有五件,但是只有两件事需要说
初始化 HMJSCExecutor
执行 builtin.js
可以把HMJSCExecutor 看做是HMJSContext的打手,嫡系,核心下属,因为Executor,听名字就知道,就是负责 execute的,「执行 JS 代码,以及监听 JS 调用 Native 的回调。」
来看看它的初始化函数
- (instancetype)init {
...
// 共用一个 JS 环境
if (!virtualMachineRef) {
virtualMachineRef = JSContextGroupCreate();
}
...
[HMExecutorMap setObject:self forKey:[NSValue valueWithPointer:_contextRef]];
// 注入对象
// 生成 JS 字符串
JSStringRef hummerCallString = JSStringCreateWithUTF8CString("hummerCall");
JSStringRef hummerCreateString = JSStringCreateWithUTF8CString("hummerCreate");
...
JSObjectRef globalThis = JSContextGetGlobalObject(_contextRef);
// 创建 C 函数的 bridge
JSObjectRef nativeLoggingHookFunction = JSObjectMakeFunctionWithCallback(_contextRef, NULL, &nativeLoggingHook);
JSObjectRef inlineHummerCallFunction = JSObjectMakeFunctionWithCallback(_contextRef, NULL, &hummerCall);
...
JSValueRef exception = NULL;
// 把 bridge 绑定到 JS 环境的 Global 对象上, 后面可以通过 globalThis.method() 来调用这里绑定好的函数
JSObjectSetProperty(_contextRef, globalThis, nativeLoggingHookStringRef, nativeLoggingHookFunction, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete, &exception);
JSObjectSetProperty(_contextRef, globalThis, hummerCallString, inlineHummerCallFunction, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete, &exception);
...
}JSObjectMakeFunctionWithCallback:把 C 函数的 callback 注册到 JS 侧,比如JS 调用hummerCall时,就会直接调用 C 函数「hummerCall」
一句话概括就是:注册了一些 C 函数给 JS。
等等,写到这儿我已经有点迷糊了,简单概括一下
Hummer 有了引擎之后,我们需要有一个HMJSContext来控制它,于是就得去初始化这玩意儿:创建一个HMJSCExecutor,注册了一些 C 函数给 JS。
OK,我们继续。
那接下来就是说说注册的都是些什么函数?
hummerCall:js 侧调用 Native方法(类方法和成员方法)
hummerCreate:创建Native对象,并返回 js 指针来持有这个对象
hummerGetProperty:(获取对象的属性值)
hummerSetProperty:(设置对象的属性值)
hummerCallFunction:(调用闭包函数 closure)
这五个函数,都会在执行 builtin.js 文件的时候使用到。所以注意看index.ts,HummerBase.ts,injectClassModel.ts这几个文件
不管是hummerCall 还是hummerGetProperty、hummerSetProperty,他们都调用了下面这个函数:
- (JSValueRef)hummerGetSetPropertyWithArgumentCount:(size_t)argumentCount
arguments:(const JSValueRef _Nonnull[])arguments
isSetter:(BOOL)isSetter {
1. 判断是类方法还是成员方法
2. 根据 arguments 获取到 target, method 和 methodSignature
3. 组装成 NSInvocation 并转发消息
4. 返回结果
}
这就是 HMJSCExecutor 的初始化工作。
初始化 HMJSContext 的第二件值得一说的事
执行 builtin.js。
找一下,发现在这儿

但是这个打开一看,您猜怎么着?混淆过的,看不了。那怎么办呢?把 Hummer 工程一整个 clone 下来,整这个路径:Hummer/iOS/builtin/src/。
底下有若干个文件,据可靠消息,这几个文件就是混淆后的 builtin.js 文件
index.ts
这个文件是给 globalThis 注册很多方法,但是没有调用,比如hummerLoadClass (这个是个非常重要的方法,执行完了 builtin.js 后,Native 侧又特地调用了这个 hummerLoadClass,在HMJSContext#init )
hummerLoadClass 的作用是:将Native导出类的信息全部注册到 JS 侧。入参就是一个 JSON 字符串,在HMJSContext#init() 的倒数第二行可以看到具体的执行代码
HummerBase.ts
据说是所有注册到 js 侧的类的基类
injectClassModel.ts
上面的hummerLoadClass 注册信息时,循环调用injectClassModel 方法来完成注册的。调用一次,注册一个导出类的信息。
export default function injectClassModel(jsClassName: string, classModel: ClassModel, classModelMap: Record<string, ClassModel | undefined | null>): typeof HummerBase | undefined {
let jsClass = HummerBase
if (isNotEmptyString(classModel.superClassName) && classModelMap[classModel.superClassName]) {
if (typeof globalThis[classModel.superClassName] !== 'function') {
// 0. 有父类并且父类没有注入则先注入父类
const innerLoaderClassModel = classModelMap[classModel.superClassName]
if (innerLoaderClassModel) {
const superClass = injectClassModel(classModel.superClassName, innerLoaderClassModel, classModelMap)
if (superClass) {
jsClass = superClass
}
}
} else {
jsClass = globalThis[classModel.superClassName]
}
}
jsClass = class extends jsClass { }
classModel.methodPropertyList?.forEach(methodPropertyModel => {
// 这里是重点,但是没看懂
const prototypeOrClsss = methodPropertyModel.isClass ? jsClass : jsClass.prototype
// 注入方法
if (methodPropertyModel.isMethod) {
prototypeOrClsss[methodPropertyModel.nameString] = function (...args: unknown[]) {
if (isOfType<HummerBase>(this, '_private')) {
// 之前在 Native 给 globalThis 注册的 hummerCall 在这时候派上用场了
return globalThis.hummerCall(this._private, jsClassName, methodPropertyModel.nameString, ...args)
} else {
// 类方法调用
return globalThis.hummerCall(jsClassName, methodPropertyModel.nameString, ...args)
}
}
} else {
Object.defineProperty(prototypeOrClsss, methodPropertyModel.nameString, {
// getter setter 注册
get: function (this: HummerBase | unknown) {
if (isOfType<HummerBase>(this, '_private')) {
return globalThis.hummerGetProperty(this._private, jsClassName, methodPropertyModel.nameString)
} else {
return globalThis.hummerGetProperty(jsClassName, methodPropertyModel.nameString)
}
},
set: function (this: HummerBase | unknown, newValue: unknown) {
if (isOfType<HummerBase>(this, '_private')) {
return globalThis.hummerSetProperty(this._private, jsClassName, methodPropertyModel.nameString, newValue)
} else {
return globalThis.hummerSetProperty(jsClassName, methodPropertyModel.nameString, newValue)
}
}
})
}
})
// 给 jsClass 添加 name 属性,值是 jsClassName(比如 'Text')
Object.defineProperty(jsClass, 'name', { value: jsClassName })
// 将 jsClass 挂载到 globalThis 上
globalThis[jsClassName] = jsClass
return jsClass // 不返也行,反正也没用
}终于:执行 JS 代码
等执行了 builtin.js, 再执行 JS 函数 hummerLoadClass,Hummer 这车引擎有了,控制器方向盘油门什么的也齐了,可以开车上路,执行业务开发者写的 ts 代码了,比如:
import {Hummer,View, Text} from '@hummer/hummer-front'
class RootView extends View {
constructor() {
super();
this.style = {
width: '100%',
height: '100%',
alignItems: 'flex-start',
justifyContent: 'center'
}
this.initElement()
}
initElement(){
this.appendText('~ Hello Hummer ~')
}
appendText(message:string){
var text = new Text()
text.style = {
fontSize: 20,
}
text.text = message
this.appendChild(text);
}
}
// 根页面渲染
Hummer.render(new RootView());总结
我们收拾一下思路,问出一个可以概括本篇文章的问题:
如何实现一个跨端框架?
首先,你要在 Native 侧要实现很多很多个类,这样js 侧才有东西可以调用。不仅要实现,还得把这些类信息记录下来放到一个对象中,在运行的时候 js 侧通过这个对象能找到这些类。
其次,你要创建 js 侧调用 Native的口子,要把某些必不可少的 C 函数注册到 js 侧,执行 Hummer 内置的builtin.js ,把 js 环境倒腾好。js 侧要只能有哪些类,并且能调用这些类,创建 Native 对象,跟 Native 对象交互。
此时,跨端框架已经准备好运行业务代码了。
参考文档
跨端框架 Hummer 通信机制 - iOS 版(滴滴内部资料)
Using attribute((section(“name”))) to place code and data(就那个宏的文档)