iOS 一个变量设置为nil后引用计数变量器是多少

block 本质是一个OC对象,也存在 isa 指针。或者说Block 是封装了函数调用和函数调用环境的OC对象。

编写一段最简单的OC代码顶一个block,代码如:

我们打开编译生成的main.cpp代码,会发现上述代码被转化为如下:

包含isa指针说明,block本质上也是一个OC对象,FuncPtr 指向block所封装的代码块地址,等执行block时会通过FuncPtr寻找将要执行的代码块,并且调用。

如果我们将main函数改为:

我们可以看到,较之前,__main_block_impl_0 结构体新增一个 int 类型变量abc,用于存储所引用的外部变量的值。因为是值存储,所以在block生成之后,无论外部变量做何更改,abc依然是之前所定义的值。

因为我们所定义的外部变量 abc 之前没有任何修饰符,也就是默认的auto变量,此时block是值捕获。如果将外部变量声明为 static 类型再观察底层实现。

转化为c++底层实现为:

使用static 修饰的变量在block内部为指针传递,block直接捕获外部变量的内存地址,此时若外部变量在block声明之后修改,block内部也会同步进行修改。

如果使用全局变量,block不会捕获。因为声明全局变量的类型会在程序的整个声明周期都不会被释放,所以在使用block时,直接会去访问全局变量的值。所以捕获就没有意义了,感兴趣的可以自行查看底层实现。

当我们声明一个block 并且打印他的继承链我们可以看到:

从而也进一步证明了block 本质上为 OC对象。并且,在不引用外部变量的情况下,block为 NSGlobalBlock 类型。

我们定义三个不同的block,分别打印他们的实际类型:

他们在内存中位置分别:

那他们是如何区分的呢?可以使用如下表格来说明:

NSStackBlock 执行 copy 后会将栈区的block 复制到堆区,便于程序员管理,那其他类型的block执行 copy 会有什么变化呢?如下表所示:

4. ARC 下某些情况下系统会对 Block 自动执行一次 copy 操作,将 Block 从栈区转移到堆区

如果此代码在MRC 环境下,会崩溃。Block访问的变量已被释放。 如果在ARC环境下,在参数的返回值为block时,系统会对block自动执行一次 copy 操作,使其变为 NSMallocBlock 类型。

2.当Block 被强指针引用时会自动执行copy操作

所以在 MAC 环境下的block属性必须使用 copy 修饰,而ARC环境下的block属性即可使用 strong 修饰,也可以使用 copy 修饰,两者都会对block自动执行copy操作,故无任何区别。

会发现当 函数体内 大括号执行完毕后 XWPerson 即被释放,此时的block 是 栈类型的Block 即 __NSStackBlock__. 存储在栈区的block即便引用了对象,也会跟随大括号一并释放。

为什么堆 block 会对外部对象强引用呢?

 
方法。最终调用的 _Block_object_assign 方法会对block引入的对象 person 进行引用计数操作,当所引入的对象使用 strong 修饰则使其引用计数加1,若使用weak修饰则引用计数不变。
 
方法,将block内部引用的对象成员引用计数减1,如果此时外部对象使用strong 修饰,引用计数在copy加1后 此时再减1.依然会强引用外部对象,不会释放,如果使用weak修饰,此时因为自身以及被释放,所以不会再持有所引用外部对象,然而此时所引用外部对象是否会被释放取决于它的引用计数是否为 0。

6. block 内部修改外部变量的值。

 
我们知道,如果block 内部捕获的外部变量为 auto 类型,在block 内部生成的是该变量的值类型变量,无法通过block内部的值修改外部变量。
如果想在block内部修改外部变量的值有几种方法?

 
使用 static 修饰的变量block内部会直接获取到变量的内存地址,可以直接修改。

 
若使用 static 变量修饰,该变量的生命周期就会无限延长,这不符合我们的设计思路,故我们可以使用 __block 来修饰外部变量,从而达到在block内部修改外部成员变量的目的。
__block 是如何实现此需求的呢?


此时在main函数内声明 __block 类型的变量会以此方式初始化:
 
