【iOS】探索ARC的实现

ARC

  • ARC在编译期和运行期做了什么?
    • 编译期:
    • 运行期:
    • block 是如何在 ARC 中工作的?
  • ARC的实现分析
    • __strong
    • 自己生成并持有
    • storeStrong
    • SideTable散列表
    • objc_retain
    • objc_release
    • sidetable_release
    • retainCount
    • 非自己生成并持有

ARC在编译期和运行期做了什么?

ARC (Automatic Reference Counting)是Objective-C在iOS 5.0之后提供的一种自动内存管理机制。它帮助开发者管理应用程序的内存使用,减少了因为忘记释放内存导致的内存泄漏问题,以及过早释放内存引发的程序崩溃问题。ARC工作在编译期和运行期做了以下事情:

编译期:

  1. 自动插入Retain(引用计数+1)和Release(引用计数-1)、Autorelease(延迟引用计数-1)代码: 当对象被创建或引用传递时,引用计数+1;当对象不再使用时,ARC会自动插入释放内存的代码,从而使引用计数-1。如果发现在同一个对象上执行了多次“保留”与“释放”操作,那么ARC有时可以成对的移除这两个操作
  2. 检查代码,如果发现明显的所有权违规问题或者循环引用,编译器会给出警告。编译器还会为你生成合适的dealloc方法
  3. 使用ARC后,编译器会自动管理autoreleasepool,进行合理的创建和释放使内存达到稳定的状态,无需开发者手动管理。
  4. ARC更新@property属性的默认语义。像强(strong)弱(weak)引用就是这种情况。强引用会自动增加对象的引用计数,而弱引用则不会。

运行期:

  1. 在运行阶段,根据对象的引用情况,自动调用release以及autorelease,以减少或延迟引用计数。引用计数为0的对象会被立即释放
  2. 通过对被引用对象的追踪,ARC能够自动破解一部分循环引用,例如:通过引入weak属性,它不会增加对象的引用计数,这样一个对象即使被另一个对象通过weak引用,也能够被正确释放。
  3. 除释放对象之外,ARC还负责清空所有弱引用(weak reference)的值,阻止野指针的问题发生。

另外:ARC并不是内存管理的终极解决方案,它并不能处理所有情况。比如,如果代码中存在强循环引用,即使采用了ARC也无法自动解决。在这种情况下,开发者需要找出并打破这种循环。所以,编程者仍然需要理解引用计数和内存管理的基本原理,合理地设计代码,避免循环引用的发生

block 是如何在 ARC 中工作的?

在ARC下,编译器会根据情况自动将栈上的block复制到堆上,比如block作为函数返回值时,这样你就不必再调用Block Copy
需要注意的一件事是,在ARC下,NSString * __block myString这样写的话,block会对NSString对象强引用,而不是造成悬垂指针问题。如果你要和MRC保持一致,请使用__block NSString * __unsafe_unretained myString或(更好的是)使用__block NSString * __weak myString

ARC的实现分析

__strong

自己生成并持有

	id  __strong obj0 = [[NSObject alloc] init];
    NSLog(@"%@", obj0);

我们转成汇编之后发现整个的执行过程如下:
在这里插入图片描述

主要经历的方法如下:

//初始化的两个方法如下:
objc_alloc_init
objc_storeStrong
//所有程序执行完之后:
objc_autoreleasePoolPop

所以我们直接来看storeStrong方法。

storeStrong

在runtime文件中找到这个函数
如下
objc_storeStrong(id *location, id obj)
{
	//用prev保留被赋值对象原来所指向的对象
    id prev = *location;
    //如果所赋的值和被赋值对象所指的对象是同一个,就直接return不进行任何操作
    if (obj == prev) {
        return;
    }
    //如果所赋的值和被赋值对象所指的对象不是同一个
    //就先objc_retain使所赋的值对象的引用计数+1(因为赋值成功之后要持有)
    objc_retain(obj);
    //改变被赋值对象所指向的对象为新的对象
    *location = obj;
    //因为prev保留了被赋值对象原来所指向的对象,所以对prev进行objc_release使原来的旧对象引用计数-1,因为现在我们的被赋值对象已经不指向它了
    objc_release(prev);
}

例子:(赋值操作时)
obj = otherObj;
//会变成如下函数调用
objc_storeStrong(&obj, otherObj);

其中做了四件事:

  1. 检查输入的 obj 地址 和指针指向的地址是否相同。
  2. 持有对象,引用计数 + 1 。
  3. 指针指向 obj。
  4. 原来指向的对象引用计数 - 1(释放对象)。

对于这里传入的NULL来说这就等同于向对象发送release消息

详细逻辑思路见上方代码中注释讲解。

SideTable散列表

内存管理主要结构代码

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

objc_retain

来学习一下objc_object的具体实现

objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

再接着看最后所调用的retain()方法:

objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

系统会对是否支持NONPOINTER_ISA进行不同的处理
每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         uintptr_t has_assoc         : 1;//->是否包含关联对象
         uintptr_t has_cxx_dtor      : 1;//->是否设置了析构函数,如果没有,释放对象更快
         uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针
         uintptr_t magic             : 6;//->固定值,用于判断是否完成初始化
         uintptr_t weakly_referenced : 1;//->对象是否被弱引用
         uintptr_t deallocating      : 1;//->对象是否正在销毁
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1,未溢出的时候为0
        uintptr_t extra_rc          : 19;  //->存储引用计数
    };
};

