[iOS]-ARC

内存管理四大原则

  1. 自己生成的对象自己持有
  2. 非自己生成的对象自己也能持有
  3. 不再需要自己持有的对象时释放
  4. 非自己持有的对象无法释放

iOS底层内存管理方式

  1. taggedPointer :标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。从他的引用计数可以看出,这个也是一个释放不掉的单例常量对象。在运行时根据实际情况创建。
  2. NONPOINTER_ISA :在 64 位架构下,isa 指针是占 64 比特位的,实际上只有 30 多位就 已经够用了,为了提高利用率,剩余的比特位存储了内存管理的相关数据内容。
    nonpointer: 表示是否对 isa 指针开启指针优化
    :0:纯isa指针。
    :1:不止是类对象地址,isa中包含了类信息、对象的引用计数等。
  3. 散列表:复杂的数据结构,包括了引用计数表和弱引用表 通过 SideTables()结构来实现的,SideTables()结构下,有很多 SideTable 的数据结构。 而 sideTable 当中包含了自旋锁,引用计数表,弱引用表。 SideTables()实际上是一个哈希表,通过对象的地址来计算该对象的引用计数在哪个 sideTable 中。

ARC规则(修饰符的讲解)

__strong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符。

不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。

对象的所有者和对象的生命周期
  1. 持有强引用的变量在超出其作用域时被废弃
  2. 随着强引用的失效
  3. 引用的对象会随之释放

__strong对象相互赋值

__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。

id __strong obj0 = [[NSObject alloc] init];//生成对象A			
id __strong obj1 = [[NSObject alloc] init];//生成对象B		
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)	
obj1 = nil;//obj1不再强引用对象B	
obj0 = nil;//obj0不再强引用对象B	
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

如如下表格所示:

obj0obj1obj2
A
AB
ABnil
ABnil
BBB
BnilB
nilnilB
nilnilnil
赋值的本质就是强引用的转变

方法参数中使用__strong

  • 废弃Test对象的同时,Test对象的obj_成员变量也被废除
    即成员变量的生存周期是与对象同步的

__strong导致的循环引用

循环引用就是两个对象相互引用导致在该释放的时候没有释放,从而一直占着内存导致内存泄漏

**内存泄漏:**在内存该被释放的时候没有释放,导致内存被浪费使用了

举个例子:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface StrongTest : NSObject {
    id __strong _obj;
}

- (void) setObject:(id __strong) obj;

@end

NS_ASSUME_NONNULL_END

#import "StrongTest.h"

@implementation StrongTest

- (void) setObject:(id)obj {
    
    _obj = obj;
    
}

@end

main函数:

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


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        StrongTest *testFirst = [[StrongTest alloc] init];//生成Test1
        StrongTest *testSecond = [[StrongTest alloc] init];//生成Test2
        
        [testFirst setObject:testSecond];
        [testSecond setObject:testFirst];
        
        //打印一下引用计数值
        NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)testFirst));
        
    }
    return 0;
}

我们创建了两个对象testFirst和testSecond
每个对象内部都有一个成员变量_obj
其中一个对象的_obj持有另一个对象

最后就会造成下面的结果:

testFirst持有Test1,testFirst.obj持有Test2,testSecond持有Test2,testSecond.obj持有Test1

失效阶段的表示:

what happenTest1引用计数Test1持有者Test2引用计数Test2持有者
初始状态2testFirst, testSecond.obj2testFirst.obj, testSecond
testFirst超出作用域1testSecond.obj2testFirst.obj, testSecond
testSecond超出作用域1testSecond.obj1testFirst.obj

这样一来,即使两个对象都超出作用域,由于其中彼此的成员变量相互持有彼此的对象而造成循环引用。

__weak修饰符

__strong 强引用 ()
__weak 弱引用(引用计数不会加一 对象随时可能会被dealloc)
内部使用__autoreleasing来维持该对象不被dealloc(这个也是__weak修饰符修饰的对象在所指向的对象销毁之后会自动指向nil的骚操作的关键所在)
对象的引用计数是记录在一张表上的,不在对象本身或者指针中,系统通过访问这张表来确定是否释放该对象。

将上面相互引用例子中的成员变量变为weak,即可避免相互引用。

weak还有个作用。在持有某对象的弱引用时,若该对象被废弃,则此若引用将自动失效且处于nil被赋值的状态(空弱引用)。

weak提供弱引用,弱引用不持有对象,NSObject对象会被销毁,所以会报一个警告
请添加图片描述
我们可以这样使用__weak修饰符的变量,将__strong修饰的对象赋值给__weak修饰的对象,这样就不会发生警告了:

	id __strong objTest = [[NSObject alloc] init];
    id __weak objTestSecond = objTest;

__weak修饰符的引用计数的问题

按以下例子,我们来看一下这两个的引用计数的值:

    id __strong obj = [[NSObject alloc] init];
    id __weak objTest = obj;
    NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)obj));
    NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)objTest));

按照常理来说应该是1 1,因为__weak修饰符不持有对象,引用计数值两个都应该是1

打印一下结果:
请添加图片描述
但是我们看结果发现,一个是1一个是2
打开汇编查看原因:

第一个NSLog:
请添加图片描述
第二个NSLog:
请添加图片描述
对比一下就能发现 两行NSLog的汇编代码并不一样

