越来越感觉自己是个井底之蛙,开发时总是喜欢反反复复重复一小撮知识点,而且这一小撮随着时间的增长会变得越来越小,到头来发现很多之前掌握的都忘记了。
个人觉得 Facebook 的开源项目的质量很高,每次看他们的代码都能收获不少,比如之前的 Tornado。他们对知识掌握的全面、运用的灵活是很多国内开发者(至少是我)所不及的。
好了,言归正传,说说 React Native。
RN 的原理其实挺简单的,基本上就是把 Objective-C 的对象封装一下,然后塞进 JavaScriptCore 的上下文中,这样在 JS 中就可以调用了。
RN 定义了一个叫做 RCTBridgeModule 的协议,所有要暴露给 JS 的对象都需要实现此协议。在运行时 RCTBridge 会查找所有的实现了该协议的类,并且找出这些类中想要暴露的方法。
在找方法这步,他们的方法挺有意思:
首先,定义了一个宏
#define RCT_EXPORT(js_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #js_name }
如果某一个方法想暴露出去,只需要在方法体内加一句 RCT_EXPORT();
即可,比如:
- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
if (!callback) {
RCTLogError(@"Called getItem without a callback.");
return;
}
// ...
这个宏的作用是在编译时,把当前方法名和 js_name 参数保存到二进制文件的 “__DATA,RCTExport” 段(具体来说,应该叫 __DATA Segment, RCTExport Section, 详见上一篇文章)里。等在运行时,再通过读取这个段来找出所需要的方法名等信息,就知道哪些方法是需要暴露的了。具体的函数如下:
static RCTSparseArray *RCTExportedMethodsByModuleID(void)
{
static RCTSparseArray *methodsByModuleID;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Dl_info info;
dladdr(&RCTExportedMethodsByModuleID, &info);
#ifdef __LP64__
typedef uint64_t RCTExportValue;
typedef struct section_64 RCTExportSection;
#define RCTGetSectByNameFromHeader getsectbynamefromheader_64
#else
typedef uint32_t RCTExportValue;
typedef struct section RCTExportSection;
#define RCTGetSectByNameFromHeader getsectbynamefromheader
#endif
const RCTExportValue mach_header = (RCTExportValue)info.dli_fbase;
const RCTExportSection *section = RCTGetSectByNameFromHeader((void *)mach_header, "__DATA", "RCTExport");
if (section == NULL) {
return;
}
NSArray *classes = RCTBridgeModuleClassesByModuleID();
NSMutableDictionary *methodsByModuleClassName = [NSMutableDictionary dictionaryWithCapacity:[classes count]];
for (RCTExportValue addr = section->offset;
addr < section->offset + section->size;
addr += sizeof(const char **) * 2) {
// Get data entry
const char **entries = (const char **)(mach_header + addr);
// Create method
RCTModuleMethod *moduleMethod =
[[RCTModuleMethod alloc] initWithMethodName:@(entries[0])
JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil];
// Cache method
NSArray *methods = methodsByModuleClassName[moduleMethod.moduleClassName];
methodsByModuleClassName[moduleMethod.moduleClassName] =
methods ? [methods arrayByAddingObject:moduleMethod] : @[moduleMethod];
}
methodsByModuleID = [[RCTSparseArray alloc] initWithCapacity:[classes count]];
[classes enumerateObjectsUsingBlock:^(Class moduleClass, NSUInteger moduleID, BOOL *stop) {
methodsByModuleID[moduleID] = methodsByModuleClassName[NSStringFromClass(moduleClass)];
}];
});
return methodsByModuleID;
}
这个函数不用太多的解释,主要流程就是找到那个数据段,遍历解析里面的数据,然后返回结果。
代码很容易看懂,但是背后的想法却很有意思。其实这种场景在平时开发中经常遇到,比如有时候子类需要向父类注册自己,父类根据触发条件来创建某一子类的实例。通常都会有几种实现方法:
- 在父类的某个地方集中注册,比如维护着一个字典,每当派生出一个子类就在字典里添加一项。
- 子类以某些特定方式命名,到时候就可以根据触发事件来构造需要调用的子类名。
- 维护一个单独的文件,保存着类似第一种方法里面的字典。
- 改写子类的
+load
方法,在里面注册子类 - ...
不能说哪种方式最好,因为应用场景不确定,但是就目前来看 RN 的这种方式无疑是最灵活的。
第1、3种方法需要维护一个大表,不利于解耦;第2种方法对类名或者方法名有要求,但是没做出限制,稍不留神就容易出问题。第4种方法在简单的需求下挺完美的,但是如果需求比较奇葩,比如 load
中依赖的过多,则可能会出现很多意想不到的结果。
再反观 RN 的实现,它的注册机制是松散的、灵活的,子类注册的时候不用告诉父类,而父类在找子类的时候从约定好的地方找就可以了。它其实整合了第1、3、4条的优点,同时又简化了很多。
当然,这种方式也有缺点,因为它完全是由子类的实现者定义的,很可能存储在两个子类互相冲突的问题,这就需要有额外的措施来保证。
不过,多知道一种有意思的方法总归是有益的,是吧?