iOS内存管理

一 iOS程序内存布局
二 Tagged Pointer内存地址优化
三 MRC概念讲解
四 引用计数的存储
五 weak指针原理
六 autorelease原理讲解
七 copy和mutableCopy
八 定时器的循环引用问题
一 iOS程序内存布局

要想了解iOS的内存管理方面的东西,首先我们要先了解iOS里程序的内存布局是怎样的,我们先看下面一张图:

iOS程序内存布局

我们可以看到内存地址由低到高依次是:
保留:程序保留的内存空间,用作其他用处,不需要我们管理
代码段:编译之后的代码
数据段
字符串常量:比如NSString *str = @"helloworld"
已初始化数据:已初始化的全局变量、静态变量等
未初始化数据:未初始化的全局变量、静态变量等

:函数调用开销,比如局部变量。 分配的内存空间地址越来越小

:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
内核区: 系统内核所需存储空间
下面我们代码演示来实际看一下,各个地址空间所存储的内容:

#import <Foundation/Foundation.h>
int a = 10;
int b;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int c = 20;
        static int d;
        int e;
        int f = 20;
        NSString *str = @"123";
        NSObject *obj =[[NSObject alloc] init];
        NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\n&str=%p\n&obj=%p",&a,&b,&c,&d,&e,&f,str,obj);
       
    }
    return 0;
}

输出结果:

&a=0x100002040
&b=0x10000204c
&c=0x100002044
&d=0x100002048
&e=0x7ffeefbff51c
&f=0x7ffeefbff518
&str=0x100001018
&obj=0x1006087b0

下面让我们来根据内存空间分布来分析各个变量所存储的位置对不对,地址是由低到高。

内存布局 对应变量 描述
保留
代码段(__TEXT) 编译之后的代码所存储的地方
数据段(__DATA):字符串常量 &str=0x100001018 比如NSString *str = @"helloworld"
数据段(__DATA):已初始化数据 &a=0x100002040
&c=0x100002044
包含全局变量,静态变量
数据段(__DATA):未初始化数据 &d=0x100002048
&b=0x10000204c
包含全局变量,静态变量
堆(heap) &obj=0x1006087b0 对象
栈(stack) &e=0x7ffeefbff51c
&f=0x7ffeefbff518
函数调用开销,比如局部变量,地址是由大到小的
内核区域

堆空间地址越来越大,栈空间地址越来越小,堆栈空间是挨着的,它们之间的空间有可能是堆空间也有可能是栈空间。

我们对于内存空间分布有了个大体的认识,下面我们再来了解一下iOS内存地址优化的一些内容。

二 Tagged Pointer内存地址优化

关于Tagged Pointer我们主要了解以下要点:

  • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

  • 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

  • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

  • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

  • objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

  • 如何判断一个指针是否为Tagged Pointer?
    iOS平台,最高有效位是1(第64bit)
    Mac平台,最低有效位是1

2.1 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值如下图所示:

没有使用Tagged Pointer之前

例如:
NSNumber *number = @10;
不使用Tagged Pointer那么它需要分配16个字节8个字节内存地址,以及8个字节来存储10,仅仅存储一个数字而已,就需要这么大的空间显然是浪费了,所以为了优化这种小对象的存储,就有了Tagged Pointer这种计数,它的主要做法是:Tag + Data详细解释如下。
2.2 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中.

 NSNumber *number1 = @4;
 NSNumber *number2 = @5;
 NSNumber *number3 = @300000000000999996;
 NSLog(@"%p,%p,%p",number1,number2,number3);

输出结果

0xc519e613390cfeb,0xc519e613390ceeb,0x102836a00

我们可以看出前两个地址跟最后一个地址是有很大的不同,在MAC平台下最低有效位是1 就是tagged Pointer。显然前两个是,后一个不是。为什么后一个不是呢,因为它的数字太大,所需存储超过了8个字节,所以无法进行优化了,于是就正常分配它的内容到堆空间去了。
下面有一个tagged Pointer的例子我们来看一下:

#import "ViewController.h"
@interface ViewController ()
@property (strong, nonatomic) NSString *name;

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self test1];
//    [self test2];   
}

- (void)test1 {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
        for (int i = 0; i < 1000; i++) {
            dispatch_async(queue, ^{
                // 加锁
                self.name = [NSString stringWithFormat:@"abcdefghijk"];
                // 解锁
            });
        }
}