其中 __forwarding 保存的原变量 a 的内存地址,size 为当前变量的内存大小,10 保存未原变量的值。
如此,我们在block 内部修改 原变量时:
直接取原变量的地址进行更改,从而实现在block内部更改外部变量。

 
对于block内部捕获的对象类型的auto变量和__block修饰的变量。如果block在栈区,不会对他们进行内存管理,即不会强引用外部变量
如果block被复制到堆区,则会调用内部 copy 函数对外部 __block 修饰的变量和对象类型的auto变量进行内存管理。
当block从内存中移除时,同样也会调用dispose函数对所引用的外部变量进行释放。

 
使用 block 很容易形成循环引用,如果一个类中定义的block内部引用了该类的外部属性,包括 类本身的 self, 均会导致 self 强引用 block,block 也强引用 self。导致self不会被释放。如下代码就会造成循环引用:
产生循环引用的本质原因是,在block内部实现里,会将self 捕获到block内部,并且strong 强引用。如下代码所示:

9. 避免产生循环引用

 

1. (ARC 环境下) __weak : 弱引用对象,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。

 

2. (ARC / MRC 环境下) __unsafe_unretained : 弱引用对象,指向的对象销毁时,不会自动将指针置为nil。再次引用该对象时可能会产生访问僵尸对象的错误,产生崩溃,故不建议使用!

 

3. (ARC / MRC 环境下) __block : 使用__block 修饰对象. 在ARC环境下-前提是一定要调用此block,并且要在block内部将所引用的外部变量手动置nil。因为 MRC 环境下,引用__block 修饰的对象不会使其引用计数加1,所以不需要手动置nil,也不是必需要使用block。


}

引用计数是一个简单而有效的管理对象生命周期的方式。

  • 当我们创建一个新对象时,它的引用计数为1
  • 当有一个新的指针指向这个对象时,我们将引用计数加1
  • 当某个指针不再指向这个对象时,我们将引用计数减1
  • 当对象的引用计数为0时,说明这个对象不再被任何指针指向了,就可以将对象销毁,回收内存

引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间传递和共享数据。

解决这个问题的办法就是在网络请求结束后,主动释放对block的持有,打破循环引用

主动断开循环引用需要程序员能够准确发现循环引用,并知道什么时机断开循环引用,所以这种解决方法并不常见,更常见的办法是使用弱引用

弱引用虽然持有对象,但是不增加引用计数,这样就避免了循环引用的产生。例如delegate模式中的weak声明。View Controller的delegate成员变量通常是一个弱引用,以避免两个View Controller互相引用对方造成循环引用。

系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。当一个对象的引用计数为0时,系统就通过这张表,找到所有的弱引用指针,将他们置为nil。弱引用的使用是有额外的开销的,虽然这个开销很小,但是如果肯定不需要使用弱引用特性时,就不应该盲目使用弱引用

Block如何避免循环引用
  • 当block本身不被self持有,而被别的对象持有,同时不产生循环引用的时候,就不需要weakself,最常见的代码就是UIView的动画代码
 
  1. 在block中引用该弱引用,考虑到多线程情况,通过强引用strongSelf来引用该弱引用,如果self不为nil,就会retain self,以防在block内部使用过程中self被释放
  2. 强引用strongSelf在block作用域结束之后,自动释放

在block块中先写一个strongSelf,是为了避免在block的执行过程中,突然出现self被释放的情况。

__weak可以避免循环引用,__block不能避免循环引用,__block的作用是提升变量的作用域

  • 引用计数是一种简单高效的管理对象生命周期的方法
  • 引用计数无法避免循环引用
  • 循环引用的解决分为弱引用(事先规避)和主动断开循环引用(事后补救)两个方案
  • __weak 可以避免循环引用,但是其存在外部对象释放后,block内部也访问不到这个对象的问题,所以我们通过在block内部声明一个 __strong的变量来指向weakObj,使得外部对象既能在block内部保持,又能避免循环引用的问题
  • __block无法避免循环引用的问题,它的作用是提升了变量的作用域,在block内外访问的都是同一个对象,如果想要在block中改变变量的值,就需要在变量声明的时候加上__block修饰符
}

引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。

