【iOS】探索ARC的实现

news2024/11/26 0:34:41

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来看函数运行是否出错。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

打包python文件成.exe程序

不带环境打包程序 &#xff0c;下载后的程序需要下载环境才能正常运行 -w:隐藏命令行窗口 -i:添加ico图标文件 最后写入参数代表程序的入口

C#图片处理

查找图片所在位置 原理&#xff1a;使用OpenCvSharp对比查找小图片在大图片上的位置 private static System.Drawing.Point Find(Mat BackGround, Mat Identify, double threshold 0.8) {using (Mat res new Mat(BackGround.Rows - Identify.Rows 1, BackGround.Cols - Iden…

【LeetCode热题100】哈希篇

两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任…

短视频抖音账号矩阵系统源码开发分享

引用&#xff1a;MySQL数据库&#xff0c;NGINX&#xff0c;PHP7.4&#xff0c;MySQL5.7&#xff0c;redis 媒体组件 组件 描述 image 图片 图片。支持 JPG、PNG、SVG、WEBP、GIF 等格式。 video 视频 视频组件。相关 API 请参考 tt.createVideoContext。 开发背景&…

Fiddler的使用方法介绍

FIDDLER下载地址fiddler Fiddler可以帮您记录&#xff0c;调试Microsoft Internet Explorer与Web应用程序的交互&#xff0c;找到Web程序运行性能的瓶颈&#xff0c;还有如查看向Web服务器发送cookies的内容&#xff0c;下载内容的大小等功能。 说多一点是&#xff0c;Fiddler站…

docker-compose部署sentinel-dashboard-1.8.5

一、创建文件夹 mkdir -p /docker/alibaba/sentinel/{config,data,logs} 二、拷贝jar包进sentinel目录下 三、Dockerfile文件 FROM openjdk:8-jre MAINTAINER yh COPY ./sentinel-dashboard.jar /app.jar EXPOSE 8718 ENTRYPOINT ["java", "-jar", &quo…

【Linux C】fseek函数使用小结

0x00 前言 演示使用的Linux版本&#xff08;#cat /etc/issue&#xff09;&#xff1a;Ubuntu 18.04.6 LTS \n \l 最后更新日期&#xff1a;2023.7.17 0x01 fseek函数使用小结 1.函数描述 设置stream文件的位置指针偏移到指定位置1。 2.声明 #include <stdio.h> in…

LVS—Linux Virtual Server

集群和分布式 系统性能拓展方式&#xff1a; Scale UP&#xff1a;垂直拓展&#xff0c;提高单个服务器的性能&#xff0c;如增加CPU、内存等&#xff0c;单台服务器的性能是有上限的&#xff0c;不可能无限制垂直拓展。Scale Out&#xff1a;水平拓展&#xff0c;增加设备&a…

用Python采集电商平台商品数据进行可视化分析

目录标题 前言环境使用:模块使用:基本流程思路:代码展示获取数据扩展知识数据可视化 尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 环境使用: python 3.8 解释器 pycharm 编辑器 模块使用: 第三方模块 需要安装 requests —> 发送 HTTP请求 内置模块 不需…

嵌入式处理器常用存储介质

一、存储器 二、XIP设备 XIP(excute in place)&#xff0c;本地执行。像上述nor flash这种设备&#xff0c;上电后不需要初始化&#xff0c;CPU通过地址总线和数据总线直连的&#xff0c;可以直接访问并取址执行&#xff0c;称之为XIP设备。 像emmc这种&#xff0c;在SOC内部有…

JVM打印GC信息

-XX:PrintGCDetails 一、代码 public class GcDemo {public static void main(String[] args) {Object obj new Object();obj null;System.gc();} } 二、配置 三、测试 [GC (System.gc()) [PSYoungGen: 5242K->872K(152576K)] 5242K->880K(500736K), 0.0011529 sec…

如何使用 Amazon Systems Manager 集中管理 Amazon IoT Greengrass 设备

对于边缘设备管理员来说&#xff0c;远程管理大量不同的系统和应用程序会是一项富有挑战性的任务。Amazon IoT Greengrass 可帮助这些系统管理员管理其边缘设备应用程序堆栈。不过&#xff0c;这些设备上的系统软件必须通过与其大型 IT 企业的运营策略一致的运营策略来单独更新…

文件共享服务器

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、概述 二、FTP连接类型 三、端口 四、工作模式 五、安装配置 1、 服务名 2、主配置文件 3、 用户控制文件 4、 用户验证类型 5、 安装 6、 匿名用户验证 7、本…

python操作Elasticsearch数据库

Elasticsearch&#xff08;ES&#xff09;,ES是一个开源的高扩展的分布式全站搜索引擎&#xff0c;是整个Elastic Stack技术栈的核心。它可以近乎实时的存储、检索数据&#xff1b;本身扩展性很好&#xff0c;可以扩展到上百台服务器&#xff0c;处理PB级别的数据。 安装好ES之…

go mod 设置国内源 windows 环境 win10

启用 go module 功能 go env -w GO111MODULEon 配置 goproxy 变量 go env -w GOPROXYhttps://goproxy.cn,direct 下载包就行了&#xff0c;速度飞快 go mod tidy 检测 goproxy 是否配置好 运行 go env | findstr goproxy 查看 goproxy Go module 从 Go v1.12 版本开始存在&a…

springboot3+vue3 前后端分离项目模板

更好的阅读体验 \color{red}{更好的阅读体验} 更好的阅读体验 项目地址 功能简介-视频演示 功能概要 支持登录和注册功能。支持密码找回和邮箱验证。支持“记住我”功能。包含完整的上述功能的基础页面。 登录功能 支持“用户名/密码”登录。登录可选“记住我”&#xff0c;勾…

域名+云服务器+springBoo:怎样零成本进行云服务器的学习

零成本进行云服务器的学习 1.项目开发需要学习云服务器的使用 2.申请域名 为了让服务器运行我的springBoot项目&#xff0c;我们首先需要一个域名&#xff0c;很多网站提供免费域名的使用申请&#xff0c;我在阿里云上申请了一年域名的体验。 3.将域名绑定到三丰云&#xff0c…

WCF的IIS宿主部署

IIS的web服务开启网上已经很多&#xff0c;不再赘述。 关键点&#xff1a;WCF发布的文件夹要加上IIS的权限能够读取。否则报没有权限。

动态规划——删除并获得点数

题目链接 leetcode在线oj题——删除并获得点数 题目描述 给你一个整数数组 nums &#xff0c;你可以对它进行一些操作。 每次操作中&#xff0c;选择任意一个 nums[i] &#xff0c;删除它并获得 nums[i] 的点数。之后&#xff0c;你必须删除 所有 等于 nums[i] - 1 和 nums…

SQL性能分析——执行频率、慢查询日志、profile详情

1.SQL的执行频率 2.慢查询日志 show variables like slow_query_log;修改完成以后&#xff0c;再次查询以后发现&#xff0c;slow_query_log的开关时开着的。 3.profile详情