OC 内存管理

Posted by LML on July 27, 2020

前言

本系列为 iOS Memory 相关内容作为主题的第一篇。主要围绕引用计数、ARC/MRC展开。

内存管理基础——引用计数

OC 中 class 实质是什么?

首先要区分两个概念

  • Class
@interface BDViewController : UIViewController
@property(nonatomic,copy) NSString *name;
@property(nonatomic,strong)NSMutableArray*ary;
@end

@implementation BDViewController
@end
  • Class 实例
BDViewController *vc = [BDViewController new];

OC 中的 class 和 class 实例的实质均是结构体,只不过二者对应的结构体不太一样。

Class 实质

Class 结构体可以从定义和 mach-o 文件分析得到(如下图),可以看出结构体维护了:

  • Isa 指针指向 metaclass
  • Superclass 指针指向 super class
  • Cache 指针指向列表,用于缓存该class近期调用过的方法指针,加快下次调用的速度
  • data指针指向另外一个结构容,该结构体维护

  • Class name
  • Method list
  • Protoclo list
  • Variables list
  • Property list

结构体中的内容完整的描述了一个class 的内容

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

Class 实例实质

Class 实例维护的结构体,维护了:

  • Isa 指针指向 class(实际不仅仅是一个指针,是一个 union,其中部分指向了class )
  • 其余的 propery 变量
    • 访问 class 实例属性的时候根据 isa 指向的 class 的 IVAR list 获取属性偏移量,然后在该结构体(实例 class 结构体)的偏移处获取对应属性的值
  union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
# if __arm64__
#   define ISA_MASK        0x00000001fffffff8ULL
#   define ISA_MAGIC_MASK  0x000003fe00000001ULL
#   define ISA_MAGIC_VALUE 0x000001a400000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
        uintptr_t magic             : 9;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x0000000000000001ULL
#   define ISA_MAGIC_VALUE 0x0000000000000001ULL
    struct {
        uintptr_t indexed           : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 14;
#       define RC_ONE   (1ULL<<50)
#       define RC_HALF  (1ULL<<13)
    };
# else
    // Available bits in isa field are architecture-specific.
#   error unknown architecture
# endif
// SUPPORT_NONPOINTER_ISA
#endif
};

那么引用计数的原理是什么呢?

引用计数原理

引用计数与 class 实例相关,而不是与 Class 相关。我们需要弄明白一个class 实例的引用计数保存在哪里?

Class 实例结构体内有个 isa 指针,上面说该指是一个 union,部分内容指向 class 实例对应的 class。因为用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。

SUPPORT_NONPOINTER_ISA 用于标记是否支持优化的 isa 指针,其字面含义意思是 isa 的内容不再是类的指针了,而是包含了更多信息,比如引用计数,析构状态,被其他 weak 变量引用情况

  • indexed 0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
  • has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
  • has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
  • shiftcls 类的指针
  • magic 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
  • weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
  • deallocating 表示该对象是否正在析构
  • has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
  • extra_rc 存储引用计数值减一后的结果

在 64 位环境下,优化的 isa 指针并不是就一定会存储引用计数,毕竟用 19bit (iOS 系统)保存引用计数不一定够。has_sidetable_rc 的值如果为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中。

SideTables

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个”s”不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。

当我们通过SideTables[key]来得到SideTable的时候,SideTable的结构如下:

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

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }
    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }
    // Address-ordered lock discipline for a pair of side tables.
    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

SideTable 主要分为 3 部分:

  1. 引用计数器 RefcountMap:refcnts

    对象具体的引用计数数量是记录在这里的。注意RefcountMap其实是个C++ 的Map。为什么Hash以后还需要个Map?其实苹果采用的是分块化的方法。

    举个例子:假设现在内存中有16个对象,0x0000、0x0001、…… 0x000e、0x000f,创建一个SideTables[8]来存放这16个对象,那么查找的时候发生Hash冲突的概率就是八分之一。假设SideTables[0x0000]和SideTables[0x0x000f]冲突,映射到相同的结果。SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一个SideTable

    苹果把两个对象的内存管理都放到里同一个SideTable中。你在这个SideTable中需要再次调用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)来找到他们真正的引用计数器。

    这里是一个分流。内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个 Map 才能精确的定位到我们要找的对象的引用计数器。

  2. 维护weak指针的结构体 weak_table_t weak_table;
    这里是一个两层结构

  • 第一层:
    • weak_entry_t *weak_entries, 是一个数组,通过循环遍历来找到对应的entry。
    • num_entries 是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。
  • 第二层weak_entry_t的结构包含3个部分
    • Referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
    • Referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
    • inline_referrers:只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

