iOS 内存管理机制

4,951 阅读27分钟

内存管理

1. 引用计数

与(基于垃圾回收的)Java 运行时不同,Objective-C 和 Swift 的 iOS 运行时使用引用计数。使用引用计数的负面影响在于,如果开发人员不够小心,那么可能会出现重复的内存释放和循环引用的情况。

  • 引用计数(Reference counting)是一个简单有效管理对象生命周期的方式。
  • 当我们新建一个新对象时候,它的引用计数+1,当一个新指针指向该对象,将引用计数+1。当指针不再指向这个对象时候,引用计数-1,当引用计数为0时,说明该对象不再被任何指针引用,将对象销毁,进而回收内存。

引用计数.png

2. TaggedPointer

对于一个 NSNumber 对象,如果存储 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占4个字节。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在32位 CPU 下为4个字节。但是迁移至64位系统中后,其占用空间达到了8字节,以此类推,所有在64位系统中占用空间会翻倍的对象,在迁移后会导致系统内存剧增,即时他们根本用不到这么多的空间。在2013年9月,苹果推出了iPhone 5s,该款机型首次采用64位架构的A7双核处理器。所以苹果对于一些小型数据(NSNumber、NSDate、NSString等),采用了 taggedPointer 这种方式管理内存。

TaggedPointer 是一种为内存高效节省空间的方法,Tagged Pointer是一个特别的指针,它分为两部分:

一部分直接保存数据 ; 另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址; 在一个程序中运行下述代码,获取输出日志:

NSNumber *number =  @(0);
NSNumber *number1 = @(1);
NSNumber *number2 = @(2);
NSNumber *number3 = @(9999999999999999999);
NSString *string = [[@"a" mutableCopy] copy];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];

NSLog(@"number ---- %@, %p", [number class], number);
NSLog(@"number1 --- %@, %p", [number1 class], number1);
NSLog(@"number2 --- %@, %p", [number2 class], number2);
NSLog(@"number3 --- %@, %p", [number3 class], number3);
NSLog(@"NSString -- %@, %p", [string class], string);
NSLog(@"indexPath - %@, %p", indexPath.class,indexPath);

/********************* 输出日志 *********************
number ---- __NSCFNumber, 0xb000000000000002
number1 --- __NSCFNumber, 0xb000000000000012
number2 --- __NSCFNumber, 0xb000000000000022
number3 --- __NSCFNumber, 0x600003b791c0
NSString -- NSTaggedPointerString, 0xa000000000000611
indexPath - NSIndexPath, 0xc000000000000016
 */

分析日志:

  1. NSNumber 存储的数据不大时,NSNumber *指针是伪指针Tagged Pointer;
  2. NSNumber存储的数据很大时,NSNumber * 指针一般指针,指向NSNumber 实例的地址,如 number3;
  3. NSTaggedPointerString 经常遇见,它就是Tagged Pointer对象; 对于Tagged Pointer,是系统实现的,无需开发者操心!但是作为开发者,也要知道 NSTaggedPointerString 等是什么东西! objc_objcet 对象中 isa 指针分为指针型 isa 与非指针型isa(NONPOINTER_ISA),运用的便是类似这种技术。下面详细解读一下NONPOINTER_ISA:

在一个64位的指针内存中

  • 第0位存储的是indexed标识符,它代表一个指针是否为NONPOINTER型,0代表不是,1代表是。
  • 第1位 has_assoc,顾名思义,1代表其指向的实例变量含有关联对象,0则为否。
  • 第2位为 has_cxx_dtor,表明该对象是否包含 C++相关的内容或者该对象是否使用 ARC 来管理内存,如果含有 C++ 相关内容或者使用了 ARC 来管理对象,这一块都表示为 YES,
  • 第3-35位 shiftcls存储的就是这个指针的地址。
  • 第42位为 weakly_referenced,表明该指针对象是否有弱引用的指针指向。
  • 第43位为 deallocing,表明该对象是否正在被回收。
  • 第44位为 has_sidetable_rc,顾名思义,该指针是否引用了 sidetable 散列表。
  • 第45-63位 extra_rc 装的就是这个实例变量的引用计数,当对象被引用时,其引用计数+1,但少量的引用计数是不会直接存放在 sideTables 表中的,对象的引用计数会先存在 NONPOINTER_ISA 的指针中的45-63位,当其被存满后,才会相应存入 sideTables 散列表中。