在这里插入图片描述

支持Nonpointer isa的处理
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            // 2、SideTable散列表方法
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        if (slowpath(tryRetain && newisa.deallocating)) {
            // 正在释放
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // 应用计数extra_rc++
        // 如果newisa.extra_rc++ 溢出, carry==1
        if (slowpath(carry)) {
            // 溢出
            if (!handleOverflow) {
                ClearExclusive(&isa.bits); // 空操作(系统预留)
                return rootRetain_overflow(tryRetain);// 再次调用rootRetain(tryRetain,YES)
            }
            // 执行rootRetain_overflow会来到这里,就把extra_rc对应的数值(一半)存到SideTable
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF; // 溢出了,设置为一半,保存一半到SideTable
            newisa.has_sidetable_rc = true; // 标记借用SideTable存储
        }
        // StoreExclusive保存newisa.bits到isa.bits,保存成功返回YES
        // 这里while判断一次就结束了
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // 借位保存:把RC_HALF(一半)存入SideTable
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}


不支持的处理

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)) {
        // 引用计数加1,(上图可知,偏移2位,SIDE_TABLE_RC_ONE = 1<<2)
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    return (id)this;
}

可以看到不支持Nonpointer isa的处理就是直接sidetable_retain,这是由于计数都存储在sidetable中了,处理逻辑较支持Nonpointer isa的情况要简单一些。

不支持Nonpointer isa 的处理
去sidetable取出计数信息 执行加一操作

支持Nonpointer isa的处理

  • 首先判断是否为标签指针类型 如果是 直接返回
  • 进入do-while处理逻辑
  1. 先判断是否为 其一定支持Nonpointer isa的架构,但是isa没有额外信息
    如果没有额外信息 那就和不支持意义一样(判断是否有优化) 引用计数存储在sidetable中,走sidetable的引用计数+1的流程
  2. 判断对象是否正在释放,如果正在释放则执行dealloc流程。
  3. 有存储额外信息,包含引用计数。我们尝试对isa中的extra_rc++加一进行测试
    3.1 如果没有溢出越界的情况,我们将isa的值修改为extra_rc++之后的值
    3.2 如果有溢出 将一半的计数存储到extra_rc,另一半存储到sidetable中去 设置设置标志位位true
    在这里插入图片描述

retain的总结:

如果isa可以存储额外信息,那么有extra_rc位用来存储引用计数,当引用计数满了之后 就会存储到sidetable中 。

retain的流程也是针对isa是否支持存储信息分别进行处理

extra_rc存储溢出了,这个时候是一半(extra_rc能表示的最大值+1的一半)在extra_rc一半存储在sidetable中,这里跟release的操作是对应的(extra_rc不够减了也是去sidetableextra_rc最大值的一半的计数),这样设计的好处避免了频繁的去sidetable中读取计数信息—假如我们溢出了把计数全部存到sidetable中去,那么有release的时候,extra_rc也不够减了,又去借,这就大大降低了效率,比起直接操作isa

这个优化的好处就是我们省去了频繁去sidetable中读取计数信息,从而大大提高了效率,这样的话因为平时绝大多处操作都是普通的retainrelease,所以都可以得到优化,而我们如果要读取引用计数的值的话就相对麻烦一点,需要sidetableextra_rc两者相加,但是读取引用计数值的方法使用率几乎为零,也就是调试的时候会用到而已。

objc_release

来学习一下objc_release的具体实现

objc_release(id obj)
{
    if (obj->isTaggedPointerOrNil()) return;
    return obj->release();
}

下面我们看一下真正的release方法:

// handleUnderflow 参数看似是一个 bool 类型的表示是否处理下溢出,
// 当溢出发生了的话是必须要处理的,如果 handleUnderflow 为 false,
// 那么它会借一个 rootRelease_underflow 函数,并再次调用 rootRelease 函数,
// 并把 handleUnderflow 参数传递 true。

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{   
    // 如果是 Tagged Pointer 直接返回 false,Tagged Pointer 不参与引用计数处理,它内存位于栈区,由系统处理
    if (isTaggedPointer()) return false;

    // 标记 SideTable 是否加锁了
    bool sideTableLocked = false;

    // 临时变量存放旧的 isa
    isa_t oldisa;
    // 临时变量存放字段修改后的 isa
    isa_t newisa;

 retry:
    do {
        // 以原子方式读到 &isa.bits 的数据
        oldisa = LoadExclusive(&isa.bits);
        // 把 oldisa 赋值给 newisa,此时 isa.bits/oldisa/newisa 三者是相同的
        newisa = oldisa;
        
        if (slowpath(!newisa.nonpointer)) {
            // 如果对象的 isa 只是原始指针 (Class isa/Class cls)
            
            // __arm64__ && !__arm642__ 平台下,取消 &isa.bits 的独占访问标记
            // x86_64 下什么都不需要做,对它而言上面的 LoadExclusive 也只是一个原子读取 (atomic_load)
            ClearExclusive(&isa.bits);
            
            // 如果当前对象是元类对象,则直接返回 false 
            if (rawISA()->isMetaClass()) return false;
            
            // 如果当前 SideTable 加锁了则进行解锁
            if (sideTableLocked) sidetable_unlock();
            
            // 只针对 isa 是原始 Class cls 的对象调用的 sidetable_release 函数
            return sidetable_release(performDealloc);
        }
        
        // don't check newisa.fast_rr; we already called any RR overrides
        // 不要检查 newisa.fast_rr; 我们之前已经调用过所有 RR 的重载
        
        // extra_rc-- 
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        
        // 如果发生了下溢出的话,要进行处理,如果没有发生的话就是结束循环,解锁并执行 return false;
        if (slowpath(carry)) {
            // don't ClearExclusive()
            // 不执行 ClearExclusive()
            // 这里直接 goto 到 underflow 中去处理溢出
            goto underflow;
        }
        
    // 这里结束循环的方式同 rootRetain 函数,都是为了保证 isa.bits 能正确修改
    // StoreExclusive 和 StoreReleaseExclusive 的区别在于 memory_order_relaxed 和 memory_order_release
    // 可参考 https://en.cppreference.com/w/cpp/atomic/memory_order
    
    // 当 &isa.bits 与 oldisa.bits 相同时,把 newisa.bits 复制给 &isa.bits,并返回 true
    // 当 &isa.bits 与 oldisa.bits 不同时,
    // 把 oldisa.bits 复制给 &isa.bits, 并返回 false (此时会继续进行 do wehile 循环)
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    
    // 如果未下溢出的话,不需要 goto underflow,如果 Sidetable 加锁了,
    // 则进行解锁,然后返回 false,函数执行结束
    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    // newisa.extra_rc-- 发生溢出时,有两种方式进行处理:
    // 1. 如果 SideTable 中有保存对象的引用计数的话可以从 SideTable 中借用
    // 2. 如果 SideTable 中没有保存对象的引用计数的话,表示对象需要执行销毁了

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        // 如果 newisa.has_sidetable_rc 为 true,表示在 SideTable 中有保存对象的引用计数
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            
            // 如果 handleUnderflow 为 false,则调用 rootRelease_underflow,“递归” 调用 rootRelease 处理溢出
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.
        // 将 retain count 从 SideTable 中转移到 isa.extra_rc 中保存。

        if (!sideTableLocked) {
            // 如果 SideTable 未加锁
            
            // 同上,清除独占标记
            ClearExclusive(&isa.bits);
            
            // 给 SideTable 加锁
            sidetable_lock();
            // 并把加锁标记置为 true
            sideTableLocked = true;
            
            // Need to start over to avoid a race against the nonpointer -> raw pointer transition.
            
            // 回到 retry
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        // 尝试从 SideTable 中移除一些引用计数。
        
        // 是从 SideTable 借一些引用计数出来,borrowed 是借到的值,可能是 0,也可能是 RC_HALF
        // refcnts 中保存的引用计数是 RC_HALF 的整数倍,
        // 每次 retain 溢出时都是往 refcnts 中转移 RC_HALF,
        // 剩下的 RC_HALF 放在 extra_rc 字段中
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set even if the side table count is now zero.
        // 为了避免竞态,即使 SideTable 计数现在为零,也必须保持 has_sidetable_rc 之前的设置。
        
        if (borrowed > 0) {
            // borrowed 表示从 SideTable 借到引用计数了
            
            // Side table retain count decreased.
            // SideTable 引用计数 减少。
            // Try to add them to the inline count.
            // 尝试将借来的引用计数增加到 extra_rc 中。
            
            // 赋值。(包含减 1 的操作)
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            
            // 原子保存修改后的 isa.bits
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // 如果失败的话
                
                // Inline update failed. 
                // extra_rc 更新失败。
                
                // Try it again right now. 
                // This prevents livelock on LL/SC architectures where the side
                // table access itself may have dropped the reservation.
                // 立即进行重试。
                // 这样可以防止在 LL/SC体系结构上发生 livelock,在这种情况下 SideTable 访问本身可能已取消预留。
                // 活锁可参考: https://www.zhihu.com/question/20566246
                
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    // 把借来的引用计数增加到 extra_rc 中
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        // 如果还是失败的话,下面 goto retry 再重来
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // 如果还是失败了。
                // Inline update failed.
                // Put the retains back in the side table.
                // 把从 SideTable 借来的引用计数还放回到 SideTable 中去。
                
                sidetable_addExtraRC_nolock(borrowed);
                
                // 然后直接 goto retry; 进行全盘重试
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // 减去从 SideTable 借来的引用计数成功。
            
            // This decrement cannot be the deallocating decrement
            // - the side table lock and has_sidetable_rc bit
            // ensure that if everyone else tried to -release while we worked, 
            // the last one would block.
            
            // 解锁
            sidetable_unlock();
            // 返回 false 
            return false;
        }
        else {
            // SideTable 是空的,执行 dealloc 分支
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.
    // 执行销毁。

    if (slowpath(newisa.deallocating)) {
        // 如果对象已经被标记了正在执行释放...
        // 这里又进行释放,明显是发生了过度释放...
        
        // 清除独占标记
        ClearExclusive(&isa.bits);
        
        // 如果 SideTable 加锁了则进行解锁
        if (sideTableLocked) sidetable_unlock();
        // 调用 overrelease_error,crash 报错...
        // 对象在销毁的过程中过度释放;中断 objc_overrelease_during_dealloc_error 进行调试
        return overrelease_error();
        // does not actually return
    }
    
    // 把对象的 isa 的 deallocating 置为 true。isa 的又一个字段被设置了,越来的越多的字段被发现设置位置了。 
    newisa.deallocating = true;
    
    // 设置 &isa.bits,如果失败,则 goto retry;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    // 如果加锁了,则进行解锁。
    if (slowpath(sideTableLocked)) sidetable_unlock();

    // 这个函数以当前的水平实在是看不懂呀...
    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        // 如果 performDealloc 为 true,则以消息发送的方式调用 dealloc 
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    
    return true;
}
}

sidetable_release

// return uintptr_t instead of bool so that the various raw-isa -release paths all return zero in eax
// 返回 uintptr_t 而不是 bool,以便各种 raw-isa -release路径在 eax 中都返回零

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
// 如果当前平台支持 isa 优化
#if SUPPORT_NONPOINTER_ISA
    // 如果 isa 是优化的 isa 则直接执行断言,
    // sidetable_release 函数只能在对象的 isa 是原始 isa 时调用(Class cls)
    ASSERT(!isa.nonpointer);
#endif
    
    // 从全局的 SideTalbes 中找到 this 所处的 SideTable
    SideTable& table = SideTables()[this];
    
    // 临时变量,标记是否需要执行 dealloc
    bool do_dealloc = false;
    
    // 加锁
    table.lock();
    
    // it 的类型是: std::pair<DenseMapIterator<std::pair<Disguised<objc_object>, size_t>>, bool>
    // try_emplace 处理两种情况:
    // 1. 如果 this 在 refcnts 中还不存在,则给 this 在 buckets 中找一个 BucketT,
    //    KeyT 放 this, ValueT 放 SIDE_TABLE_DEALLOCATING,然后使用这个 BucketT 构建一个 iterator,
    //    然后用这个 iterator 和 true 构造一个 std::pair<iterator, true> 返回。
    // 2. 如果 this 在 refcnts 中已经存在了,则用 this 对应的 BucketT 构建一个 iterator,
    //    然后用这个 iterator 和 false 构造一个 std::pair<iterator, false> 返回。
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    
    // refcnt 是引用计数值的引用。
    // it.first 是 DenseMapIterator,它的操作符 -> 被重写了返回的是 DenseMpaIterator 的 Ptr 成员变量,
    // 然后 Ptr 的类型是 BucketT 指针,
    // 然后这里的 ->second 其实就是 BucketT->second,其实就是 size_t,正是保存的对象的引用计数数据。
    auto &refcnt = it.first->second;
    
    if (it.second) {
        // 如果 it.second 为 true,表示 this 第一次放进 refcnts 中,且 BucketT.second 已经被置为 SIDE_TABLE_DEALLOCATING,
        // 标记为需要执行 dealloc
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        // 如果 refcnt < SIDE_TABLE_DEALLOCATING,那可能的情况就是 SIDE_TABLE_WEAKLY_REFERENCED 或者为 0
        // 标记为需要执行 dealloc
        do_dealloc = true;
        
        // 与 SIDE_TABLE_DEALLOCATING 执行或操作,表示把 refcnt 标记为 DEALLOCATING
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        // refcnt & SIDE_TABLE_RC_PINNED 值为 false 的话表示,
        // rcfcnts 中保存的 this 对应的 BucketT 的 size_t 还没有溢出,还可正常进行操作存储 this 的引用计数
        // refcnt 减去 SIDE_TABLE_RC_ONE
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    
    // 解锁
    table.unlock();
    
    if (do_dealloc  &&  performDealloc) {
        // 如果 do_dealloc 被标记为需要 dealloc 并且入参 performDealloc 为 true,
        // 则以 objc_msgSend 消息发送的方式调用对象的 dealloc 方法
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    
    return do_dealloc;
}

在上面的代码当中有两个宏定义:

在这里插入图片描述

现在release也很好理解了:

  1. 依旧是判断是否为taggedPointer,如果是,直接返回false,不需要dealloc
  2. 判断是否有优化 如果没有 就直接操作散列表,使引用计数-1
  3. 判断是引用计数为否为0 如果是0则执行dealloc流程
  4. isa有优化,则对象的isa位存储的引用计数减一,且通过carry判断是否向下溢出了 结果为负数(下图有点问题 应该是判断是有向下溢出),如果是,如果到-1 就放弃newisa改为old,并将散列表中一半引用计数取出来,然后将这一半引用计数减一在存到isaextra_rc
  5. 如果sidetable的引用计数为0,对象进行dealloc流程

在这里插入图片描述

其实和retain一样 不过release操作变成-1 并且需要注意从sidetable中的一半减一放入

retainCount

如果对象的isa是非指针的话,引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和。如果对象的isa是原始isa的话,对象的引用计数数据只保存在 SideTable 中。

  1. 当对象的isa经过优化,首先获取isa位域extra_rc中的引用计数,默认会+1(防止你没持有就要打印),uintptr_t rc = 1 + bits.extra_rc;然后获取散列表的引用计数表中的引用计数,两者相加得到对象的最终的引用计数
  2. 当对象的isa没有经过优化,则直接获取散列表的引用计数表中的引用计数,返回。
  3. 当我们alloc一个对象时,然后调用retainCount函数,得到对象的引用计数为1。这是因为在底层rootRetainCount方法中,引用计数默认+1了,这里只有对引用计数的读取操作,是没有写入操作的,简单来说就是:为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然为0(因为extra_rc中存放的引用计数值是除该对象本身之外的引用计数数量)

所以 通过alloc或者new这样赋值来新建一个对象 ARC MRC环境下都是1 这个1是底层默认的返回值加一 没有调用retain 其他强引用 才会调用objc_retain来持有

runtime源码中找到retainCount供大家参考:

inline uintptr_t 
objc_object::rootRetainCount()
{
    // 如果是 Tagged Pointer 的话,获取它的引用计数则直接返回 (uintptr_t)this
    if (isTaggedPointer()) return (uintptr_t)this;
    
    // 加锁
    sidetable_lock();
    
    // 以原子方式加载 &isa.bits 数据
    isa_t bits = LoadExclusive(&isa.bits);
    // 如果是 __arm64__ && !__arm64e__ 平台下,要清除独占标记
    ClearExclusive(&isa.bits);
    
    if (bits.nonpointer) {
        // 如果对象的 isa 是非指针的话,引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和
        // 这里加 1, 是因为 extra_rc 存储的是对象本身之外的引用计数的数量(这个加1操作也就是为什么我们新alloc等初始化一个对象之后,打印它的引用计数值为1)
        uintptr_t rc = 1 + bits.extra_rc;
        
        // 如果 has_sidetable_rc 位为 1,则表示在 SideTable 中也保存有对象的引用计数数据
        if (bits.has_sidetable_rc) {
            // 找到对象的在 SideTable 中的引用计数并增加到 rc 中
            rc += sidetable_getExtraRC_nolock();
        }
        // 解锁
        sidetable_unlock();
        // 返回 rc
        return rc;
    }

    sidetable_unlock();
    // 如果对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中
    return sidetable_retainCount();
}

isa如果优化过,即支持Nonpointer isa,则在sidetable中查找引用计数的函数如下:

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    // 此函数只限定 isa 是非指针的对象调用
    ASSERT(isa.nonpointer);
    
    // 从全局的 SideTables 中找到 this 所处的 SideTable
    SideTable& table = SideTables()[this];
    // 查找对象的引用计数
    RefcountMap::iterator it = table.refcnts.find(this);
    // 如果未找到,返回 0
    if (it == table.refcnts.end()) return 0;
    // 如果找到了做一次右移操作,后两位是预留的标记位
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

不支持Nonpointer isa的话,在sidetable中查找引用计数的函数如下:

uintptr_t
objc_object::sidetable_retainCount()
{
    // 找到 this 所在的 SideTable
    SideTable& table = SideTables()[this];

    // refcnt_result 初始为 1,因为 SideTable 中存储的是对象本身之外的引用计数的数量
    size_t refcnt_result = 1;
    
    // 加锁
    table.lock();
    
    // 在 refcnts 中查找对象的引用计数
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        // 这也对 SIDE_TABLE_RC_PINNED 有效
        
        // 移位并增加到 refcnt_result
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    
    // 解锁
    table.unlock();
    return refcnt_result;
}

retainCount相关流程图如下:

在这里插入图片描述

在学完了releaseretain之后,我们浅浅地总结一下: 在我们alloc初始化完一个对象的过程中,系统在编译阶段,程序底层默认对对象进行了引用计数+1操作,但是这个1不会出现在sidetable中,也不会出现在extra_rc中,因为sidetableextra_rc当中存放的都是该对象本身之外的引用计数的数量,所以初始状态sidetableextra_rc中的值都是0,然后我们后续进行的retainrelease操作都是针对sidetableextra_rc中的引用计数进行+1或-1。

非自己生成并持有

	id __strong obj = [NSMutableArray array];
    NSLog(@"%@", obj);

在这里插入图片描述

我们发现出现了objc_retainAutoreleasedReturnValue这个方法

接着我们就探究其原理:

先看一个例子:

	@autoreleasepool {
       __autoreleasing NSObject *obj = [NSObject new];
    }
	该代码对应的伪代码是:
	// 获取哨兵POOL_SENTINEL
    void * atautoreleasepoolobj = objc_autoreleasePoolPush();
    {
        __autoreleasing NSObject *obj = [NSObject new];
    }
    // 就是release哨兵之后的autorelease对象。
    objc_autoreleasePoolPop(atautoreleasepoolobj);

autorelease调用栈如下:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └─ id objc_object::rootAutorelease2()
       └─ static id AutoreleasePoolPage::autorelease(id obj)
          └─ static id AutoreleasePoolPage::autoreleaseFast(id obj)
             ├─ id *add(id obj)
             ├─ static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
             │  ├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
             │  └─ id *add(id obj)
             └─ static id *autoreleaseNoPage(id obj)
                ├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                └─ id *add(id obj)

一个autorelease对象在什么时刻释放?
答案是:

  1. 手动指定Autoreleasepool:当前Autoreleasepool作用域大括号结束时释放;
  2. 不手动指定:autorelease对象会被添加到最近一次创建的autoreleasepool中,并在当前的runloop迭代结束时候释放。

例如: 主RunloopAutoreleasepool管理的流程:
Runloop中,检测到触摸事件,创建事件,创建Autoreleasepoolautorelease对象加入pool中,事件完成,Runloop运行循环将要结束,释放Autoreleasepool,向pool中对象发送release消息,Runloop休眠。

autorelease 进行的非持有方法的优化(自动添加到自动释放池):

  1. alloc/new/copy/mutableCopy—持有对象方法会自动添加到自动释放池
  2. 其他类方法返回的对象,如下面的createObject就会自动添加到自动释放池
@implementation ObjectTest
+ (instancetype)createObject {
    return [self new];
}

接着我们来看这两个方法本尊:

id objc_autoreleaseReturnValue(id obj)
{
    // prepareOptimizedReturn判断是否可以TSL优化,可以则标记,YES--就不需要调用 objc_autorelease(),优化性能
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}
id objc_retainAutoreleasedReturnValue(id obj)
{
  // 如果之前 objc_autoreleaseReturnValue() 存入的标志位为 ReturnAtPlus1,则直接返回对象,无需调用 objc_retain(),优化性能
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
    return objc_retain(obj);
}

然后是这两个方法中if判断的条件调用的函数:



static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
	//获取返回标记
    assert(getReturnDisposition() == ReturnAtPlus0);

    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false;
}

static ALWAYS_INLINE ReturnDisposition 
acceptOptimizedReturn()
{
    ReturnDisposition disposition = getReturnDisposition();
    setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state
    return disposition;
}

TLS 全称为 Thread Local Storage,是每个线程专有的键值存储:

/*在某个线程上的函数调用栈上相邻两个函数对 TLS 进行了存取,这中间肯定不会有别的程序『插手』。
所以 getReturnDisposition() 和 setReturnDisposition() 的实现比较简单,不需要判断考虑是针对哪个对象的 Disposition 进行存取,因为当前线程上下文中只处理唯一的对象,保证不会乱掉。 */

static ALWAYS_INLINE void 
setReturnDisposition(ReturnDisposition disposition)
{
    tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}

callerAcceptsOptimizedReturn(__builtin_return_address(0))函数在不同架构的 CPU 上实现也是不一样的。 主要作用:

__builtin_return_address(0)获取当前函数返回地址,传入 callerAcceptsOptimizedReturn 判断调用方是否紧接着调用了 objc_retainAutoreleasedReturnValue
当判断调用方紧接着调用了 objc_retainAutoreleasedReturnValue 或者 objc_unsafeClaimAutoreleasedReturnValue
直接返回当前对象地址,而不执行retain与autorelease操作.

其作用就是得到函数的返回地址,0–表示返回当前函数的返回地址,1–表示返回当前函数的调用方的返回地址;

在这里插入图片描述

ARC 会视情况在调用方法时可能会添加 retain ,在方法内部返回时可能会添加 autorelease ,经过优化后很可能会抵消。

在这里插入图片描述

1、持有、无引用:

- (void)test {
    [BBObject new];
}

编译器编译后的伪代码:

- (void)test {
    objc_release([BBObject new]) ;
}

2、持有、局部变量引用 __strong:

- (void)test {
    __strong BBObject * obj = [BBObject new];
}

编译器编译后的伪代码:

- (void)test {
    id temp = [BBObject new];
    objc_storeStrong(&temp,nil);//相当于tmp指向对象执行release
}

3、持有、外部变量引用:

- (void)test {
    self.obj = [BBObject new];
}

编译器编译后的伪代码:

- (void)test{
    id temp = [BBObject new];
    [self setObj:temp];//setter方法执行objc_storeStrong
    objc_release(temp);
}
- (void)setObj:(id aObj) {
    objc_storeStrong(&_obj, aObj);
}

4、不持有、无引用:

- (void)test {
    [BBObject createObj];
}

编译器编译后的伪代码:

+ (instancetype) createObj {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease] 
}
- (void)test { 
    objc_unsafeClaimAutoreleasedReturnValue([BBObject createObj]); 
}

5、不持有、局部变量引用:

- (void)test {
    BBObject * obj1 = [BBObject createObj];
}

编译器编译后的伪代码:

+ (instancetype) createObj {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease] 
}
- (void)test {
    id obj1 = objc_retainAutoreleasedReturnValue([BBObject createObj]);  
    objc_storeStrong(& obj1,nil); 
}