引用计数原理

了解了引用计数保存在哪里,我们可以比较清晰的知道了引用计数的原理

  • Retain
    • 引用计数+1.先是isat本身的加1,超过承载的最大值的时候注意访问sidetable,然后加1
  • Release 的时候
    • 计数减1.若等于0了则开始dealloc对象
    • Weak 设置为nil,且清理sidetable
    • 关联对象清理,根据isa 判断是否又关联对象,去全局对象中清理与当前实例关联的对象

其余需要注意的是:

  • 即使引用计数没有超过isa承载的最大值,如果用到了weak,也需要sidetable
  • 申请了一个 weak 会把它挂到 sidetable
  • weak 出作用域了,会去 sidetable 删除

MRC 下内存管理

规则

四个法则

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不在需要自己持有的对象的时候,释放。
  • 非自己持有的对象无法释放。

如下是四个黄金法则对应的代码示例:

例子1
 //自己生成并持有该对象
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];

//持有非自己生成的对象
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有,因为arry内部对返回值调用了autorelease
[obj retain]; // 自己持有对象

例子2
// 不在需要自己持有的对象的时候,释放
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象

例子3
//非自己持有的对象无法释放
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; // 此时将运行时crash 或编译器报error
其中 非自己生成的对象,且该对象存在,但自己不持有 这个特性是使用 autorelease 来实现的,示例代码如下:
- (id) getObjNotRetain {
    id obj = [[NSObject alloc] init]; // 自己持有对象
    [obj autorelease]; // 取得的对象存在,但自己不持有该对象
    return obj;
} 

关键字

关键字会影响默认的 set 方法的实现

assign

指针赋值时不对引用计数操作,如果没有及时正确置nil,可能就会产生悬空指针。

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

retain

  • 会对引用计数进行操作,进行+1
  • Retain 返回值与被retain的对象指向同一块内存
- (NSString *)setName:(NSString *)name {
    if (_name!=name) {
        [_name release];
        _name = [name retain];
    }
    return _name;
}

copy

  • Copy 不是操作引用计数加1,而是调用 copywithzone 函数,重新申请内存,用被copy的对象对新的内存进行初始化
  • 返回值与被copy对象指向的内存不是同一块内存
  • copy修饰的属性需要实现 NSCopying 协议,实现 copywithzone 函数
默认实现
- (NSString *)setName:(NSString *)name {
    if (_name!=name) {
        [_name release];
        _name = [name copy];
    }
    return _name;
}

一个自定义对象,若想提供 copy方法的调用,必须遵循 NSCopying 的协议。实现协议中规定的方法:- (id)copyWithZone:(NSZone *)zone ,且:

  • 若父类不是 NSObject,则父类必须也遵守该协议
  • 父类不是 NSObject,子类必须先调用父类的(id)copyWithZone:(NSZone *)zone方法

ARC 下内存管理

同样关键字会影响默认的 set 方法的实现。系统自动帮忙在合适的位置调用 retain 和 release 方法

copy

ARC 下 copy 修饰属性和 MRC 下一样,实际上调用了copy方法,需要对应属性遵守NSCopy协议。

默认实现
- (NSString *)setName:(NSString *)name {
    if (_name!=name) {
       _name = [name copy];
    }
    return _name;
}  

Strong

与MRC下的retain类似,引用计数加1,指向同一块内存。Block 存在一定特殊性,下一篇文章详细介绍Block。

Weak

引用计数不加1,但是当关联的strong修饰的对象dealloc后,weak修饰的对象自动设为nil,原理在上面介绍 class 实例的时候已经介绍。

assign

不会引起引用计数的变化,有野指针的风险

Autorelease原理

实现原理

AutoreleasePool 并没有单独的结构,而是由若干个 AutoreleasePoolPage 以双向链表的形式组合而成:

  • Parent 和 child 指向上下两个 page
  • next 指针指向 poolpage 有内容的下一个位置:
  • POOL_BOUNDARY 是一个边界对象 nil,用来区别每个page即每个 AutoreleasePool 边界
    • 边界指经常存在嵌套的 AutoreleasePool,POOL_BOUNDARY用于标识嵌套的pool的边界,每次释放pool,释放到POOL_BOUNDARY就停止,代表一个pool已经释放完毕

