Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

内存管理 #38

Open
ShannonChenCHN opened this issue Apr 27, 2017 · 9 comments
Open

内存管理 #38

ShannonChenCHN opened this issue Apr 27, 2017 · 9 comments

Comments

@ShannonChenCHN
Copy link
Owner

ShannonChenCHN commented Apr 27, 2017

关联主题

@ShannonChenCHN ShannonChenCHN changed the title 【专题】内存管理 内存管理 Jul 2, 2017
@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Dec 20, 2017

造成内存泄漏的几种常见原因

  • 造成内存泄漏的几种常见原因

    • delegate 循环引用
    • block 循环引用
    • NSTimer 循环引用
    • ARC 下使用 performSelector: 方法调用 alloc/ new 等方法创建对象时
    • 使用 C 语言库的 API 时忘记释放,比如 CGImageRef,CoreFoundation 的一些对象
    • UIWebView
  • 如何检测和避免?

    • Xcode 代码静态分析
    • Instruments
    • 检查 dealloc 方法是否被调用
    • 第三方工具
      • Facebook 开源的 FBMemoryProfiler
      • 微信读书团队开源的 MLeakFinder
    • 将一些容易产生内存泄漏的 API 封装起来
    • 将需要写 weakSelf 的代码块集中放到一个方法中去,只需用一个 weakSelf 调用这个方法,或者用 delegate 机制代替 block

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 2, 2021

内存管理总结(《Objective-C高级编程》学习总结)

目录

一、内存管理简介

  1. 引用计数与垃圾回收

二、 iOS 的内存管理和手动引用计数

  1. 引用计数内存管理简介
  2. 引用计数内存管理的规则
  3. alloc/retain/release/dealloc 的实现
  4. autoreleasepool 和 autorelease 的实现 ⭐️
    • 什么时候需要自己创建一个 autoreleasepool?

三、ARC

  1. ARC 简介
  • 什么是 ARC
  • ARC 和非 ARC 的区别
  1. 所有权修饰符
  2. ARC 的规则
  3. Core Foundation 和 Toll-Free Bridge ⭐️
  4. 属性

四、ARC 的实现 ⭐️

  1. __strong 的实现
  2. __weak 的实现
  3. __autoreleasing 的实现

五、实践

  1. 内存泄漏
    • 什么是内存泄漏
    • 导致内存泄漏的原因
      • block 循环引用
      • 其他
    • 内存泄漏的检测
  2. 内存警告
  • 收到内存警告怎么办
  • 缓存管理

@ShannonChenCHN
Copy link
Owner Author

一、内存管理简介

1. 引用计数与垃圾回收

引用计数是计算机编程语言中的一种**内存管理技术**,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 2, 2021

二、 iOS 的内存管理和手动引用计数(非 ARC)

1. 引用计数内存管理简介

当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为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

参考:

2. 引用计数内存管理的规则

几个规则(结合书中的示例代码看):

  • 自己生成的对象,自己持有(p7)
  • 非自己生成的对象,自己也能持有(p8)
  • 自己不再需要使用某个对象时,必须要释放这个对象(p9)
  • 绝对不能释放自己不(再)持有的对象(p10)

对象操作与对应的 Objective-C 方法:

对象操作 Objective-C 方法
生成并持有对象 alloc/new/copy/mutableCopy 等方法,以及使用这些单词开头的方法
持有对象 retain
释放对象 release
废弃对象 dealloc

3. alloc/retain/release/dealloc 的实现

(1)GNUstep 的实现

  • 通过 alloc 创建一个对象后,除了对象本身需要的内存空间外,在对象内存头部还有一块用于记录引用计数(retained)的内存区域。当调用 retain/release 时,程序都会访问对象内存头部的 retained 变量来获取引用计数。
  • 调用 alloc 方法后,引用计数值 +1,但是调用 alloc 方法实际上并没有增加 retained 变量的值,此时 retained 为 0,只是 retainCount 方法中每次返回的都是 retained 的值加 1,所以只有调用 retain 或者 release 方法,才会对 retained 变量产生影响。
  • 调用 retain 方法后,引用计数值 +1。
  • 调用 release 方法后,引用计数值 -1。
  • 引用计数为 0 时,release 方法内部会调用 dealloc 方法销毁对象。

(2)Apple 的实现

  • 这几个方法的实现在 objc4 runtime 源码中可以看到,与 GNUstep 的实现差不多
  • GNUstep 与 Apple 的实现的不同点:GNUstep 将引用计数值保存在对象占用内存块头部的变量中,而 Apple 的实现,是将引用计数值保存在引用计数表(runtime 源码中是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;
}

4. autorelease 和 autoreleasepool 的实现

(1)什么是 autorelease?和 C 语言中的自动变量(局部变量和函数参数)有什么不同?

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;
}

参考

(2)autorelease 的具体使用方法

(p21)

示例代码:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; // 这里会调 [obj release]

(3)autorelease pool 一般什么时候创建?什么时候需要自己创建一个 autoreleasepool?(p22)

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。

参考

(5)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_autoreleaseReturnValueobjc_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 源码中,我们可以看到 AutoreleasePoolPageobjc_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;
}

AutoreleasePoolPageautorelease 函数的实现对我们来说就比较容量理解了,它跟 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 只对应一个线程。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 2, 2021

三、ARC 内存管理

1. ARC 简介

(1)什么是 ARC?ARC 和非 ARC 的区别是什么?

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)。