- (void)test2 {
    dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
       
           for (int i = 0; i < 1000; i++) {
               dispatch_async(queue2, ^{
                   self.name = [NSString stringWithFormat:@"abc"];
               });
           }
}
@end

我们运行test2不会报错,运行test1就会报下面的错误


tagged Pointer

为什么回报上面的错误呢,两段代码几乎一摸一样,就是一个字符串长些一个短些。下面我们分析一下:
1 self.name就是调用 [self setName:name];
2 setName底层实现大致如下

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

3 Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储,使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中那么他就不是一个对象了。
结合以上三点我们可以看出:
1 当字符串很短的时候iOS会使用Tagged Pointer对NSString进行优化,此时NSString内存地址是Tag + Data,那么它就不是一个对象了,因此它也就没有setName方法,所以他也就不会调用setName方法而是直接赋值
2 那么当字符串很长,那么就不会使用Tagged Pointer技术,那么他就是一个普通的字符串对象,当开启多个线程并行调用setName方法时在调用[_name release];时有可能_name已经被上一个线程给release过了,它的引用计数为零了,那么这是再有一个线程调用release就会报坏内存访问。解决办法就是name声明成atomic,或者在调用self.name时加锁。
最后我们再给出一个方法判断一个一个指针是否为Tagged Pointer

static inline bool 
_objc_isTaggedPointer(const void *ptr) 
{
    return ((intptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1ULL<<63)
#else
#   define _OBJC_TAG_MASK 1
#endif

#if TARGET_OS_OSX && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

讲完了内存分配布局方面的内容,下面我们就讲一下内存管理的一些内容。

三 MRC概念讲解

在iOS中,使用引用计数来管理OC对象的内存
1 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
2 调用alloc/new/copy/mutableCopy会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
3 MRC指的是手动内存管理,在开发过程中需要开发者手动去编写内存管理的代码
下面让我们代码来实践一下。
要想使用MRC首先要关掉ARC(自动内存管理)如下图:

关掉ARC

先举一个简单的例子:


#import "People.h"

@implementation People

- (void)dealloc
{
    [super dealloc];
    NSLog(@"%s",__func__);
}
@end

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        People *people = [[People alloc] init]; //调用alloc引用计数+1
        NSLog(@"%zd",people.retainCount); //1
        [people release];  //调用release引用计数-1,当引用计数为零对象销毁,就会调用对象dealloc方法
    }
    return 0;
}

output:
2019-01-29 17:31:16.170303+0800 thread[59318:4909101] 1
2019-01-29 17:31:16.170950+0800 thread[59318:4909101] -[People dealloc]
Program ended with exit code: 0

当一个对象,引用计数不为零,而你又没有释放,就会造成内存泄漏

我们再来一个举个例子加深一下MRC手动管理内存的操作

#import <Foundation/Foundation.h>

@interface Dog : NSObject

- (void)run;

@end

#import "Dog.h"

@implementation Dog

- (void)run
{
    NSLog(@"%s", __func__);
}

- (void)dealloc
{
    [super dealloc];
    
    NSLog(@"%s", __func__);
}

@end

#import <Foundation/Foundation.h>

@interface Car : NSObject

@end

#import "Car.h"

@implementation Car

@end

#import <Foundation/Foundation.h>
#import "Dog.h"
#import "Car.h"

@interface Person : NSObject
{
    Dog *_dog;
    Car *_car;
    int _age;
}

- (void)setAge:(int)age;
- (int)age;

- (void)setDog:(Dog *)dog;
- (Dog *)dog;

- (void)setCar:(Car *)car;
- (Car *)car;

@end


#import "Person.h"

@implementation Person

- (void)setAge:(int)age
{
    _age = age;
}

- (int)age
{
    return _age;
}

- (void)setDog:(Dog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog retain];
    }
}

- (Dog *)dog
{
    return _dog;
}

- (void)setCar:(Car *)car
{
    if (_car != car) {
        [_car release];
        _car = [car retain];
    }
}

- (Car *)car
{
    return _car;
}

- (void)dealloc
{
//    [_dog release];
//    _dog = nil;
    self.dog = nil;
    self.car = nil;
    
    NSLog(@"%s", __func__);
    
    // 父类的dealloc放到最后
    [super dealloc];
}

@end
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Dog.h"

// Manual Reference Counting : MRC

