ARC `基础` & `原理` 总结

本文章属于原创,转载请注明出处

参考资料
sunnyxxx
深入理解Objective C的ARC机制
Objective-C引用计数原理
基础参考 iOS高级编程

基础

内存管理的思考方式
  • 自己生成的对象自己持有
  • 非自己生成的对象自己也能持有
  • 不需要自己持有的对象释放
  • 非自己持有的对象无法释放
内存管理语义
  • 以下方法表示自己生成的对象自己持有
  1. alloc/new/copy/mutableCopy/retain
  • 非自己生成的对象可以使用retain持有
// 生成非自己持有对象
id obj = [NSMutableArray array];
// 自己持有对象
[obj retain];
// 不需要时释放
[obj release];
// autorealease 使对象超出指定范围并可以正确释放
- (id)object {
	id obj = [[NSObject alloc] init];
	[objc autorelease];
	return obj;
}	
__strong & __weak & __unsafe_unretained

引用循环容易发生内存泄露,内存泄漏是指应当废弃的对象在超过其生命周期后继续存在

  1. __strong
  • 可以使用__strong持有非自己生成并持有的对象
  • __strong完全遵循ARC有效时的内存管理语义,使用__strong,__weak,__autoreleasing修饰符不赋值时对象的值为nil
  1. __weak
  • 两个__strong的对象互相引用对方会造成引用循环并发生内存泄露
  • 使用__weak给其中一个对象赋值可以避免引用循环,废弃对象的同时__weak赋值的对象的值变成nil
  • 不能使用__weak直接初始化对象,初始化的一瞬间,会变为nil
id __weak obj = [[NSObject alloc]init];
// obj = nil
  1. unsafe_unretained
  • unsafed_unretained是不安全的,ARC有效时内存管理是编译器的工作,被此修饰符修饰的变量的内存管理不属于编译器
  • 被修饰对象即不持有强引用也不持有若引用,使用unsafed_unretained时要保证赋值给__strong的对象的确存在,不然程序会崩溃
id __unsafe_unretained obj = [[NSObject alloc]init];
// 即使有 unsafed 标示,编译器还是会警告,这里的值瞬间变为 nil
  1. __autoreleasing
  • 在ARC有效的情况下,使用@autoreleasePool代替NSAutoreleasePool类,__autoreleasing修饰符,代替autorelease方法
  • 很少显示的声明__autoreleasing,非自己持有的对象自动声明为__autoreleasing,在作为返回值是自动声明为__autoreleasing
  • 被声明为__weak的变量自动被声明为__autoreleasing,将其注册到autoreleasePool里,在@autoreleasePool的块结束前,都能确保对象存在
id __weak obj = obj1;
id __autoreleasing tmp = obj;
  • 最后一个显示声明__autoreleasing的例子,id指针或对象指针在没有被显示声明的时候声明为__autoreleasing,主要一个用处是*error传递NSError对象的时候
id *obj = [[NSObject alloc]init];
id __autoreleasing *obj = [[NSObject alloc]init];
ARC规则

ARC有效时

  1. 不能使用retain/release/retainCount/autorelease/new
  2. 不能使用NSAllocateObject/NSDeallocateObject
  3. 必须遵循内存管理方法命名规则
  4. 不显示使用dealloc
  5. 使用@autoreleasePool代替NSAutoreleasePool
  6. 不使用区域NSZone
  7. 对象型变量不能作为C语言结构体变量
  8. 显示转换id和void*在ARC下会报错
ARC有效时的属性

属性的内存管理语义对应的所有权修饰符

  • assign == __unsafe_unreatined
  • copy == _strong 但赋值被复制的对象
  • retain == __strong
  • strong == __strong
  • unsafe_unretained == __unsafe_unretained
  • weak == __weak

原理

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

  • block
  • Objective-C对象,如id,Class,NSError *等
  • 以attribute作为标记的

CF开头的都是适用用ARC的

