从汇编层面看Objective-C的实现
Motivation
最近需要给组内分享一些iOS的知识,其中大部分听众是C/C++的工程师。由于FB还是在大量使用Objective-C,工作中一个误区是很多人认为Objective-C只是语法层面和C/C++不同,而实际上,这不完全正确。为了把Objective-C将清楚,本文尝试从汇编的角度来分析Objective-C的一些实现细节。一部分资料来自Apple’s Objective-C runtime open source release,以及Github上这个mirror。所有例子使用下面命令编译,需要本地安装XCode。
#!/usr/bin/env bash
xcrun --sdk iphoneos clang -arch arm64 -S -Os $@
Class Metadata
Objective-C中的class包含两部分@interface
和@implementation
@interface
Objective-C的类通常包含下面两部分@interface
和@implementation
。编译器对@interface
本身并不产生有意义的汇编代码,如下面例子
#import <Foundation/Foundation.h>
@interface Noop{
@private
NSString *aIvar;
}
- (int)aMethod;
@property (nonatomic, strong) NSString *aProperty;
@end
编译器产生的汇编代码为
.section __TEXT,__text,regular,pure_instructions
.build_version ios, 15, 0 sdk_version 15, 0
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
@implementation
@implementation
会产生具体的汇编代码,我们先看一个Empty class
#import <Foundation/Foundation.h>
@interface SomeClass : NSObject
@end
@implementation SomeClass
@end
编译器会对上面的类产生下面代码
L_OBJC_CLASS_NAME_
__OBJC_CLASS_RO_$_SomeClass
和__OBJC_METACLASS_RO_$_SomeClass
对应objc-runtime-new.h中的struct class_ro_t
_OBJC_CLASS_$_SomeClass
和_OBJC_METACLASS_$_SomeClass
对应objc-runtime-new.h中的struct objc_class
- 一个指向
__DATA.__objc_classlist
的指针
具体的汇编代码如下
.section __TEXT,__objc_classname,cstring_literals
l_OBJC_CLASS_NAME_: ; @OBJC_CLASS_NAME_
.asciz "SomeClass"
.section __DATA,__objc_const
.p2align 3 ; @"_OBJC_METACLASS_RO_$_SomeClass"
__OBJC_METACLASS_RO_$_SomeClass:
.long 1 ; 0x1
.long 40 ; 0x28
.long 40 ; 0x28
.space 4
.quad 0
.quad l_OBJC_CLASS_NAME_
.quad 0
.quad 0
.quad 0
.quad 0
.quad 0
.section __DATA,__objc_data
.globl _OBJC_METACLASS_$_SomeClass ; @"OBJC_METACLASS_$_SomeClass"
.p2align 3
_OBJC_METACLASS_$_SomeClass:
.quad _OBJC_METACLASS_$_NSObject
.quad _OBJC_METACLASS_$_NSObject
.quad __objc_empty_cache
.quad 0
.quad __OBJC_METACLASS_RO_$_SomeClass
.section __DATA,__objc_const
.p2align 3 ; @"_OBJC_CLASS_RO_$_SomeClass"
__OBJC_CLASS_RO_$_SomeClass:
.long 0 ; 0x0
.long 8 ; 0x8
.long 8 ; 0x8
.space 4
.quad 0
.quad l_OBJC_CLASS_NAME_
.quad 0
.quad 0
.quad 0
.quad 0
.quad 0
.section __DATA,__objc_data
.globl _OBJC_CLASS_$_SomeClass ; @"OBJC_CLASS_$_SomeClass"
.p2align 3
_OBJC_CLASS_$_SomeClass:
.quad _OBJC_METACLASS_$_SomeClass
.quad _OBJC_CLASS_$_NSObject
.quad __objc_empty_cache
.quad 0
.quad __OBJC_CLASS_RO_$_SomeClass
.section __DATA,__objc_classlist,regular,no_dead_strip
.p2align 3 ; @"OBJC_LABEL_CLASS_$"
l_OBJC_LABEL_CLASS_$:
.quad _OBJC_CLASS_$_SomeClass
在64bit的ARM系统中,根据ARM手册,一个.quad
占8
字节,一个.long
占4
字节。每一个@implemention
至少有25个.quad
和6个long
,因此共占200+24=224
字节。
ivars
如果在类中增加一个ivar
@implementation SomeClass {
NSString* _anIVar;
}
@end
观察汇编代码的变化
_OBJC_METACLASS_$_SomeClass:
.quad _OBJC_METACLASS_$_NSObject
.quad _OBJC_METACLASS_$_NSObject
.quad __objc_empty_cache
.quad 0
.quad __OBJC_METACLASS_RO_$_SomeClass
.private_extern _OBJC_IVAR_$_SomeClass._anIVar1 ; @"OBJC_IVAR_$_SomeClass._anIVar1"
.section __DATA,__objc_ivar
.globl _OBJC_IVAR_$_SomeClass._anIVar1
.p2align 2
_OBJC_IVAR_$_SomeClass._anIVar1:
.long 8 ; 0x8
.section __TEXT,__objc_methname,cstring_literals
l_OBJC_METH_VAR_NAME_: ; @OBJC_METH_VAR_NAME_
.asciz "_anIVar1"
.section __TEXT,__objc_methtype,cstring_literals
l_OBJC_METH_VAR_TYPE_: ; @OBJC_METH_VAR_TYPE_
.asciz "@\"NSString\""
.section __DATA,__objc_const
.p2align 3 ; @"_OBJC_$_INSTANCE_VARIABLES_SomeClass"
__OBJC_$_INSTANCE_VARIABLES_SomeClass:
.long 32 ; 0x20
.long 1 ; 0x1
.quad _OBJC_IVAR_$_SomeClass._anIVar1
.quad l_OBJC_METH_VAR_NAME_
.quad l_OBJC_METH_VAR_TYPE_
.long 3 ; 0x3
.long 8 ; 0x8
__OBJC_CLASS_RO_$_SomeClass:
.long 0 ; 0x0
.long 8 ; 0x8
.long 16 ; 0x10
.space 4
.quad 0
.quad l_OBJC_CLASS_NAME_
.quad 0
.quad 0
.quad __OBJC_$_INSTANCE_VARIABLES_SomeClass
.quad 0
.quad 0
从定义上看 __OBJC_$_INSTANCE_VARIABLES_SomeClass
对应objc-runtime-new.h中的struct ivar_list_t
加上一个.long
(4 bytes)的overhead,其中
.long 1 ; 0x1
.quad _OBJC_IVAR_$_SomeClass._anIVar1
.quad l_OBJC_METH_VAR_NAME_
.quad l_OBJC_METH_VAR_TYPE_
.long 3 ; 0x3
.long 8 ; 0x8
对应objc-runtime-new.h中的struct ivar_t
。最后一段表明struct class_ro_t
中的ivars
指向上面提到的ivar_list_t
。注意,这里保存ivars
的是class_ro_t
而不是struct objc_class
。猜想这可能和struct swift_class_t
有关,这里不做更多展开。另外,如果我们有一个C++的ivar
,并且它是template的,那么这个ivar的名字将会非常长,很不利与debug。下面例子是一个std::vector<int> _vec
的ivar的名字
l_OBJC_METH_VAR_TYPE_.4: ; @OBJC_METH_VAR_TYPE_.4
.asciz "{vector<int, std::allocator<int> >=\"__begin_\"^i\"__end_\"^i\"__end_cap_\"{__compressed_pair<int *, std::allocator<int> >=\"__value_\"^i}}"
Methods
如果在类中加一个method
@implementation SomeClass
- (void)doSomething {}
@end
它所产生的的代码和ivar
非常类似。编译器产生了一段代码用来保存method,对应objc-runtime-new.h中的struct method_list_t
__OBJC_$_INSTANCE_METHODS_SomeClass:
.long 24 ; 0x18
.long 1 ; 0x1
.quad l_OBJC_METH_VAR_NAME_
.quad l_OBJC_METH_VAR_TYPE_
.quad "-[SomeClass doSomething]"
前两个.long
是8 bytes的overhead,后面则是struct method_t
对象。同样,__OBJC_$_INSTANCE_METHODS_SomeClass
保存在struct class_ro_t
中,而不是struct class_objc
中。
Properties
接下来我们看property
#import <Foundation/Foundation.h>
@interface SomeClass : NSObject
@property(nonatomic, strong) NSString* aString;
@end
@implementation SomeClass
@end
编译器为property生成的代码较多,首先会为其自动生成getter和setter,并放到上面提到的method_list_t
中
__OBJC_$_INSTANCE_METHODS_SomeClass:
.long 24 ; 0x18
.long 2 ; 0x2
.quad l_OBJC_METH_VAR_NAME_
.quad l_OBJC_METH_VAR_TYPE_
.quad "-[SomeClass aString]"
.quad l_OBJC_METH_VAR_NAME_.1
.quad l_OBJC_METH_VAR_TYPE_.2
.quad "-[SomeClass setAString:]"
其次,由于property是ivar的封装,上面提到ivar_list_t
中会保存其对应的ivar
__OBJC_$_INSTANCE_VARIABLES_SomeClass:
.long 32 ; 0x20
.long 1 ; 0x1
.quad _OBJC_IVAR_$_SomeClass._aString
.quad l_OBJC_METH_VAR_NAME_.3
.quad l_OBJC_METH_VAR_TYPE_.4
.long 3 ; 0x3
.long 8 ; 0x8
同时,Objective-C的每个类也有一个property_list_t
中,同样的,它也被保存在struct class_ro_t
中
__OBJC_$_PROP_LIST_SomeClass:
.long 16 ; 0x10
.long 1 ; 0x1
.quad l_OBJC_PROP_NAME_ATTR_
.quad l_OBJC_PROP_NAME_ATTR_.5
__OBJC_CLASS_RO_$_SomeClass:
.long 0 ; 0x0
.long 8 ; 0x8
.long 16 ; 0x10
.space 4
.quad 0
.quad l_OBJC_CLASS_NAME_
.quad __OBJC_$_INSTANCE_METHODS_SomeClass
.quad 0
.quad __OBJC_$_INSTANCE_VARIABLES_SomeClass
.quad 0
.quad __OBJC_$_PROP_LIST_SomeClass
每个property_list_t
保存的是一个struct property_t
的object,其定义可参考objc-runtime-new.h。其中,property的name和attribute在汇编中均为C string
l_OBJC_PROP_NAME_ATTR_: ; @OBJC_PROP_NAME_ATTR_
.asciz "aString"
l_OBJC_PROP_NAME_ATTR_.5: ; @OBJC_PROP_NAME_ATTR_.5
.asciz "T@\"NSString\",&,N,V_aString"
Ivar Access
OC对ivar的访问和C/C++类似,都是使用offset。为了展示方便,我们先来看下C是如何访问ivar的
struct SomeStruct {
// giving x a nonzero offset.
double dbl;
int x;
int y;
};
int accessMember(struct SomeStruct *o){
return o->x + o->y;
}
int accessArray(int o[4]) {
return o[2] + o[3];
}
_accessMember:
; %bb.0:
ldp w8, w9, [x0, #8]
add w0, w9, w8
ret
_accessArray:
.cfi_startproc
; %bb.0:
ldp w8, w9, [x0, #8]
add w0, w9, w8
ret
左边是一个简单的C struct,其中accessMember
函数会访问x
和y
两个ivar
。accessArray
是用来做参照,通过对比汇编代码可以发现,struct对ivar的访问是用offset。具体来说,x0
保存了SomeStruct*
的地址,offset为8字节,因此[x0, #8]
是找到x
在内存中位置,ldp
是load pair的意思,它可以一次load两个int。可以看到,这和数组访问的汇编代码相同。我们再来看看Objective-C的ivar access
@implementation SomeClass{
int x;
int y;
}
- (int)accessIvar
{
return x + y;
}
@end
"-[SomeClass accessIvar]":
; %bb.0:
ldp w8, w9, [x0, #8]
add w0, w9, w8
生成的汇编和上面C/C++产生的汇编基本一致。
Functions and Methods
这个可能是被误解最多的一块内容,大部分C++的程序员认为Objective-C的动态性来自于和C++类似的virtual table,但实际上这是不正确的,Objective-C的动态性来自”call by name”的设计,结合它的runtime库,相比virtual table更加灵活,但却有更高的overhead。
C++中的一个virtual function call用的是virtual table,例如o->doStuff()
会被编译成o->vtbl->doStuffFunctionPointer()
,对应到汇编代码,基本上是一个load加上一个jump。下面我们OC中一个简单的function call和它的汇编代码如下所示
void callIndirect(id o){
[o doSomeStuff];
}
"-[SomeClass callIndirect:]":
.cfi_startproc
; %bb.0:
mov x0, x2
Lloh0:
adrp x8, _OBJC_SELECTOR_REFERENCES_@PAGE
Lloh1:
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF]
b _objc_msgSend
基本上也是一个load加jump,_objc_msgSend
定义在动态库中,其函数原型如下
objc_msgSend(obj, @selector(message));
对应上面的汇编,x0
保存了o
的地址,x1
保存了selector的地址。具体来说,_OBJC_SELECTOR_REFERENCES
是一个非常大的数组,里面保存每个selector的pointer。[x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF]
类似OBJC_SELECTOR_REFERENCES[DO_STUFF_OFFSET]
,用来寻址具体的selector。
关于objc_msgSend
的实现这里就不具体展开了,推荐阅读Resouce中Mike Ash的这篇文章。但是这里可以明显的看到OC的这种”call by name”的设计是有比较大的overhead的,虽然每个Object上面有method的cache,但是cache需要一个warm up的过程,比起C++这种纯静态的调用,还是会慢很多。而且也会带来更大的code size。
Class Methods
C++中的静态类方法效率很高,基本上就是一个C function call,而Objective-C中的类方法的调用和成员方法类似,但确有更大的overhead,相比于成员函的调用,它多了一步fetch global class pointer - ` _OBJC_CLASSLIST_REFERENCES[DUMMY_CLASS_OFFSET]`
void callDummy(){
[Dummy dummy];
}
__Z9callDummyv:
; %bb.0:
Lloh0:
adrp x8, _OBJC_CLASSLIST_REFERENCES_$_@PAGE
Lloh1:
ldr x0, [x8, _OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF]
Lloh2:
adrp x8, _OBJC_SELECTOR_REFERENCES_@PAGE
Lloh3:
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF]
b _objc_msgSend
.loh AdrpLdr Lloh2, Lloh3
.loh AdrpAdrp Lloh0, Lloh2
.loh AdrpLdr Lloh0, Lloh1
.cfi_endproc
Literals
Objective-C有很方便的literal syntax用来创建NSString, NSNumber, NSArray以及NSDictionary。其中大部分都是syntax sugar,会简介调用这些类的构造函数,但是这里有一个例外是NSString
NSString *getLiteral()
{
return @"Hello";
}
.section __TEXT,__cstring,cstring_literals
l_.str: ; @.str
.asciz "Hello"
.section __DATA,__cfstring
l__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992
.space 4
.quad l_.str
.quad 5
@"Hello"
被保存在了一个具有4个element的struct中,___CFConstantStringClassReference
看着像isa
pointer,第三个参数指向一个C string,第四个参数是字符串的长度。这说明@"Hello"
并没有调用[NSString alloc]
,而是用了更加efficient的一种方式。