void test()
{
    // 内存泄漏:该释放的对象没有释放
//    MJPerson *person1 = [[[MJPerson alloc] init] autorelease];
//    MJPerson *person2 = [[[MJPerson alloc] init] autorelease];
}

void test2()
{
    Dog *dog = [[Dog alloc] init]; // 1
    
    Person *person1 = [[Person alloc] init];
    [person1 setDog:dog]; // 2
    
    Person *person2 = [[Person alloc] init];
    [person2 setDog:dog]; // 3
    
    [dog release]; // 2
    
    [person1 release]; // 1
    
    [[person2 dog] run];
    [person2 release]; // 0
}

void test3()
{
    Dog *dog1 = [[Dog alloc] init]; // dog1 : 1
    Dog *dog2 = [[Dog alloc] init]; // dog2 : 1
    
    Person *person = [[Person alloc] init];
    [person setDog:dog1]; // dog1 : 2
    [person setDog:dog2]; // dog2 : 2, dog1 : 1
    
    [dog1 release]; // dog1 : 0
    [dog2 release]; // dog2 : 1
    [person release]; // dog2 : 0
}

void test4()
{
    Dog *dog = [[Dog alloc] init]; // dog:1
    
    Person *person = [[Person alloc] init];
    [person setDog:dog]; // dog:2
    
    [dog release]; // dog:1
    
    [person setDog:dog];
    [person setDog:dog];
    [person setDog:dog];
   
    [person release]; // dog:0
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

关于Person类里属性的声明以及get,set方法的生成,还有一个简单的方法

#import <Foundation/Foundation.h>
#import "MJDog.h"
#import "MJCar.h"

@interface MJPerson : NSObject
//{
//    MJDog *_dog;
//    MJCar *_car;
//    int _age;
//}
//
//- (void)setAge:(int)age;
//- (int)age;
//
//- (void)setDog:(MJDog *)dog;
//- (MJDog *)dog;
//
//- (void)setCar:(MJCar *)car;
//- (MJCar *)car;

@property (nonatomic, assign) int age;
@property (nonatomic, retain) MJDog *dog;
@property (nonatomic, retain) MJCar *car;

+ (instancetype)person;

@end

使用@property编译器会自动帮我们生成get,set方法,你属性描述不同,assgin/ retain/copy.那么生成的set方法的方式也不一样assgin的set方法描述值面量就不需要内存管理

@property (nonatomic, assign) int age;
- (void)setAge:(Age *)age
{
        _age = age;
}

retain

@property (nonatomic, retain) MJDog *dog;
- (void)setDog:(Dog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog retain];
    }
}

copy

@property (nonatomic, copy) MJDog *dog;
- (void)setDog:(Dog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog copy];
    }
}

OK 上面讲解了一个稍微复杂写的MRC使用的例子,那么在实际iOS开发中,我们在MRC环境下是如何开发的呢,我们举个简单的例子

#import "ViewController.h"

@interface ViewController ()
@property (retain, nonatomic) NSMutableArray *data;
@property (retain, nonatomic) UITabBarController *tabBarController;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tabBarController = [[[UITabBarController alloc] init] autorelease];
    
    self.data = [NSMutableArray array];
    
//    self.data = [[[NSMutableArray alloc] init] autorelease];
    
//    self.data = [[NSMutableArray alloc] init];
//    [self.data release];
    
//    NSMutableArray *data = [[NSMutableArray alloc] init];
//    self.data = data;
//    [data release];
}


- (void)dealloc {
    self.data = nil;
    self.tabBarController = nil;
    
    [super dealloc];
}


@end

根据上面的例子我们总结一下几点:

  • 页面中声明了对象属性要在页面销毁时清空,在controller的dealloc清空对象属性
  • 如果使用了alloc生成对象要及时释放
    self.data = [[NSMutableArray alloc] init];
    [self.data release];
  • 使用Fundation框架提供的便捷方法不需要自己做对象释放 [NSMutableArray array] //dictionnary,NSSet等

讲解到这里,我们就会发现,其实对象的内存管理挺容易的,完全就是要靠引用计数器来实现管理,而引用计数器可以通过代码来进行操纵。

所以,什么时候为对象发送retain消息,什么时候为对象发送release消息成为了内存管理中的重中之重。

那么,什么时候为对象发送retain消息呢,什么时候为对象发送release消息呢?

