365bet体育|www.635288com-365体育手机在线

热门关键词: 365bet体育,www.635288com,365体育手机在线

当对象的引用计数变为 0

2020-04-15 作者:365bet体育   |   浏览(177)

问题简单介绍 ARC 以及 ARC 实现的原理。考查点

我记得在刚接触iOS的时候对这个ARC和MRC就讨论颇深,认为ARC是对程序员的一种福利,让我们节省了大量的代码,那么ARC是什么呢?

ARC 是苹果在 WWDC 2011 提出来的技术,因此很多新入行的同学可能对此技术细节并不熟悉。但是,虽然 ARC 极大地简化了我们的内存管理工作,但是引用计数这种内存管理方案如果不被理解,那么就无法处理好那些棘手的循环引用问题。所以,这道面试题其实是考查同学对于 iOS 程序内存管理的理解深度。答案

自动的引用计数(Automatic Reference Count 简称 ARC),是苹果在 WWDC 2011 年大会上提出的用于内存管理的技术。

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 语言外,微软的 COM(Component Object Model )、C 11(C 11 提供了基于引用计数的智能指针 share_prt) 等语言也提供了基于引用计数的内存管理方式。

引用计数这种内存管理方式虽然简单,但是手工写大量的操作引用计数的代码不但繁琐,而且容易被遗漏。于是苹果在 2011 年引入了 ARC。ARC 顾名思义,是自动帮我们填写引用计数代码的一项功能。

ARC 的想法来源于苹果在早期设计 Xcode 的 Analyzer 的时候,发现编译器在编译时可以帮助大家发现很多内存管理中的问题。后来苹果就想,能不能干脆编译器在编译的时候,把内存管理的代码都自动补上,带着这种想法,苹果修改了一些内存管理代码的书写方式(例如引入了 @autoreleasepool 关键字)后,在 Xcode 中实现了这个想法。

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 1 的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

但是也有人认为,ARC 也附带有运行期的一些机制来使 ARC 能够更好的工作,他们主要是指 weak 关键字。weak 变量能够在引用计数为 0 时被自动设置成 nil,显然是有运行时逻辑在工作的。我通常并没有把这个算在 ARC 的概念当中,当然,这更多是一个概念或定义上的分歧,因为除开 weak 逻辑之外,ARC 核心的代码都是在编译期填充的。

高级解析

前言

本文的ARC特指Objective C的ARC,并不会讲解其他语言。另外,本文涉及到的原理部分较多,适合有一定经验的开发者。

什么是ARC?

ARC的全称Auto Reference Counting. 也就是自动引用计数。那么,为什么要有ARC呢?

我们从C语言开始。使用C语言编程的时候,如果要在堆上分配一块内存,代码如下

`//分配内存(malloc/calloc均可)``int * array = calloc(10, sizeof ;``//释放内存``free;1234512345`

C是面向过程的语言(Procedural programming),这种内存的管理方式简单直接。但是,对于面向对象编程,这种手动的分配释放毫无疑问会大大的增加代码的复杂度。

于是,OOP的语言引入了各种各样的内存管理方法,比如Java的垃圾回收和Objective C的引用计数。关于垃圾回收和饮用计数的对比,可以参见Brad Larson的这个SO回答。

Objective C的引用计数理解起来很容易,当一个对象被持有的时候计数加一,不再被持有的时候引用计数减一,当引用计数为零的时候,说明这个对象已经无用了,则将其释放。

引用计数分为两种:

  • 手动引用计数

  • 自动引用计数

iOS开发早期,编写代码是采用MRC的

`// MRC代码``NSObject * obj = [[NSObject alloc] init]; ``//引用计数为1``//不需要的时候``[obj release] ``//引用计数减1``//持有这个对象``[obj retain] ``//引用计数加1``//放到AutoReleasePool``[obj autorelease]``//在auto release pool释放的时候,引用计数减1`

虽说这种方式提供了面向对象的内存管理接口,但是开发者不得不花大量的时间在内存管理上,并且容易出现内存泄漏或者release一个已被释放的对象,导致crash。

再后来,Apple对iOS/Mac OS开发引入了ARC。使用ARC,开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码,再结合Objective C的runtime,实现自动引用计数。

比如如下ARC代码:

`NSObject * obj;``{``obj = [[NSObject alloc] init]; ``//引用计数为1``}``NSLog(@``"%@"``,obj);`

等同于如下MRC代码

`NSObject * obj;``{``obj = [[NSObject alloc] init]; ``//引用计数为1``[obj relrease]``}``NSLog(@``"%@"``,obj);`

在Objective C中,有三种类型是ARC适用的:

  • block

  • objective 对象,id, Class, NSError*等

  • attribute()标记的类型。

像double *,CFStringRef等不是ARC适用的,仍然需要手动管理内存。

Tips: 以CF开头的(Core Foundation)的对象往往需要手动管理内存。

属性所有权

最后,我们在看看ARC中常见的所有权关键字,

  • assign对应关键字__unsafe_unretained, 顾名思义,就是指向的对象被释放的时候,仍然指向之前的地址,容易引起野指针。

  • copy对应关键字__strong,只不过在赋值的时候,调用copy方法。

  • retain对应__strong

  • strong对应__strong

  • unsafe_unretained对应__unsafe_unretained

  • weak对应__weak。