第二个__weak修饰符的NSLog在于开始先loadWeakRetained
然后在打印结束后有一个release操作

在大印__weak的引用计数时NSLog先将其以强引用,防止没有打印就释放掉了造成程序的崩溃,在NSLog结束时,会调用objc_release使引用计数减一。

__unsafe_unretained

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。
附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。

为什么是不安全的呢

weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为 nil。 _Unsafe_Unretain 不会置为 nil,容易出现 悬垂指针,发生崩溃。但是 _Unsafe_Unretain 比 __weak 效率高。
悬垂指针 指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针。野指针,没有进行初始化的指针,其实都是 野指针

附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,生成的对象会立即释放。

在使用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在,如果不存在,那么程序就会崩溃。

__autoreleasing修饰符

与MRC进行比较:

  • MRC中autorelease的使用方法
  1. 生成并持有NSAutoreleasePool对象
  2. 调用已分配对象的autorelease方法(将对象注册到pool中)
  3. 废弃NSAutoreleasePool对象

在ARC环境下:
__autoreleasing如下:
在这里插入图片描述
自动调用:
编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是讲自动将返回值的对象注册到autoreleasepool中

下面情况不使用__autoreleasing修饰符也能使对象注册到autoreleasepool中。

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

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

由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器也会自动注册到自动释放池
自动调用时的失效过程:
随着obj超出其作用域,强引用失效,所以自动释放自己持有的对象。
同时,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放。 因为对象的拥有者不存在,所以废弃对象。
weak修饰符与autoreleasing修饰符:
访问__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢,这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在,因此,在使用附有__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象。

id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
与以下源代码相同
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [temp class]);

下面是详细例子:

例一:

	//这个例子中obj0没有加入到自动释放池中
    id  obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    NSLog(@"class = %@", [obj1 class]);

打印结果:
请添加图片描述
池中没有出现obj0

例二:

	//这个例子中obj0加入了自动释放池中
    id __autoreleasing obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    NSLog(@"class = %@", [obj1 class]);

打印结果:
请添加图片描述
池中出现了obj0

例三:

    id __autoreleasing obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    id __autoreleasing obj2 = obj1;
    NSLog(@"class = %@", [obj2 class]);

打印结果:
请添加图片描述
池中出现了obj0,且看到count为2
例四:

    id  obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    id __autoreleasing obj2 = obj1;
    NSLog(@"class = %@", [obj2 class]);

打印结果:
请添加图片描述
池中出现了obj0

总结: __weak修饰符并不会将对象加入到自动释放池,但是我们使用__weak修饰的对象一定要是本身已经加入到自动释放池的或者后续使用__autoreleasing将__weak所修饰的对象加入释放池

具体ARC规则:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必须遵守内存管理的方法名规则
  • 不要显式调用dealloc
  • 使用@autorelease块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体的成员
  • 显式转换id和void*

重要的点:

不能显式调用dealloc
dealloc无法释放不属于该对象的一些东西,需要我们重写时加上去,例如:

  • 通知的观察者,或KVO的观察者
  • 对象强委托/引用的解除(例如XMPPMannerger的delegateQueue)
  • 做一些其他的注销之类的操作(关闭程序运行期间没有关闭的资源)

官方文档的描述:

In the implementation of dealloc, do not call the implementation of superclass. You should try to avoid using dealloc to manage the lifetime of limited resources, such as file descriptors.
You never send a dealloc message directly. Instead, the dealloc method of the object is called by the runtime.
在dealloc的实现中,不要调用超类的实现。您应该尽量避免使用dealloc管理有限资源(如文件描述符)的生存期。
不要直接发送dealloc消息。与直接发送dealloc消息不同,对象的dealloc方法由运行时调用。

Special Considerations
When not using ARC, your implementation of dealloc must invoke the superclass’s implementation as its last instruction.
特别注意事项
当不使用ARC时,dealloc的实现必须调用父类(super)的实现作为它的最后一条指令 [super dealloc]

__bridge

在这里插入图片描述

属性关键字和所有权修饰符

属性关键字所有权修饰符
assign_unsafe_unretained
copy__strong
retain__strong
strong__strong
__unsafe_unretained__unsafe_unretained
weak__weak

ARC实现

__strong

自己生成并持有

OC代码:

	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的总结:

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

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

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

  4. 这个优化的好处就是我们省去了频繁去sidetable中读取计数信息,从而大大提高了效率,这样的话因为平时绝大多处操作都是普通的retain和release,所以都可以得到优化,而我们如果要读取引用计数的值的话就相对麻烦一点,需要sidetable和extra_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,并将散列表中一半引用计数取出来,然后将这一半引用计数减一在存到isa的extra_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相关流程图如下:
在这里插入图片描述

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

关于引用计数的相关runtime函数,详情可以参考这篇博客:iOS 从源码解析Runtime (五):聚焦objc_object(retain、release、retaincount)

非自己生成并持有

该部分学习自:OC Autorelease

例如该例子:

	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迭代结束时候释放。

例如: 主Runloop对Autoreleasepool管理的流程:
Runloop中,检测到触摸事件,创建事件,创建Autoreleasepool,autorelease对象加入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_retain是retainAutoreleaseReturnValue调用的。

一个问题:

为什么要传入(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来看函数运行是否出错。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值