首页 iOS 方法内局部变量 (对象) 的内存释放
文章
取消

iOS 方法内局部变量 (对象) 的内存释放

描述

最早被人问及 iOS 方法内的 局部变量 是在什么时机释放的时候, 肤浅的认为: 可能编译器给每个方法都加了 自动释放池?

后来随着成长, 发现事实并没有自己当初想象的那么简单. 虽然网上有很多从源码方面总结 局部变量 内存相关的文章, 但是都是从源码的角度来分析的, 今天正好有空, 就从更深入的地方来探索一番, 整理一下思路.

今天的主角就是下面这段代码:

1
2
3
4
5
- (void)memoryReleaseTest {
    NSObject *obj = nil;
    obj = [[NSObject alloc] init];
    NSObject *p = obj;
}

正文

准备工作

  1. memoryReleaseTest 方法的第一行打断点
  2. Xcode 中选择 Debug -> Debug Workflow -> Always Show Disassembly 并勾选
  3. viewDidLoad 中调用 memoryReleaseTest (保证方法被调用即可)

然后在运行之后就能看到如下 arm64 汇编代码 (真机运行).

Demo`-[ViewController memoryReleaseTest]:
    0x104d550a8 <+0>:   sub    sp, sp, #0x30             ; =0x30 
    0x104d550ac <+4>:   stp    x29, x30, [sp, #0x20]
    0x104d550b0 <+8>:   add    x29, sp, #0x20            ; =0x20 
    0x104d550b4 <+12>:  adrp   x8, 8
    0x104d550b8 <+16>:  add    x8, x8, #0xa00            ; =0xa00 
    0x104d550bc <+20>:  stur   x0, [x29, #-0x8]
    0x104d550c0 <+24>:  str    x1, [sp, #0x10]
    0x104d550c4 <+28>:  mov    x0, #0x0
    0x104d550c8 <+32>:  str    x0, [sp, #0x8]
    0x104d550cc <+36>:  ldr    x8, [x8]
    0x104d550d0 <+40>:  mov    x0, x8
    0x104d550d4 <+44>:  bl     0x104d561d0               ; symbol stub for: objc_alloc
    0x104d550d8 <+48>:  adrp   x8, 8
    0x104d550dc <+52>:  add    x8, x8, #0x9a8            ; =0x9a8 
    0x104d550e0 <+56>:  ldr    x1, [x8]
    0x104d550e4 <+60>:  bl     0x104d56200               ; symbol stub for: objc_msgSend
    0x104d550e8 <+64>:  ldr    x8, [sp, #0x8]
    0x104d550ec <+68>:  str    x0, [sp, #0x8]
    0x104d550f0 <+72>:  mov    x0, x8
    0x104d550f4 <+76>:  bl     0x104d56224               ; symbol stub for: objc_release
    0x104d550f8 <+80>:  ldr    x8, [sp, #0x8]
    0x104d550fc <+84>:  mov    x0, x8
    0x104d55100 <+88>:  bl     0x104d56230               ; symbol stub for: objc_retain
    0x104d55104 <+92>:  str    x0, [sp]
    0x104d55108 <+96>:  mov    x8, sp
    0x104d5510c <+100>: mov    x0, x8
    0x104d55110 <+104>: mov    x8, #0x0
    0x104d55114 <+108>: mov    x1, x8
    0x104d55118 <+112>: bl     0x104d56248               ; symbol stub for: objc_storeStrong
    0x104d5511c <+116>: add    x8, sp, #0x8              ; =0x8 
    0x104d55120 <+120>: mov    x0, x8
    0x104d55124 <+124>: mov    x8, #0x0
    0x104d55128 <+128>: mov    x1, x8
    0x104d5512c <+132>: bl     0x104d56248               ; symbol stub for: objc_storeStrong
    0x104d55130 <+136>: ldp    x29, x30, [sp, #0x20]
    0x104d55134 <+140>: add    sp, sp, #0x30             ; =0x30 
    0x104d55138 <+144>: ret    

这段代码如果看不懂 arm64 汇编也没关系, 我们只挑能看懂的看即可, 影响不大.

省略掉栈空间操作相关指令后, 我们需要分析的指令代码如下:

0x000000000 <+44>:  bl     0x104d561d0               ; symbol stub for: objc_alloc
    
0x000000001 <+60>:  bl     0x104d56200               ; symbol stub for: objc_msgSend
    
0x000000002 <+76>:  bl     0x104d56224               ; symbol stub for: objc_release
 
0x000000003 <+88>:  bl     0x104d56230               ; symbol stub for: objc_retain
   
0x000000004 <+112>: bl     0x104d56248               ; symbol stub for: objc_storeStrong
    
0x000000005 <+132>: bl     0x104d56248               ; symbol stub for: objc_storeStrong

0x000000006 <+144>: ret 

省略掉的代码都是栈区 栈空间拉伸, 栈平衡, 寄存器存储 等指令, 忽略也不影响理解. 为了方便理解, 把指令地址也替换为了 0x000000000 ~ 0x000000006 这样的序号.

解释 bl 是跳转指令, 可以简单理解为方法调用. ret 指令可以简单的理解为代码中的 return, 函数执行到这个地方就结束了.

分析指令

0x000000000 指令分析

0x000000000 <+44>:  bl     0x104d561d0               ; symbol stub for: objc_alloc

这条指令根据符号备注可以知道是调用了 objc_alloc 方法.

在控制台执行如下指令:

提示 因为汇编层面不存在 实参 形参 的概念, 函数传参是直接由 x0, x1, x2 ... 来存储传入的 第0个, 第1个, 第2个 ... 参数. 当然, 当一个函数有多个参数的时候, 会有其他传参方法, 此处不做解释.

上图读取 x0 寄存器目的是为了获取 objc_alloc 传入的参数. 由函数名我们就可以知道他是为一个对象分配一段内存空间.

可以得知 x0 寄存器内存储的地址对应的内容为 NSObject. 也就是对应了如下代码:

1
[NSObject alloc]

0x000000001 指令分析

0x000000001 <+60>:  bl     0x104d56200               ; symbol stub for: objc_msgSend

这条指令, 了解过 runtime 的应该对 objc_msgSend 都不陌生.

在控制台执行如下指令

x0, x1 寄存器内容, 可以了解到 objc_msgSend 传入的内容为: NSObject 实例对象的地址 和 init 字符串 (SEL 类型的本质就是 C 字符串).

x1 寄存器存储的内存地址为 0x00000001c91b2a3b, 在该地址之后紧接着的 4 个字节里面存储的内容为: 69 6e 69 74 实际上是 i n i t 4个字符的 ASCII 码的 16 进制.

到此为止 0x0000000000x000000001 2 条指令就是我们的代码:

1
[[NSObject alloc] init]

提示 objc_msgSend 方法类型是这样的: objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

po 为打印对象, x 读取内存, 均为 lldb 指令, 可以另作了解

0x000000002 指令分析

0x000000002 <+76>:  bl     0x104d56224               ; symbol stub for: objc_release

这条指令是调用了 objc_release 这个方法. 我们差看一下这条指令释放的是哪个对象:

在这个地方给 objc_release 函数传入的地址为 0x0000000000000000, 也就是传入的是 nil. 这个地方之所以有这么一条指令其实是因为我们的代码:

1
NSObject *obj = nil;

obj 指针初始化时候赋值为 nil, 因为在把其他对象的指针赋值给 obj 的时候, 就要对应的对原来 obj 所指向的内存地址的对象执行一次 release 操作.

0x000000003 指令分析

0x000000003 <+88>:  bl     0x104d56230               ; symbol stub for: objc_retain

这个地方调用了 objc_retain 函数对某个对象执行了 retain 操作, 我猜应该是我们上面初始化的 NSObject 对象.

打印一下 x0 寄存器的内容:

如猜测得一样, 这个对 NSObject 对象执行了 release 操作. 对应了我们的赋值操作:

1
NSObject *p = obj;

也正好印证了: 在 ARC 下, 指针默认是 __storng 类型, 赋值时候, 对应的对象会被 retain 一次.

0x000000004 & 0x000000005 指令分析

这 2 条指令一模一样, 就放在一起分析了

0x000000004 <+112>: bl     0x104d56248               ; symbol stub for: objc_storeStrong
    
0x000000005 <+132>: bl     0x104d56248               ; symbol stub for: objc_storeStrong

在这个方法将要结束的地方调用了 2 次 objc_storeStrong 函数.

objc_storeStrong 函数的实现如下:

1
2
3
4
5
6
7
8
9
void objc_storeStrong(id *location, id obj) {
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

函数有点像我们在 MRC 下的 Setter 方法.

查看一下这 2 次调用传入的参数是什么内容:

这个地方略为特殊, objc_storeStrong 的第一个参数 locationid * 类型, 也就是 二级指针. 因此 x0 寄存器读取到的内存地址 0x000000016f08dc30 里面放的内容才是传入参数的本体. 本次测试的机器为 XR64 位架构的 CPU, 因此指针所占用的内存空间为 8 个字节, 如图种红框所示. 所以我们需要的对象的真实地址为 0x0280d08000 (省略掉了头部的 0).

因此得知传入的第一个参数的本体实际上就是 NSObject 对象.

x1 寄存器内容为 0x0000000000000000, 也就是第二个参数是 nil.

因此在 objc_storeStrong 内部 NSObject 对象会被 release 一次.

总结 此处调用了 2 次 objc_storeStrong 函数, NSObject 就被 release 2次, 对应了 allocp 指针赋值之后的 retain 操作. 随着 objp 指针被置为 nil, NSObject 对象的生命周期也到此结束了.

0x000000006 指令分析

0x000000006 <+144>: ret 

函数返回指令, 指令跳转到 PC 寄存器所指向的指令地址. 至此 memoryReleaseTest 方法执行结束. 进入到 viewDidLoad 方法内的下一句代码的执行流程.

总结

ARC 环境下, 方法在将要执行结束的时候, 局部变量的指针都会被置为 nil, 此时在 objc_storeStrong 函数内部, 被局部变量强引用的对象会被执行 release 操作. 最终对象是否会被释放还是要取决于是否依旧有其他指针强引用 (比如: 全局变量/属性 等).

MRC 环境下, 汇编代码就干净的多, objp 引用 NSObject 对象时候不会执行 retain 操作, 在方法执行结束的时候也不会因为被置为 nil 而对 NSObject 执行 release 操作 (况且也不会被自动置为 nil). 因此在 MRC 下, 如果开发者不主动对对象执行 release/autorelease 操作, 对象就会一直存在于内存中. 并随着方法的结束, 局部变量的指针被释放, 对象就再也无法得到释放, 发生内存泄露.

附加内容

顺便附上 MRC 的汇编:

Demo `-[ViewController memoryReleaseTest]:
    0x1007f5268 <+0>:  sub    sp, sp, #0x30             ; =0x30 
    0x1007f526c <+4>:  stp    x29, x30, [sp, #0x20]
    0x1007f5270 <+8>:  add    x29, sp, #0x20            ; =0x20 
    0x1007f5274 <+12>: adrp   x8, 8
    0x1007f5278 <+16>: add    x8, x8, #0x9f8            ; =0x9f8 
    0x1007f527c <+20>: stur   x0, [x29, #-0x8]
    0x1007f5280 <+24>: str    x1, [sp, #0x10]
    0x1007f5284 <+28>: mov    x0, #0x0
    0x1007f5288 <+32>: str    x0, [sp, #0x8]
    0x1007f528c <+36>: ldr    x8, [x8]
    0x1007f5290 <+40>: mov    x0, x8
    0x1007f5294 <+44>: bl     0x1007f61ac               ; symbol stub for: objc_alloc_init
    0x1007f5298 <+48>: str    x0, [sp, #0x8]
    0x1007f529c <+52>: ldr    x8, [sp, #0x8]
    0x1007f52a0 <+56>: str    x8, [sp]
    0x1007f52a4 <+60>: ldp    x29, x30, [sp, #0x20]
    0x1007f52a8 <+64>: add    sp, sp, #0x30             ; =0x30 
    0x1007f52ac <+68>: ret    
本文由作者按照 CC BY 4.0 进行授权