这就是内存管理的原则,具体原则内容如下:
A. 当对象被创建出来以后,对象的引用计数默认为1,所以在这个对象使用完毕以后我们应该为这个对象发送一条release消息,保证这个对象在使用完毕以后引用计数变为0,并且占用的内存空间被回收。
B. 当对象被别人使用的时候,别人就会为这个对象发送retain消息,表示使用的人多了一个,当别人不再使用对象的时候,别人就会为对象发送release消息,表示使用的人少了一个。
C. 当对象还有人正在使用的时候,对象就不应该被回收(引用计数器不为0)
D. 谁发送了retain消息,当它使用完毕之后,谁就应该发送release消息
以上就是MRC内存管理的基本原理,接下来上面我们一直提到引用计数,那么对象的引用计数存储在哪呢,下面我们一起来探究一下。

四 引用计数的存储

我们之前讲过的isa指针结构,我们现在来回顾一下:

union isa_t {
     uintptr_t nonpointer        : 1;                                       \
      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;                                       \
      uintptr_t extra_rc          : 19
};

ISA_BITFIELD ISA位域信息解释
nonpointer
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息
has_assoc
是否有设置过关联对象,如果没有,释放时会更快
has_cxx_dtor
是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
shiftcls
存储着Class、Meta-Class对象的内存地址信息
magic
用于在调试时分辨对象是否未完成初始化
weakly_referenced
是否有被弱引用指向过,如果没有,释放时会更快
deallocating
对象是否正在释放
extra_rc
里面存储的值是引用计数器减1
has_sidetable_rc
引用计数器是否过大无法存储在isa中
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
我们可以看到arm64架构之后isa 不仅存储着Class、Meta-Class对象的内存地址还存储着其他更多信息, Class、Meta-Class对象的内存地址被存储在shiftcls 中占用33位
uintptr_t shiftcls : 33
所以我们从一个对象isa指针看到的并不是对象真实的内存地址,我们要做一下位运算才能得到真实的地址要 & MASK.

我们可以看到extra_rc就是存储引用计数器的。所以在64bit中,引用计数可以直接存储在优化过的isa指针中 。extra_rc只有19位,那如果存不下怎么办呢,我们可以看到还有has_sidetable_rc字段,当不够存储的时候has_sidetable_rc就会被置为1,如果它为1的话引用计数就会存储在SideTable类中 。

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

引用计数就会存储在SideTable 的refcnts散列表里面。到此我们可以知道,引用计数会存在于两个地方:
1 引用计数可以直接存储在优化过的isa指针中
2 引用计数也可能会存储在SideTable类中

下面我们通过objc源码来看一下引用计数的存取过程,当我们要获取一个对象的引用计数我们会调用它的retainCount方法

[people retainCount]

我们看一下retainCount的源码实现:NSObject.mm

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}
下一步
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this; //如果是TaggedPointer优化过的直接返回

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits); //获取isa指针里的内容
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc; //extra_rc里面存储的值是引用计数器减1,所以返回引用计数要加1
        if (bits.has_sidetable_rc) { //引用计数器是否过大无法存储在isa中
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
下一步
objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this]; //根据当前对象获取SideTables
    RefcountMap::iterator it = table.refcnts.find(this);//取出sidetable里的refcnts散列表数据
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT; //返回引用计数
}

当调用retain方法引用计数会加1我们来看一下它是怎么实现的

 [people retain] //返回这个对象 引用计数加1

源码实现:NSObject.mm

- (id)retain {
    return ((id)self)->rootRetain();
}
下一步
objc_object::rootRetain()
{
    return rootRetain(false, false);
}
下一步
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)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        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++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}
下一步
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE; //引用计数+1
    }
    table.unlock();

    return (id)this;
}

#define SIDE_TABLE_RC_ONE            (1UL<<2) 

接下来当调用 release方法引用计数会减1,我们来看一下

[people release]

源码实现:NSObject.mm

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