一、自动引用计数的工作机制

  • 当你每次创建一个类的新的实例的时候,ARC 会分配一块内存来储存该实例信息。内存中会包含实例的类型信息,以及这个实例所有相关的存储型属性的值。
  • 当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。这确保了不再被使用的实例,不会一直占用内存空间。
  • 然而,当 ARC 收回和释放了正在被使用中的实例,该实例的属性和方法将不能再被访问和调用。实际上,如果你试图访问这个实例,你的应用程序很可能会崩溃。
  • 为了使上述成为可能,无论你将实例赋值给属性、常量或变量,它们都会创建此实例的强引用。之所以称之为“强”引用,是因为它会将实例牢牢地保持住,只要强引用还在,实例是不允许被销毁的。
  • 下面的例子展示了自动引用计数的工作机制。例子以一个简单的 Person 类开始,并定义了一个叫 name 的常量属性:
  • 接下来的代码片段定义了三个类型为 Person? 的变量,用来按照代码片段中的顺序,为新的 Person 实例建立多个引用。由于这些变量是被定义为可选类型(Person?,而不是 Person),它们的值会被自动初始化为 nil,目前还不会引用到
  • 现在你可以创建 Person 类的新实例,并且将它赋值给三个变量中的一个:
  • 由于 Person 类的新实例被赋值给了 reference1 变量,所以 reference1Person 类的新实例之间建立了一个强引用。正是因为这一个强引用,ARC 会保证 Person 实例被保持在内存中不被销毁。

  • 如果你将同一个 Person 实例也赋值给其他两个变量,该实例又会多出两个强引用:

  • 如果你通过给其中两个变量赋值 nil 的方式断开两个强引用(包括最先的那个强引用),只留下一个强引用,Person 实例不会被销毁:
  • 在你清楚地表明不再使用这个 Person 实例时,即第三个也就是最后一个强引用被断开时,ARC 会销毁它:

三、类实例之间的循环强引用

  • 在上面的例子中,ARC 会跟踪你所新创建的 Person 实例的引用数量,并且会在 Person 实例不再被需要时销毁它。
  • 然而,我们可能会写出一个类实例的强引用数永远不能变成 0 的代码。如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,就是这种情况。这就是所谓的循环强引用。
  • 下面展示了一个不经意产生循环强引用的例子。例子定义了两个类:PersonApartment,用来建模公寓和它其中的居民:
  • 接下来的代码片段定义了两个可选类型的变量 johnunit4A,并分别被设定为下面的 ApartmentPerson 的实例。
  • 现在你能够将这两个实例关联在一起,这样人就能有公寓住了,而公寓也有了房客。
  • 此时两个实例互相引用这对方, 产生了一个循环强引用, 因此,当你断开 johnunit4A 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁:
  • 注意,当你把这两个变量设为 nil 时,没有任何一个析构函数被调用。循环强引用会一直阻止 PersonApartment 类实例的销毁,这就在你的应用程序中造成了内存泄漏。

四、解决实例之间的循环强引用

  • Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)无主引用(unowned reference)
  • 弱引用和无主引用允许循环引用中的一个实例引用而另外一个实例不保持强引用。这样实例能够互相引用而不产生循环强引用。
  • 弱引用不会对其引用的实例保持强引用,因而不会阻止 ARC 销毁被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。
  • 因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其赋值为 nil。并且因为弱引用可以允许它们的值在运行时被赋值为 nil,所以它们会被定义为可选类型变量,而不是常量。
  • 你可以像其他可选值一样,检查弱引用的值是否存在,你将永远不会访问已销毁的实例的引用。

当 ARC 设置弱引用为 nil 时,属性观察不会被触发。

  • 下面的例子跟上面 PersonApartment 的例子一致,但是有一个重要的区别。这一次,Apartmenttenant 属性被声明为弱引用:
  • 然后跟之前一样,建立两个变量(johnunit4A)之间的强引用,并关联两个实例:
  • Person 实例依然保持对 Apartment 实例的强引用,但是 Apartment 实例只持有对 Person 实例的弱引用。这意味着当你断开 john 变量所保持的强引用时,再也没有指向 Person 实例的强引用了:

  • 由于再也没有指向 Person 实例的强引用,该实例会被销毁:

  • 唯一剩下的指向 Apartment 实例的强引用来自于变量 unit4A。如果你断开这个强引用,再也没有指向 Apartment 实例的强引用了:
  • 和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用在其他实例有相同或者更长的生命周期时使用。你可以在声明属性或者变量时,在前面加上关键字 unowned 表示这是一个无主引用。

  • 无主引用通常都被期望拥有值。不过 ARC 无法在实例被销毁后将无主引用设为 nil,因为非可选类型的变量不允许被赋值为 nil

使用无主引用,你必须确保引用始终指向一个未销毁的实例。
如果你试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。

  • 下面的例子定义了两个类,CustomerCreditCard,模拟了银行客户和客户的信用卡。这两个类中,每一个都将另外一个类的实例作为自身的属性。这种关系可能会造成循环强引用。

  • 一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。为了表示这种关系,Customer 类有一个可选类型的 card 属性,但是 CreditCard 类有一个非可选类型的 customer 属性。

  • 由于信用卡总是关联着一个客户,因此将 customer 属性定义为无主引用,用以避免循环强引用:

  • 下面的代码片段定义了一个叫 john 的可选类型 Customer 变量,用来保存某个特定客户的引用。由于是可选类型,所以变量被初始化为 nil