其中,__weak和__strong是本文要讲解的核心内容。

ARC的内部实现

ARC背后的引用计数主要依赖于这三个方法:

  • retain 增加引用计数

  • release 降低引用计数,引用计数为0的时候,释放对象。

  • autorelease 在当前的auto release pool结束后,降低引用计数。

在Cocoa Touch中,NSObject协议中定义了这三个方法,由于Cocoa Touch中,绝大部分类都继承自NSObject(NSObject类本身实现了NSObject协议),所以可以“免费”获得NSObject提供的运行时和ARC管理方法,这就是为什么适用OC开发iOS的时候,你的类要继承自NSObject。

既然ARC是引用计数,那么对应一个对象,内存中必然会有一个地方来存储这个对象的引用计数。iOS的Runtime是开源的,在这里可以下载到全部的代码,我们通过源代码一探究竟。

我们从retain入手,

`- retain {``return` `->rootRetain();``}``inline id objc_object::rootRetain()``{``if` `(isTaggedPointer ``return` ```this``;``return` `sidetable_retain();``}`

所以说,本质上retain就是调用sidetable_retain,再看看sitetable_retain的实现:

`id objc_object::sidetable_retain()``{``//获取table``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` ```this``;``}`

到这里,retain如何实现就很清楚了,通过SideTable这个数据结构来存储引用计数。我们看看这个数据结构的实现:

图片 1QQ截图20170421165138.png

可以看到,这个数据结构就是存储了一个自旋锁,一个引用计数map。这个引用计数的map以对象的地址作为key,引用计数作为value。到这里,引用计数的底层实现我们就很清楚了。

存在全局的map,这个map以地址作为key,引用计数的值作为value。

再来看看release的实现:

`SideTable& table = SideTables()[``this``];``bool do_dealloc = ``false``;``table.lock();``//找到对应地址的``RefcountMap::iterator it = table.refcnts.find;``if` `(it == table.refcnts.end { ``//找不到的话,执行dellloc``do_dealloc = ``true``;``table.refcnts[``this``] = SIDE_TABLE_DEALLOCATING;``} ``else` `if` `(it->second < SIDE_TABLE_DEALLOCATING) {``//引用计数小于阈值,dealloc``do_dealloc = ``true``;``it->second |= SIDE_TABLE_DEALLOCATING;``} ``else` `if` `(! (it->second & SIDE_TABLE_RC_PINNED)) {``//引用计数减去1``it->second -= SIDE_TABLE_RC_ONE;``}``table.unlock();``if` `(do_dealloc && performDealloc) {``//执行dealloc``(objc_object *, SEL))objc_msgSend)(``this``, SEL_dealloc);``}``return` `do_dealloc;`

release的到这里也比较清楚了:查找map,对引用计数减1,如果引用计数小于阈值,则调用SEL_dealloc

Autorelease pool

上文提到了,autorelease方法的作用是把对象放到autorelease pool中,到pool drain的时候,会释放池中的对象。举个例子

`__weak NSObject * obj;``NSObject * temp = [[NSObject alloc] init];``obj = temp;``NSLog(@``"%@"``,obj); ``//非空` |放到auto release pool中,`__weak NSObject * obj;``@autoreleasepool {``NSObject * temp = [[NSObject alloc] init];``obj = temp;``}``NSLog(@``"%@"``,obj); ``//null`

可以看到,放到自动释放池的对象是在超出自动释放池作用域后立即释放的。事实上在iOS 程序启动之后,主线程会启动一个Runloop,这个Runloop在每一次循环是被自动释放池包裹的,在合适的时候对池子进行清空。

对于Cocoa框架来说,提供了两种方式来把对象显式的放入AutoReleasePool.

  • NSAutoreleasePool(只能在MRC下使用)

  • @autoreleasepool {}代码块(ARC和MRC下均可以使用)

那么AutoRelease pool又是如何实现的呢?

我们先从autorelease方法源码入手

`//autorelease方法``- autorelease {``return` `->rootAutorelease();``}``//rootAutorelease 方法``inline id objc_object::rootAutorelease()``{``if` `(isTaggedPointer ``return` ```this``;``//检查是否可以优化``if` `(prepareOptimizedReturn(ReturnAtPlus1)) ``return` ```this``;``//放到auto release pool中。``return` `rootAutorelease2();``}``// rootAutorelease2``id objc_object::rootAutorelease2()``{``assert(!isTaggedPointer;``return` `AutoreleasePoolPage::autorelease``this``);``}`

可以看到,把一个对象放到auto release pool中,是调用了AutoreleasePoolPage::autorelease这个方法。

我们继续查看对应的实现:

