本文章使用环境:
Xcode: 13.3
源码objc4-838.1
测试设备: iPnone7 15.3.1
alloc , init方法,作为开发者相信都已经敲过无数遍了。
如果问:alloc, init分别有什么作用,相信大家都能回答出来分配内存和初始化。
如果再问:alloc, init具体是怎样实现的呢?这时候就需要通过苹果底层源码来看了。
init
我们先看一个例子:
@interface TestClass : NSObject
@property (nonatomic, copy) NSString *name;
@end
TestClass *test = [TestClass alloc];
TestClass *a = test;
TestClass *b = test;
test.name = @"test_name";
NSLog(@"name test: %@, a: %@, b: %@", test.name, a.name, b.name);
NSLog(@"地址 test: %p, a: %p, b: %p", test, a, b);
大家可能会说,代码有问题,实例创建没有进行init,但是这个代码是可以正常运行的,输出如下:
根据输出信息,我们可以看出,alloc之后就有分配一块完整可用的内存了,实例已经可以正常使用了。咦?那init做了什么,这不是不需要init了吗。
对此,我的理解是,init是苹果给提供的一个工厂方法,系统原来的init什么也没做。现在到源码中来验证一下。因为OC中所有类都继承自NSObject, 所以我们找到源码的NSObject.mm文件,从中可以找到:
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
由此可以证明: init方法的确什么都没有做,就是一个工厂方法。
我们还可以使用汇编来证明这一点。本次需要的汇编相关知识,请查看文章底部 汇编相关 部分。 首先修改代码
TestClass *test = [TestClass alloc];
TestClass *a = [test init];
在项目中加一个 [NSObject init] 的符号断点,当运行到 TestClass *a = [test init];, 时打开符号断点。
可见,
init方法中就只有一个返回,没有其他的操作。我们可以打印一下寄存器的值,看到正是当前测试类的实例。
alloc
把上面示例中的类拿到源码项目中
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestClass *test = [TestClass alloc];
NSLog(@"");
}
return 0;
}
通过断点来查看,方法的调用顺序是:
callAlloc -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone
奇怪了,明明是调用的alloc方法,为什么前面会多出一个callAlloc方法。
我们离开源码项目,到示例项目中,打上断点,当运行到 alloc 方法时,显示汇编。
从注释可以看出,这是调用了
objc_alloc, 可是我们明明调用的是 alloc方法。从源码中可以找到答案。
在 objc-runtime-new.mm 中,可以找到如下代码:
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == @selector(alloc)) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == @selector(allocWithZone:)) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == @selector(retain)) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == @selector(release)) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == @selector(autorelease)) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
}
}
else if (msg->imp == &objc_msgSendSuper2_fixup) {
msg->imp = &objc_msgSendSuper2_fixedup;
}
else if (msg->imp == &objc_msgSend_stret_fixup) {
msg->imp = &objc_msgSend_stret_fixedup;
}
else if (msg->imp == &objc_msgSendSuper2_stret_fixup) {
msg->imp = &objc_msgSendSuper2_stret_fixedup;
}
#if defined(__i386__) || defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fpret_fixup) {
msg->imp = &objc_msgSend_fpret_fixedup;
}
#endif
#if defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fp2ret_fixup) {
msg->imp = &objc_msgSend_fp2ret_fixedup;
}
#endif
}
从中可以看到,方法名的大概意思是修正消息,而当消息的 sel 是 alloc 时,让 imp 指向 objc_alloc 。所以当我们调用 alloc 时,实际上调用的是 objc_alloc 的方法实现。
我们在 objc_alloc 方法打上断点,果然,调用 alloc 方法,先会走到 objc_alloc, 然后, objc_alloc 的方法中,调用了 callAlloc。
所以上面的调用流程是不完整的,完整的调用流程应该是:
objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone
我们到示例代码中,通过汇编和符号断点验证一下。
打一个 objc_alloc 的符号断点
有2个调用,通过逐步运行调试,发现走的是第2个方法调用
这里可以通过寄存器数据读取( register read )来确定相关信息, 寄存器中 x0 , x1, x2 ... 按顺序存储参数,而且 objc_msgSend 前2个默认参数 id self, sel _cmd, 所以我们可以打印出来,当前的类和方法。
再加一个
alloc 符号断点
再加一个 _objc_rootAlloc 符号断点
再加一个 _objc_rootAllocWithZone 符号断点
在
ret 处加个断点,打印
可见 _objc_rootAllocWithZone 方法创建了实例。
汇编看到的流程跟源码中的类似,但是我们没有看到 callAlloc 方法和 _class_createInstanceFromZone 方法。这是因为有进行编译器优化的原因。
编译器优化
我们到 Build Settings 中搜索 Optimization Level , 可见有好多种优化等级
虽然我们 Debug 模式下是 None,但是并不意味着没有进行编译器优化,这只是可选的最低等级的优化。我们可以通过修改优化等级来证明。例如,把Debug 模式下指定为与 Release 相同的 Fastest, Smallest 模式。
None 模式下汇编
Fastest, Smallest 模式下汇编
可见优化等级越高,汇编中的指令越少。少的部分就是编译器优化掉的部分。
相关方法源码
objc_alloc
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
alloc
+ (id)alloc {
return _objc_rootAlloc(self);
}
_objc_rootAlloc
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
_objc_rootAllocWithZone
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
_class_createInstanceFromZone
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
new
TestClass *test = [TestClass new];
到源码中查看
// Calls [cls new]
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
可见 new 方法 就等于 alloc + init
汇编相关
开/关汇编显示
Xcode中 Debug -> Debug Workflow -> Always Show Disassembly
汇编指令
b 和 bl : 跳转指令, 可以理解为函数调用
ret : return的意思,可以理解为函数的返回
; 和 # : 代表注释