引用计数的存储方式

主要有三种:

  1. TaggedPointer:将其指针值作为引用计数返回
  2. isa:有的对象会将isa指针的一部分用于存储引用计数
  3. Sidetable:使用散列表来管理引用计数
TaggedPoint

判断标志位是否为一,确定对象有没有使用TaggedPoint,id的本质就是objc_object结构体的指针类型,可以使用id的方法isTaggedPoint()方法来判读其是否使用TaggedPoint来存储引用计数

#if SUPPORT_MSB_TAGGED_POINTERS
#   define TAG_MASK (1ULL<<63)
#else
#   define TAG_MASK 1

inline bool 
objc_object::isTaggedPointer() 
{
#if SUPPORT_TAGGED_POINTERS
    return ((uintptr_t)this & TAG_MASK);
#else
    return false;
#endif
}
isa

在64位环境并且是Objective-C 2.0的情况下,有些对象会使用isa的一部分作为引用计数存储

因为64位环境存储一个指针地址很浪费,所以作为优化,使用isa指针的一部分来存储,

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x0000000000000001ULL
#   define ISA_MAGIC_VALUE 0x0000000000000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 14;
#       define RC_ONE   (1ULL<<50)
#       define RC_HALF  (1ULL<<13)
    };

# else
    // Available bits in isa field are architecture-specific.
#   error unknown architecture
# endif

// SUPPORT_NONPOINTER_ISA
#endif

};

SUPPORT_NONPOINTER_ISA用来标记是否使用isa指针存储引用计数

#if !__LP64__  ||  TARGET_OS_WIN32  ||  TARGET_IPHONE_SIMULATOR  ||  __x86_64__
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif

isa指针中一些变量的定义如下

  • indexed:0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
  • has_assoc:表示该对象是否包含 associated object,如果没有,则析构时会更快
  • shiftcls:类的指针
  • weakly_referenced:表示该对象是否有过 weak 对象,如果没有,则析构时更快
  • has_sidetable_rc:表示该对象的引用计数值是否过大无法存储在 isa 指针
  • extra_rc:存储引用计数值减一后的结果

isa指针不一定会存储引用计数,has_sideable_rc中,如果为1,就是用Sidetable来存储

散列表 sidetable

sidetable里存储的引用计数,总是为真正的引用计数减1。使用sidetable来存储引用计数,当调试时,即使对象内存地址损坏,也能使用散列表来找到损坏的对象的地址

sidetable用于管理引用计数表和weak表,使用 spinlock_t slock来保证可能发生的竞态条件

spinlock_t slock;//保证原子操作的自选锁
RefcountMap refcnts;//保存引用计数的散列表
weak_table_t weak_table;//保存 weak 引用的全局散列表

weak表的作用是在对象全部dealloc的时候,将weak的对象都设置为nil,避免悬挂指针
weak表是一个全局的表,其中对象为键,weak_entry_t 为值,weak_entry_t 中保存了所有指向该对象的weak指针

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
获取引用计数
  • taggedPoint可以直接获取引用计数的值,isa指针使用其中一个变量保存
  • sidetbale:使用sidetable_retainCount()方法,其实现是获取当前sidetable对象的实例,其refcnts属性是存储引用计数的散列表,使用迭代器匹配到当前实例的键值对,返回其 值 + 1
retain方法
- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

可以看出retain先判断是否支持isTaggedPoinnter,然后调用sidetable_retain()方法操作sidetable的refcnts属性,将引用计数加1,
再看看sidetable的实现

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 (id)this;
}
release

sidetable_release()返回是否执行dealloc方法

bool 
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable *table = SideTable::tableForPointer(this);

    bool do_dealloc = false;

    if (spinlock_trylock(&table->slock)) {
        RefcountMap::iterator it = table->refcnts.find(this);
        if (it == table->refcnts.end()) {
            do_dealloc = true;
            table->refcnts[this] = SIDE_TABLE_DEALLOCATING;
        } else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
            do_dealloc = true;
            it->second |= SIDE_TABLE_DEALLOCATING;
        } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
            it->second -= SIDE_TABLE_RC_ONE;
        }
        spinlock_unlock(&table->slock);
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