下一步
objc_object::rootRelease()
{
    return rootRelease(true, false);
}
下一步
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;
********
}
下一步
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE; //引用计数减1
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) { //引用计数为零要执行dealloc方法
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

OK到此我们看过了引用计数的加减以及获取的源码过程,相信你对引用计数的存储过程又有一个更深的认识了。下面还有一个跟引用计数有关的东西那就是weak指针,下面让我们来看一下。

五 weak指针原理

weak指针对对象是弱引用关系,不持有引用对象,引用对象销毁了,弱指针对象也会被销毁,我们来看一下示例代码:

#import <UIKit/UIKit.h>
@interface People : NSObject

@end

#import "People.h"

@implementation People
  
- (void)dealloc
{
    NSLog(@"%s",__func__);
}

@end
#import "ViewController.h"
#import "People.h"
@interface ViewController ()

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
       
    __strong People *p1;
    __weak People *p2;
    __unsafe_unretained People *p3;
    
    NSLog(@"start");
    {
        People *people = [[People alloc] init];
        p1 = people;
    }
    NSLog(@"end");
}

@end
output:
2019-01-30 12:40:35.029133+0800 runloop[68072:5735314] start
2019-01-30 12:40:35.029265+0800 runloop[68072:5735314] end
2019-01-30 12:40:35.029370+0800 runloop[68072:5735314] -[People dealloc]

p1 是强引用,当离开中间代码块的时候p1并不会立马销毁,只有当p1声明的函数体执行完,它才会销毁。

- (void)viewDidLoad {
    [super viewDidLoad];
       
    __strong People *p1;
    __weak People *p2;
    __unsafe_unretained People *p3;
    
    NSLog(@"start");
    {
        People *people = [[People alloc] init];
        p2 = people;
    }
    NSLog(@"end");
}
output:
2019-01-30 12:47:10.275646+0800 runloop[68132:5739736] start
2019-01-30 12:47:10.275786+0800 runloop[68132:5739736] -[People dealloc]
2019-01-30 12:47:10.275875+0800 runloop[68132:5739736] end

当是弱引用的时候,当离开中间代码块,弱引用对象会立即释放掉。

- (void)viewDidLoad {
    [super viewDidLoad];
       
    __strong People *p1;
    __weak People *p2;
    __unsafe_unretained People *p3;
    
    NSLog(@"start");
    {
        People *people = [[People alloc] init];
        p3 = people;
    }
    NSLog(@"end");
}
output:
2019-01-30 12:47:10.275646+0800 runloop[68132:5739736] start
2019-01-30 12:47:10.275786+0800 runloop[68132:5739736] -[People dealloc]
2019-01-30 12:47:10.275875+0800 runloop[68132:5739736] end

当是__unsafe_unretained引用的时候,当离开中间代码块,对象会立即释放掉。那么它跟__weak有什么区别呢,区别就是它是不安全的,对象销毁了,但是对象没有清空,对象地址还在,再次访问会出现野指针访问,而__weak会清空对象。我们知道当一个对象要销毁的时候会调用对象的delloc方法,我们来看一下它的底层实现:
大致流程如下:
1 dealloc
2 _objc_rootDealloc
3 rootDealloc
4 object_dispose
5 objc_destructInstance、free

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

id object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //清除对象的成员变量
        if (cxx) object_cxxDestruct(obj); 
        //清除对象的关联对象
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}


inline void objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}


// Slow path of clearDeallocating() 
// for objects with nonpointer isa
// that were ever weakly referenced 
// or whose retain count ever overflowed to the side table.
NEVER_INLINE void objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}


/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[I];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}


/** 
 * Return the weak reference table entry for the given referent. 
 * If there is no entry for referent, return NULL. 
 * Performs a lookup.
 *
 * @param weak_table 
 * @param referent The object. Must not be nil.
 * 
 * @return The table of weak referrers to this object. 
 */
static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

总体流程就是,程序运行时将弱引用存到一个哈希表中,当对象obj要销毁的时候,哈希函数根据obj地址获取到索引,然后从哈希表中取出obj对应的弱引用集合weak_entries,遍历weak_entries并一一清空,弱引用常在存在循环引用的情况下使用,比如delegate,block里等。
好了讲完weak指针,我在再来看看autorelease方面的东西。

六 autorelease原理讲解

在MRC环境下释放一个对象我们可以使用[people release],我们还可以使用如下方式

 @autoreleasepool {
        People *people = [[[People alloc] init] autorelease];
       // [people release]  调用release方法后就不能在调用people其他方法了
        people.name=@"zhang shan";//调用autorelease后还可以继续操作people对象
    }