ARC 下创建AutoreleasePool:

@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}
编译器改写成
void *context = objc_autoreleasePoolPush();  
// {}中的代码  
objc_autoreleasePoolPop(context);  

⚠️ 只有 __autorelease 修饰才加入了自动释放池,不是自动加入的,默认是strong修饰

MRC 下创建AutoreleasePool:

NSAutoreleasePool *pool = [NSAutoreleasePool new];
id obj = [NSobject new];
[obj autoreleasiing];
[pool drain];

⚠️只有调用了 autorelease 方法才加入了自动释放池,不是自动加入的

那么 pool 创建和销毁的时候都发生了什么呢?一个对象调用 autorelease方法或者被 __ autoreleaseing 修饰又发生了什么呢?

AutoreleasePool创建

AutoreleasePool 的创建调用了方法 objc_autoreleasePoolPush ,在这里会进入一个比较关键的方法 autoreleaseFast,并传入边界对象 (POOL_BOUNDARY):

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}
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);
   }
}

可以看出逻辑是

  • hotpage指当前正在使用的page
  • 当前没有 hotpage 的时候,例如当前线程第一次创建 AutoreleasePool,通过 autoreleaseNoPage方法先创建一个 AutoreleasePool
  • 若存在hotpage且没有满,只需要插入一个哨兵就可以了,例如一个嵌套的pool
  • 若当前有hotpage且满了需要申请新的pge,根据parent 和 child 指针建立双向链表的关系,然后插入一个哨兵,代表一个嵌套的pool

AutoreleasePool销毁

pool 的销毁调用的是 objc_autoreleasePoolPop(atautoreleasepoolobj); ,该过程主要分为两步:

  • page->releaseUntil(stop),对栈顶(page->next)到stop地址(POOL_SENTINEL)之间的所有对象调用objc_release(),进行引用计数减1
  • 清空page对象 page->kill()
    • 当前的page没有对象后清理page节省内存空间

一个对象加入 AutoreleasePool

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方法的调用栈中,最终都会调用上面提到的 autoreleaseFast方法,将当前对象加到AutoreleasePoolPage 中,和加入哨兵对象的流程一样

AutoreleasePool 与 runloop

  • 主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建 Autoreleasepool,监听 Runloop 的休眠和唤醒,休眠时 pop autoreleasePool,唤醒时 push autoreleasepol
    • The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event
  • 非主线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?
    • 具体参考:这里
    • 简要回答:不会出现内存泄漏,会以懒加载的形式(非主线程以一次调用__autoreleasing 修饰符)通过autoreleaseNopagePool方法创建 hotpoolpage,生命周期与线程一致。即非主线程即使不开启 runloop 也不会产生内存泄漏。
    • 如果非主线程人为的开启了 runloop,那么原理与主线程一致
  • 自定义的autoreleasePool 将变量生命延长到了pool pop时候
  • 每个线程都拥有自己的 AutoreleasePool 双向链表,保证能正确释放 pool 里面的对象。

函数参数返回值

参数

暂时需要更深入的了解

返回值

如果一个函数的返回值是指向一个对象的指针,那么这个对象肯定不能在函数返回之前进行 release,否则调用者在调用这个函数时得到的就是野指针了,在函数返回之后也不能立刻就 release,因为我们不知道调用者是不是 retain 了这个对象,如果我们直接 release 了,可能导致后面在使用这个对象时它已经成为 nil 了。

为了解决这个纠结的问题, Objective-C 中对对象指针的返回值进行了区分,一种叫做 retained return value ,另一种叫做 unretained return value 。前者表示调用者拥有这个返回值,后者表示调用者不拥有这个返回值,按照“谁拥有谁释放”的原则,对于前者调用者是要负责释放的,对于后者就不需要了。

按照苹果的命名 convention,以 alloc , copy , init , mutableCopy 和 new 这些方法打头的方法,返回的都是 retained return value,例如 [[NSString alloc] initWithFormat:] ,而其他的则是 unretained return value,例如 [NSString stringWithFormat:] 。我们在编写代码时也应该遵守这个 convention 我们分别在 MRC 和 ARC 情况下,分析一下两种返回值类型的区别。

