Open ShannonChenCHN opened 7 years ago
造成内存泄漏的几种常见原因
performSelector:
方法调用 alloc
/ new
等方法创建对象时如何检测和避免?
参考:
一、内存管理简介
二、 iOS 的内存管理和手动引用计数
alloc
/retain
/release
/dealloc
的实现autorelease
的实现 ⭐️
三、ARC
四、ARC 的实现 ⭐️
__strong
的实现__weak
的实现__autoreleasing
的实现五、实践
当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。
示例代码:
NSObject* obj = [[NSObject alloc] init]; //obj retain count is 1
obj = [obj1 retain]; //obj retain count is 2
[obj release]; //obj retain count is 1
[obj release]; //obj retain count is 0, obj was released
参考:
几个规则(结合书中的示例代码看):
对象操作与对应的 Objective-C 方法:
对象操作 | Objective-C 方法 |
---|---|
生成并持有对象 | alloc /new /copy /mutableCopy 等方法,以及使用这些单词开头的方法 |
持有对象 | retain |
释放对象 | release |
废弃对象 | dealloc |
alloc
/retain
/release
/dealloc
的实现alloc
创建一个对象后,除了对象本身需要的内存空间外,在对象内存头部还有一块用于记录引用计数(retained
)的内存区域。当调用 retain
/release
时,程序都会访问对象内存头部的retained
变量来获取引用计数。alloc
方法后,引用计数值 +1,但是调用 alloc
方法实际上并没有增加 retained
变量的值,此时 retained
为 0,只是 retainCount
方法中每次返回的都是 retained
的值加 1,所以只有调用 retain
或者 release
方法,才会对 retained
变量产生影响。retain
方法后,引用计数值 +1。release
方法后,引用计数值 -1。release
方法内部会调用 dealloc
方法销毁对象。SideTable
里的RefcountMap
)的记录中。以 retain
方法为例,retain
方法中一层一层往下找,会看到一个 objc_object::sidetable_retain
函数:
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
autorelease
和 autoreleasepool 的实现autorelease 能让对象在超出其作用域时不立即释放,在之后的某个恰当时机(autorelease pool 清理时)自动释放。也就是延长了对象的“寿命”。(p20)
The most common reason for using autorelease is returning an object you’ve created (and therefore own) to a caller. You can’t release the object inside your method because then you’ll return an invalid object. On the other hand, if you don’t release the object, you’ll create a memory leak (breaking the basic rule that says you must release objects you own).
The solution is to put the object in the autorelease pool. Its release is then delayed until the autorelease pool is drained. Let’s look at an example.
最常见的例子就是方法内返回一个对象,我们来看看如果返回一个对象,不调用 autorelease
会怎样(注:下面的示例代码是非 ARC):
int main(int argc, char * argv[]) {
ClassB *objB = [ClassB new];
NSObject *obj = [objB getAnObject];
// 按照”谁持有谁释放“和”非自己创建的对象也可以直接使用“的原则,
// 这里不能调用 retain,所以 obj 的引用数仍然是 1
//do something..
[objB release];
}
// 不再使用 obj,但是 obj 的引用数仍然是 1,没有被销毁,出现内存泄漏
/* ClassB */
- (NSObject *)getAnObject {
NSObject *newObj = [NSObject new]; // 引用计数为 1
// 这里不能调用 [newObj release],不然返回的就是 nil 了
return newObj;
}
参考:
(p21)
示例代码:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; // 这里会调 [obj release]
The AppKit and UIKit frameworks process each event-loop iteration (such as a mouse down event or a tap) within an autorelease pool block. Therefore you typically do not have to create an autorelease pool block yourself, or even see the code that is used to create one. There are, however, three occasions when you might use your own autorelease pool blocks.
根据苹果官方文档中对 Using Autorelease Pool Blocks 的描述,有三种情况是需要我们手动添加 autoreleasepool 的:
如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
如果你编写的循环中创建了大量的临时对象(主要是非alloc
/new
/copy
/mutableCopy
方法创建的对象);
如果你创建了一个辅助线程。
下面是 ARC 场景下 autoreleasepool 的示例:
int main(int argc, const char * argv[]) {
__weak NSArray *temp;
for (int i = 0; i < 2; i++) {
// 自己生成并持有(alloc)
NSMutableArray *arr = [[NSMutableArray alloc] init];
[arr addObject:@"1"];
if (i == 0) {
temp = arr;
} else {
NSLog(@"temp: %@", temp); // 输出 "temp: null"
}
} // 作用域结束,[arr release],所以对象就释放了
__weak NSArray *temp1;
for (int i = 0; i < 2; i++) {
// 非自己生成且不持有(auotorelease)
NSArray *arr = [NSArray arrayWithObjects:@"1", nil];
if (i == 0) {
temp1 = arr;
} else {
NSLog(@"temp1: %@", temp1); // 输出 "temp1: [1]"
}
} // 作用域结束,[arr release],但是 for 循环中没有 auotorelease pool,所以没有释放
__weak NSArray *temp2;
for (int i = 0; i < 2; i++) {
// for 循环中加了 auotorelease pool
@autoreleasepool {
// 非自己生成且不持有(auotorelease)
NSArray *arr = [NSArray arrayWithObjects:@"1", nil];
if (i == 0) {
temp2 = arr;
} else {
NSLog(@"temp2: %@", temp2); // 输出 "temp2: null"
}
} // 作用域结束,[arr release],同时,auotorelease drain,所以释放了
}
return 0;
}
结论:for 循环花括号作用域结束,不代表 auotorelease 对象被释放,所以如果 for 循环中有用到 auotorelease 对象,需要手动添加 auotorelease pool。
参考:
autorelease
和 Autorelease Pool 的实现问题1: autorelease 对象什么时候释放?当前作用域花括号结束时就释放了?
在没有手加 Autorelease Pool 的情况下,autorelease 对象是在当前的 runloop 迭代结束时释放的,而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 Push 和 Pop。比如 main 函数中就有一个 ``
所以,”当前作用域花括号结束时就释放了“这个说法是不对的。
sunnyxx 写的黑幕背后的Autorelease这篇文章中的例子可以验证(文中用的 NSString
类方法的实现机制已经变了,所以我把它换成了 NSArray
):
__weak id weakReference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *arr = [NSArray arrayWithObjects:@"shannon", nil];
weakReference = arr;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// viewDidLoad 和 -viewWillAppear 是在同一个 runloop 中调用的
NSLog(@"%@", weakReference); // 输出: shannon
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", weakReference); // 输出: (null)
}
autorelease 返回值的快速释放机制详见第四章节中 ARC 的实现(objc_autoreleaseReturnValue
和 objc_retainAutoreleasedReturnValue
),具体细节可参考 sunnyxx 写的那篇文章。
问题2:Autorelease Pool 和 autorelease 方法的实现原理
(1)@autoreleasepool{}
代码块的实现
在 hello.m
中实现如下方法:
- (void)foo {
@autoreleasepool {
id __autoreleasing obj = [NSObject new];
}
}
通过执行 clang -rewrite-objc hello.m
命令生成的代码,可以看到 @autoreleasepool{}
代码块的实现:
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
// 之前的 @autoreleasepool 代码块变成了下面的代码
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
// do somthing...
}
通过声明一个 __AtAutoreleasePool
类型的局部变量 __autoreleasepool
来实现 @autoreleasepool {}
。
当声明 __autoreleasepool
变量时,构造函数 __AtAutoreleasePool()
被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush();
;当出了当前作用域时,析构函数 ~__AtAutoreleasePool()
被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj);
。
将hello.m
代码编译成汇编代码,逻辑简化一下大概是这样的:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(new));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
跟前面执行 clang -rewrite-objc hello.m
命令看到的效果是一样的。
(2)AutoreleasePoolPage
的结构和 objc_autorelease
方法的实现
在 objc4 runtime 源码中,我们可以看到 AutoreleasePoolPage
和 objc_autorelease
方法的实现。
AutoreleasePoolPage
的结构如下(注释中就讲解了 autorelease pool 的实现):
/***********************************************************************
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
**********************************************************************/
class AutoreleasePoolPage
{
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE = PAGE_MAX_SIZE; // must be multiple of vm page size
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic; // 用来校验 AutoreleasePoolPage 的结构是否完整
id *next; // 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()
pthread_t const thread; // 指向当前所在的线程
AutoreleasePoolPage * const parent; // 指向父结点,第一个结点的 parent 值为 nil
AutoreleasePoolPage *child; // 指向子结点,最后一个结点的 child 值为 nil
uint32_t const depth; // 代表深度,从 0 开始,往后递增 1
uint32_t hiwat; // 代表 high water mark
...
...
}
objc_autoreleasePoolPush
方法的实现:
objc_autoreleasePoolPush
内部调用的是 AutoreleasePoolPage::push
:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY); // 正常流程走这个
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj); // 返回的地址就是 pool token
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
每调用一次 objc_autoreleasePoolPush
操作,就会创建一个新的 autorelease pool ,也就是往 AutoreleasePoolPage
中插入一个 POOL_BOUNDARY
(也就是新加入的这个 autorelease pool 的边界),最后返回插入的 POOL_BOUNDARY
的内存地址作为 pool token。
objc_autoreleasePoolPop
方法的实现:
pop
函数的入参就是 push
函数的返回值,也就是 POOL_BOUNDARY
的内存地址,即 pool token 。当执行 pop
操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。
pop
过程的图解示例见雷纯锋写的这篇文章。
objc_autorelease
方法的实现:
objc_autorelease
objc_object::autorelease
objc_object::rootAutorelease
objc_object::rootAutorelease2
AutoreleasePoolPage::autorelease
AutoreleasePoolPage::autorelease
的实现如下:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
AutoreleasePoolPage
的 autorelease
函数的实现对我们来说就比较容量理解了,它跟 push
操作的实现非常相似。只不过 push
操作插入的是一个 POOL_BOUNDARY
,而 autorelease
操作插入的是一个具体的 autoreleased 对象。当 pop
被调用时,POOL_BOUNDARY
之后的对象就全部都被 release 了。
问题3:Autorelease Pool 与 Runloop 的关系
每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。
在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。
每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。
参考:
Automatic Reference Counting (ARC) in Objective-C makes memory management the job of the compiler. By enabling ARC with the new Apple LLVM compiler, you will never need to type retain or release again, dramatically simplifying the development process, while reducing crashes and memory leaks. The compiler has a complete understanding of your objects, and releases each object the instant it is no longer used, so apps run as fast as ever, with predictable, smooth performance.
实际上,虽然我们在 ARC 下不用写 retain,release 这些代码了,但是引用计数管理内存的本质在 ARC 中并没有发生变化,就像 ARC(Automatic Reference Counting)和 MRR(manual retain-release)在名字上的差别一样,ARC 只不过把引用计数的操作变得自动化了——ARC在我们编译程序时自动在需要的地方加上了内存管理的代码(retain、release、autorelease)。
ARC中ARC 只是在编译层做手脚,所以我们可以对整个项目设置 ARC 有效或者无效,也可以针对单个文件设置 ARC 有效或无效:
-fobjc-arc
-fno-objc-arc
ARC 模式下,对象类型和 C 语言基本类型不同,对象类型前必须声明所有权修饰符:
__strong
:对象类型默认的所有权修饰符,什么都不写的话,默认就是这个__weak
__unsafe_unretained
__autoreleasing
__strong
示例代码:
{
// You create an object and have ownership.
id __strong obj = [[NSObject alloc] init];
// The variable obj is qualified with __strong.
// Which means, it has ownership of the object.
}
// Leaving the scope of variable obj, its strong reference disappears.
// The object is released automatically.
// Because no one has ownership, the object is disposed of.
等同于非 ARC 下的:
// non-ARC
{
// You create an object and have ownership.
id obj = [[NSObject alloc] init];
// Now you have ownership of the object.
[obj release];
// The object is relinquished.
}
使用 __strong
、__weak
或者 __autoreleasing
修饰符的变量,默认初始值都是 nil
:
// 下面的变量初始值都为 nil
id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;
问题:ARC 是怎么做到,通过 __strong
修饰符,不必手写 retain/release
,就可实现自动引用计数的四条法则呢?
__strong
修饰符的变量赋值实现的__strong
修饰符的变量(超出作用域或者置为 nil
)或者对变量重新赋值,都可以做到第三条“不再需要自己持有的对象时自己主动释放”release
,所以原本就不会执行__weak
__weak
主要是为了解决两个问题:
__weak
变量被置为 nil
(不然会出现野指针)使用场景:
@property (weak, nonatomic) IBOutlet UIButton *nextButton;
__unsafe_unretained
__unsafe_unretained
修饰符修饰的变量不属于编译器的内存管理范围。如果将一个对象赋值给它,它既不会持有该对象的强引用,也不会持有该对象的弱引用,当该对象被废弃时,它也不会被赋值为 nil
。跟直接给 C 语言变量赋值的效果差不多。
问题 :__unsafe_unretained
存在的意义是什么?
因为 __weak
修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中需要使用 __unsafe_unretained
修饰符来代替。
__autoreleasing
下面是非 ARC 情况下的 autorelease 代码:
/* non-ARC */
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
等同于 ARC 下的代码:
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
在 ARC 模式下,显式的使用 __autoreleasing 的场景很少见,但是 autorelease 的机制却依然在很多地方默默起着作用。我们来看看这些场景:
__weak
修饰的变量id
的指针或对象的指针(比如 id *obj
,NSError **
error)问题1 :为什么方法的返回值不显式声明 __autoreleasing
修饰符,也可以将对象注册到 autorelease pool?
+ (id) array {
id obj = [[NSMutableArray alloc] init];
return obj;
}
上面的代码中,作为函数返回值的 obj
就被注册到了 autorelease pool 中。因为编译器会检查方法名是否以 alloc
/new
/copy
/mutableCopy
开头,如果不是的话,就自动将最为返回值的对象注册到 autorelease pool 中。
延伸:
__autoreleasing
对象什么时候释放呢?(答案见第二章节中 autorelease pool 的实现)冷知识:虽然
__weak
修饰符是为了避免循环引用而使用的,但是在访问带有__weak
修饰符的变量时,实际上必定要访问注册到 autorelease pool 中的对象。
问题2:为什么在访问带有 __weak
修饰符的变量时,必须要访问注册到 autorelease pool 中的对象?
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
等同于:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);
因为 __weak
修饰符只持有对象的弱引用,而在访问对象的过程中,该对象有可能被废弃,如果把被访问的对象注册到 Autorelease Pool 中,就能保证 Autorelease Pool 被销毁前对象是存在的。
问题3:ARC 是如何对对象指针类型的参数进行内存管理的呢?比如 + (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error
方法中的 error
参数。
在 ARC 中,编译器会默认给对象指针类型的变量或者参数加上 __autoreleasing
修饰符。
上述问题中的代码等同于:
+ (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError * __autoreleasing *)error {
...
// 隐式创建 autoreleasepool
@autoreleasepool {
if (there is some error && error != nil) {
*error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
}
 }
...
}
这是因为以alloc
/new
/copy
/mutableCopy
开头的方法返回的对象都是自己生成并持有的,其他情况下获得的对象都是非自己生成并持有的。所以,使用带有 __autoreleasing
修饰符的对象指针类型变量获取到的对象,与不是以alloc
/new
/copy
/mutableCopy
开头的方法的返回值一样,都会注册到 autorelease pool 中,这样就能得到非自己生成并持有的对象。
retain
/release
/retainCount
/autorelease
NSAllocateObject
/NSDeallocateObject
alloc
/new
/copy
/mutableCopy
开头的方法在返回对象时都必须返回给调用方所应当持有的对象(补充:在 ARC 模式下,以 init
开头的方法必须是实例方法并且必须要返回对象。返回的对象应为 id
类型或声明该方法的类的对象类型,或是该类的超类型或子类型。该返回的对象并不注册到 Autorelease Pool 中,基本上只是对 alloc
方法返回值的对象进行初始化处理并返回该对象)dealloc
@autoreleasepoo
l 块替代 NSAutoreleasePool
NSZone
)id
和 void *
问题: ARC 中为什么还要使用 @autoreleasepool
ARC 并不是舍弃了 @autoreleasepool
,而是在编译阶段帮你插入必要的 retain
/release
/autorelease
, 以及objc_retainAutoreleasedReturnValue
/objc_autoreleaseReturnValue
等代码调用。
所以,ARC 之下依然是延时释放的(这是 ObjC 引用计数内存管理必须要的),依然是依赖于 NSAutoreleasePool
,跟非 ARC 模式下手动调用那些函数本质上毫无差别,只是编译器和 objc runtime 来做会保证引用计数的正确性。
参考:
Toll-Free Briding 保证了在程序中,可以方便和谐的使用 Core Foundation 类型的对象和Objective-C 类型的对象。
在 MRC 时代,由于 Objective-C 类型的对象和 Core Foundation 类型的对象都是相同的 release 和 retain 操作规则,所以 Toll-Free Bridging 的使用比较简单,但是自从切换到 ARC 后,Objective-C 类型的对象内存管理规则改变了,而 Core Foundation 依然是之前的机制,换句话说,Core Foundation 不支持 ARC。
苹果在引入 ARC 之后对 Toll-Free Bridging 的操作也加入了对应的方法与修饰符,用来指明什么时候用哪种规则管理内存,或者说是内存管理权的归属。这些方法和修饰符分别是:
__bridge
:可用于对象变量和指针变量相互之间的单纯赋值,一般在非 ARC 情况下才会用 __bridge
__bridge_retained
和 CFBridgingRetain
函数:将对象赋值给指针类型的变量时使用(ARC->非ARC)__bridge_transfer
和 CFBridgingRelease
函数:将指针赋值给对象类型的变量时使用(非ARC->ARC)__bridge
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
对象变量转指针变量时相当于 __unsafe_unretained
的效果,可能会出现野指针:
CFMutableArrayRef cfObject = NULL;
{
// 创建并持有对象,retain count 为 1
id obj = [[NSMutableArray alloc] init];
cfObject = (__bridge CFMutableArrayRef)obj;
CFShow(cfObject);
printf("retain count = %d\n", CFGetRetainCount(cfObject));
}
// obj 因为超出作用域,强引用失效,retain count 变成了 0,对象被释放
// 此后再访问该对象出现野指针
printf("retain count after the scope = %d\n", CFGetRetainCount(cfObject));
CFRelease(cfObject);
指针变量转__strong
对象变量时可能会出现内存泄漏:
{
// 此时 retain count 是 1
CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
printf("retain count = %d\n", CFGetRetainCount(cfObject));
// obj 是 __strong类型,强引用,所以此时 retain count 是 2
id obj = (__bridge id)cfObject;
printf("retain count after the cast = %d\n", CFGetRetainCount(cfObject));
// 因为 obj 是强引用,而且没有调 CFRelease,所以 retain count 依然是 2
NSLog(@"class=%@", obj);
}
// obj 因为超出作用域,强引用失效,retain count 变成了 1,出现内存泄漏
__bridge_retained
和 CFBridgingRetain
函数id obj = [[NSObject alloc] init]; // 引用计数为 1
void *p = (__bridge_retained void *)obj; // 非 ARC 开始接管(引用计数+1)
上面的代码相当于非 ARC 下的:
/* non-ARC */
id obj = [[NSObject alloc] init];
void *p = obj;
[(id)p retain];
在 NSObject.h 中可以看到 CFBridgingRetain
函数的定义:
// After using a CFBridgingRetain on an NSObject, the caller must take responsibility for calling CFRelease at an appropriate time.
NS_INLINE CF_RETURNS_RETAINED CFTypeRef _Nullable CFBridgingRetain(id _Nullable X) {
return (__bridge_retained CFTypeRef)X;
}
__bridge_transfer
和 CFBridgingRelease
函数id obj = (__bridge_transfer id)p; // ARC 开始接管,把原来的引用释放掉
上面的代码相当于非 ARC 下的:
/* non-ARC */
id obj = (id)p;
[obj retain];
[(id)p release];
在 NSObject.h 中可以看到 CFBridgingRelease
函数的定义:
NS_INLINE id _Nullable CFBridgingRelease(CFTypeRef CF_CONSUMED _Nullable X) {
return (__bridge_transfer id)X;
}
属性声明的修饰符 | 所有权修饰符 |
---|---|
assign |
__unsafe_unretained |
copy |
__strong (但是赋值的是被复制的对象) |
retain |
__strong |
strong |
__strong |
unsafe_unretained |
__unsafe_unretained |
weak |
__weak |
苹果官方声称,ARC 是“由编译器进行内存管理”的,但是实际上只有编译器是无法完全胜任的,在此基础上还需要 Objective-C 运行时库的支持。
也就是说,ARC 由以下工具、库来实现:
如果没有运行时库的支持,无论怎样静态链接用于 ARC 的库,都无法实现在对象废弃时将所有引用它的 __weak
变量设置为 nil
。
在 LLVM 的文档 Objective-C Automatic Reference Counting (ARC) — Clang 13 documentation 中,也可以看到关于编译层面实现 ARC 更详细的描述。
__strong
的实现- (void)foo {
id __strong obj = [[NSObject alloc] init];
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Pig.m
命令,得到如下汇编指令:
"-[Pig foo]": ## @"\01-[Pig foo]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip) ; 调用[NSObject alloc]
movq _OBJC_SELECTOR_REFERENCES_.2(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip) ; 调用[NSObject init]
xorl %ecx, %ecx
movl %ecx, %esi
movq %rax, -24(%rbp)
leaq -24(%rbp), %rdi
callq _objc_storeStrong ; 调用 objc_storeStrong()函数
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
上面的指令可以简化成:
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_storeStrong(&obj, nil); // 超出作用域,将 obj 变量置为 nil,释放所指向的对象(objc_storeStrong 内部调用了 objc_release)
objc4 runtime 源码中相关函数实现如下:
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
结论:
自己生成的对象自己持有,这一点不需要附加的操作就可实现,跟非 ARC 一样
当 __strong
变量作用域结束或者对 __strong
变量赋值时,编译器自动插入了 objc_storeStrong
函数释放所持有的对象
- (void)foo {
id __strong obj = [Pig pig];
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Pig.m
命令,得到如下汇编指令:
"-[Pig foo]": ## @"\01-[Pig foo]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip) ; 这里是相当于 [Pig pig]
movq %rax, %rdi
callq _objc_retainAutoreleasedReturnValue ; 这里调用了 objc_retainAutoreleasedReturnValue
xorl %ecx, %ecx
movl %ecx, %esi
movq %rax, -24(%rbp)
leaq -24(%rbp), %rdi
callq _objc_storeStrong ; 调用 objc_storeStrong()函数
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
上面的指令可以简化成:
id obj = objc_msgSend(Pig, @selector(pig));
objc_retainAutoreleasedReturnValue(obj);
objc_storeStrong(&obj, nil); // 超出作用域,将 obj 变量置为 nil,释放所指向的对象
objc4 runtime 源码中相关函数实现如下:
// Accept a value returned through a +0 autoreleasing convention for use at +1.
id objc_retainAutoreleasedReturnValue(id obj)
{
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
return objc_retain(obj);
}
结论:
alloc
/init
/copy
/mutableCopy
开头的方法返回的对象赋值给 __strong
变量,编译器会自动插入objc_retainAutoreleasedReturnValue
函数,这样就实现了“非自己生成的对象自己也能持有”objc_retainAutoreleasedReturnValue
和 objc_autoreleaseReturnValue
下面这个方法不是以 alloc
/init
/copy
/mutableCopy
开头,但是返回了对象:
+ (instancetype)pig {
return [[Pig alloc] init];
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Pig.m
命令,得到如下汇编指令:
"+[Pig pig]": ## @"\01+[Pig pig]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq _objc_msgSend@GOTPCREL(%rip), %rax
movq %rax, -24(%rbp) ## 8-byte Spill
callq *%rax
movq _OBJC_SELECTOR_REFERENCES_.2(%rip), %rsi
movq %rax, %rdi
movq -24(%rbp), %rax ## 8-byte Reload
callq *%rax
movq %rax, %rdi
addq $32, %rsp
popq %rbp
jmp _objc_autoreleaseReturnValue ## TAILCALL
.cfi_endproc
上面的代码可以简化成:
+ (instancetype)pig {
id obj = objc_msgSend(Pig, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
objc4 runtime 源码中相关函数实现如下:
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id objc_autoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
结论:
objc_retainAutoreleasedReturnValue
和 objc_autoreleaseReturnValue
这两个由编译器插入的函数时主要是用于优化性能:
autoreleasePool
的操作,在执行objc_autoreleaseReturnValue
时,根据查看后续调用该函数的方法/函数中是否接着调用objc_retainAutoreleasedReturnValue
函数,如果是则直接返回对象,而不再调用 objc_autorelease
。objc_autoreleaseReturnValue
时,优化流程将一个标志位存储在 TLS (Thread Local Storage) 中后直接返回对象。objc_retainAutoreleasedReturnValue
时检查 TLS 的标志位判断是否处于优化流程,如果处于优化流程中则直接返回对象不再调用objc_retain
,并且将 TLS 的状态还原。未优化的效果:
+ (instancetype)pig {
id obj = objc_msgSend(Pig, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autorelease(obj);
}
id obj = objc_msgSend(Pig, @selector(pig));
objc_retainAutoreleasedReturnValue(obj); // 内部会调用 objc_retain
objc_storeStrong(&obj, nil);
走优化流程的效果:
+ (instancetype)pig {
id obj = objc_msgSend(Pig, @selector(alloc));
objc_msgSend(obj, @selector(init));
// 直接返回 obj,不调用 objc_autorelease
return obj;
}
objc_msgSend(Pig, @selector(pig));
// 没有使用返回值,所以后面这两个函数也就不会调
// objc_retainAutoreleasedReturnValue(obj);
// objc_storeStrong(&obj, nil);
参考:
__weak
的实现关于 __weak
的实现,我们需要知道的有两点:
__weak
的变量所引用的对象被销毁的话,该变量会被赋值为 nil
__weak
的变量时,其所引用的对象会被注册到 autorelease pool 中(注:实际验证跟这个有点出入)(1)对象被销毁时,__weak
变量会被自动赋值为 nil
- (void)foo {
id __weak obj1 = obj;
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Pig.m
命令,得到如下汇编指令:
"-[Pig foo]": ## @"\01-[Pig foo]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -8(%rbp), %rax
movq 8(%rax), %rsi
leaq -24(%rbp), %rdi
callq _objc_initWeak
leaq -24(%rbp), %rdi
movq %rax, -32(%rbp) ## 8-byte Spill
callq _objc_destroyWeak
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
上面的指令可以简化成:
id obj1;
objc_initWeak(&obj1, obj); // 内部调用了 storeWeak(&obj1, obj)
objc_destroyWeak(&obj1); // 内部调用了 storeWeak(&obj1, nil)
runtime 源码中相关函数的实现如下:
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
storeWeak
函数的实现(删除部分逻辑):
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
...
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
...
return (id)newObj;
}
/**
* Registers a new (object, weak pointer) pair. Creates a new weak
* object entry if it does not exist.
*
* @param weak_table The global weak table.
* @param referent The object pointed to by the weak reference.
* @param referrer The weak pointer address.
*/
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (!referent || referent->isTaggedPointer()) return referent_id;
...
...
// 所引用的对象对应的弱引用记录(object, weak pointer) pair
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 如果当前表中有该对象对应的弱引用记录,则直接加入该 weak 表中对应记录
append_referrer(entry, referrer);
}
else {
// 没有则新建一个记录,并将新记录插入 weak 表中
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
weak_table
查找entry
的过程(见 weak_entry_for_referent
函数),也是哈希表寻址过程,使用线性探测的方法解决哈希冲突的问题。
weak 引用表的实现:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table; // 全局的 weak 引用表
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
...
...
};
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
// weak_entry_t 结构体作为哈希表中的 value
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被引用对象地址作为 key 用来计算哈希表下标
union {
struct {
weak_referrer_t *referrers; // 保存 weak 变量的地址
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; // 保存 weak 变量的地址
};
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
// 构造函数
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
问题:当对象被销毁时,引用它的__weak
变量是怎样被赋值为 nil
的?
dealloc
的实现:
dealloc
_objc_rootDealloc
objc_object::rootDealloc
object_dispose
objc_destructInstance
object_cxxDestruct
_object_remove_assocations
objc_object::clearDeallocating // 清除操作
objc_object::clearDeallocating
的实现:
objc_object::clearDeallocating
sidetable_clearDeallocating
sidetable_clearDeallocating
weak_clear_no_lock(&table.weak_table, (id)this); // 清除 weak 表
weak_clear_no_lock
的实现:
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
// 对象的地址作为 key
objc_object *referent = (objc_object *)referent_id;
// 从 weak 表中找到对象的引用记录
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
// 将 weak 引用的变量逐个赋值为 nil
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 从 weak 表中将该对象的引用记录删除掉
weak_entry_remove(weak_table, entry);
}
(2)__weak
变量与 autorelease pool
注:实际验证跟书中所说的有一点出入
- (void)foo {
id __weak obj1 = obj;
// 使用上面的 weak 变量
id __unsafe_unretained obj2 = obj1;
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Pig.m
命令,得到如下汇编指令:
"-[Pig foo]": ## @"\01-[Pig foo]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $48, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -8(%rbp), %rax
movq 8(%rax), %rsi
leaq -24(%rbp), %rdi
callq _objc_initWeak
leaq -24(%rbp), %rdi
movq %rax, -40(%rbp) ## 8-byte Spill
callq _objc_loadWeakRetained
movq %rax, -32(%rbp)
movq %rax, %rdi
callq *_objc_release@GOTPCREL(%rip)
leaq -24(%rbp), %rdi
callq _objc_destroyWeak
addq $48, %rsp
popq %rbp
retq
.cfi_endproc
上面的指令可以简化成:
id obj1;
objc_initWeak(&obj1, obj); // 内部调用了 storeWeak(&obj1, obj)
id tmp = objc_loadWeakRetained(&obj1); // 内部调用了 retain
objc_release(tmp); // 作用域结束,释放 tmp
objc_destroyWeak(&obj1); // 内部调用了 storeWeak(&obj1, nil)
结论:在没有返回值的情况下,使用 weak 变量时,编译器会对引用的对象 retain 一次,并生成一个临时变量来引用它,以此保证使用 weak 变量时不会因为其所引用对象被释放导致出错,然后在用完后立即释放。
再看看另一个场景:
- (id)foo {
id __weak obj1 = obj;
// 使用上面的 weak 变量
return obj1;
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Dog.m
命令,得到如下汇编指令:
"-[Dog foo]": ## @"\01-[Dog foo]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $48, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -8(%rbp), %rax
movq 8(%rax), %rsi
leaq -24(%rbp), %rax
movq %rax, %rdi
movq %rax, -32(%rbp) ## 8-byte Spill
callq _objc_initWeak
movq -32(%rbp), %rdi ## 8-byte Reload
movq %rax, -40(%rbp) ## 8-byte Spill
callq _objc_loadWeakRetained
movq -32(%rbp), %rdi ## 8-byte Reload
movq %rax, -48(%rbp) ## 8-byte Spill
callq _objc_destroyWeak
movq -48(%rbp), %rdi ## 8-byte Reload
addq $48, %rsp
popq %rbp
jmp _objc_autoreleaseReturnValue ## TAILCALL
.cfi_endproc
上面的指令可以简化成:
- (id)foo {
id obj1;
objc_initWeak(&obj1, obj); // 内部调用了 storeWeak(&obj1, obj)
id tmp = objc_loadWeakRetained(&obj1); // 内部调用了 retain
objc_destroyWeak(&obj1); // 内部调用了 storeWeak(&obj1, nil)
return objc_autoreleaseReturnValue(tmp)
}
结论:如果把 weak 变量作为返回值,编译器会会对引用的对象 retain 一次,并生成一个临时变量来引用它,以此保证使用 weak 变量时不会因为其所引用对象被释放导致出错,最后返回时再调用 objc_autoreleaseReturnValue
将这个对象加入到 autorelease pool 中去。
说明:
objc_loadWeakRetained
的实现如下:
id
objc_loadWeakRetained(id *location)
{
id obj;
id result;
Class cls;
SideTable *table;
retry:
// 取出 weak 变量引用的对象
obj = *location;
if (!obj) return nil;
if (obj->isTaggedPointer()) return obj;
table = &SideTables()[obj];
table->lock();
if (*location != obj) {
table->unlock();
goto retry;
}
result = obj;
...
// 调用 retain
obj->rootTryRetain()
...
table->unlock();
return result;
}
__autoreleasing
的实现- (void)foo {
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
}
执行 clang -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations -S ./Pig.m
命令,得到如下汇编指令:
"-[Pig foo]": ## @"\01-[Pig foo]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
callq _objc_autoreleasePoolPush
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rcx, %rdi
movq %rax, -32(%rbp) ## 8-byte Spill
callq *_objc_msgSend@GOTPCREL(%rip)
movq _OBJC_SELECTOR_REFERENCES_.2(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip)
movq %rax, %rdi
callq _objc_autorelease
movq %rax, -24(%rbp)
movq -32(%rbp), %rdi ## 8-byte Reload
callq _objc_autoreleasePoolPop
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
上面的指令可以简化成:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(Pig, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj); // 编译器自动插入了 objc_autorelease 函数
objc_autoreleasePoolPop();
结论:因为变量 obj
前面有 __autoreleasing
修饰符,所以编译器自动插入了 objc_autorelease
函数。
而 @autoreleasepool
代码块就转成了 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
这两个函数。
关联主题