美文网首页iOS开发攻城狮的集散地iOS 面试题iOS面试知识点
20·iOS 面试题·请解释以下 keywords 的区别: a

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

作者: pengxuyuan | 来源:发表于2018-11-02 16:45 被阅读5次

    前言

    关于 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 截获外部用 __block 修饰的变量,不会增加对象的引用计数
    • ARC 环境下,block 截获外部用 __block 修饰的变量,会增加对象的引用计数

    这里我们可以得知,在 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 弱引用的实现方式

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/ucucxqtx.html