`public: static inline id autorelease``{``assert;``assert(!obj->isTaggedPointer;``id *dest __unused = autoreleaseFast;``assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);``return` `obj;``}``static inline id *autoreleaseFast``{``AutoreleasePoolPage *page = hotPage();``if` `(page && !page->full {``return` `page->add;``} ``else` `if` ` {``return` `autoreleaseFullPage(obj, page);``} ``else` `{``return` `autoreleaseNoPage;``}``}``id *add``{``assert;``unprotect();``id *ret = next; ``// faster than `return next-1` because of aliasing``*next   = obj;``protect();``return` `ret;``}`

到这里,autorelease方法的实现就比较清楚了,

autorelease方法会把对象存储到AutoreleasePoolPage的链表里。等到auto release pool被释放的时候,把链表内存储的对象删除。所以,AutoreleasePoolPage就是自动释放池的内部实现。

__weak与__strong

用过block的同学一定写过类似的代码:

`__weak typeSelf weakSelf = self;``[object fetchSomeFromRemote:^{``__strong typeSelf strongSelf = weakSelf;``//从这里开始用strongSelf``}];`

那么,为什么要这么用呢?原因是:

block会捕获外部变量,用weakSelf保证self不会被block被捕获,防止引起循环引用或者不必要的额外生命周期。

用strongSelf则保证在block的执行过程中,对象不会被释放掉。

首先__strong和__weak都是关键字,是给编译器理解的。为了理解其原理,我们需要查看它们编译后的代码,使用XCode,我们可以容易的获得一个文件的汇编代码。

比如,对于Test.m文件,当源代码如下时:

`#import "Test.h"``@implementation Test``- testFunction{``{``__strong NSObject * temp = [[NSObject alloc] init];``}``}``@end`

转换后的汇编代码如下:

`Ltmp3:``.loc 2 15 37 prologue_end ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:37``ldr x9, [x9]``ldr x1, [x8]``mov x0, x9``bl _objc_msgSend``adrp x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE``add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF``.loc 2 15 36 is_stmt 0 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36``ldr x1, [x8]``.loc 2 15 36 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36``bl _objc_msgSend``mov x8, ``#0``add x9, sp, ``#8 ; =8``.loc 2 15 29 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:29``str x0, [sp, ``#8]``Ltmp4:``.loc 2 16 5 is_stmt 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5``mov x0, x9``mov x1, x8``bl _objc_storeStrong``.loc 2 17 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1``ldp x29, x30, [sp, ``#32] ; 8-byte Folded Reload``add sp, sp, ``#48 ; =48``ret``Ltmp5:`

即使你不懂汇编,也能很轻易的获取到调用顺序如下

`_objc_msgSend ``// alloc``_objc_msgSend ``// init``_objc_storeStrong ``// 强引用`

在结合Runtime的源码,我们看看最关键的objc_storeStrong的实现

`void objc_storeStrong(id *location, id obj)``{``id prev = *location;``if` `(obj == prev) {``return``;``}``objc_retain;``*location = obj;``objc_release;``}``id objc_retain { ``return` `[obj retain]; }``void objc_release { [obj release]; }`

我们再来看看__weak. 将Test.m修改成为如下代码,同样我们分析其汇编实现

`.loc 2 15 35 prologue_end ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:35``ldr x9, [x9]``ldr x1, [x8]``mov x0, x9``bl _objc_msgSend``adrp x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE``add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF``.loc 2 15 34 is_stmt 0 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34``ldr x1, [x8]``.loc 2 15 34 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34``bl _objc_msgSend``add x8, sp, ``#24 ; =24``.loc 2 15 27 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27``mov x1, x0``.loc 2 15 27 discriminator 2 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27``str x0, [sp, ``#16] ; 8-byte Folded Spill``mov x0, x8``bl _objc_initWeak``.loc 2 15 27 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27``ldr x1, [sp, ``#16] ; 8-byte Folded Reload``.loc 2 15 27 discriminator 3 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27``str x0, [sp, ``#8] ; 8-byte Folded Spill``mov x0, x1``bl _objc_release``add x8, sp, ``#24 ``Ltmp4:``.loc 2 16 5 is_stmt 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5``mov x0, x8``bl _objc_destroyWeak``.loc 2 17 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1``ldp x29, x30, [sp, ``#48] ; 8-byte Folded Reload``add sp, sp, ``#64 ; =64``ret`

可以看到,__weak本身实现的核心就是以下两个方法

  • _objc_initWeak

  • _objc_destroyWeak

我们通过Runtime的源码分析这两个方法的实现:

<false></false>

`id objc_initWeak(id *location, id newObj)``{``//省略....``return` `storeWeak (location, (objc_object*)newObj);``}``void objc_destroyWeak(id *location)``{``storeWeak (location, nil);``}`

所以,本质上都是调用了storeWeak函数,这个函数内容较多,主要做了以下事情

  • 获取存储weak对象的map,这个map的key是对象的地址,value是weak引用的地址。

  • 当对象被释放的时候,根据对象的地址可以找到对应的weak引用的地址,将其置为nil即可。

这就是在weak背后的黑魔法。

总结

这篇文章属于想到哪里写到哪里的类型,后边有时间了在继续总结ARC的东西吧。

本文由365bet体育发布于365bet体育,转载请注明出处:当对象的引用计数变为 0

关键词: bet356体育 iOS ARC