在自动释放池里面创建对象的同时调用autorelease对象,这样对象它会自动释放,并且还有其他好处,代码注释里我已经写出来了。那么autoreleasepool,autorelease,它们底层是怎么实现的,它们是如何配合做对象释放的呢,我们接着往下看。
我们把上面的代码转换成c++代码,来看一下它底层转换成了什么。

转换这段代码
@autoreleasepool {
        People *people = [[[People alloc] init] autorelease];
    }

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        People *people = ((People *(*)(id, SEL))(void *)objc_msgSend)((id)((People *(*)(id, SEL))(void *)objc_msgSend)((id)((People *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("People"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
    }
    return 0;
}


struct __AtAutoreleasePool {
 //构造函数,结构体创建的时候
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
//析构函数,结构体销毁的时候调用
  ~__AtAutoreleasePool()  //{objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

通过上面转换的代码,我们精简一下,如下:

atautoreleasepoolobj = objc_autoreleasePoolPush();
People *people = [[[People alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);

我们可以看到有一个自动释放池开始有一个入栈操作,在释放池结束的时候有一个出栈的操作。那么objc_autoreleasePoolPush objc_autoreleasePoolPop底层是怎么样的呢,我们来看一下objc源码.
NSObject.mm

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

 static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);//创建一个新的autoreleasePage
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

 static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

 static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) { //hotPage就是当前页面
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

我们可以看到这两个方法底层实现跟一个 AutoreleasePoolPage有关系,下面我们来看一下AutoreleasePoolPage的结构

class AutoreleasePoolPage 
{
    
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2  //4096
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

下面我们来讲一下AutoreleasePoolPage 基本结构

  • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址

  • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

  • AutoreleasePoolPage它有自己的线程一个AutoreleasePoolPage对应一个线程

  • AutoreleasePoolPage通过双向链表连接在一起 那么parent就是它的前置指针,child就是它的后置指针,链表第一个元素的AutoreleasePoolPage的parent为null ,链表最后一个元素的AutoreleasePoolPage的child为null .

  • AutoreleasePoolPage为什么要使用双向链表,那是因为一个AutoreleasePoolPage对象占用4096字节除了自己成员变量其他的用来放autorelease对象的地址,当你往AutoreleasePoolPage加入很多对象时,一个AutoreleasePoolPage对象他就放不下了,所以它采用链表的形式进行了扩展。
    AutoreleasePoolPage整体结构图如下:


    AutoreleasePoolPage
  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

  • id *next指向了下一个能存放autorelease对象地址的区域

    AutoreleasePoolPage入栈出栈操作

    下面我们通过实际代码操作来看一下

#import <Foundation/Foundation.h>
#import "People.h"
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        People *people = [[[People alloc] init] autorelease];
        _objc_autoreleasePoolPrint();
        @autoreleasepool {
            People *people = [[[People alloc] init] autorelease];
            _objc_autoreleasePoolPrint();
                @autoreleasepool {
                    People *people = [[[People alloc] init] autorelease];
                    _objc_autoreleasePoolPrint();
                }
        }
    }
    return 0;
}
output:
objc[68876]: ##############
objc[68876]: AUTORELEASE POOLS for thread 0x1000d2dc0
objc[68876]: 2 releases pending.
objc[68876]: [0x101806000]  ................  PAGE  (hot) (cold)
objc[68876]: [0x101806038]  ################  POOL 0x101806038
objc[68876]: [0x101806040]       0x102033960  People
objc[68876]: ##############
objc[68876]: ##############
objc[68876]: AUTORELEASE POOLS for thread 0x1000d2dc0
objc[68876]: 4 releases pending.
objc[68876]: [0x101806000]  ................  PAGE  (hot) (cold)
objc[68876]: [0x101806038]  ################  POOL 0x101806038
objc[68876]: [0x101806040]       0x102033960  People
objc[68876]: [0x101806048]  ################  POOL 0x101806048
objc[68876]: [0x101806050]       0x10202daf0  People
objc[68876]: ##############
objc[68876]: ##############
objc[68876]: AUTORELEASE POOLS for thread 0x1000d2dc0
objc[68876]: 6 releases pending.
objc[68876]: [0x101806000]  ................  PAGE  (hot) (cold)
objc[68876]: [0x101806038]  ################  POOL 0x101806038
objc[68876]: [0x101806040]       0x102033960  People
objc[68876]: [0x101806048]  ################  POOL 0x101806048
objc[68876]: [0x101806050]       0x10202daf0  People
objc[68876]: [0x101806058]  ################  POOL 0x101806058
objc[68876]: [0x101806060]       0x10056c1e0  People
objc[68876]: ##############

我们可以看到,即使自动释放池嵌套也是往同一AutoreleasePoolPage添加autorelease对象地址只不过没到一个新的autoreleasepool会添加一个POOL_BOUNDARY(nil)。
那么当超过一页的时候呢,

#import <Foundation/Foundation.h>
#import "People.h"
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for(int i = 0; i < 1000;i++) {
            People *people = [[[People alloc] init] autorelease];
        }
         _objc_autoreleasePoolPrint();
    }
    return 0;
}
output:
objc[68958]: ##############
objc[68958]: AUTORELEASE POOLS for thread 0x1000d2dc0
objc[68958]: 1001 releases pending.
objc[68958]: [0x100801000]  ................  PAGE (full)  (cold)
objc[68958]: [0x100801038]  ################  POOL 0x100801038
objc[68958]: [0x100801040]       0x1005147f0  People
objc[68958]: [0x100801048]       0x100513840  People
objc[68958]: [0x100801050]       0x100512a80  People
objc[68958]: [0x100801058]       0x100512300  People
objc[68958]: [0x100801060]       0x100505390  People
....省略
objc[68958]: [0x100801fd8]       0x10051ac50  People
objc[68958]: [0x100801fe0]       0x10051ac60  People
objc[68958]: [0x100801fe8]       0x10051ac70  People
objc[68958]: [0x100801ff0]       0x10051ac80  People
objc[68958]: [0x100801ff8]       0x10051ac90  People
objc[68958]: [0x100803000]  ................  PAGE  (hot) 
objc[68958]: [0x100803038]       0x10051aca0  People
objc[68958]: [0x100803040]       0x10051acb0  People
objc[68958]: [0x100803048]       0x10051acc0  People
....省略
objc[68958]: [0x100803f98]       0x10051cb60  People
objc[68958]: [0x100803fa0]       0x10051cb70  People
objc[68958]: [0x100803fa8]       0x10051cb80  People
objc[68958]: [0x100803fb0]       0x10051cb90  People
objc[68958]: ##############
Program ended with exit code: 0

中间有一行objc[68958]: [0x100803000] ................ PAGE (hot)我们可以看到当一页放不下的时候他就会换页,最后我们再看一下[[[People alloc] init] autorelease]autorelease干了什么,我们看一下它的源码实现

 static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();//获取当前页面
        if (page && !page->full()) { //如果当前页面没满,直接把当前地址添加进去
            return page->add(obj);
        } else if (page) { //否则就新建一个页面添加进去。
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);//新建一个page
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

最后我们再总结下:@autoreleasepool {}就相当于建了一个AutoreleasePoolPage页面(一段存储空间),[object autorelease];就是向这个页面添加要释放的内存地址,AutoreleasePoolPage满了的时候它会新建一个AutoreleasePoolPage进行扩容,并且以双向链表的形式展现,当遇到autoreleasepool结束时就会从AutoreleasePoolPage中查找带释放对象的地址并倒序调用它们的release方法,直到遇到POOL_BOUNDARY。

最后iOS开发中如果没有@autoreleasepool {}调用[object autorelease];那么对象什么时候会释放呢,这时就是Runloop和Autorelease的关系了。

Runloop和Autorelease 的关系

  • OS在主线程的Runloop中注册了2个Observer
    第1个Observer监听了kCFRunLoopEntry事件,会调用 objc_autoreleasePoolPush()
  • 第2个Observer
  • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
  • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
    所以每一个调用了autorelease方法的对象会在下一次runloop休眠之前进行释放。
    OK自动释放池讲完了,接下来我们再看一下copy,mutableCopy内存管理问题
七 copy和mutableCopy

我们知道当对象调用copy和mutableCopy时都会使对象引用计数器加1,但是这两个方法针对不同对象还是有区别的。
copy:不可变拷贝,产生的不可修改副本
mutableCopy: 可变拷贝,产生的可修改的副本
我们简单的看一下调用它们的示例代码:

#import <Foundation/Foundation.h>
#import "People.h"
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSString *str1 = [NSString stringWithFormat:@"hello world 你好世界"];
        NSString *str2 = [str1 copy];
        NSString *str3 = [str1 mutableCopy];
        NSLog(@"%zd,%zd,%zd",str1.retainCount,str2.retainCount,str3.retainCount);
       
    }
    return 0;
}
output:
2019-01-30 16:47:09.647307+0800 thread[69323:5855250] 2,2,1