3. 散列表(sideTables)

sideTables

runtime 中,有四个数据结构非常重要,分别是 SideTables,SideTable,weak_table_t和weak_entry_t。它们和对象的引用计数,以及 weak引用 相关。

先说一下这四个数据结构的关系。 在 runtime 内存空间中,SideTables是一个8个元素长度 的hash数组,里面存储了 SideTableSideTableshash键值 就是一个 对象objaddress。 因此可以说,一个obj,对应了一个 SideTable。但是一个 SideTable,会对应多个 obj。因为 SideTable 的数量只有64个,所以会有很多 obj 共用同一个 SideTable

而在一个 SideTable 中,又有两个成员,分别是

RefcountMap refcnts;        // 对象引用计数相关 map
weak_table_t weak_table;    // 对象弱引用相关 table

其中,refcents 是一个 hash map,其keyobj的地址,而value,则是obj对象的引用计数。

weak_table 则存储了 弱引用obj 的指针的地址,其本质是一个以 obj 地址为 key弱引用obj 的指针的地址作为 valuehash表hash表 的节点类型是 weak_entry_t

这四个数据结构的关系如下图:

SideTables.png

散列表在系统中的体现是一个 sideTables的哈希映射表,其中所有对象的引用计数(除上述存在 NONPOINTER_ISA 中的外)都存在这个 sideTables 散列表中,而一个散列表中又包含众多 sideTable 结构体。每个 SideTable 中又包含了三个元素,spinlock_t 自旋锁RefcountMap 引用计数表weak_table_t 弱引用表。 它使用对象的内存地址当它的 key。管理引用计数和 weak 指针 就靠它了。

  • 对于 slockrefcnts,第一个是为了防止竞争选择的自旋锁,第二个是协助对象的isa指针extra_rc 共同引用计数的变量。

SideTable

SideTable 翻译过来的意思是“边桌”,可以放一下小东西。这里,主要存放了OC对象的引用计数和弱引用相关信息。定义如下:

struct SideTable {
    spinlock_t slock; // 自旋锁,防止多线程访问冲突
    RefcountMap refcnts; // 对象引用计数
    weak_table_t weak_table; // 对象弱引用map

    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 的定义很清晰,有三个成员:

  1. spinlock_t slock : 保持原子属性的自旋锁,,用于上锁/解锁 SideTable

  2. RefcountMap refcnts :以 DisguisedPtr<objc_object>keyhash表,用来存储 OC对象 的引用计数(仅在未开启 isa优化 或 在 isa优化 情况下 isa_t 的引用计数溢出时才会用到)。

  3. weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC weak功能实现的核心数据结构。 除了三个成员外,苹果为 SideTable 还写了构造和析构函数:

    // 构造函数
    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    //析构函数(看看函数体,苹果设计的SideTable其实不希望被析构,不然会引起fatal 错误)
    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

通过析构函数可以知道,SideTable 是不能被析构的。

最后是一堆锁的操作,用于多线程访问 SideTable, 同时,也符合我们上面提到的 StripedMap 中关于valuelock 接口定义:

    // 锁操作 符合StripedMap对T的定义
    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);
spinlock_t slock

spinlock_t 的最终定义实际上是一个 uint32_t 类型的非公平的自旋锁。所谓非公平,就是说获得锁的顺序和申请锁的顺序无关,也就是说,第一个申请锁的线程有可能会是最后一个获得到该锁,或者是刚获得锁的线程会再次立刻获得到该锁,造成饥饿等待。 同时,在OC中,_os_unfair_lock_opaque 也记录了获取它的线程信息,只有获得该锁的线程才能够解开这把锁。