3、无主引用和隐式解析可选属性
  • 上面弱引用和无主引用的例子涵盖了两种常用的需要打破循环强引用的场景。
  • PersonApartment 的例子展示了两个属性的值都允许为 nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。
  • CustomerCreditCard 的例子展示了一个属性的值允许为 nil,而另一个属性的值不允许为 nil,这也可能会产生循环强引用。这种场景最适合通过无主引用来解决。
  • 然而,存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为 nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。
  • 这使两个属性在初始化完成后能被直接访问(不需要可选展开),同时避免了循环引用。这一节将为你展示如何建立这种关系。
  • 下面的例子定义了两个类,CountryCity,每个类将另外一个类的实例保存为属性。在这个模型中,每个国家必须有首都,每个城市必须属于一个国家。为了实现这种关系,Country 类拥有一个 capitalCity 属性,而 City 类有一个
  • 为了建立两个类的依赖关系,City 的构造函数接受一个 Country 实例作为参数,并且将实例保存到 country 属性。

  • Country 的构造函数调用了 City 的构造函数。然而,只有 Country 的实例完全初始化后,Country 的构造函数才能把 self 传给 City 的构造函数。

  • 为了满足这种需求,通过在类型结尾处加上感叹号(City!)的方式,将 CountrycapitalCity 属性声明为隐式解析可选类型的属性。这意味着像其他可选类型一样,capitalCity 属性的默认值为 nil,但是不需要展开它的值就能访问它。

  • 由于 capitalCity 默认值为 nil,一旦 Country 的实例在构造函数中给 name 属性赋值后,整个初始化过程就完成了。这意味着一旦 name 属性被赋值后,Country 的构造函数就能引用并传递隐式的

  • 以上的意义在于你可以通过一条语句同时创建 CountryCity 的实例,而不产生循环强引用,并且 capitalCity 的属性能被直接访问,而不需要通过感叹号来展开它的可选值:

五、闭包引起的循环强引用

  • 循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时。这个闭包体中可能访问了实例的某个属性,例如 self.someProperty,或者闭包中调用了实例的某个方法,例如 self.someMethod()。这两种情况都导致了闭包“捕获”self,从而产生了循环强引用。
  • 循环强引用的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你是将这个闭包的引用赋值给了属性。实质上,这跟之前的问题是一样的——两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
  • 下面的例子为你展示了当一个闭包引用了 self 后是如何产生一个循环强引用的。例子中定义了一个叫 HTMLElement 的类,用一种简单的模型表示 HTML 文档中的一个单独的元素:
  • 实例的 asHTML 属性持有闭包的强引用。但是,闭包在其闭包体内使用了 self(引用了 self.nameself.text),因此闭包捕获了 self,这意味着闭包又反过来持有了 HTMLElement 实例的强引用。这样两个对象就产生了循环强引用。

虽然闭包多次使用了 self,它只捕获 HTMLElement 实例的一个强引用。

六、解决闭包引起的循环强引用

  • 在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。
  • 捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。
  • 跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。
  • 应当根据代码关系来决定使用弱引用还是无主引用。
  • 捕获列表中的每一项都由一对元素组成,一个元素是 weakunowned 关键字,另一个元素是类实例的引用(例如 self)或初始化过的变量(如 delegate = self.delegate!)。这些项在方括号中用逗号分开。

  • 如果闭包有参数列表和返回类型,把捕获列表放在它们前面:

// 这里是闭包的函数体
  • 如果闭包没有指明参数列表或者返回类型,它们会通过上下文推断,那么可以把捕获列表和关键字 in 放在闭包最开始的地方:
// 这里是闭包的函数体
  • 在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为 无主引用

  • 相反的,在被捕获的引用可能会变为 nil 时,将闭包内的捕获定义为 弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil。这使我们可以在闭包体内检查它们是否存在。

如果被捕获的引用绝对不会变为 nil,应该用无主引用,而不是弱引用。

  • 前面的 HTMLElement 例子中,无主引用是正确的解决循环强引用的方法。这样编写 HTMLElement 类来避免循环强引用:
  • 上面的 HTMLElement 实现和之前的实现一致,除了在 asHTML 闭包中多了一个捕获列表。这里,捕获列表是 [unowned self],表示“将 self 捕获为无主引用而不是强引用”。
  • 这一次,闭包以无主引用的形式捕获 self,并不会持有 HTMLElement 实例的强引用。
}

我要回帖

更多关于 计数变量 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信