Hummer是目前滴滴货运司机端正在使用的跨端框架,之前在开发需求的时候碰到了诡异的布局问题,由于自己是刚接触,完全不知道从哪里下手排查问题,于是请来了兄弟部门 Hummer iOS 现在的维护者史广远,看着他一步一步地打断点,打日志,最终成功定位问题,给出了解决方案。心里敬佩得很。
所以既为了以后开发能够有独立排查问题的能力,也为了提高自己对于跨端技术的理解,这就来整理了下Hummer 的渲染机制,我看以后还有什么问题能难倒我?
从我们的题目开始:Hummer在 iOS 侧是如何渲染的?
如同把大象塞进冰箱,我分为 5 步:
- 创建:把组件实例化,创建出来
- 设置:设置它的 style 和属性
- 渲染:把准备好的组件添加到 yoga 节点树上,也是添加到屏幕上
- 标记:标记需要重新计算、刷新的组件
- 布局:调用 yoga 对组件进行布局
Hummer 既可以用 TypeScript 写,也可以用Tenon 写,本文采用第一种写法。
首先来看用TypeScript 写的业务代码:
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'
}
// 创建了一个 Text 组件
var text = new Text()
text.style = {
fontSize: 20
}
text.text = '~ Hello Hummer ~'
this.appendChild(text);
}
}
// 根页面渲染
Hummer.render(new RootView());
简单,天真,几乎只写了一个 Text 组件。
1 实例化:「特别鸣谢你制造更欢乐的我」
在执行 var text = new Text() 时,在 Native 侧就得创建一个 HMLabel实例,并且要把实例装入一个 JS 对象中返回给 JS,它还得持有一下。这一步交给谁来做 ——hummerCreate: 函数。
1 调用 HumemrBase 的 constructor 方法
2 根据 name 属性获取 className & check cache
3调用 globalThis 的 hummerCreate 方法生成 native 对应的视图实例 & 被 this._ptivate 持有
——《跨端框架 Hummer 通信机制 - iOS 版》
JSValueRef _Nullable hummerCreate(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef _Nonnull arguments[], JSValueRef *exception) {
HMAssertMainQueue();
if (argumentCount < 2) {
HMLogError(HUMMER_CREATE_ERROR);
return NULL;
}
HMJSCExecutor *executor = (HMJSCExecutor *) [HMExecutorMap objectForKey:[NSValue valueWithPointer:JSContextGetGlobalContext(ctx)]];
NSString *className = [executor convertValueRefToString:arguments[0] isForce:NO];
if (className.length == 0) {
HMLogError(HUMMER_CREATE_ERROR);
return NULL;
}
// 获取导出类
HMExportClass *exportClass = HMExportManager.sharedInstance.jsClasses[className];
NSString *objcClassName = exportClass.className;
if (objcClassName.length == 0) {
HMLogError(HUMMER_CREATE_CLASS_NOT_FOUND, className);
return NULL;
}
// className 是 Hummer 侧的类名(比如 Text)
// objcClassName 是 Native 侧的类名(比如 HMLabel)
Class clazz = NSClassFromString(objcClassName);
if (!clazz) {
HMLogError(HUMMER_CREATE_CLASS_NOT_FOUND, className);
return NULL;
}
// jscall回调
HMJSContext *context = [[HMJSGlobal globalObject] currentContext:executor];
// 创建不透明指针
NSObject *opaquePointer = NULL;
NSMutableArray<HMBaseValue *> *argumentArray = nil;
for (int i = 2; i < argumentCount; ++i) {
HMBaseValue *value = [[HMJSCStrongValue alloc] initWithValueRef:arguments[i] executor:executor];
if (!argumentArray) {
argumentArray = [NSMutableArray arrayWithCapacity:argumentCount - 2];
}
if (value) {
[argumentArray addObject:value];
}
}
#ifdef HMDEBUG
[HMJSCallerInterceptor callNativeWithClassName:className functionName:@"constructor" objectRef:nil args:argumentArray context:context];
#endif
HMCurrentExecutor = executor;
// 支持 HMJSObject,如果不支持则回退 init
// 不判断 argumentCount > 2,因为 UIView 必须调用 HMJSObject 初始化方法
if ([clazz conformsToProtocol:@protocol(HMJSObject)]) {
opaquePointer = (id) [(id) [clazz alloc] initWithHMValues:argumentArray];
} else {
HMOtherArguments = argumentArray.copy;
opaquePointer = [[clazz alloc] init];
}
HMCurrentExecutor = nil;
HMOtherArguments = nil;
if (!opaquePointer) {
HMLogError(HUMMER_CAN_NOT_CREATE_NATIVE_OBJECT, className);
return NULL;
}
// 关联 hm_value
opaquePointer.hmValue = [[HMJSCStrongValue alloc] initWithValueRef:arguments[1] executor:executor];
// 引用计数 +1
HMLogDebug(HUMMER_CREATE_TEMPLATE, className);
JSClassDefinition hostObjectClassDef = kJSClassDefinitionEmpty;
hostObjectClassDef.version = 0;
hostObjectClassDef.attributes = kJSClassAttributeNoAutomaticPrototype;
hostObjectClassDef.finalize = hummerFinalize;
JSClassRef hostObjectClass = JSClassCreate(&hostObjectClassDef);
// 填充不透明指针
JSObjectRef objectRef = JSObjectMake(ctx, hostObjectClass, (__bridge void *) opaquePointer);
if (objectRef) {
CFRetain((__bridge CFTypeRef) opaquePointer);
}
JSClassRelease(hostObjectClass);
return objectRef;
}
这个函数做了:
- 根据 Hummer 侧传过来的消息获取到了在 Native 侧注册的类
- 创建这个类
- 创建不透明指针,把这个类填到不透明指针当中
- 返回不透明指针
再次强调:最终返回的是JSObjectRef 对象,不是简单的 HMLabel 对象。
我猜测这样做的原因是 Native需要跟 JavaScriptCore 进行通信,只创建一个 HMLabel 你怎么跟 Hummer 侧进行交流,后续 Hummer 怎么拿到这个对象进行设置?
2.1设置 style:
HMLabel 创建出来后,就需要设置它的样式,比如 height、width等。Hummer 侧会一个发 setStyle 的信号,Native会调用hm_setStyle: 方法
这层转化是通过 HM_EXPORT_PROPERTY 这个宏实现的
- (void)hm_setStyle:(HMBaseValue *)style {
// 转化交给 HMJSCExecutor 完成
id styleObject = style.toDictionary;
NSDictionary *styleDic = nil;
if ([styleObject isKindOfClass:NSDictionary.class]) {
styleDic = styleObject;
}
if (styleDic.count == 0) {
HMLogError(@"style 必须有键值对");
return;
}
// 把 style 信息划分成三个部分:布局、属性(样式)、过渡信息(动画相关)
NSMutableDictionary<NSString *, NSObject *> *layoutInfo = NSMutableDictionary.dictionary;
NSMutableDictionary<NSString *, NSObject *> *attributes = NSMutableDictionary.dictionary;
NSMutableDictionary<NSString *, NSObject *> *transitions = NSMutableDictionary.dictionary;
[styleDic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:NSString.class]) {
return;
}
// 比如 left,flexDirection
if ([[self hm_layoutInfoKeys] containsObject:key]) {
layoutInfo[key] = obj;
} else if ([[self hm_transtionInfoKeys] containsObject:key]) {
// 比如 transitionDelay,
transitions[key] = obj;
} else {
// 比如 backgroundColor
attributes[key] = obj;
}
// 布局、属性和过渡信息可以看文档,看看有什么
// https://hummer.didi.cn/doc#/zh-CN/normal_view_style
}];
NSMutableDictionary<NSString *, NSObject *> *mutableStyleDictionary = self.hm_styleStore.mutableCopy;
if (!mutableStyleDictionary) {
mutableStyleDictionary = [NSMutableDictionary dictionaryWithCapacity:layoutInfo.count + attributes.count + transitions.count];
}
// 把分好的三部分又装了回去,让 self.hm_styleStore 持有
self.hm_styleStore = nil;
[mutableStyleDictionary addEntriesFromDictionary:layoutInfo];
[mutableStyleDictionary addEntriesFromDictionary:attributes];
[mutableStyleDictionary addEntriesFromDictionary:transitions];
self.hm_styleStore = mutableStyleDictionary;
...
[attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSObject *obj, BOOL *stop) {
// 过滤需要动画的属性,通过动画展示
if (self.hm_transitionAnimation && [self.hm_transitionAnimation.needAnimations.allKeys containsObject:key]) {
[transitionAnimations addEntriesFromDictionary:@{key: obj}];
} else {
// layoutInfo、attributes 就通过这个设置
[self hm_configureWithTarget:self cssAttribute:key value:obj converterManager:HMAttrManager.sharedManager];
}
}];
...
// 标记:该结点需要重新计算位置
[self hm_markDirty];
}
用我易(粗)懂(暴)的语言说就是:
- HMJSCExecutor 把 Hummer 侧的 style 字典转成NSDictionary
- 把NSDictionary里的元素划分成三种不同的 style:布局、属性、过渡相关的
- 设置给hm_styleStore持有
- 通过hm_configureWithTarget: cssAttribute: value:converterManager: 函数实际去设置对应的 style 值
2.2 设置 Property (比如 Text 的 text 属性)
// 不管是 get 还是 set 都是调用这个方法
- (JSValueRef)hummerGetSetPropertyWithArgumentCount:(size_t)argumentCount arguments:(const JSValueRef _Nonnull[])arguments isSetter:(BOOL)isSetter {
...
// 这里为什么要把 argument[0] 转成 JSObjectRef?
JSObjectRef objectRef = NULL;
if (JSValueIsObject(self.contextRef, arguments[0])) {
JSValueRef exception = NULL;
objectRef = JSValueToObject(self.contextRef, arguments[0], &exception);
[self popExceptionWithErrorObject:&exception];
}
...
// 获取对应的类名
NSString *className = [self convertValueRefToString:arguments[objectRef ? 1 : 0] isForce:NO];
if (className.length == 0) {
return NULL;
}
NSString *propertyName = [self convertValueRefToString:arguments[objectRef ? 2 : 1] isForce:NO];
if (propertyName.length == 0) {
return NULL;
}
id target = nil;
SEL selector = nil;
NSMethodSignature *methodSignature = nil;
[self hummerExtractExportWithFunctionPropertyName:propertyName objectRef:objectRef target:&target selector:&selector methodSignature:&methodSignature isSetter:isSetter jsClassName:className];
// 通过导出类、导出方法,获取到了 Native 类、对应的方法
...
// 转发消息,调用原生方法
return [self hummerCallNativeWithArgumentCount:argumentCount arguments:arguments target:target selector:selector methodSignature:methodSignature];
}
- (JSValueRef)hummerCallNativeWithArgumentCount:(size_t)argumentCount arguments:(JSValueRef const[])arguments target:(id)target selector:(SEL)selector methodSignature:(NSMethodSignature *)methodSignature {
...
BOOL isClass = object_isClass(target);
NSMutableArray<HMBaseValue *> *otherArguments = nil;
HMCurrentExecutor = self;
// 隐含着 numerOfArguments + 0/1 <= argumentCount
for (NSUInteger i = methodSignature.numberOfArguments + (isClass ? 0 : 1); i < argumentCount; ++i) {
// 多余的转数组
HMBaseValue *hummerValue = [[HMJSCStrongValue alloc] initWithValueRef:arguments[i] executor:HMCurrentExecutor];
if (!otherArguments) {
otherArguments = [NSMutableArray arrayWithCapacity:argumentCount - methodSignature.numberOfArguments];
}
if (hummerValue) {
[otherArguments addObject:hummerValue];
}
}
// 存储额外参数
HMOtherArguments = otherArguments.copy;
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = target;
invocation.selector = selector;
// 后续做循环,都是临时变量,如果不做 retain,会导致野指针
[invocation retainArguments];
// 参数
// 本质为 MIN(methodSignature.numberOfArguments, argumentCount - (isClass : 0 : 1)),主要为了防止无符号数字溢出
for (NSUInteger i = 2; i < MIN(methodSignature.numberOfArguments + (isClass ? 0 : 1), argumentCount) - (isClass ? 0 : 1); ++i) {
const char *objCType = [methodSignature getArgumentTypeAtIndex:i];
HMEncodingType type = HMEncodingGetType(objCType);
id param = nil;
if (type == HMEncodingTypeBlock) {
// Block
param = [(HMJSCExecutor *) HMCurrentExecutor convertValueRefToFunction:arguments[i + (isClass ? 0 : 1)]];
} else if (type == HMEncodingTypeObject) {
// HMJSCValue
param = [[HMJSCStrongValue alloc] initWithValueRef:arguments[i + (isClass ? 0 : 1)] executor:HMCurrentExecutor];
} else if (HMEncodingTypeIsCNumber(type)) {
// js 只存在 double 和 bool 类型,但原生需要区分具体类型。
param = [(HMJSCExecutor *) HMCurrentExecutor convertValueRefToNumber:arguments[i + (isClass ? 0 : 1)] isForce:NO];
} else {
HMLogError(HUMMER_UN_SUPPORT_TYPE_TEMPLATE, objCType);
}
[invocation hm_setArgument:param atIndex:i encodingType:type];
}
// 拼装完毕,执行方法
[invocation invoke];
HMOtherArguments = nil;
// 返回值
JSValueRef returnValueRef = NULL;
const char *objCReturnType = methodSignature.methodReturnType;
HMEncodingType returnType = HMEncodingGetType(objCReturnType);
if (returnType != HMEncodingTypeVoid && returnType != HMEncodingTypeUnknown) {
id returnObject = [invocation hm_getReturnValueObject];
if (returnObject) {
returnValueRef = [(HMJSCExecutor *) HMCurrentExecutor convertObjectToValueRef:returnObject];
}
}
HMCurrentExecutor = nil;
return returnValueRef;
}
继续粗暴描述:
- 获取到 Native 侧已经注册的类名,获取到 getter 和 setter
- 拼装NSInvocation,处理参数,然后调用 invoke 方法执行Native 的 getter 和 setter
- 执行完成后,获取返回值
- 最后把JSValueRef返回给JSC(我理解就是 Hummer 侧)
3 render渲染
该创建的创了,该设置的设了,然后得把它们渲染到屏幕上
- (void)render:(HMBaseValue *)page {
id<HMBaseExecutorProtocol> executor = page.context;
HMJSContext *context = [HMJSGlobal.globalObject currentContext:executor ? executor : HMCurrentExecutor];
context.didCallRender = YES;
// 里面还是用 HMJSCExecutor 将 HMBaseValue 转成具体的类,比如 UIView
NSObject *viewObject = page.toNativeObject;
if (!viewObject || ![viewObject isKindOfClass:UIView.class]) {
return;
}
UIView *view = (UIView *) viewObject;
[context didRenderPage:page nativeView:view];
}
- (void)didRenderPage:(HMBaseValue *)page nativeView:(nonnull UIView *)view{
self.componentView = page;
// 把这个 View 添加到屏幕上(会调用 Yoga 把节点插入到节点树上)
[self.rootView addSubview:view];
self.rootView.isHmLayoutEnabled = YES;
[self.rootView hm_markDirty];
if ([self.delegate respondsToSelector:@selector(context:didRenderPage:)]) {
[self.delegate context:self didRenderPage:page];
}
if (self.renderCompletion) {
self.renderCompletion();
}
// 排序当前视图
[UIView hm_reSortFixedView:self];
...
}
复杂地概括下:
- 将 page 转成 Native 的 UIView 对象
- 渲染 UIView
- 把这个 UIView 添加到 rootView 上,插入到 Yoga节点树
4 markDirty:
- (void)hm_markDirty {
if (!self.isHmLayoutEnabled) {
return;
}
// 1. 叶节点 -> 叶节点(脏),需要 markDirty + setNeedsLayout
// 2. 容器节点 -> 容器节点(脏),只需要 setNeedsLayout
// 3. 叶节点 -> 容器节点(脏),只需要 setNeedsLayout
// 4. 容器节点 -> 叶节点(脏),只需要 setNeedsLayout
// YGAttachNodesFromViewHierachy 会针对 2 3 4 情况自行做出标记脏节点的动作
if (self.hm_renderObject.numberOfChildren == 0 && self.hm_renderObject.isLeaf) {
// 原先是叶节点,现在也是叶节点
[self.hm_renderObject markDirty];
}
NSAssert(NSThread.isMainThread, @"必须是主线程");
if (!viewSet) {
viewSet = NSHashTable.weakObjectsHashTable;
dispatch_async(dispatch_get_main_queue(), ^{
// 布局对应的 View(最终是通过 frame 的方式进行布局的)
[UIView hm_layoutIfNeeded];
});
}
[viewSet addObject:self];
}
5 layout 布局
+ (void)hm_layoutIfNeeded {
if (viewSet.count == 0) {
return;
}
NSHashTable<__kindof UIView *> *rootViewSet = nil;
// viewSet 是静态变量,我猜测
NSEnumerator<__kindof UIView *> *enumerator = viewSet.objectEnumerator;
UIView *value = nil;
// 找到 yoga 节点树的 rootView
while ((value = enumerator.nextObject)) {
UIView *rootView = hm_yoga_get_root_view(value);
if (!rootViewSet) {
rootViewSet = NSHashTable.weakObjectsHashTable;
}
[rootViewSet addObject:rootView];
}
viewSet = nil;
enumerator = rootViewSet.objectEnumerator;
// 调用 yoga 进行布局
while ((value = enumerator.nextObject)) {
[value hm_layoutYogaRootView];
}
}
// 最终会调用这个函数:
// preserveOriginpreserveOrigin 为 YES,表示布局的时候继承上一次的 frame
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOriginpreserveOrigin dimensionFlexibility:(HummerDimensionFlexibility)dimensionFlexibility view:(UIView *)view affectedShadowViews:(NSHashTable<HMRenderObject *> *)affectedShadowViews {
// 把 View 插入到 yoga 节点树中
[self attachRenderObjectFromViewHierarchyForRootView:view];
if (!affectedShadowViews) {
affectedShadowViews = [NSHashTable weakObjectsHashTable];
}
HMRenderObject *renderObject = (HMRenderObject *) view.hm_renderObject;
HMRootRenderObject *rootRenderObject = [[HMRootRenderObject alloc] init];
rootRenderObject.renderObject = renderObject;
// 最小大小直接忽略
CGSize oldMinimumSize = CGSizeMake(HMCoreGraphicsFloatFromYogaValue(YOGA_TYPE_WRAPPER(YGNodeStyleGetMinWidth)(renderObject.yogaNode), 0), HMCoreGraphicsFloatFromYogaValue(YOGA_TYPE_WRAPPER(YGNodeStyleGetMinHeight)(renderObject.yogaNode), 0));
if (!CGSizeEqualToSize(oldMinimumSize, rootRenderObject.minimumSize)) {
rootRenderObject.minimumSize = oldMinimumSize;
}
// 默认直接使用 rootView 大小
rootRenderObject.availableSize = view.bounds.size;
if (dimensionFlexibility & HummerDimensionFlexibilityWidth) {
CGSize availableSize = rootRenderObject.availableSize;
availableSize.width = CGFLOAT_MAX;
rootRenderObject.availableSize = availableSize;
}
if (dimensionFlexibility & HummerDimensionFlexibilityWidth) {
CGSize availableSize = rootRenderObject.availableSize;
availableSize.height = CGFLOAT_MAX;
rootRenderObject.availableSize = availableSize;
}
// 调用 yoga 进行布局
[rootRenderObject layoutWithAffectedShadowViews:affectedShadowViews];
if (affectedShadowViews.count <= 0) {
// no frame change results in no UI update block
return;
}
for (HMRenderObject *shadowView in affectedShadowViews) {
HMLayoutMetrics layoutMetrics = shadowView.layoutMetrics;
UIView *inlineView = shadowView.view;
hm_safe_main_thread(^{
inlineView.hm_animationPropertyBounds = inlineView.bounds;
inlineView.hm_animationPropertyCenter = inlineView.center;
CGRect frame = layoutMetrics.frame;
// 只有在 HMDisplayTypeNone 或 visibility: hidden 情况下,隐藏视图
BOOL isHidden = layoutMetrics.displayType == HMDisplayTypeNone || inlineView.hm_visibility == YES;
if (inlineView.isHidden != isHidden) {
inlineView.hidden = isHidden;
}
if (view == inlineView) {
// 需要继承上次的 origin
if (preserveOrigin) {
frame = (CGRect) {
.origin = {
.x = (CGRectGetMinX(frame) + inlineView.frame.origin.x),
.y = (CGRectGetMinY(frame) + inlineView.frame.origin.y),
},
.size = frame.size
};
}
}
// 为 shadowView 设置 frame
[inlineView hummerSetFrame:frame];
});
}
}
shadowView 是什么?——辅助类,负责在 shadow 线程进行布局相关的计算
至此,我们的 Text 组件就添加到了屏幕上,并最终显示了出来
总结
Hummer 的渲染机制跟 ReactNative 的比较像:
不过还是有很多问题浮现了出来:
- 如果组件发生了改变,Hummer 是如何刷新视图树的呢?
- hummerCreate 函数是怎么一步步从 Hummer 侧调用的呢?还有 render:?
- Native 和 JS 是如何通信的?
- Hummer 的启动流程都做了些什么?
这些问题我会在后续的文章中逐渐进行解答。
自测习题
根据《认知天性》,考试是最有效的学习策略之一,你能解答下面的问题吗:
为什么以下写法不能生效?
text.style.color = '#ffffff'
。
。
。
。
请诸君思考,思考
。
。
。
解答:那种写法会调用 getter 而不是 setter,也就是hm_style: 方法,而不是hm_setStyle: 方法。当然不会生效。
为什么这行代码放在一开始就会导致render 不执行?
appendText(message:string){
var text = new Text()
// 放在这里, 练render 函数都不会调用了,为啥?
text.style.color = '#666666'
text.style = {
fontSize: 20,
}
text.style = {
color: '#999999'
}
// 放在这里,不生效,因为这行代码翻译过去是调用了 getter 而不是 setter
text.style.color = '#666666'
text.text = message
this.appendChild(text);
}
暂时没答案,我猜是因为调用 getter 后得到的返回值类型不对,导致后面的代码不执行了?
参考文献
- 跨端框架 Hummer 通信机制 - iOS 版(滴滴内部资料)
- ReactNative 源码解析 —— 渲染机制详解
- 「ReactNative 原理」 Native 层的渲染流程
- 详解 React Native 渲染原理
- Hummer