在执行release方法前,存储引用计数会先预留一个1,这也是为什么总返回真正的引用计数减1,因为release方法总会将引用计数减1,在减1之前先看存储引用值是否为0,当为0时,直接进行dealloc操作,否则就减1,这样避免了产生负数

Autorelease

autorelease对象什么时候释放
  • 在手动加入autoreleasePool的情况下,ARC有效时,在@autoreleasePool的大括号结束前释放对象,MRC下,调用[pool drain]时释放对象
  • 在没有手动加入autoreleasePool的情况下,Autorelease对象是在当前Runloop迭代结束时释放的,能释放的正确的原因是,系统在每个runloop迭代中,都加入自动释放池的push和pop
小实验
__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = [NSString stringWithFormat:@"xxx"];
    // str是一个autorelease对象,设置一个weak的引用来观察它
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%@", reference); 
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%@", reference); 
}

由于viewController在loadview之后便加到了windom视图的层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个对象依然有值

Autorelease原理
  • autoreleasePoolPage
    在ARC下,使用@autoreleasePool {} 来使用一个AutoreleasePool,随后编译器将其改写为
void *context = objc_autoreleasePoolPush();
// 括号里的代码
objc_autoreleasePoolPop(context);

这两个函数都是对autoreleasePoolPage的封装,因此关键在这个类

// AutoreleasePoolPage
{
	magic_t const magic;
	id *next;
	phread_t const thread;
	AutoreleasePoolPage *const parent;
	AutoreleasePoolPage *child;
	uint32_t const depth;
	uint32_t hiwait;
}
  • autoreleasePool并没有单独的数据结构,而其是有若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应上面的child和parent指针
  • autoreleasePool是按线程一一对应的
  • autoreleasePoolPage会为对象申请4096的内存空间(一页的大小),除了上面实例变量所占的空间,剩下的空间全部用来存储autorelease对象的地址
  • id *next指针作为标记新加进来(add)的autorelease对象的下一个
  • 一个AutoreleasePoolPage的空间被占满,会创建一个新的AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的autoreleasePoolPage中加入
// end 栈顶
// <-- next指针指向
// id objN 最新的autorelease对象的位置
// ...
// id obj2 
// id obj1(begin)栈底
// 类实例所占内存

如上,当这一页再加入一个autorelease对象就要满了的时候(next指针马上指向栈顶),这时执行上面的操作,新建下一页Page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底,然后继续向新的栈顶添加对象

所以向一个对象发送autorelease消息就相当于把对象加到autoreleasePoolPage栈顶的next指针指向的位置

释放时刻
// AutoreleasePoolPage
// (end) 栈顶
// <-- next指针指向
// id objN 最新的autorelease对象的地址
// ...
// obj3 
// 0 哨兵位置
// id obj2
// id obj1 (begin)栈底
// 类实例所占内存

每当进行AutoreleasePoolPush调用时,runtime就向当前的AutoreleasePoolPage中add一个哨兵对象,值为0(nil),Page就变成上面的样子

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是

  • 根据传入的哨兵对象找到哨兵对象所处的page
  • 在当前page中,在晚于哨兵对象插入的所有autorelease对象都发送一次release消息,并移动next指针到正确位置
  • 从最新加入的对象一直向前清理,可以跨越若干个page,直到哨兵所处的page
    刚才的objc_autoreleasePoolPop执行后,变成如下
// (end)栈顶
// ... 
// <-- next指针指向
// id obj2
// id obj1 (栈底)
// 

嵌套的autoreleasrPool

  • pop的时候总是释放到上次的push为止,多层pool是多个哨兵对象,释放时每次一层,互补影响
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章