下面我们来解释下为什么它们的引用计数打印出来使2,2,1 首先:
1 str1调用alloc 引用计数加1 str : 1
2 str2 是str1 的copy副本,引用计数加1 此时调用copy只是把str1的指针地址copy给str2,所以它们两个是同一个对象 str引用计数一开始为1这里又加1,所以它为2,str2相应的也是2
3 str3是str1的可变副本,所以会生成一个新的地址,把str1内容拷贝给新地址,同时把新地址赋值给str3,此时str1跟跟str3并不是同一个对象,所以str3引用计数加1并不影响str1.
这里NSString 是一个不可变的字符串,那如果NSMutableString调用它的copy,mutableCopy ,是什么情况呢,我们来看一下:

#import <Foundation/Foundation.h>
#import "People.h"
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSMutableString *str1 = [NSMutableString stringWithFormat:@"hello world 你好世界"];
        NSString *str2 = [str1 copy];
        NSString *str3 = [str1 mutableCopy];
        NSLog(@"%zd,%zd,%zd",str1.retainCount,str2.retainCount,str3.retainCount);
       
    }
    return 0;
}
output
2019-01-30 16:58:20.869114+0800 thread[69373:5860899] 1,1,1
Program ended with exit code: 0

这里只有中间一步不一样,当调用一个可变对象的copy方法,会生成一个新对象,并把一个新对象的地址赋值给str2,所以这里三个对象都是独立的对象,互不影响。
这里我们介绍两个概念,深拷贝,浅拷贝。
深拷贝: 在计算机中开辟了一块内存地址用于存放复制的对象,深拷贝出来的对象跟原来的对象没什么关系了。
浅拷贝:指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。
上面NSString的copy就是浅拷贝,NSMutableString的cop就是深拷贝,其实这种现象不仅仅存在于NSString对象,在其他fundation框架中同时拥有可变和不可变对象的对象也有此现象,下面我们给出一张表,如下:

Fundation对象 copy mutableCopy
NSString NSString
浅拷贝
NSMutableString
深拷贝
NSMutableString NSString
深拷贝
NSMutableString
深拷贝
NSArray NSArray
浅拷贝
NSMutableArray
深拷贝
NSMutableArray NSArray
深拷贝
NSMutableArray
深拷贝
NSDictionary NSArray
浅拷贝
NSMutableDictionary
深拷贝
NSMutableDictionary NSDictionary
深拷贝
NSMutableDictionary
深拷贝

有此表我们可以总结出:除了不可变对象的copy是浅拷贝,其它的都是深拷贝。OK对于copy,mutableCopy的问题我们就讲到这儿吧,最后我们再讲下定时器的循环引用问题。

八 定时器的循环引用问题

CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用 :

#import "ViewController.h"
@interface ViewController ()
@property(strong,nonatomic) CADisplayLink *link;

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test)];
    [self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
       
}
-(void)test{
    NSLog(@"%s",__func__);
}

- (void)dealloc
{
    [self.link invalidate];
}

@end

CADisplayLink、会对target产生强引用页面关闭定时器并不会停止。

#import "ViewController.h"
@interface ViewController ()
@property(strong,nonatomic) NSTimer *timer;

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
       
}
-(void)test{
    NSLog(@"%s",__func__);
}

- (void)dealloc
{
    [self.timer invalidate];
}

@end

NSTimer、会对target产生强引用页面关闭定时器并不会停止。
解决办法我们以NStimer为例子讲解:
1 使用block;
2 使用一个中间代理


使用block;
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
}

使用一个中间代理
#import <Foundation/Foundation.h>

@interface MiddleProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end


#import "MiddleProxy.h"

@implementation MiddleProxy

+ (instancetype)proxyWithTarget:(id)target
{
    MiddleProxy *proxy = [[MiddleProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MiddleProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
   
}

上面是使用继承NSProxy作为代理对象来做中间消息转发,这样效率高,NSProxy继承的类会直接进入消息转发,比继承NSObject作为代理对象 然后走普通的方法调用流程,最后才会进入消息转发流程的效率要高的多。
好了到此iOS内存管理零零散散讲了这么多就算完了,希望对你能有所收获。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容