typedef struct os_unfair_lock_s {
	uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;

关于自旋锁的实现,苹果并未公布,但是大体上应该是通过操作 _os_unfair_lock_opaque 这个 uint32_t 的值,当大于0时,锁可用,当等于或小于0时,需要锁等待。

RefcountMap refcnts

RefcountMap refcnts 用来存储OC对象的引用计数。它实质上是一个以 objc_objectkeyhash表,其 vaule 就是 OC 对象的引用计数。同时,当OC对象的引用计数变为0时,会自动将相关的信息从 hash表 中剔除。RefcountMap 的定义如下:

// RefcountMap disguises its pointers because we 
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

实质上是模板类型 objc::DenseMap。模板的三个类型参数 DisguisedPtr<objc_object>size_t, true 分别表示 DenseMap的hash key 类型,value类型,是否需要在 value==0 的时候自动释放掉响应的 hash节点,这里是true

DenseMap 这个模板类型又继承与另一个Base 模板类型 DenseMapBase

template<typename KeyT, typename ValueT,
         bool ZeroValuesArePurgeable = false, 
         typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
    : public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
                          KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable> 

关于DenseMap的定义,苹果写了一大坨,有些复杂,这里就不去深究了,有兴趣的同学可以自己去看下相关的源码部分。

weak_table_t weak_table

重点来了,weak_table_t weak_table 用来存储OC对象弱引用的相关信息。我们知道,SideTables 一共只有64个节点,而在我们的APP中,一般都会不只有64个对象,因此,多个对象一定会重用同一个 SideTable节点 ,也就是说,一个 weak_table 会存储多个对象的弱引用信息。因此在一个 SideTable 中,又会通过 weak_table作为 hash表 再次分散存储每一个对象的弱引用信息。

weak_table_t 的定义如下:

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;        // hash数组,用来存储弱引用对象的相关信息weak_entry_t
    size_t    num_entries;             // hash数组中的元素个数
    uintptr_t mask;                    // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
    uintptr_t max_hash_displacement;   // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};

weak_table_t 是一个典型的 hash结构。其中 weak_entry_t *weak_entries 是一个动态数组,用来存储 weak_table_t 的数据元素 weak_entry_t。 剩下的三个元素将会用于 hash表 的相关操作。weak_tablehash定位操作 如下所示:

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;  // 这里通过 & weak_table->mask的位操作,来确保index不会越界
    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); // 触发bad weak table crash
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) { // 当hash冲突超过了可能的max hash 冲突时,说明元素没有在hash表中,返回nil 
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

上面的定位操作还是比较清晰的,首先通过

 size_t begin = hash_pointer(referent) & weak_table->mask;

来尝试确定 hash 的初始位置。注意,这里做了 & weak_table->mask 位操作来确保index不会越界,这同我们平时用到的取余%操作是一样的功能。只不过这里改用了位操作,提升了效率。

然后,就开始对比 hash表 中的数据是否与目标数据相等 while (weak_table->weak_entries[index].referent != referent),如果不相等,则 index +1 , 直到 index == begin(绕了一圈)或超过了可能的 hash冲突 最大值。

这是 weak_table_t 如何进行 hash定位 的相关操作

weak_entry_t

weak_table_t 中存储的元素是 weak_entry_t类型,每个 weak_entry_t类型 对应了一个OC对象的弱引用信息。

weak_entry_t 的结构和 weak_table_t 很像,同样也是一个 hash表,其存储的元素是 weak_referrer_t,实质上是弱引用该对象的指针的指针,即 objc_object **new_referrer , 通过操作指针的指针,就可以使得 weak 引用的指针在对象析构后,指向nil

ARC 和 MRC 的区别?

持续跟踪 retain、release 和 autorelease 并不容易。要想找出是谁在什么时间和地点向谁发送了这些消息就更难了。苹果公司在 2011 年的全球开发者大会上介绍了解决这一问题的方案——ARC。iOS 应用的新兴语言 Swift 同样也在使用 ARC。与 Objective-C 不同的是,Swift 不支持 MRC。

ARC 是一种编译器特性。它评估了对象在代码中的生命周期,并在编译时自动注入适合的内存管理调用。编译器还会生成适合的 dealloc 方法。这意味着与跟踪内存使用(如确保对象被及时回收了)有关的最大难题被解决了。

MRCARC
strongARC特有,在MRC时代没有,相当于MRC模式下的retain
retainMRC、ARC两种内存管理方式下相同MRC、ARC两种内存管理方式下相同
assign可以用来修饰对象类型,也可以用来修饰基本数据类型。修饰对象类型的时候,对象的引用计数不会随着引用次数的增加而增加,也就是说被释放之前,引用计数永远是1。只能用来修饰基本数据类型,不能用来修饰对象类型。除此之外,还用来修饰代理对象。
weak相当于MRC模式下的assign
copy1. block访问外部局部变量,block存放在栈里面 2. 只要block访问整个APP都存在的变量,肯定是在全局区 3. 不能使用retain引用block,因为block不在堆里面,只有使用copy才会把block放在堆区里面1. 只要block访问外部局部变量,block就会存放在堆区 2. 可以使用strong去引用,因为本身就已经存放在堆区的 3. 也可以使用copy进行修饰,但是strong性能更好

strong:强引用,它是ARC特有。在MRC时代没有,相当于retain。由于MRC时代是靠引用计数器来管理对象什么时候被销毁所以用retain,而ARC时代管理对象的销毁是有系统自动判断,判断的依据就是该对象是否有强引用对象。如果对象没有被任何地方强引用就会被销毁。所以在ARC时代基本都用的strong来声明代替了retain。只能用于声明OC对象(ARC特有)

ARC

ARC默认属性修饰符

  • ARC下对象类型属性:(atomic, readwrite, strong)

  • ARC下非对象类型:(atomic, readwrite, unsafe_unretained)

  • assignassign 只可以用来修饰基本数据类型,该方式会对象直接赋值而不会进行 retain 操作。

  • copy:表⽰重新建立一个新的计数为1的对象,然后释放掉旧的值。NSString、NSArray、NSDictionary 等经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,为确保对象中的属性值不会无意间变动,应该在设置新属性值时拷贝一份。而 NSMutableString、NSMutableArray、NSMutableDictionary 则往往使用 strong 关键字更为妥当。

    浅copy,类似strong,持有原始对象的指针,会使 retainCount 加一。

    深copy,会创建一个新的对象,不会对原始对象的 retainCount 变化。

  • retain和strong区别strong 修饰 block,相当于 copy,此时 block 是放在堆上的,生命周期不会随函数周期结束而出栈,但是 retain 修饰的 block 是存放在栈上, 因此 block 在函数调用结束时,对象会变成 nil,对象的指针会变成野指针,因此对象继续调用会产生异常。

  • weak: 指定了 __weak 关系。

  • unsafe_unretained: 指定了 __unsafe_unretained 关系。

ARC属性的所有权修饰符

属性的所有权修饰符的重要作用就是可以帮助管理和精确控制对象的生命周期,确保它们既不会太长也不会过短。

属性声明的属性所有权修饰符
assign_unsafe_unretained 修饰符
copy_strong 修饰符 (但是赋值的是被复制的对象)
retain_strong 修饰符
strong_strong 修饰符(默认的限定符,无需显式引入。)
_unsafe_unretained_unsafe_unretained修饰符(没有强引用指向对象时,__unsafe_unretained 不会被置为 nil)
weak_weak 修饰符(对象被回收时,指针将自动被设置为 nil)
_autoreleasing 修饰符

Person * __strong p1 = [[Person alloc] init]; // 创建对象后引用计数为 1,并且对象在 p1 引用期间不会被回收。

Person * __weak p2 = [[Person alloc] init]; // 创建对象后引用计数为 0,对象会被立即释放,且 p2 将被设置为 nil。

Person * __unsafe_unretained p3 = [[Person alloc] init]; // 创建对象后引用计数为 1,对象会被立即释放,但 p3 不会被设置为 nil。

Person * __autoreleasing p4 = [[Person alloc] init]; // 创建对象后引用计数为 1,当方法返回时对象会被立即释放。

WWDC2011iOS5 所引入自动管理机制——自动引用计数(ARC),它不是垃圾回收机制而是编译器的一种特性。ARC管理机制MRC手动机制 差不多,只是不再需要手动调用retain、release、autorelease;当你使用ARC时,编译器会在在适当位置插入 releaseautoreleaseARC 时代引入了 strong强引用 来带代替 retain,引入了weak弱引用

RunLoop

apple 官方文档(多线程编程指南)描述: “runloop 是用来在线程上管理事件异步到达的基础设施......

runloop 在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗CPU周期轮询,并防止处理器本身进入休眠状态并节省电源。" 看见没,消除CPU空转才是它最大的用处。

runloop.png

图中第1步 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前

图中第6步 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observerorder2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

图中第10 Observer 监视事件是exit(即讲退出runloop),其回调内会调用 _objc_autoreleasePoolpop() 释放自动释放池。

从上面就能看出,Runloop 中系统自动创建的 @autoreleasepool 是在准备进入休眠状态才被销毁的。所以在 ARC 下,在线程中的临时对象是在当前线程的 Runloop 进入休眠或者退出 loop 或者退出线程时被执行 release 的。

AutoreleasePool

自动释放池是OC中的一种内存自动回收机制,它可以将加入 AutoreleasePool 中的变量 release 的时机延迟,简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即 release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到 runloop 休眠/超出 autoreleasepool 作用域{}之后才会被释放。其机制如下图所示

Autoreleasepool.png

  1. 从程序启动到加载完成,主线程对应的 runloop 会处于休眠状态,等待用户交互来唤醒 runloop

  2. 用户的每一次交互都会启动一次 runloop,用于处理用户的所有点击、触摸事件等

  3. runloop 在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中

  4. 在一次完整的 runloop 结束之前,会向自动释放池中所有对象发送 release 消息,然后销毁自动释放池

MRC

MRC默认属性修饰符:

MRC:(atomic, readwrite, assign)

MRC手动内存管理

引用计数器:在MRC时代,系统判定一个对象是否销毁是根据这个对象的引用计数器来判断的。

  • 每个对象被创建时引用计数都为1
  • 每当对象被其他指针引用时,需要手动使用[obj retain];让该对象引用计数+1。
  • 当指针变量不在使用这个对象的时候,需要手动释放release这个对象。 让其的引用计数-1.
  • 当一个对象的引用计数为0的时候,系统就会销毁这个对象。
  • 在MRC模式下必须遵循谁创建,谁释放,谁引用,谁管理

弱类型:id

在这里我举个大体的例子,实际情况要比这复杂的多:假设一个对象将其声明为 id 类型,这个对象在程序运行期间无法相应某个选择子 selector,那么编程便会崩溃。如果多个类有完全相同的方法签名(名称、参数、返回值),那么便会发生类型匹配错误。所以我们在使用常规命名的方法时,应避免使用 id。尽量使用具体的类型取而代之。

内存泄漏

内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。该被释放的时候未释放,一直被其内部的对象所持有,循环引用就属于内存泄漏。

ARC下哪些情况会造成内存泄漏?

block的属性修饰词为什么是copy?使用block有哪些使用注意?

block一旦没有进行copy操作,就不会在堆上 使用注意:循环引用问题

block在修改NSMutableArray,需不需要添加__block?

不需要 当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。所以,在block里面对指针指向内容做的修改,在block外面也一样生效。

野指针和僵尸对象

野指针:指向的对象已经被回收掉了,这个指针就叫做野指针。 产生原因: 指针创建时未初始化。指针变量刚被创建时不会自动成为NULL指针,它会随机指向一个内存地址。

僵尸对象:一个OC对象引用计数为0被释放后就变成僵尸对象了,僵尸对象的内存已经被系统回收,虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的。

@property 属性声明的关键字: weak 表示非持有特性,为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。当属性所指的对象释放的时候,属性也会被置为 nilassign用来修饰基本数据类型和对象。当 assign 用来修饰对象的时候,和 weak 类似。唯一的区别就是当属性所指的对象释放的时候,属性指针不会被置为 nil,这就会产生野指针,此时向对象发消息会崩溃。 unsafe_unretained 用来修饰属性的时候,和 assign 修饰对象的时候是一模一样的。为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为 nil,这就会产生 悬垂指针,所以是不安全的。

情形一weak和assign的区别: 释放对象是否产生野指针,适用 OC 类型还是基本类型。

1. 修饰类型不同

  • weak只能用于修饰OC对象类型;
  • assign既可以修饰OC类型也可以修饰基本类型;

2. 释放是是否产生野指针weak 对象被释放是,对象的指针会被设置为 nil,因此再次去对该对象发送消息,不会崩溃,因此 weak 是安全的; assign 如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。 当对象被释放的时候,对象的指针不会置空,该对象会变成野指针,再次对该对象发送消息,会直接崩溃。

总结: assign:适用于基本数据类型(int),因为基本数据类型存放在栈中,采用先进先出的原则,用系统自动分配释放管理内存。如果使用在对象类型,存放在堆中,需要考虑野指针的问题,则程序员要手动分配释放或者 ARC 下内存自动管理分配。 weak:适用于 OC 对象类型,同时也适用于 delegate,不会产生野指针,也不会循环引用,非常安全。 情形二block为什么用copy修饰 ? 默认情况下,block 是存放在栈中即 NSStackBlock ,因此 block 在函数调用结束时,对象会变成 nil,但是对象的指针变成野指针,因此对象继续调用会产生异常。使用 copy 修饰之后,会将 block 对象保存到堆中 NSMallocBlock,它的生命周期会随着对象的销毁而结束的。所以函数调用结束之后指针也会被设置为 nil,再次调用该对象也不会产生异常。

解决循环引用常见的方式

  • weak-strong-dance

  • __block修饰对象(需要注意的是在block内部需要置空对象,而且 block 必须调用)

  • 传递对象 self 作为 block 的参数,提供给 block内部 使用

  • 使用 NSProxy

定时器 NSTimer 中的循环引用

首先控制器 self 强引用 timer,然后timer 又强引用我们的控制器 self,这就造成了循环引用,释放不掉造成内存泄露。

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

NSTimer循环引用解决方案:

方案一(失败):weak-strong-dance

一想到循环引用的解决方案,我们首先想到的肯定是 weak-strong-dance,那么我们来尝试一下:

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

我们再次运行程序,进行 push-pop 跳转。发现问题还是存在,即定时器方法仍然在执行,并没有执行控制器 selfdealloc 方法,为什么呢?

我们使用 __weak 虽然打破了 self -> timer -> self 之前的循环引用,即引用链变成了self -> timer -> weakSelf -> self。但是在这里我们的分析并不全面,此时还有一个 Runlooptimer 的强持有,因为 Runloop 的生命周期比控制器 self 界面更长,所以导致了 timer 无法释放,同时也导致了控制器 self 界面也无法释放。它们之间的引用链如下图所示:

timer引用链.png

我们的定时器 timer 捕获的是控制器 self,是一个对象,其引用链关系为:NSRunLoop -> timer -> weakSelf -> self。所以RunLoop对整个对象的空间有强持有,runloop没停,timer 和 weakSelf是无法释放的。

方案二(成功):pop时在其他方法中销毁timer

根据前面的解释,我们知道由于 Runlooptimer 的强持有,导致了 Runloop 间接的强持有了self(因为 timer 中捕获的是 self 对象)。所以导致 dealloc 方法无法执行。需要查看在 pop 时,是否还有其他方法可以销毁 timer。这个方法就是 didMoveToParentViewController

didMoveToParentViewController方法,是用于当一个视图控制器中添加或者移除 viewController 后,必须调用的方法。目的是为了告诉iOS,已经完成添加/删除子控制器的操作。

在控制器 self中重写 didMoveToParentViewController方法

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 无论push 进来 还是 pop 出去 正常跑
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}
方案三(成功):中介者模式,即不使用self,依赖于其他对象

timer 模式中,我们重点关注的是 fireInTheHole 能执行,并不关心 timer 捕获的 target 是谁,由于这里不方便使用 self(因为会有强持有问题),所以可以将 target 换成其他对象,例如将 target 换成NSObject 对象,将 fireInTheHole 交给 target 执行

timertargetself 改成 objc

@property (nonatomic, strong) id target;

//**********1、修改target**********
    self.target = [[NSObject alloc] init];
    class_addMethod([NSObject class], @selector(fireInTheHole), (IMP)fireInTheHoleObjc, "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireInTheHole) userInfo:nil repeats:YES];
    
//**********imp**********
void fireInTheHoleObjc(id obj){
    NSLog(@"%s -- %@",__func__,obj);
}

- (void)fireInTheHole {
    NSLog(@"fire in the hole");
}
    

运行结果如下:

中介者模式1.png

运行发现执行 dealloc 之后,timer还是会继续执行。原因是解决了中介者的释放,但是没有解决中介者的回收,即 self.target 的回收。所以这种方式有缺陷

可以通过在 dealloc 方法中,取消定时器来解决,代码如下:

-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
    self.timer = nil;
}

发现pop之后,timer释放,从而中介者也会进行回收释放,运行结果如下:

中介者模式.png

方案四(成功):NSProxy虚基类的方式

