iOS 进阶—— iOS 内存管理 & Block

2014招聘 3年前
   <p>第一篇 iOS 内存管理</p>    <h2>1 似乎每个人在学习 iOS 过程中都考虑过的问题</h2>    <ol>     <li>alloc retain release delloc 做了什么?</li>     <li>autoreleasepool 是怎样实现的?</li>     <li>__unsafe_unretained 是什么?</li>     <li>Block 是怎样实现的</li>     <li>什么时候会引起循环引用,什么时候不会引起循环引用?</li>    </ol>    <p>所以我将在本篇博文中详细的从 ARC 解释到 iOS 的内存管理,以及 Block 相关的原理、源码。</p>    <h2>2 从 ARC 说起</h2>    <p>说 iOS 的内存管理,就不得不从 ARC(Automatic Reference Counting / 自动引用计数) 说起, ARC 是 WWDC2011 和 iOS5 引入的变化。ARC 是 LLVM 3.0 编译器的特性,用来自动管理内存。</p>    <p>与 Java 中 GC 不同,ARC 是编译器特性,而不是基于运行时的,所以 ARC 其实是在编译阶段自动帮开发者插入了管理内存的代码,而不是实时监控与回收内存。</p>    <p><img src="https://simg.open-open.com/show/5678a88ce12746ce4d43a8cef332c55d.png"></p>    <p>ARC 的内存管理规则可以简述为:</p>    <ol>     <li>每个对象都有一个『被引用计数』</li>     <li>对象被持有,『被引用计数』+1</li>     <li>对象被放弃持有,『被引用计数』-1</li>     <li>『引用计数』=0,释放对象</li>    </ol>    <h2>3 你需要知道</h2>    <ol>     <li>包含 NSObject 类的 Foundation 框架并没有公开</li>     <li>Core Foundation 框架源代码,以及通过 NSObject 进行内存管理的部分源代码是公开的。</li>     <li>GNUstep 是 Foundation 框架的互换框架</li>    </ol>    <p>GNUstep 也是 GNU 计划之一。将 Cocoa Objective-C 软件库以自由软件方式重新实现</p>    <p>某种意义上,GNUstep 和 Foundation 框架的实现是相似的</p>    <p>通过 GNUstep 的源码来分析 Foundation 的内存管理</p>    <h2>4 alloc retain release dealloc 的实现</h2>    <h3>4.1 GNU – alloc</h3>    <p>查看 GNUStep 中的 alloc 函数。</p>    <p>GNUstep/modules/core/base/Source/NSObject.m alloc:</p>    <pre>  <code class="language-objectivec">+ (id) alloc  {  return [selfallocWithZone: NSDefaultMallocZone()];  }     + (id)allocWithZone: (NSZone*)z  {  return NSAllocateObject (self, 0, z);  }  </code></pre>    <p>GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:</p>    <pre>  <code class="language-objectivec">struct obj_layout {  NSUInteger retained;  };     NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)  {  int size = 计算容纳对象所需内存大小;  id new = NSZoneCalloc(zone, 1, size);  memset (new, 0, size);  new = (id)&((obj)new)[1];  }  </code></pre>    <p>NSAllocateObject 函数通过调用 NSZoneCalloc 函数来分配存放对象所需的空间,之后将该内存空间置为 nil,最后返回作为对象而使用的指针。</p>    <p>我们将上面的代码做简化整理:</p>    <p>GNUstep/modules/core/base/Source/NSObject.m alloc 简化版本:</p>    <pre>  <code class="language-objectivec">struct obj_layout {  NSUInteger retained;  };     + (id) alloc  {  int size = sizeof(struct obj_layout) + 对象大小;  struct obj_layout*p = (struct obj_layout*)calloc(1, size);  return (id)(p+1)  return [selfallocWithZone: NSDefaultMallocZone()];  }  </code></pre>    <p>alloc 类方法用 struct obj_layout 中的 retained 整数来保存引用计数,并将其写入对象的内存头部,该对象内存块全部置为 0 后返回。</p>    <p>一个对象的表示便如下图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4216112259c55a71b696043500855e47.png"></p>    <h3>4.2 GNU – retain</h3>    <p>GNUstep/modules/core/base/Source/NSObject.m retainCount:</p>    <pre>  <code class="language-objectivec">- (NSUInteger) retainCount  {  return NSExtraRefCount(self) + 1;  }     inline NSUInteger  NSExtraRefCount(id anObject)  {  return ((obj_layout)anObject)[-1].retained;  }  </code></pre>    <p>GNUstep/modules/core/base/Source/NSObject.m retain:</p>    <pre>  <code class="language-objectivec">- (id) retain  {  NSIncrementExtraRefCount(self);  return self;  }     inline void  NSIncrementExtraRefCount(id anObject)  {  if (((obj)anObject)[-1].retained == UINT_MAX - 1)  [NSExceptionraise: NSInternalInconsistencyException  format: @"NSIncrementExtraRefCount() askedtoincrementtoofar”];  ((obj_layout)anObject)[-1].retained++;  }  </code></pre>    <p>以上代码中, NSIncrementExtraRefCount 方法首先写入了当 retained 变量超出最大值时发生异常的代码(因为 retained 是 NSUInteger 变量),然后进行 retain ++ 代码。</p>    <h3>4.3 GNU – release</h3>    <p>和 retain 相应的,release 方法做的就是 retain -- 。</p>    <p>GNUstep/modules/core/base/Source/NSObject.m release</p>    <pre>  <code class="language-objectivec">- (oneway void) release  {  if (NSDecrementExtraRefCountWasZero(self))  {  [self dealloc];  }  }     BOOL  NSDecrementExtraRefCountWasZero(id anObject)  {  if (((obj)anObject)[-1].retained == 0)  {  return YES;  }  ((obj)anObject)[-1].retained--;  return NO;  }  </code></pre>    <h3>4.4 GNU – dealloc</h3>    <p>dealloc 将会对对象进行释放。</p>    <p>GNUstep/modules/core/base/Source/NSObject.m dealloc:</p>    <pre>  <code class="language-objectivec">- (void) dealloc  {  NSDeallocateObject (self);  }     inline void  NSDeallocateObject(id anObject)  {  obj_layout o = &((obj_layout)anObject)[-1];  free(o);  }  </code></pre>    <h3>4.5 Apple 实现</h3>    <p>在 Xcode 中 设置 Debug -> Debug Workflow -> Always Show Disassenbly 打开。这样在打断点后,可以看到更详细的方法调用。</p>    <p>通过在 NSObject 类的 alloc 等方法上设置断点追踪可以看到几个方法内部分别调用了:</p>    <p>retainCount</p>    <p>__CFdoExternRefOperation</p>    <p>CFBasicHashGetCountOfKey</p>    <p>retain</p>    <p>__CFdoExternRefOperation</p>    <p>CFBasicHashAddValue</p>    <p>release</p>    <p>__CFdoExternRefOperation</p>    <p>CFBasicHashRemoveValue</p>    <p>可以看到他们都调用了一个共同的 __CFdoExternRefOperation 方法。</p>    <p>该方法从前缀可以看到是包含在 Core Foundation,在 CFRuntime.c 中可以找到,做简化后列出源码:</p>    <p>CFRuntime.c __CFDoExternRefOperation:</p>    <pre>  <code class="language-objectivec">int __CFDoExternRefOperation(uintptr_top, id obj) {  CFBasicHashRef table = 取得对象的散列表(obj);  int count;     switch (op) {  caseOPERATION_retainCount:  count = CFBasicHashGetCountOfKey(table, obj);  return count;  break;  caseOPERATION_retain:  count = CFBasicHashAddValue(table, obj);  return obj;  caseOPERATION_release:  count = CFBasicHashRemoveValue(table, obj);  return 0 == count;  }  }  </code></pre>    <p>所以 __CFDoExternRefOperation 是针对不同的操作,进行具体的方法调用,如果 op 是 OPERATION_retain ,就去掉用具体实现 retain 的方法。</p>    <p>从 BasicHash 这样的方法名可以看出,其实引用计数表就是散列表。</p>    <p>key 为 hash(对象的地址) value 为 引用计数。</p>    <p>下图是 Apple 和 GNU 的实现对比:</p>    <p><img src="https://simg.open-open.com/show/551db15698e5f8e06f53d1a471db7568.png"></p>    <h2>5 autorelease 和 autorelaesepool</h2>    <p>在苹果对于 NSAutoreleasePool 的 文档 中表示:</p>    <p>每个线程(包括主线程),都维护了一个管理 NSAutoreleasePool 的栈。当创先新的 Pool 时,他们会被添加到栈顶。当 Pool 被销毁时,他们会被从栈中移除。</p>    <p>autorelease 的对象会被添加到当前线程的栈顶的 Pool 中。当 Pool 被销毁,其中的对象也会被释放。</p>    <p>当线程结束时,所有的 Pool 被销毁释放。</p>    <p>对 NSAutoreleasePool 类方法和 autorelease 方法打断点,查看其运行过程,可以看到调用了以下函数:</p>    <pre>  <code class="language-objectivec">NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];  // 等同于 objc_autoreleasePoolPush     id obj = [[NSObject alloc]init];  [objautorelease];  // 等同于 objc_autorelease(obj)     [NSAutoreleasePool showPools];  // 查看 NSAutoreleasePool 状况     [pooldrain];  // 等同于 objc_autoreleasePoolPop(pool)  </code></pre>    <p>[NSAutoreleasePool showPools] 可以看到当前线程所有 pool 的情况:</p>    <pre>  <code class="language-objectivec">objc[21536]: ##############  objc[21536]: AUTORELEASEPOOLSfor thread 0x10011e3c0  objc[21536]: 2 releasespending.  objc[21536]: [0x101802000] ................ PAGE (hot) (cold)  objc[21536]: [0x101802038] ################ POOL 0x101802038  objc[21536]: [0x101802040] 0x1003062e0 NSObject  objc[21536]: ##############  Programendedwithexitcode: 0  </code></pre>    <p>在 <a href="/misc/goto?guid=4959736758130034331" rel="nofollow,noindex">objc4</a> 中可以查看到 AutoreleasePoolPage:</p>    <pre>  <code class="language-objectivec">objc4/NSObject.mmAutoreleasePoolPage     class AutoreleasePoolPage  {  static inlinevoid *push()  {  生成或者持有 NSAutoreleasePool 类对象  }  static inlinevoid pop(void *token)  {  废弃 NSAutoreleasePool 类对象  releaseAll();  }  static inlineidautorelease(idobj)  {  相当于 NSAutoreleasePool 类的 addObject 类方法  AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;  }  id *add(idobj)  {  将对象追加到内部数组  }  void releaseAll()  {  调用内部数组中对象的 release 方法  }  };     void *  objc_autoreleasePoolPush(void)  {  if (UseGC) return nil;  return AutoreleasePoolPage::push();  }     void  objc_autoreleasePoolPop(void *ctxt)  {  if (UseGC) return;  AutoreleasePoolPage::pop(ctxt);  }  </code></pre>    <p>AutoreleasePoolPage 以双向链表的形式组合而成(分别对应结构中的 parent 指针和 child 指针)。</p>    <p>thread 指针指向当前线程。</p>    <p>每个 AutoreleasePoolPage 对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。</p>    <p>next 指针指向下一个 add 进来的 autorelease 的对象即将存放的位置。</p>    <p>一个 Page 的空间被占满时,会新建一个 AutoreleasePoolPage 对象,连接链表。</p>    <p><img src="https://simg.open-open.com/show/94cee44c592a66920b332c53fc988b42.png"></p>    <h2>6 __unsafe_unretained</h2>    <p>有时候我们除了 __weak 和 __strong 之外也会用到 __unsafe_unretained 这个修饰符,那么我们对 __unsafe_unretained 了解多少?</p>    <p>__unsafe_unretained 是不安全的所有权修饰符,尽管 ARC 的内存管理是编译器的工作,但附有 __unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。 <strong>赋值时即不获得强引用也不获得弱引用</strong> 。</p>    <p>来运行一段代码:</p>    <pre>  <code class="language-objectivec">id __unsafe_unretainedobj1 = nil;  {  id __strongobj0 = [[NSObject alloc]init];     obj1 = obj0;     NSLog(@"A: %@", obj1);  }     NSLog(@"B: %@", obj1);  </code></pre>    <p>运行结果:</p>    <pre>  <code class="language-objectivec">2017-01-12 19:24:47.245220 __unsafe_unretained[55726:4408416] A:  2017-01-12 19:24:47.246670 __unsafe_unretained[55726:4408416] B:  Programendedwithexitcode: 0  </code></pre>    <p>对代码进行详细分析:</p>    <pre>  <code class="language-objectivec">id __unsafe_unretainedobj1 = nil;  {  // 自己生成并持有对象  id __strongobj0 = [[NSObject alloc]init];     // 因为 obj0 变量为强引用,  // 所以自己持有对象  obj1 = obj0;     // 虽然 obj0 变量赋值给 obj1  // 但是 obj1 变量既不持有对象的强引用,也不持有对象的弱引用  NSLog(@"A: %@", obj1);  // 输出 obj1 变量所表示的对象  }     NSLog(@"B: %@", obj1);  // 输出 obj1 变量所表示的对象  // obj1 变量表示的对象已经被废弃  // 所以此时获得的是悬垂指针  // 错误访问  </code></pre>    <p>所以,最后的 NSLog 只是碰巧正常运行,如果错误访问,会造成 crash</p>    <p>在使用 __unsafe_unretained 修饰符时,赋值给附有 __strong 修饰符变量时,要确保对象确实存在</p>    <h2>第二篇 Block</h2>    <p>花几分钟时间看下面三个小题目,写下你的答案。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/749d7068701e8988dfd7e9c603553a48.png"></p>    <p>这个三个小题目,我在整理此片博文之前给了三位朋友去解答,最后的结果,除了一位朋友 3 题全部正确,其他两个朋友均只答中 1 题。</p>    <p>说明还是有很多 iOS 的朋友对于 Block 并没有透彻理解。本篇博文会对 Block 进行详细的解说。</p>    <h2>1 Block 使用的简单规则</h2>    <p>先了解简单规则,再去分析原理和实现:</p>    <p>Block 中,Block <strong>表达式截获</strong> 所使用的自动变量的值,即保存该自动变量的 <strong>瞬间值</strong> 。</p>    <p>修饰为 __block 的变量,在捕获时,获取的 <strong>不再是瞬间值</strong> 。</p>    <p>至于 Why,后面将会继续说。</p>    <h2>2 Block 的实现</h2>    <p>Block 是带有自动变量(局部变量)的匿名函数。</p>    <p>Block 表达式很简单,总体可以描述为:『 ^ 返回值类型 参数列表 表达式 』。</p>    <p>但是 Block 并不是 Objective-C 中才有的语法,这是怎么一回事?</p>    <p>clang 编译器提供给程序员了解 Objective-C 背后机制的方法,通过 clang 的转换可以看到 Block 的实现原理。</p>    <p>通过 clang -rewrite-objc yourfile.m clang 将会把 Objective-C 的代码转换成 C 语言的代码。</p>    <h3>2.1 Block 基本实现剖析</h3>    <p>用 Xcode 创建 Command Line 项目,写如下代码:</p>    <pre>  <code class="language-objectivec">int main(int argc, const char * argv[]) {  void (^blk)(void) = ^{NSLog(@"Block")};  blk();  return 0;  }  </code></pre>    <p>用 clang 转换:</p>    <p><img src="https://simg.open-open.com/show/042b3489e0ed96e509ee78f69bb5fd3f.png"></p>    <p>以上是转换后的代码,不要方,一段一段看。</p>    <p>可以看到,Block 内部的内容, <strong> 被转换成了一个普通的静态函数 __main_func_0 </strong> 。</p>    <p>再看其他部分:</p>    <p>main.cpp __block_impl:</p>    <pre>  <code class="language-objectivec">struct __block_impl {  void *isa;  int Flags;  int Reserved;  void *FuncPtr;  };  </code></pre>    <p>__block_impl 结构体包括了一些标志、今后版本升级 <strong>预留的变量</strong> 、 <strong>函数指针</strong> 。</p>    <p>main.cpp __main_block_desc_0:</p>    <pre>  <code class="language-objectivec">static struct __main_block_desc_0 {  size_treserved;  size_tBlock_size;  } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};  </code></pre>    <p>__main_block_desc_0 结构体包括了今后版本升级预留的变量、block 大小。</p>    <p>main.cpp __main_block_impl_0:</p>    <p>__main_block_impl_0 结构体含有两个成员变量,分别是 __block_impl 和 __main_block_desc_0 实例变量。</p>    <p>此外,还含有一个构造方法。该构造方法在 main 函数中被如下调用:</p>    <p>main.cpp __main_block_impl_0 构造函数的调用:</p>    <pre>  <code class="language-objectivec">void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,  &__main_block_desc_0_DATA));  </code></pre>    <p>去掉各种强制转换,做简化:</p>    <p>main.cpp __main_block_impl_0 构造函数的调用 简化:</p>    <pre>  <code class="language-objectivec">struct __main_block_impl_0tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);  struct __main_block_impl_0*blk = &tmp;  </code></pre>    <p>以上代码即:将 __main_block_impl_0 结构体实例的指针,赋值给 __main_block_impl_0 结构体指针类型的变量 blk 。也就是我们最初的结构体定义:</p>    <pre>  <code class="language-objectivec">void (^blk)(void) = ^{NSLog(@"Block");};  </code></pre>    <p>另外,main 函数中还有另外一段:</p>    <pre>  <code class="language-objectivec">((void (*)(__block_impl*))((__block_impl*)blk)->FuncPtr)((__block_impl*)blk);  </code></pre>    <p>去掉各种转换:</p>    <pre>  <code class="language-objectivec">(*blk->impl.FuncPtr)(blk);  </code></pre>    <p>实际就是最初的:</p>    <pre>  <code class="language-objectivec">blk();  </code></pre>    <h3>2.2 Block 截获外部变量瞬间值的实现剖析</h3>    <p>2.1 中对最简单的 <em>无参数 Block 声明、调用</em> 进行了 clang 转换。接下来再看一段『截获自动变量』的代码(可以使用命令 clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 main.m ):</p>    <pre>  <code class="language-objectivec">int main(int argc, const char * argv[]) {     int val = 10;  const char *fmt = "val = %d\n";  void (^blk)(void) = ^{printf(fmt, val);};     val = 2;  fmt = "These values were changed, val = %d\n";     blk();     return 0;  }  </code></pre>    <p>clang 转换之后:</p>    <p><img src="https://simg.open-open.com/show/fccddd5c92fecbdc32eabfed96fc368e.png"></p>    <p>和 2.1 节中的转换代码对比,可以发现多了一些代码。</p>    <p>首先, __main_block_impl_0 多了一个变量 val ,并在构造函数的参数中加入了 val 的赋值:</p>    <p>main.cpp __main_block_impl_0:</p>    <pre>  <code class="language-objectivec">struct __main_block_impl_0 {  struct __block_implimpl;  struct __main_block_desc_0* Desc;  const char *fmt;  int val;  __main_block_impl_0(void *fp, struct __main_block_desc_0*desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {  impl.isa = &_NSConcreteStackBlock;  impl.Flags = flags;  impl.FuncPtr = fp;  Desc = desc;  }  };  </code></pre>    <p>而在 main 函数中,对 Block 的声明变为此句:</p>    <p>main.cpp __main_block_impl_0 构造函数的调用:</p>    <pre>  <code class="language-objectivec">void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));  </code></pre>    <p>去掉转换:</p>    <p>main.cpp __main_block_impl_0 构造函数的调用 简化:</p>    <pre>  <code class="language-objectivec">struct __main_block_impl_0tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, val);  struct __main_block_impl_0*blk = &tmp;  </code></pre>    <p>_所以,在 Block 被声明时,Block 已经将 val 作为 __main_block_impl_0 的内部变量保存下来了。无论在在声明之后怎样更改 val 的值,都不会影响,Block 调用时访问的内部 val 值。这就是 Block 捕获变量瞬间值的原理。_</p>    <p>本节所有代码在 EX05 中</p>    <h3>2.3 __block 变量的访问实现剖析</h3>    <p>我们知道,Block 中能够读取,但是不能更改一个局部变量,如果去更改,Xcode 会提示你无法在 Block 内部更改变量。</p>    <p>Block 内部只是对局部变量只读,但是 Block 能读写以下几种变量:</p>    <ol>     <li>静态变量</li>     <li>静态全局变量</li>     <li>全局变量</li>    </ol>    <p>也就是说以下代码是没有问题的:</p>    <pre>  <code class="language-objectivec">int global_val = 1;  static int static_global_val = 2;     int main(int argc, const char * argv[]) {  static int static_val = 3;     void (^blk)(void) = ^ {  global_val = 1 * 2;  static_global_val = 2 * 2;  static_val = 3 * 2;  }     return 0;  }  </code></pre>    <p>如果想在 Block 内部写局部变量,需要对访问的局部变量增加 __block 修饰。</p>    <p>__block 修饰符其实类似于 C 语言中 static、auto、register 修饰符。用于指定将变量值设置到哪个存储域中。</p>    <p>具体 __block 之后究竟做了哪些变化我们可以写代码测试:</p>    <p>EX07:</p>    <pre>  <code class="language-objectivec">int main(int argc, const char * argv[]) {     __blockint val = 10;  void (^blk)(void) = ^{val = 1;};     return 0;  }  </code></pre>    <p>clang 转换之后:</p>    <p><img src="https://simg.open-open.com/show/7aa8ff3854dd5fbf0bfef2959f5e2269.png"></p>    <p>跟 2.2 对比,似乎又加了非常代码。发现多了两个结构体。</p>    <p>main.cpp __Block_byref_val_0:</p>    <pre>  <code class="language-objectivec">struct __Block_byref_val_0 {  void *__isa;  __Block_byref_val_0*__forwarding;  int __flags;  int __size;  int val;  };  </code></pre>    <p>很惊奇的发现, <strong> block 类型的 val 变成了结构体 </strong> Block_byref_val_0 的实例。这个实例内,包含了 <strong> isa 指针、一个标志位 </strong> flags 、一个记录大小的 <strong> size 。最最重要的,多了一个 </strong> forwarding 指针和 val变量。这是怎么回事?</p>    <p>在 main 函数部分,实例化了该结构体:</p>    <p>main.cpp main.m 部分:</p>    <pre>  <code class="language-objectivec">__Block_byref_val_0val = {(void*)0,  (__Block_byref_val_0*)&val,  0,  sizeof(__Block_byref_val_0),  10};  </code></pre>    <p>我们可以看出该结构体对象初始化时:</p>    <ol>     <li><strong>__forwarding 指向了结构体实例本身在内存中的地址</strong></li>     <li>val = 10</li>    </ol>    <p>而在 main 函数中, val = 1 这句赋值语句变成了:</p>    <p>main.cpp val = 1; 对应的函数</p>    <pre>  <code class="language-objectivec">(val->__forwarding->val) = 1;  </code></pre>    <p>这里就可以看出其精髓,val = 1,实际上更改的是 __Block_byref_val_0 结构体实例 val 中的 __forwarding 指针(也就是本身)指向的 val 变量。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0f0f0a54894e1cdd49ed8b518b154d1c.png"></p>    <p>而对 val 访问也是如此。你可以理解为通过取地址改变变量的值,这和 C 语言中取地址改变变量类似。</p>    <p>所以,声明 <strong> block 的变量可以被改变。至于 </strong> forwarding的其他巨大作用,会继续分析。</p>    <p>本节代码在 <a href="/misc/goto?guid=4959736758238952811" rel="nofollow,noindex">EX05</a> 中</p>    <h2>3 Block 的存储域</h2>    <p>Block 有三种类型,分别是:</p>    <ol>     <li>__NSConcreteStackBlock ————————栈中</li>     <li>__NSConcreteGlobalBlock ————————数据区域中</li>     <li>__NSConcreteMallocBlock ————————堆中</li>    </ol>    <p>__NSConcreteGlobalBlock 出现的地方有:</p>    <ol>     <li>设置全局变量的地方有 Block 语法时</li>     <li>Block 语法的表达式中不使用任何外部变量时</li>    </ol>    <p>设置在栈上的 Block,如果所属的变量作用域结束,Block 就会被废弃。如果其中用到了 <strong>block,</strong> block 所属的变量作用域结束也会被废弃。</p>    <p>为了解决这个问题,Block 在必要的时候就需要从栈中移到堆中。ARC 有效时,很多情况下,编译器会帮助完成 Block 的 copy,但很多情况下,我们需要手动 copy Block。</p>    <p>对不同存储域的 Block copy 时,影响如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0ec952856a6277fd1f42ef74072895f7.png"></p>    <p>copy 时,对访问到的 __block 类型对象影响如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b9a45fdfc9ca679d54e96ad4d336603f.png"></p>    <p>此时可以看出 __forwarding 的巨大作用——无论 Block 此时在堆中还是在栈中,由于 __forwarding 指向局部变量转换成的结构体实例的真是地址,所以都能确保正确的访问。</p>    <p>具体的来说:</p>    <ol>     <li>当 block 变量被一个 Block 使用时,Block 从栈复制到堆, block 变量也会被复制到,并被该 Block 持有。</li>     <li>在 block 变量被多个 Block 使用时,在任何一个 Block 从栈复制到堆时, block 变量也会被复制到堆,并被该 Block 持有。但由于 __forwarding 指针的存在,无论 block 变量和 Block 在不在同一个存储域,都可以正确的访问 block 变量。</li>     <li>如果堆上的 Block 被废弃,那么它所使用的 __block 变量也会被释放。</li>    </ol>    <p style="text-align:center"><img src="https://simg.open-open.com/show/db884e791b6eb9980b6320285d43fdd3.png"></p>    <p>前面说到编译器会帮助完成一些 Block 的 copy,也有手动 copy Block。那么 Block 被复制到堆上的情况有(此段摘自于『Objective-C高级编程 iOS与OS X多线程和内存管理』):</p>    <ol>     <li>调用 Block 的 copy 方法时</li>     <li>Block 作为返回值时</li>     <li>将 Block 赋值给附有 __strong 修饰符的成员变量时(id类型或 Block 类型)时</li>     <li>在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时</li>    </ol>    <h2>4 Block 循环引用</h2>    <p>Block 循环引用,是在编程中非常常见的问题,甚至很多时候,我们并不知道发生了循环引用,直到我们突然某一天发现『怎么这个对象没有调用 delloc』,才意识到有问题存在。</p>    <p>在『Block 存储域』中也说明了 Block 在 copy 后对 __block 对象会 retain 一次。</p>    <p>那么对于如下情况就会发生循环引用:</p>    <p>block_retain_cycle:</p>    <pre>  <code class="language-objectivec">@interfaceMyObject: NSObject     @property (nonatomic, copy) blk_tblk;  @property (nonatomic, strong) NSObject *obj;     @end     @implementation MyObject     - (instancetype)init {  self = [super init];  _blk = ^{NSLog(@"self = %@", self);};  return self;  }     - (void)dealloc {  NSLog(@"%@ dealloc", self.class);  }     @end     int main(int argc, const char * argv[]) {  id myobj = [[MyObject alloc]init];  NSLog(@"%@", myobj);  return 0;  }  </code></pre>    <p>由于 self -> blk,blk -> self,双方都无法释放。</p>    <p>但要注意的是,对于以下情况,同样会发生循环引用:</p>    <pre>  <code class="language-objectivec">block_retain_cycle     @interfaceMyObject: NSObject     @property (nonatomic, copy) blk_tblk;     // 下面是多加的一句  @property (nonatomic, strong) NSObject *obj;     @end     @implementation MyObject     - (instancetype)init {  self = [super init];     // 下面是多加的一句  _blk = ^{NSLog(@"self = %@", _obj);};     return self;  }     - (void)dealloc {  NSLog(@"%@ dealloc", self.class);  }     @end     int main(int argc, const char * argv[]) {  id myobj = [[MyObject alloc]init];  NSLog(@"%@", myobj);  return 0;  }  </code></pre>    <p>这是由于 self -> obj,self -> blk,blk -> obj。这种情况是非常容易被忽视的。</p>    <h2>5 重审问题</h2>    <p>我们再来看看最初的几个小题目:</p>    <p><img src="https://simg.open-open.com/show/4a9a87aea6ea84af63498de62012c284.png" alt="iOS 进阶—— iOS 内存管理 & Block" width="550" height="351"></p>    <ol>     <li>第一题: <p>由于 Block 捕获瞬间值,所以输出为 in block val = 0</p> </li>     <li>第二题: <p>由于 val 为 __block,外部更改会影响到内部访问,所以输出为 in block val = 1</p> </li>     <li>第三题: <p>和第二题类似, val = 1 能影响到 Block 内部访问,所以先输出 in block val = 1 ,之后在 Block 内部更改 val 值,再次访问时输出 after block val = 2 。</p> </li>    </ol>    <h2>Other</h2>    <p>我写这篇文章是在我阅读了『Objective-C高级编程 iOS与OS X多线程和内存管理』一书之后,博文中也有很内容源于『Objective-C高级编程 iOS与OS X多线程和内存管理』。</p>    <p>非常向大家推荐此书。这本书里记录了关于 iOS 内存管理的深入内容。但要注意的是,此书中的多处知识点并不是很详细,需要你以拓展的心态去学习。在有解释不详细的地方,自己主动去探索,去拓展,找更多的资料,最后,你会发现你对 iOS 内存管理有了更多的深入的理解。</p>    <p> </p>    <p> </p>    <p>来自:http://ios.jobbole.com/92903/</p>    <p> </p>