发现obj1指向的对象不会加入autoreleasepool

6、不持有、外部变量引用:

+ (instancetype) createObj {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease] 
}
- (void)test {
    self.obj = [BBObject createObj];
}

编译后的伪代码:

- (void)test {
    id temp = _objc_retainAutoreleasedReturnValue([Foo createFoo]); 
    [self setObj:temp]; // setter方法执行objc_storeStrong
    objc_release(temp);
}

总结非自己生成并持有

  • objc_autoreleasedReturnValue会检验调用者是否会对该对象执行retain操作,如果会的话就不执行autorelease,直接设置标志符ReturnAtPlus1
  • objc_retainAutoreleaseReturnValue在检验到标志符后,也不retain了(后面retain操作),直接返回对象本身,同样,如果检测到标识符显示后面没有retain操作,那么就走一遍retain使其引用计数加1

所以array这样的赋值新建一个对象,ARC环境下引用计数的1是底层默认的返回值加一 没有调用retain 其他强引用,才会调用objc_retain来使引用计数加一。objc_retainretainAutoreleaseReturnValue调用的。

一个问题:为什么要传入(NSError **)这种类型的参数

这是一个二级指针(指向指针的指针),将一个基本类型的变量通过函数参数传入函数内,在函数内如何改变都不会影响到外部变量的值,那如果我们要在函数内部改变外部变量的值,就应该将指针的值传入函数,然后函数中根据指针去找到指向的内存进行修改。