NSProxy 是一个虚基类,它的地位等同于 NSObjectcommand+shift+0 打开 Xcode 参考文档搜索 NSProxy,说明如下:

NSProxy

An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.

Declaration

@interface NSProxy

Overview

Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistant<wbr>Object) or for lazy instantiation of objects that are expensive to create. NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forward<wbr>Invocation: and method<wbr>Signature<wbr>For<wbr>Selector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forward<wbr>Invocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. method<wbr>Signature<wbr>For<wbr>Selector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethod<wbr>Signatureobject accordingly. See the NSDistant<wbr>Object, NSInvocation, and NSMethod<wbr>Signature class specifications for more information.

我们不用 self 来响应 timer 方法的 target,而是用 NSProxy来响应。

首先定义一个继承自 NSProxy 的子类

FXProxy.h

@interface FXProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

FXProxy.m

@interface FXProxy ()
@property (nonatomic, weak) id object;
@end

@implementation FXProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    FXProxy *proxy = [FXProxy alloc];
    proxy.object = object;
    return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}
@end

timer 中的 target 传入 NSProxy 子类对象,即 timer 持有 NSProxy 子类对象

//************解决timer强持有问题************
self.proxy = [FXProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireInTheHole) userInfo:nil repeats:YES];
    
-(void)dealloc {
  NSLog(@"%s",__func__);
  [self.timer invalidate];
  self.timer = nil;
}  

这样做的主要目的是将强引用的注意力转移成了消息转发。虚基类只负责消息转发,即使用 NSProxy 作为中间代理、中间者 这里有个疑问,定义的 proxy 对象,在 dealloc 释放时,还存在吗?

proxy 对象会正常释放,因为 self 正常释放了,所以可以释放其持有者,即 timerproxytimer 的释放也打破了 runLoopproxy 的强持有。完美的达到了两层释放,解释如下:

  • self释放,导致了 proxy 的释放

  • dealloc 方法中,timer 进行了释放,所以 runloop 强引用也释放了

它们之间的引用链如下图所示:

proxy.png

最佳实践

  1. 对子对象使用 __strong
  2. 对父对象使用 __weak
  3. 对使引用图闭合的对象(如委托)使用 __weak
  4. 对数值属性(NSInterger、SEL、CGFloat等)而言,使用 assign 限定符
  5. 对于块属性,使用 copy 限定符
  6. 当声明使用 NSError ** 参数的方法时,需要使用 __autoreleasing,并要注意用正确的语法:NSError * __autoreleasing *。
  7. 避免在块内直接引用外部的变量。在块外面将它们 weakify,并在块内再将它们 strongify。参见libextobjc 库 来了解 @weakify 和 @strongify。
  8. 进行必要清理时遵循以下准则:
    • 销毁计时器
    • 移除观察者(具体来说,移除对通知的注册)
    • 解除回调(具体来说,将强引用的委托设置为 nil)

参考链接:

Objective-C runtime机制(7)——SideTables, SideTable, weak_table, weak_entry_t

iOS 造成内存泄露的原因有哪些【面试】