[iOS]深入理解ivar及property

以下所有类和对象的描述均以Objective-C为参考, cpu架构为arm64

0x0 一句话描述类和对象与内存的关系

  • 类本身是一个描述, 描述里包含实例化这个类需要多大的内存, 以及内存的每个byte什么内容, 这个内容的头部是一个isa, 其他内容是ivar的值或指针.
  • 对象是按类的描述所从内存空间里面开辟出对应大小的空间并填充isa指针(alloc), 类的初始化方法往这个空间里的byte里面存初始化的内容.

0x1 ivar

举个例子:

@interface AClass : NSObject
{
    NSString *_aString;
    NSInteger _aInt;
}
@end

@implementation AClass
- (instancetype)init
{
    if (self = [super init]) {
        _aInt = 1;
    }
    return self;
}
@end

这个类被编译之后变成一个描述(arm64), AClass占用24个字节, 前8个字节是isa指针, 中间八个字节是NSString的指针, 后八个字节是一个NSInteger的值.:

| isa | NSString* _aString | NSInteger _aInt |

调用[[AClass alloc] init]在alloc的时候, 会分配24个字节的内存出来:

| 0 | 0 | 0 |

然后往前8个字节放isa的地址(mask之后的), alloc完成后的内存长这样:

| isa | 0 | 0 |

然后调用alloc出来的这个对象的init方法, init方法会把aInt的位置设为1, 而aString的位置没有做初始化, 因此还是0:

| isa | 0 | 1 |

isa指向了这个类的meta, meta里面存了父类/ivar结构/方法等内容, 后续再做详解.

0x2 ivar实操

代码贴到XCode里面, 然后XCode -> Debug ->Debug Workflow -> Always Show Disassembly

int main(int argc, char * argv[]) {
    [[AClass alloc] init]; // 断点断这一行
    return 0;
}

运行断点会断在这里:
[iOS]深入理解ivar及property

通过lldb的register read指令读出当前的寄存器的值:
[iOS]深入理解ivar及property

可以看到x8是alloc方法, x9是AClass. 断点下面两行指令会分别把AClass和alloc方法分别作为self和selector放到x0/x1寄存器作为objc_msgSend的第一个(self)和第二个参数(cmd).

把断点断在16行位置, 看看objc_msgSend调完之后x0的值:
[iOS]深入理解ivar及property

得到的结果是:
[iOS]深入理解ivar及property

这已经是AClass的一个对象了, 因为这里并没有调用init, 所以这个对象的状态应该是:

| isa | 0 | 0 |

通过 Shift + Command + M看看内存0x15d602780:
[iOS]深入理解ivar及property

前8个byte是isa, 中8个和后8个byte都是0, 跟第一节中的描述一致.

在把断点断到第20行(第19行是调用init方法, 大家可以自己尝试), 看看init调用之后的内存内容:
[iOS]深入理解ivar及property
前8个byte是isa, 中8个byte是0, 后8个byte的内容是数字1, 跟第一节中的描述一致.

0x3 property

@property实际上是一个编译器指令, 在编译器会根据指令后的参数自动生成ivar和ivar的getter和setter.

把示例代码做一个修改, 把ivar改为property, 再看看不同属性下自定义getter和setter的不同实现.

@interface AClass : NSObject
@property (nonatomic, strong) NSString *aString;  // nonatomic, strong
@property (nonatomic, assign)    NSInteger aInt;  // atomic, assign
@end

main实现改为如下代码, 为了直观表示代码直接用getter和setter访问和赋值:

int main(int argc, char * argv[]) {
    AClass *object =[[AClass alloc] init];
    [object setAString:@"test"];
    [object aString];
    [object setAInt:2];
    [object aInt];
    return 0;
}

前置知识: 在OC发消息对应的方法的实现进行调用时, x0是调用方法的对象, x1是selector, 之后的x2/x3/x4...是传进来的参数(如果有参数). 方法调用完成后, 返回值放在x0(如果有返回值). 注意, 这段描述并不完全准确也不完整, 具体请参考苹果官方文档[1].

在用Hopper Disassembler分析下编译产物, 先来setter'-[AClass setAString:]'.

                     -[AClass setAString:]:
0000000100006940         stp        x29, x30, [sp, #0xfffffff0]!                ; Objective C Implementation defined at 0x100008cf8 (instance)
0000000100006944         mov        x29, sp
0000000100006948         sub        sp, sp, #0x20
// 前面是保存方法调用的现场, 用于后面恢复用
000000010000694c         adrp       x8, #0x100008000                            ; imp___got____gxx_personality_v0
0000000100006950         add        x8, x8, #0xe58                              ; _OBJC_IVAR_$_AClass._aString
// 动态定位获取AClass._aString的描述地址, 放入x8
0000000100006954         stur       x0, [x29, #0xfffffff8]
0000000100006958         str        x1, [sp, #0x10]
000000010000695c         str        x2, [sp, #0x8]
// 把参数self/selector/传进来的string对象, 存到栈里
0000000100006960         ldr        x0, [sp, #0x8]
0000000100006964         ldur       x1, [x29, #0xfffffff8]
// 把传进来的对象从栈里捞出来放到x0, 把self从栈里捞出来放到x1
0000000100006968         ldrsw      x8, [x8]                                    ; _OBJC_IVAR_$_AClass._aString
000000010000696c         add        x8, x1, x8
// 从x8里把_aString的在AClass对象的偏移量捞出来, 并与x1相加, 也就是`self指针+偏移量`, 结果是一个指针
0000000100006970         str        x0, [sp]
// 把传进来的对象存入栈
0000000100006974         mov        x0, x8
// 把`self指针+偏移量`指针放入x0
0000000100006978         ldr        x1, [sp]
// 把传进来的对象从栈里捞出来放到x1
000000010000697c         bl         imp___stubs__objc_storeStrong
// 把x1里传进来的对象赋值给x0, 然后强引用一次
0000000100006980         mov        sp, x29
0000000100006984         ldp        x29, x30, [sp], #0x10
// 恢复最前面保存的现场
0000000100006988         ret        
// 返回
                        ; endp

上面代码干的事情, 就是把要赋值的对象存一份到对象的ivar偏移量对应的位置. 同时strong属性这里会通过objc_storeStrong[2]把引用计数+1.

再来setter -[AClass aString]:

                     -[AClass aString]:
0000000100006914         sub        sp, sp, #0x10                               ; Objective C Implementation defined at 0x100008ce0 (instance)
// 移动一下栈用来存方法里要用到的数据
0000000100006918         adrp       x8, #0x100008000                            ; imp___got____gxx_personality_v0
000000010000691c         add        x8, x8, #0xe58                              ; _OBJC_IVAR_$_AClass._aString
// 动态定位获取AClass._aString的描述地址, 放入x8
0000000100006920         str        x0, [sp, #0x8]
0000000100006924         str        x1, [sp]
// 把self和selector存入栈
0000000100006928         ldr        x0, [sp, #0x8]
// 把self从栈里捞出来
000000010000692c         ldrsw      x8, [x8]                                    ; _OBJC_IVAR_$_AClass._aString
0000000100006930         add        x8, x0, x8
// 从x8里把_aString的在AClass对象的偏移量捞出来, 并与x1相加, 也就是`self指针+偏移量`, 结果是一个指针
0000000100006934         ldr        x0, [x8]
// 从`self指针+偏移量`指针里面的数据捞回来
0000000100006938         add        sp, sp, #0x10
000000010000693c         ret       
// 恢复栈到调用前状态, 和返回 
                        ; endp

上面代码干的事情, 就是把内容从对象的ivar偏移量所在的位置取出来.

在来看看另一个属性, 还是先来setter-[AClass setAInt:]:

                     -[AClass setAInt:]:
00000001000069b0         sub        sp, sp, #0x20                               ; Objective C Implementation defined at 0x100008d28 (instance)
// 移动一下栈用来存方法里要用到的数据
00000001000069b4         str        x0, [sp, #0x18]
00000001000069b8         str        x1, [sp, #0x10]
00000001000069bc         str        x2, [sp, #0x8]
// 把参数self/selector/传进来的NSInteger, 存到栈里
00000001000069c0         ldr        x0, [sp, #0x18]
// 从栈里把self捞出来
00000001000069c4         adrp       x1, #0x100008000                            ; imp___got____gxx_personality_v0
00000001000069c8         ldrsw      x1, [x1, #0xe54]                            ; _OBJC_IVAR_$_AClass._aInt
// 获取AClass._aInt的偏移量
00000001000069cc         str        x2, [x0, x1]
// 把整数塞到`self+偏移量`指针指向的内容
00000001000069d0         add        sp, sp, #0x20
00000001000069d4         ret        
// 恢复栈到调用前状态, 和返回 
                        ; endp

由于NSInteger本身不是对象没有引用计数等操作, 这里的代码比较简单, getter与上面的getter也类似就不做额外解析了.

strong和weak和assign的区别在于objc_storeStrong和objc_storeWeak和直接赋值.

0x4 参考

  1. ARM64 Function Calling Conventions :https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html#//apple_ref/doc/uid/TP40013702-SW1
  2. objc_storeStrong: http://opensource.apple.com/source/objc4/objc4-647/runtime/NSObject.mm
  3. objc-msg-arm64.s: http://opensource.apple.com/source/objc4/objc4-647/runtime/Messengers.subproj/objc-msg-arm64.s
上一篇:iOS 新特性分列式 之 iOS 9.x - 主要内容:苹果笔 API 引入、3D Touch、iPad 多任务加强、应用瘦身、应用传输安全、Swift 加强


下一篇:科技改造了农业,可也改出了问题