如果函数参数本身是一个对象,我们传入一个对象,对象本身就是一个地址(但是一个一级指针)。

如以下例子:

#import <Foundation/Foundation.h>
#import "StrongTest.h"

void test(StrongTest *obj) {
    obj.name = @"3G Group";
    //重新初始化obj,也就是改变参数obj的值(因为划分新的内存,对象的地址会变,而obj就是对象在内存中的地址)
    obj = [[StrongTest alloc] init];
    obj.name = @"iOS Club";
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        StrongTest *obj = [[StrongTest alloc] init];
        obj.name = @"Xi You";
        
        test(obj);
        NSLog(@"obj.name = %@", obj.name);
    }
    return 0;
}

在这里插入图片描述

可以看到我们最后的打印结果中,并没有打印新初始化的对象的字符串。

这是因为我们使用alloc init后系统会在内存中新开辟一块存储空间存储一个新的对象,然后将函数中的obj存储的指针值改为这个新的内存地址,而函数外的obj并没有发生改变,还是指向原来的这个对象的地址

如果想在函数中改变函数外的对象,就需要用到二级指针,即指向指针的指针。

例子如下:

#import <Foundation/Foundation.h>
#import "StrongTest.h"

void test(StrongTest **obj) {
    (*obj).name = @"3G Group";
    
    *obj = [[StrongTest alloc] init];
    (*obj).name = @"iOS Club";
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        StrongTest *obj = [[StrongTest alloc] init];
        obj.name = @"Xi You";
        
        test(&obj);
        NSLog(@"obj.name = %@", obj.name);
    }
    return 0;
}