MRC 返回值

在 MRC 中我们需要关注这两种函数返回类型的区别,否则可能会导致内存泄露。

  • 对于 retained return value,需要负责释放,否则内存泄漏
@property (nonatomic, retain) NSObject *property;

//错误写法: retain count 增加到 2(alloc本身+1 retain 的 set +1),而我们少执行了一次 release,就会导致 retain count 不能被减为 0
self.property = [[NSObject alloc] init];
[_property release];
_property = nil;

//正确写法
self.property = [[[NSObject alloc] init] autorelease];
[_property release];
_property = nil;

//错误写法(谁拥有谁释放)
id test = [[[NSObject alloc] init] autorelease];
//正确写法
id test = [[NSObject alloc] init];
[test release];
test  = nil;

例子中 alloc init 是 retained return value,调用者拥有这个返回值如果是一个非属性,则直接接收返回值就行了,拥有了返回值。但是如果是retain 修饰的属性,必须对返回值 atuorelease,因为 retain 修饰的 set 方法的实现如下:

-(void) setName: (id) nameStr
{
      if (name != nameStr) {
        [name release];
        name = [nameStr retain];
     }
}
  • 对于 unretained return value,不需要负责释放
    • 此类方法的结尾已经对返回值调用了autorelease 方法,加入了自动释放池
  • 我们在编写自己的代码时,也应该遵守上面的原则,同样是使用 autorelease:
// 注意函数名的区别
+ (MyCustomClass *) myCustomClass
{
    return [[[MyCustomClass alloc] init] autorelease]; // 需要 autorelease
}
- (MyCustomClass *) initWithName:(NSString *) name
{
    return [[MyCustomClass alloc] init]; // 不需要 autorelease
}

ARC返回值

在 ARC 中我们完全不需要考虑这两种返回值类型的区别,ARC 会自动加入必要的代码,因此我们可以放心大胆地写:

  • retain_return_value

对于 retained return value, Clang 是这样做的:

When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, before leaving all local scopes.

When receiving a return result from such a function or method, ARC releases the value at the end of the full-expression it is contained within, subject to the usual optimizations for local values.

NSObject * a = [[NSObject alloc] init];
self.property = a;
//[a release]; 我们不需要写这一句,因为 ARC 会帮我们把这一句加上

所以下面语句在MRC下内存泄漏,ARC不会内存泄漏
[[NSObject alloc] init];

这里大家可能会有个疑问,ARC 下 strong/retain 修饰的属性,怎么保证不像 MRC 下那样引用计数为 2 的?

  • no_retain_return_value
    • 由于 retain_return_value ARC帮我们实现了MRC下的代码,所以也猜测 no_retain_return_value 在此类方法的末尾对返回值调用了 autorelease
    • 但是实际上 ARC不是这样做的,它做了优化

就是说为了兼容 ARC MRC 混编,也为了提高纯 ARC 处理速度,no_retain_return_value 类方法对返回值用 objc_autoreleaseReturnValue修饰,objc_autoreleaseReturnValue 的实现是:检测 caller 中是否有objc_retainAutoreleasedReturnValue 的调用:

  • 若有 runtime 将这个返回值 object 储存在 TLS(thread-local storage)中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的 objc_retainAutoreleasedReturnValue 里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain);
    • TLS作用是区分直接处理普通返回值,objc_retainAutoreleasedReturnValue 直接去TLS里面取,没有的直接处理返回值
  • 若没有objc_retainAutoreleasedReturnValue,则 objc_autoreleaseReturnValue 对返回值调用 autorelease,caller 处有多种处理类型,例如 ARC 但是 weak 修饰,就做 weak 的处理,若是 MRC 则看调用方是否打算持有,由调用方自己规定,反正返回值没有归他持有

objc_retainAutoreleasedReturnValue 一般什么时候会出现呢?ARC下 strong 修饰的变量接收 no_retain_return_value 的返回值就会出现.

看到上面的解释,可能有个疑问,如何检测caller 是否有objc_retainAutoreleasedReturnValue呢?

利用了__builtin_return_address , 作用是得到函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。

通过上面的__builtin_return_address加某些偏移量,被调方可以定位到主调方在返回值后面的汇编指令, 可以检验了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue。

