likes
comments
collection
share

滴滴跨端框架 Hummer 的启动流程

作者站长头像
站长
· 阅读数 12

Hummer 是滴滴开源的跨端框架,目前是滴滴货运司机端重要的开发三方库

一、加载导出类、方法和属性(在 OC 侧准备好被 JS 调用的类、方法和属性)

  1. 加载所有「已经导出的类

    什么叫「导出的类 Exported Class」?指的是那些在 Native 侧已经写好了,TypeScript 代码可以使用的类。导出的方法,导出的属性同理。导出的类在Mach-O 文件中

导出一个类
#define HM_EXPORT_CLASS(jsClass, objcClass) \
__attribute__((used, section("__DATA, hm_export_class"))) \
static const HMExportStruct __hm_export_class_##jsClass##__ = {#jsClass, #objcClass};

举个例子
HM_EXPORT_CLASS(Loading, HMActivityIndicatorView)

存储的时候,用到了__attribute__((section("name"))) ,这个编译属性,改变了数据的存储特性。也就是说通过魔法一般的代码,我们得以在编译链接的时候就把对应的结构体(也就是 HMExportStruct)写到可执行文件 Mach-O 中,在后续的初始化代码中能读出来。

ARM 文档在这里,有兴趣就点去看看吧。

used 参数是告诉编译器:不管我用不用都不需要优化掉这个函数。这里有一篇文章讲了怎么使用这个编译属性

于是我们的疑惑得以解开了:

举个栗子🌰给观众朋友们整明白点:

我们在 .m 文件中写下 HM_EXPORT_CLASS(Loading, HMActivityIndicatorView), 就足以让编译器把对应的 hm_export_class_Loding 结构体写到 Mach-O 文件中,然后在startEngine 的时候就能获取到对应的导出类信息(一组字符串而已)。

读取的时候使用到dladdr 函数,获取到 DL_info,然后根据 info获取共享对象的基地址(我猜测的Mach-O 文件的基地址),然后通过getsectbynamefromheader_64函数,获取Mach-O 文件的段数据。

const struct section_64 *section = getsectbynamefromheader_64((void *) mach_header, "__DATA", "hm_export_class");
// 注意看这里的 hm_export_class 字符串,跟存储类信息时候的字符串对应上了

这时,就成功获取到了一个 HMExportClass 对象了(这是个单例对象)

@interface HMExportClass : NSObject

@property (nonatomic, nullable, copy) NSString *className;
@property (nonatomic, nullable, copy) NSString *jsClass;

那什么叫加载呢?就是把这些Native 类起一个objc 的类名,再起一个 js 类名,然后放入一个 dictionary 中,这样既可以通过 objc 类名获取到这个类,也能通过 js 类名获取到这个类。一类,两名。就像给我家的仆人起一个中文名玛丽,起一个英文名 Mary,那既可以通过「玛丽」呼唤她,也可以通过「Mary」呼唤她。

  1. 加载导出类对应的导出的方法和属性

    导出的方法没放 Mach-O 文件里,而是定义了一个类方法

#define HM_EXPORT_PROPERTY(jsProp, getter, setter) \
+ (HMExportProperty *)__hm_export_property_##jsProp##__ { \
    HMExportProperty *exportProperty = [[HMExportProperty alloc] init]; \
    exportProperty.jsFieldName = @#jsProp; \
    exportProperty.propertyGetterSelector = @selector(getter); \
    exportProperty.propertySetterSelector = @selector(setter); \
\
    return exportProperty; \
}


#define HM_EXPORT_CLASS_METHOD(jsMethod, sel) \
+ (HMExportMethod *)__hm_export_method_class_##jsMethod##__ { \
    HMExportMethod *exportMethod = [[HMExportMethod alloc] init]; \
    exportMethod.jsFieldName = @#jsMethod; \
    exportMethod.selector = @selector(sel); \
\
    return exportMethod; \
}

所以直接通过class_copyMethodList 函数就能获取到所有方法和属性了,然后存到这个HMExportClass 对象的classMethodPropertyList 和 instanceMethodPropertyList

二、初始化 JSContext,创建 JS 执行的上下文,注册 C 函数到 JS执行上下文中

为了执行 JS 代码,我们需要一个 JSContext

在 Hummer 框架中,就是HMJSContext,抛开别的事情不看,实际执行 JS 代码的是HMJSContext的一个属性 HMJSCExecutor 的 contextRef 属性,它是 C 语言的类型,表示 JS 代码执行的上下文。具体类型是 JSGlobalContextRef

它跟 OC 语言中的JSContext可以互相转化。

所以,这一步最重要的,其实就是创建这个 context,给它的全局对象注册 C 函数,好让 js 能够调用这些C 函数,从而实现通信。

// 创建 JS 执行上下文
_contextRef = JSGlobalContextCreateInGroup(virtualMachineRef, NULL);

// 「通过 JS 执行上下文」执行JS 代码
JSValueRef result = JSEvaluateScript(self.contextRef, scriptRef, NULL, sourceRef, 1, &exception);

这一步还需要注册一些 C 函数给 JS 侧调用,具体代码是:

以 hummerCall 为例

JSObjectRef inlineHummerCallFunction = JSObjectMakeFunctionWithCallback(_contextRef, **NULL**, &hummerCall);

JSObjectSetProperty(_contextRef, globalThis, hummerCallString, inlineHummerCallFunction, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete, &exception);

原理是通过 JSObjectMakeFunctionWithCallbackJSObjectSetProperty 函数,把 C 函数 hummerCall 注册给了 JSContext 中的 globalThis 对象。JS 侧就能调用这些函数了。

参考资料