iOS内存管理|所有权修饰符

所有权修饰符

Object-C中为了处理对象,可将变量类型定义为id类型或各种对象类型。

所谓对象类型就是指向NSObject这样的OC类的指针,例如NSObject *id类型用于隐藏对象类型的类名部分,相当于C语言的void *

ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符,所有权修饰符一共有4种:

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing
__strong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符,也就是说,下面两行代码是相同的:

id obj = [[NSObject alloc] init];
id __strong obj = [[NSObject alloc] init];

另外,__strong修饰符同后面的__weak__autoreleasing修饰符一起,可以保证将附有这些修饰符的自动变量初始化为nil,下面两部分代码是相同的:

id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;
id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;
__weak修饰符

__weak修饰符的存在更多的是为了处理内存管理中必然会发生的循环引用的问题。

循环引用容易引起内存泄露,所谓内存泄露就是应当废弃的对象在超出其生存周期后继续存在。

__weak修饰符还有另外一个优点,在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态(空弱引用)。

除了以上这些,关于__weak修饰符的内存管理也是我们目前所需要掌握的。

当我们使用__weak对一个对象进行弱引用时,这个weak变量是怎样被添加到弱引用表当中的呢?
Person *p = [Person new];
__weak Person *p1 = p;

实际上经过runtime处理后,上面的代码会转换成如下形式:

Person *p = [Person new];
Person *p1;
objc_initWeak(&p1, p);
objc_destroyWeak(&p1);

通过objc_initWeak函数初始化附有__weak修饰符的变量,在变量作用域结束时通过objc_destroyWeak函数释放该变量。

objc源码中对objc_initWeak是这样描述的:

/** 
 * Initialize a fresh weak pointer to some object location. 
 * It would be used for code like: 
 *
 * (The nil case) 
 * __weak id weakPtr;
 * (The non-nil case) 
 * NSObject *o = ...;
 * __weak id weakPtr = o;
 * 
 * This function IS NOT thread-safe with respect to concurrent 
 * modifications to the weak variable. (Concurrent weak clear is safe.)
 *
 * @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

通过源码可以看出,objc_initWeak内部实际上又调用了storeWeak函数,storeWeak函数把第二个参数的赋值对象的地址作为键值,将第一个参数的附有__weak修饰符的变量的地址注册到weak表中,如果第二个参数是0,则会把变量的地址从weak表中删除。

再来看一下storeWeak函数的实现:

static id 
storeWeak(id *location, objc_object *newObj)
{
    //有新对象,没有老对象
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;    //只需关注newTable

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        //weak_table:取sidetable的weak_table
        //newObj:被弱引用指向的原对象
        //location:弱引用指针
        //crashIfDeallocating:对象在废弃的过程是否crash
        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.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

看起来代码特别多,但其中关键的是又调用了weak_register_no_lock函数:

//referent_id是原来的对象
//referrer_id是弱引用指针
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;

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }

    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    weak_entry_t *entry;
    //weak_table:弱引用表
    //referent:原对象
    //通过原对象指针到弱引用表中查找弱引用的数组entry,看下面的weak_entry_for_referent实现
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //把新的弱引用指针添加到数组中
        append_referrer(entry, referrer);
    } 
    else {
        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_entry_for_referent实现:

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    //通过弱引用表获取弱引用结构体数组
    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    //通过原对象的指针地址做一次hash计算,获取到在弱引用表中的索引位置
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    //hash冲突算法
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    //通过查找的索引位置返回给调用方原对象对应的弱引用数组
    return &weak_table->weak_entries[index];
}

通过以上源码可以看出,系统是在weak_register_no_lock函数中进行一个弱引用变量的添加,具体添加的位置是通过hash算法来计算位置查找的,如果查获到对应位置中已经有了当前对象所对应的弱引用数组,就会把新的弱引用变量添加到数组当中;如果没有,就重新创建一个弱引用数组,把第0个位置添加上最新的弱引用指针,后面的都初始化为nil

当一个对象被释放或废弃后,weak变量是怎样处理的呢?

当一个对象被释放或废弃后,runtime会调用该对象的dealloc方法,在dealloc方法中还会调用一系列的方法,其中就包括weak_clear_no_lock函数,weak_clear_no_lock函数的实现如下:

//参数1:弱引用表
//参数2:dealloc的对象
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //创建一个局部变量
    objc_object *referent = (objc_object *)referent_id;

    //在弱引用表中根据局部变量找到弱引用数组
    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;
    }
    
    //遍历弱应用指针数组
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            //如果弱引用指针存在,并且弱引用指针的地址就是当前被废弃对象的地址,就弱引用指针置为nil
            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_entry_remove(weak_table, entry);
}

通过这部分源码我们可以知道,当一个对象被调用dealloc后,在dealloc内部实现当中会去调用清除弱引用指针的相关函数weak_clear_no_lock,然后在函数内部实现当中根据当前对象指针查找弱引用表,把当前对象相对应的弱引用指针都拿出来,这些弱引用指针都放在了一个数组里,然后遍历这个数组中的所有弱引用指针,分别置为nil

__unsafe_unretained修饰符

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符,尽管ARC式的内存管理是编译器的工作,但附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,这一点需要注意。

id __unsafe_unretained obj = [[NSObject alloc] init];

该源代码将自己生成并持有的对象赋值给附有__unsafe_unretained修饰符的变量中,虽然使用了unsafe的变量,但编译器并不会忽略,而是给出适当的警告,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放,到这里,__unsafe_unretained修饰符和__weak修饰符是一样的。那在什么情况下是不一样的呢?

id __unsafe_unretained obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    obj1 = obj0;
    NSLog(@"A:%@", obj1);
 }
NSLog(@"B:%@", obj1);

输出:

A:<NSObject: 0x753e180>
B:<NSObject: 0x753e180>

B只是碰巧正常而已,虽然访问了已经被废弃的对象,但应用程序在个别运行状况下才会崩溃。
在使用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在,因为__unsafe_unretained会引发悬垂指针。

__autoreleasing修饰符

ARC有效时,要通过将对象赋值给附加了__autoreleasing修饰符的变量来替代调用autorelease方法,对象赋值给附有__autoreleasing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,将对象注册到autoreleasepool中。

也就是说可以理解为,在ARC有效时,用@autoreleasepool块替代NSAutoreleasepool类,用附有__autoreleasing修饰符的变量替代autorelease方法。

编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool

下面来看一种特别的注册形式,代码如下:

+ (id)array {
    return [[NSMutableArray alloc] init];
}

该代码没有使用__autoreleaseing修饰符,可写成如下形式:

+ (id)array {
    id obj = [[NSMutableArray alloc] init];
    return obj;
}

因为没有显式指定所有权修饰符,所以id obj同附有__strong修饰符的id __strong obj是完全一样的,由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器会自动将其注册到autoreleasepool

以下是使用__weak修饰符的例子,虽然__weak修饰符是为了避免循环引用而使用的,但在访问__weak变量时,实际上必定要访问注册到autoreleasepool的对象。

下面两段代码是相同的:

id __weak obj1 = obj0;
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;

为什么在访问附有__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢?
这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象可能被废弃,如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在,因此,在使用附有__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象。

最后一个问题,是非现式使用__autoreleasing修饰符。同前面说的id objid __strong obj完全一样。那么id的指针id *obj又是怎样的呢?可以由id *obj推出id __strong *obj吗?其实,推出来的是id __autoreleasing *obj。同样,对象的指针NSObject **obj便成为NSObject * __autoreleasing *obj

也就是说,id的指针或对象的指针在没有显式指定时会被附加上__autoreleasing修饰符。

举个例子,为了得到详细的错误信息,经常会在方法的参数中传递NSError对象的指针,而不是函数返回值。Cocoa框架中,大多数方法也是用这种方式,如NSStringstringWithContentOfFile:encoding:error:类方法等,如下所示:

NSError *error = nil;
Bool result = [obj performOperationWithError:&error];
- (BOOL) performOperationWithError:(NSError **)error;

同刚才说的一样,id的指针或对象的指针会默认附加上__autoreleasing修饰符,所以上面的代码等同于下面这段代码:

- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error;

参数中持有NSError对象指针的方法,虽然为响应其执行结果,需要生成NSError类对象,但也必须符合内存管理的思考方式。
作为alloc/new/copy/mutableCopy方法返回值取得的对象是自己生成并持有的,其他情况下是取得非自己生成并持有的对象,因此,使用附有__autoreleasing修饰符的变量作为对象取得参数,与除alloc/new/copy/mutableCopy外其他方法的返回值取得对象完全一样,都会注册到autoreleasepool,并取得非自己生成并持有的对象。
比如performOperationWithError方法的源码应该是这样的:

- (BOOL) performOperationWithError:(NSError **)error {
    *error = [[NSError alloc] initWith...];
    return NO;
}

因为声明为NSError * __autoreleasing *类型的error作为*error被赋值,所以能够返回注册到autoreleasepool中的对象。

但是,下面的代码就会产生编译错误:

NSError *error = nil;
NSError **pError = &error;

原因是:赋值给对象指针时,所有权修饰符必须一致
此时,对象指针必须附加__strong修饰符:

NSError *error = nil;
NSError * __strong *pError = &error;

但回过头看前面的方法参数中使用了附有__autoreleasing修饰符的对象指针类型:

- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error;

然后调用方却使用了__strong修饰符的对象指针类型:

NSError *error = nil;
Bool result = [obj performOperationWithError:&error];

对象指针型赋值时,其所有权修饰符必须一致,但为什么这里没有警告就顺利通过编译了呢?实际上,编译器自动将该部分代码转化成了下面形式:

NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
Bool result = [obj performOperationWithError:&tmp];
error = tmp

完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容