深浅拷贝

上问提到 copy 修饰的属性在 set 的时候调用 copy方法,而 copy方法的实现分为:浅拷贝、深拷贝、完全深拷贝。这里简单介绍一下

非容器对象的深浅拷贝

一个自定义对象的拷贝其实有四种类型

  • 赋值
  • 浅拷贝
  • 半深拷贝
  • 深拷贝

赋值就是对 set 方法的调用,set方法实质是深拷贝还是浅拷贝受属性修饰影响,上面已介绍。

浅拷贝、半深拷贝、深拷贝你是指自定义对象提供copy方法,在实现copy方法的时候即可以深拷贝也可以浅拷贝,也可以是半深拷贝。

默认情况下一些非自定义的对象的的 copy 一般都是半深拷贝,比如 NSString、NSdata,获得了一块新的内存。但是容器类比较特殊,容器类的深浅拷贝下面会详细介绍.

  • 浅拷贝指拷贝对象与被拷贝你对象指向同一块内存
@implementation Product
- (instancetype)copyWithZone:(NSZone *)zone {
    Product *vc = self
    return vc;
}

@end

@implementation pen

- (instancetype)copyWithZone:(NSZone *)zone {
    pen *vc = self;
    return vc;
}
@end
  • 半深拷贝指自定义对象本身 copy 得到的对象与被copy的对象不指向同一块内存;但属性与被拷贝的对象的属性指向同一块内存
@implementation Product
- (instancetype)copyWithZone:(NSZone *)zone {
    Product *vc = [[self class] allocWithZone:zone];
    vc.name = name
    return vc;
}

@end

@implementation pen

- (instancetype)copyWithZone:(NSZone *)zone {
    pen *vc = [super copyWithZone:zone];
    vc.ary = self.ary;
    return vc;
}
@end
  • 完全深拷贝指不仅对象本身 copy 得到的对象与被copy的对象不指向同一块内存;且属性也不与被拷贝的对象的属性指向同一块内存
@implementation Product
- (instancetype)copyWithZone:(NSZone *)zone {
    Product *vc = [[self class] allocWithZone:zone];
    vc.name = [self.name copy];
    return vc;
}

@end

@implementation pen

- (instancetype)copyWithZone:(NSZone *)zone {
    pen *vc = [super copyWithZone:zone];
    vc.ary = [self.ary mutableCopy];
    return vc;
}
@end

容器对象的深浅拷贝

容器对象也分为:赋值、浅拷贝、半深拷贝、完全深拷贝。但是效果会因为是可变容器还是非可变容器影响

  • 赋值:受属性修饰词影响
    • 可变容器
      • copy修饰
        • 调用copy方法,重新申请了内存,实现了半深拷贝,容器里面的内容没有重新申请内存
        • 对可变对象是个灾难,导致本身实质变成了一个不可变对象
      • strong修饰
        • 效果是 retain,浅拷贝,容器指向同一块内存
    • 非可变容器
      • Copy 修饰
        • 调用copy方法,重新申请了内存,实现了半深拷贝,容器里面的内容没有重新申请内存
      • Strong 修饰
        • 效果是retain,浅拷贝
        • 对于不可变对象可能存在问题,比如把一个可变对象赋值给一个strong修饰的不可变对象,虽然不能通过不可变对象add或者delete,但是由于是浅拷贝,与可变对象指向同一块内存,导致可变对象的变化会影响这个不可变对象
  • 拷贝
    • 容器类均实现了 NScopying协议,且提供了接口 copy mutablecopy,实现的效果是半深拷贝
    • 若想实现完全深拷贝需要自己 hook copywithzone
    • 值得注意的是:可变对象不可变对象使用 copy mutablecopy效果不一样

参考

  • 苹果开源了runtime的代码
    • https://opensource.apple.com/source/objc4/
  • 引用计数原理
    • http://yulingtianxia.com/blog/2015/12/06/The-Principle-of-Refenrence-Counting/
  • Autoreleae
    • http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
    • https://www.jianshu.com/p/f87f40592023
  • 内存管理
    • https://www.open-open.com/lib/view/open1460693431491.html#articleHeader6
    • https://segmentfault.com/a/1190000004943276
  • 深入理解runloop
    • https://blog.ibireme.com/2015/05/18/runloop/