在这里插入图片描述

可以看到打印的结果就是我们新初始化后的对象中的字符串。

所以,所以对于NSError **,我们可以在外面新建一个NSError,当函数有错误时,新建一个NSError对象并存储到我们新建的这个NSError对象中。我们就可以通过判断NSError是否为nil来看函数运行是否出错。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/39523.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python3GUI--仿win10任务管理器By:PyQt5(附UI源码)

文章目录 一&#xff0e;前言二&#xff0e;展示1.主界面1.进程2.性能1.CPU2.内存 3.简略信息4.详细信息5.新建任务 三&#xff0e;设计思路1.UI设计1.主界面1.进程2.性能3.详细信息4.新建任务5.图表信息组件 2.代码整体设计1.项目设计心得2.项目设计其他心得 3.其他心得 四&am…

华为无线ac+ap旁挂二层组网常用配置案例

AC控制器理解配置步骤&#xff1a; capwap source interface Vlanif 100 //源IP回包地址 wlan ssid-profile name test //新建个模版名称为test ssid test //wifi名称 wlan security-profile name test //建立安全模版也叫test security wpa-wpa2 psk pass-phrase admin123 a…

【PDFBox】PDFBox操作PDF文档之读取指定页面文本内容、读取所有页面文本内容、根据模板文件生成PDF文档