(2)ARC 与非 ARC 的混编

ARC中ARC 只是在编译层做手脚,所以我们可以对整个项目设置 ARC 有效或者无效,也可以针对单个文件设置 ARC 有效或无效:

  • 支持 ARC:-fobjc-arc
  • 不支持 ARC:-fno-objc-arc
(3)内存管理法则(与非 ARC 中一样,只不过在代码表现上不一样)
  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 不再需要自己持有的对象时自己主动释放
  • 不能释放非自己持有的对象

2. 所有权修饰符

ARC 模式下,对象类型和 C 语言基本类型不同,对象类型前必须声明所有权修饰符:

  • __strong:对象类型默认的所有权修饰符,什么都不写的话,默认就是这个
  • __weak
  • __unsafe_unretained
  • __autoreleasing
(1)__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,所以原本就不会执行
(2)__weak

__weak 主要是为了解决两个问题:

  • 避免循环引用(不然会导致内存泄漏)
  • 所持有的对象被废弃后 __weak 变量被置为 nil (不然会出现野指针)

使用场景:

  • delegate 防止循环引用
  • block 防止循环引用
  • 指向 Interface Builder 中创建的控件的属性,比如 @property (weak, nonatomic) IBOutlet UIButton *nextButton;
(3)__unsafe_unretained

__unsafe_unretained 修饰符修饰的变量不属于编译器的内存管理范围。如果将一个对象赋值给它,它既不会持有该对象的强引用,也不会持有该对象的弱引用,当该对象被废弃时,它也不会被赋值为 nil。跟直接给 C 语言变量赋值的效果差不多。

问题__unsafe_unretained 存在的意义是什么?

因为 __weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中需要使用 __unsafe_unretained 修饰符来代替。

(4)__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 的实现)
  • 为什么方法返回值的时候需要用到 autorelease 机制呢?(答案见第四章节中 ARC 的实现)

冷知识:虽然 __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 中,这样就能得到非自己生成并持有的对象。

3. ARC 的规则

  • 不能显式使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 需要遵守内存管理的方法命名规则。在 ARC 模式和 MRC 模式下,以 alloc/new/copy/mutableCopy 开头的方法在返回对象时都必须返回给调用方所应当持有的对象(补充:在 ARC 模式下,以 init 开头的方法必须是实例方法并且必须要返回对象。返回的对象应为 id 类型或声明该方法的类的对象类型,或是该类的超类型或子类型。该返回的对象并不注册到 Autorelease Pool 中,基本上只是对 alloc 方法返回值的对象进行初始化处理并返回该对象)
  • 不要显式调用 dealloc
  • 使用 @autoreleasepool 块替代 NSAutoreleasePool
  • 不能使用区域(NSZone
  • 对象型变量不能作为 C 语言结构体(struct/union)的成员(理论上是可以的,但是这种场景下编译器不知道怎么进行内存管理)
  • 显式转换 idvoid *

问题: ARC 中为什么还要使用 @autoreleasepool

ARC 并不是舍弃了 @autoreleasepool,而是在编译阶段帮你插入必要的 retain/release/autorelease , 以及objc_retainAutoreleasedReturnValue/objc_autoreleaseReturnValue 等代码调用。

所以,ARC 之下依然是延时释放的(这是 ObjC 引用计数内存管理必须要的),依然是依赖于 NSAutoreleasePool,跟非 ARC 模式下手动调用那些函数本质上毫无差别,只是编译器和 objc runtime 来做会保证引用计数的正确性。

参考:

4. Core Foundation 和 Toll-Free Bridge

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_retainedCFBridgingRetain函数:将对象赋值给指针类型的变量时使用(ARC->非ARC)
  • __bridge_transferCFBridgingRelease函数:将指针赋值给对象类型的变量时使用(非ARC->ARC)

(1)__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,出现内存泄漏

(2)__bridge_retainedCFBridgingRetain函数

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;
}

(3)__bridge_transferCFBridgingRelease函数

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;
}

5. 属性

属性声明的修饰符 所有权修饰符
assign __unsafe_unretained
copy __strong (但是赋值的是被复制的对象)
retain __strong
strong __strong
unsafe_unretained __unsafe_unretained
weak __weak

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 2, 2021

四、ARC 的实现(一)

苹果官方声称,ARC 是“由编译器进行内存管理”的,但是实际上只有编译器是无法完全胜任的,在此基础上还需要 Objective-C 运行时库的支持。

也就是说,ARC 由以下工具、库来实现:

  • clang(LLVM编译器)3.0以上
  • objc4 Objective-C 运行时库 493.9 以上

如果没有运行时库的支持,无论怎样静态链接用于 ARC 的库,都无法实现在对象废弃时将所有引用它的 __weak 变量设置为 nil

在 LLVM 的文档 Objective-C Automatic Reference Counting (ARC) — Clang 13 documentation 中,也可以看到关于编译层面实现 ARC 更详细的描述。

1. __strong 的实现

(1)自己生成并持有对象
- (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函数释放所持有的对象

(2)非自己生成并持有对象
- (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函数,这样就实现了“非自己生成的对象自己也能持有”
(3)objc_retainAutoreleasedReturnValueobjc_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_retainAutoreleasedReturnValueobjc_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);

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 4, 2021

四、ARC 的实现(二)

2. __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;
}

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 4, 2021

四、ARC 的实现(三)

3. __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_autoreleasePoolPushobjc_autoreleasePoolPop 这两个函数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant