iOS开发攻城狮的集散地iOS 面试题iOS面试知识点

20·iOS 面试题·请解释以下 keywords 的区别: a

2018-11-02  本文已影响5人  pengxuyuan

前言

关于 keywords 的区别,这里主要涉及到引用计数相关知识,对于 ARC 相关的介绍可以参考上一篇面试题:19·iOS 面试题·什么是 ARC ?(ARC 是为了解决什么问题诞生的?)

这篇文章我们简单了解下引用计数的实现机制,再分别对比这个几个 keywords 的区别,最后再说明 __block 和 __weak 的应用场景。

引用计数机制

在 Objective-C 中,使用引用计数机制来实现内存管理:每个对象都有与之对应的引用计数值,所以在底层中要维护对象的引用计数值。在 MRC 环境下,是通过调用 retainreleaseautorelease 这些方法来控制对象的引用计数,而在 ARC 环境下,则是由编译器自动给我们添加这些方法。现在我们来看下这些方法的底层源码,以便更加深层次的了解引用计数机制。(以下代码摘抄自:iOS内存管理机制分析 )

Retain 方法

- (id)retain {
    return ((id)self)->rootRetain();
}

inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    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;
}

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    // 省略...
};

从上面源码可以知道 retain 方法的调用过程:retain -> rootRetain -> sidetable_retain,具体操作是在 sidetable_retain 方法中。sidetable_retain 主要是操作 SideTable 中的 RefcountMap(对象引用计数的 Map,这个引用计数的 Map 以对象的地址作为 Key,引用计数值作为 Value)。

到这里,我们可以知道内存中有一张表维护对象的引用计数值。

Release 操作

- (oneway void)release {
    ((id)self)->rootRelease();
}

inline bool objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}
uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock(); // 加锁
    RefcountMap::iterator it = table.refcnts.find(this); // 先找到对象的地址
    if (it == table.refcnts.end()) {
        do_dealloc = true; //引用计数小于阈值,最后执行dealloc
        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; //引用计数减去1
    }
    table.unlock(); // 解锁
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

release 操作也是一样的,在 sidetable_release 中,通过操作 SideTable 的 RefcountMap 来将对象的引用计数值 -1,当对象的引用计数值小于阈值则调用 dealloc 方法销毁对象。

strong vs copy

首先 strong 和 copy 都会增加对象的引用计数(强引用),这里需要注意的是用 copy 修饰的变量,必须遵循 NSCopying 协议,不然在赋值的时候会 Crash。这里把 strong 跟 copy 放到一起进行比较,主要想考察在修饰变量的时候如何选择正确的修饰符。

在平时开发的时候,对于 NSString、NSArray、NSDictionary 类型的对象经常使用 copy 来修饰,这里主要是为了避免这种情况:将可变类型对象赋值给不可变类型对象之后,之后再去修改可变类型对象,会导致不可变类型对象也遭到修改。但是,我们往往用不可变类型修饰对象,都是希望不会被修改,这里用 copy 就不会出现这个问题。

对于更加详尽的解释:iOS 声明属性时,到底用 strong 还是用 copy,二者有何区别?,这里就不重复描述了。

PS:但是如果你能确定修改不会出现问题,对于这些对象也可以用 strong 来修饰。

assign vs weak

assign 和 weak 都是不会增加对象的引用计数(弱引用),它们之间的区别是:当对象被销毁时,weak 修饰的变量指针会置为 nil,但是 assign 修饰的变量还是会指向原来的地址(这里就会出现野指针)。

在我们平时开发一般都是用 assign 来修饰基本类型,weak 来修饰对象。

weak 底层实现

首先用 weak 是弱引用,不会增加对象的引用计数,并且在对象释放的时候,weak 修饰的指针会置为 nil,这样子就可以防止野指针。

概括 weak 底层实现:有一张 weak hash 表,维护了指向对象的 weak 指针。对象的地址作为 Key,Value 则是指向该对象的 weak 指针数组(因为可能会出现多个 weak 指针指向同一个对象)。这里看下相关的结构体:

struct SideTable {
// 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table
}
//RefcountMap 这个是引用计数值的表
//weak_table_t 这个是弱引用的表

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtrobjc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1;
            uintptr_t        num_refs : PTR_MINUS_1;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
 }

所以 weak 底层也是通过操作 weak_table_t 表来实现相关功能功能,核心步骤如下,具体底层代码可以参阅文末链接:

1、初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。

2、添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

__block vs __weak

先来说一下 __weak,通过 __weak 来增加一个弱引用,这里常用来打破循环引用。

对于 __block,主要是为了解决 block 中匿名函数截获变量,产生的生命周期的问题。block 截获外部变量,默认是不可以取修改变量的,但是通过 __block 修饰的变量,在 block 内部可以修改,对于 block 更加详细的介绍以及 __ block 底层的实现可以参阅: 04·iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?

__block MRC 和 ARC 的区别

__block 修饰在变量在 MRC 环境和 ARC 环境也是有区别的:

这里我们可以得知,在 MRC 环境下,可以通过 __block 来打破循环引用,在 ARC 环境下,则需要用 __weak 来打破循环引用。

在 MRC 环境下,block 内部截获 __block 修饰的变量,为什么不会增加对象的引用计数?

将 block 从栈区拷贝到堆区,需要调用 block 的 copy 方法,copy 方法实际上调用的是 Block_object_assign 方法,我们这里看一下底层实现:(这里主要是通过 block 的 flag 来确定是否增加对象的引用计数。)

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    
    const void **dest = (const void **)destArg;
    switch ((flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);  //object 引用计数+1
        *dest = object;  //赋值
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

            /// 在mrc的情况下,你对对象添加__block, block是不会对这个对象引用计数+1
        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}

以上。

参考文献

iOS内存管理机制分析

04·iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?

19·iOS 面试题·什么是 ARC ?(ARC 是为了解决什么问题诞生的?)

iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

【iOS】weak的底层实现

weak 弱引用的实现方式

上一篇 下一篇

猜你喜欢

热点阅读