这篇文章&#xff0c;主要介绍PDFBox操作PDF文档之读取指定页面文本内容、读取所有页面文本内容、根据模板文件生成PDF文档。 目录 一、PDFBox操作文本 1.1、读取所有页面文本内容 1.2、读取指定页面文本内容 1.3、写入文本内容 1.4、替换文本内容 &#xff08;1&#xf…

在 Amazon 上以高可用性模式实现 Microsoft SQL 数据库服务现代化的注意事项

许多企业都有需要 Microsoft SQL Server 来运行关系数据库工作负载的应用程序&#xff1a;一些应用程序可能是专有软件&#xff0c;供应商可使用它强制 Microsoft SQL Server 运行数据库服务&#xff1b;其他应用程序可能是长期存在的、自主开发的应用程序&#xff0c;它们在最…

XUbuntu22.04之vim无法复制内容到系统(一百八十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

将大模型集成到语音识别系统中的例子

概述 本文旨在探索将大型语言模型&#xff08;LLMs&#xff09;集成到自动语音识别&#xff08;ASR&#xff09;系统中以提高转录准确性的潜力。 文章介绍了目前的ASR方法及其存在的问题&#xff0c;并对使用LLMs的上下文学习能力来改进ASR系统的性能进行了合理的动机论证。 本…

VIM文本如何复制到系统剪切板?

今天从vim上用鼠标复制代码&#xff0c;发现把VIM当中的行号也复制进去了&#xff0c;就很麻烦&#xff0c;于是简单研究了下&#xff0c;如果vim支持clipboard的话就比较好办&#xff0c;具体支持与否&#xff0c;使用命令查看&#xff1a; vim --version | grep "clipb…

Android系统启动流程分析

当按下Android系统的开机电源按键时候&#xff0c;硬件会触发引导芯片&#xff0c;执行预定义的代码&#xff0c;然后加载引导程序(BootLoader)到RAM&#xff0c;Bootloader是Android系统起来前第一个程序&#xff0c;主要用来拉起Android系统程序&#xff0c;Android系统被拉起…

基于Java+Swingl实现拼图游戏

基于JavaSwingl实现拼图游戏 一、系统介绍二、效果展示三、其他系统实现四、获取源码 一、系统介绍 拼图游戏是一个简单的小程序&#xff0c;游戏规则如下&#xff1a;将一张大图分成9张小图&#xff0c;然后任意挑8张图&#xff0c;随意放在3行3列的矩阵中。 通过点击鼠标移动…

Maven 项目构建生命周期

Maven 项目构建生命周期 一句话: Maven 构建生命周期描述的是一次构建过程经历了多少个事件 生命周期的3 大阶段 clean 清理工作 default 核心工作&#xff0c;例如编译&#xff0c;测试&#xff0c;打包&#xff0c;部署等 site 产生报告&#xff0c;发布站点等 生命周期…

react和vue2/3父子组件的双向绑定(sync、emit、v-model)

目录 Vue .sync&#xff08;2.3.0&#xff09; $emit &#xff08;2.3后&#xff09; 自定义组件的 v-model 2.2.0 v-modelemits(3.0取消了.sync) React 父组件回调函数 相关基础 框架 MVC &#xff08;Model View Controller&#xff09;/MVP&#xff08;Model View…

RHCSA——Linux网络、磁盘及软件包管理

ZY目录 Linux操作系统讲解&#xff1a;一、网络管理1、NetworkManager1.1、nmtui界面&#xff1a;1.2、nmcli使用方法&#xff1a; 2、配置网络2.1、网络接口以及网络连接2.2、配置方法&#xff1a;2.3、ping命令&#xff1a;2.4、wget命令 二、磁盘管理2.1、分区得两种格式2.1…

日撸java三百行day77-79

文章目录 说明GUI1. GUI 总体布局2. GUI 代码理解2.1 对话框相关控件2.1.1 ApplicationShowdown.java&#xff08;关闭应用程序&#xff09;2.1.2 DialogCloser.java&#xff08;关闭对话框&#xff09;2.1.3 ErrorDialog.java&#xff08;显示错误信息&#xff09;2.1.4 HelpD…

day35-Postman/ajax

0目录 1.postman 2.ajax 1.Postman 1.1 定义&#xff1a;postman用于测试http协议接口&#xff0c;无论是开发还是测试人员 1.2 Servlet中的doGet&#xff08;&#xff09;/doPost…

idea 常用快捷键总结

IDEA常用快捷键总结 很多新手小白在使用IDEA进行代码编写的时候 对快捷键很感兴趣 这里泡泡给大家总结了一些常用的快捷键 希望能帮助到你 记得要收藏下来时常观看并且练习&#xff0c;才能熟练哦~ 1. 根据psvm或者main快速生成主函数 我们可以在类中输入psvm 或者main 然后I…

C# Winfrom将DataGridView数据导入Excel

1.项目添加Word和Excel的COM类型库引用 2.创建Excel工作表 //定义Excel操作对象Microsoft.Office.Interop.Excel.Application excelApp new Microsoft.Office.Interop.Excel.Application();//定义Excel工作表Microsoft.Office.Interop.Excel.Worksheet worksheet excelApp.Wo…

TCP的窗口控制和重发控制【TCP原理(笔记三)】

文章目录 利用窗口控制提高速度窗口控制与重发控制确认应答未能返回的情况某个报文段丢失的情况 控制流 利用窗口控制提高速度 TCP以1个段为单位&#xff0c;每发一个段进行一次确认应答的处理&#xff0c;如图。这样的传输方式有一个缺点。那就是&#xff0c;包的往返时间越长…

Centos使用docker部署nacos

Centos使用docker部署nacos 对于使用Docker部署Nacos&#xff0c;您可以按照以下步骤进行操作&#xff1a; 在您的服务器上安装Docker和Docker Compose。创建一个用于存储Nacos数据的目录&#xff0c;例如/path/to/nacos/data。创建一个docker-compose.yml文件&#xff0c;并…

心电前置放大电路制作与原理详细分析(附电路板实物图)

心电前置放大电路制作与原理详细分析(附电路板实物图) 实验目的实验结果实验电路图原理解释与计算实验测试过程实验参数测量实验洞洞板焊接实验目的 心电信号具有微弱、低频、和高阻抗等特性,极其容易受到干扰。为了实现心电信号的放大,前置放大器需要满足高输入阻抗、高共…

前端开发如何更好的避免样式冲突?级联层(CSS@layer)

目录 前言 一、什么是级联层 (Cascade Layers)&#xff1f; 1.1 级联层的官方定义 1.2 级联层为了解决什么问题&#xff1f; 二、理解级联层的前提 —— 级联 (cascade) 2.1 什么是级联&#xff1f; 2.2 当前级联的排序标准 2.3 级联起源&#xff08;Cascading Origins…