iOS 内存管理机制
↓推荐关注↓
内存管理
1. 引用计数
引用计数(Reference counting)是一个简单有效管理对象生命周期的方式。 当我们新建一个新对象时候,它的引用计数+1,当一个新指针指向该对象,将引用计数+1。当指针不再指向这个对象时候,引用计数-1,当引用计数为0时,说明该对象不再被任何指针引用,将对象销毁,进而回收内存。
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
*/
分析日志:
NSNumber 存储的数据不大时,NSNumber *指针是伪指针Tagged Pointer; NSNumber存储的数据很大时,NSNumber * 指针一般指针,指向NSNumber 实例的地址,如 number3; 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
是一个64个元素长度8个元素长度 的hash数组,里面存储了 SideTable
。SideTables
的 hash键值
就是一个 对象obj
的 address
。因此可以说,一个obj
,对应了一个 SideTable
。但是一个 SideTable
,会对应多个 obj
。因为 SideTable
的数量只有64个,所以会有很多 obj
共用同一个 SideTable
。
而在一个 SideTable
中,又有两个成员,分别是
RefcountMap refcnts; // 对象引用计数相关 map
weak_table_t weak_table; // 对象弱引用相关 table
其中,refcents
是一个 hash map
,其key
是obj
的地址,而value
,则是obj对象
的引用计数。
而 weak_table
则存储了 弱引用obj
的指针的地址,其本质是一个以 obj
地址为 key
,弱引用obj
的指针的地址作为 value
的 hash表
。hash表
的节点类型是 weak_entry_t
。
这四个数据结构的关系如下图:
散列表在系统中的体现是一个 sideTables的哈希映射表,其中所有对象的引用计数(除上述存在 NONPOINTER_ISA
中的外)都存在这个 sideTables
散列表中,而一个散列表中又包含众多 sideTable 结构体。每个 SideTable 中又包含了三个元素,spinlock_t 自旋锁
,RefcountMap 引用计数表
,weak_table_t 弱引用表
。它使用对象的内存地址当它的 key
。管理引用计数和 weak 指针
就靠它了。
对于 slock
和refcnts
,第一个是为了防止竞争选择的自旋锁,第二个是协助对象的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
的定义很清晰,有三个成员:
spinlock_t slock
: 自旋锁,用于上锁/解锁SideTable
。RefcountMap refcnts
:以DisguisedPtr<objc_object>
为key
的hash表
,用来存储OC对象
的引用计数(仅在未开启isa优化
或 在isa优化
情况下isa_t
的引用计数溢出时才会用到)。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
中关于value
的 lock
接口定义:
// 锁操作 符合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_object
为 key
的 hash
表,其 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_table
的 hash定位操作
如下所示:
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 的区别?
MRC | ARC | |
---|---|---|
strong | 无 | ARC特有,在MRC时代没有,相当于MRC模式下的retain |
retain | MRC、ARC两种内存管理方式下相同 | MRC、ARC两种内存管理方式下相同 |
assign | 可以用来修饰对象类型,也可以用来修饰基本数据类型。修饰对象类型的时候,对象的引用计数不会随着引用次数的增加而增加,也就是说被释放之前,引用计数永远是1。 | 只能用来修饰基本数据类型,不能用来修饰对象类型。除此之外,还用来修饰代理对象。 |
weak | 无 | 相当于MRC模式下的assign |
copy | 1. 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)
ARC属性的所有权修饰符
属性声明的属性 | 所有权修饰符 |
---|---|
assign | _unsafe_unretained 修饰符 |
copy | _strong 修饰符 (但是赋值的是被复制的对象) |
retain | _strong 修饰符 |
strong | _strong 修饰符 |
_unsafe_unretained | _unsafe_unretained修饰符 |
weak | _weak 修饰符 |
_autoreleasing 修饰符 |
assign:
assign
只可以用来修饰基本数据类型,该方式会对象直接赋值而不会进行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
,对象的指针会变成野指针,因此对象继续调用会产生异常。_autoreleasing:
_autoreleasing
修饰符变量引用的对象,相当于在MRC
情况下调用对象的autorelease
方法
WWDC2011
和 iOS5
所引入自动管理机制——自动引用计数(ARC
),它不是垃圾回收机制而是编译器的一种特性。ARC管理机制
与 MRC手动机制
差不多,只是不再需要手动调用retain、release、autorelease
;当你使用ARC时,编译器会在在适当位置插入 release
和 autorelease
;ARC
时代引入了 strong强引用
来带代替 retain
,引入了weak弱引用
。
RunLoop:
看 apple
官方文档(多线程编程指南)描述: “runloop
是用来在线程上管理事件异步到达的基础设施......
runloop
在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗CPU
周期轮询,并防止处理器本身进入休眠状态并节省电源。" 看见没,消除CPU空转
才是它最大的用处。
图中第1步 Observer
监视的事件是 Entry
(即将进入Loop
),其回调内会调用 _objc_autoreleasePoolPush()
创建自动释放池。其 order
是-2147483647
,优先级最高,保证创建释放池发生在其他所有回调之前
图中第6步 Observer
监视了两个事件:BeforeWaiting
(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit
(即将退出Loop
) 时调用 _objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer
的 order
是 2147483647
,优先级最低,保证其释放池子发生在其他所有回调之后。
图中第10 Observer
监视事件是exit
(即讲退出runloop
),其回调内会调用 _objc_autoreleasePoolpop()
释放自动释放池。
从上面就能看出,Runloop
中系统自动创建的 @autoreleasepool
是在准备进入休眠状态才被销毁的。所以在 ARC
下,在线程中的临时对象是在当前线程的 Runloop
进入休眠或者退出 loop
或者退出线程时被执行 release
的。
AutoreleasePool
自动释放池是OC
中的一种内存自动回收机制,它可以将加入 AutoreleasePool
中的变量 release
的时机延迟,简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即 release
。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到 runloop
休眠/超出 autoreleasepool
作用域{}之后才会被释放。其机制如下图所示
从程序启动到加载完成,主线程对应的
runloop
会处于休眠状态,等待用户交互来唤醒runloop
用户的每一次交互都会启动一次
runloop
,用于处理用户的所有点击、触摸事件等runloop
在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中在一次完整的
runloop
结束之前,会向自动释放池中所有对象发送release
消息,然后销毁自动释放池
MRC
MRC默认属性修饰符:
MRC:(atomic, readwrite, assign)
MRC手动内存管理
引用计数器:在MRC时代,系统判定一个对象是否销毁是根据这个对象的引用计数器来判断的。
每个对象被创建时引用计数都为1 每当对象被其他指针引用时,需要手动使用[obj retain];让该对象引用计数+1。 当指针变量不在使用这个对象的时候,需要手动释放release这个对象。让其的引用计数-1. 当一个对象的引用计数为0的时候,系统就会销毁这个对象。 在MRC模式下必须遵循谁创建,谁释放,谁引用,谁管理
内存泄漏
内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。该被释放的时候未释放,一直被其内部的对象所持有,循环引用就属于内存泄漏。
block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,就不会在堆上 使用注意:循环引用问题
block在修改NSMutableArray,需不需要添加__block?
不需要 当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。所以,在block里面对指针指向内容做的修改,在block外面也一样生效。
野指针和悬垂指针
野指针
是指向“垃圾”内存(不可用内存)的指针 产生原因:
指针创建时未初始化。指针变量刚被创建时不会自动成为NULL
指针,它会随机指向一个内存地址。
悬垂指针
:指针所指向的对象已经被释放或者回收了,但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。此类指针称为垂悬指针。
@property 属性声明的关键字
: weak
表示非持有特性,为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。当属性所指的对象释放的时候,属性也会被置为 nil
。assign
用来修饰基本数据类型和对象。当 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
跳转。发现问题还是存在,即定时器方法仍然在执行,并没有执行控制器 self
的 dealloc
方法,为什么呢?
我们使用 __weak
虽然打破了 self \-> timer \-> self
之前的循环引用,即引用链变成了self \-> timer \-> weakSelf \-> self
。但是在这里我们的分析并不全面,此时还有一个 Runloop
对 timer
的强持有,因为 Runloop
的生命周期比控制器 self
界面更长,所以导致了 timer
无法释放,同时也导致了控制器 self
界面也无法释放。它们之间的引用链如下图所示:
我们的定时器 timer
捕获的是控制器 self
,是一个对象,其引用链关系为:NSRunLoop -> timer -> weakSelf -> self
。所以RunLoop对整个对象的空间有强持有,runloop没停,timer 和 weakSelf是无法释放的。
方案二(成功):pop时在其他方法中销毁timer
根据前面的解释,我们知道由于 Runloop
对 timer
的强持有,导致了 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
执行
将 timer
的 target
由 self
改成 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");
}
运行结果如下:
运行发现执行 dealloc
之后,timer
还是会继续执行。原因是解决了中介者的释放,但是没有解决中介者的回收,即 self.target
的回收。所以这种方式有缺陷
可以通过在 dealloc
方法中,取消定时器来解决,代码如下:
-(void)dealloc {
NSLog(@"%s",__func__);
[self.timer invalidate];
self.timer = nil;
}
发现pop之后,timer释放,从而中介者也会进行回收释放,运行结果如下:
方案四(成功):NSProxy虚基类的方式
NSProxy
是一个虚基类,它的地位等同于 NSObject
。command+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 theNSObject
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 theforward<wbr>Invocation:
andmethod<wbr>Signature<wbr>For<wbr>Selector:
methods to handle messages that it doesn’t implement itself. A subclass’s implementation offorward<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 anNSMethod<wbr>Signatureobject
accordingly. See theNSDistant<wbr>Object
,NSInvocation
, andNSMethod<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
正常释放了,所以可以释放其持有者,即 timer
和 proxy
,timer
的释放也打破了 runLoop
对 proxy
的强持有。完美的达到了两层释放,解释如下:
self
释放,导致了proxy
的释放dealloc
方法中,timer
进行了释放,所以runloop
强引用也释放了
它们之间的引用链如下图所示:
参考链接:Objective-C runtime机制(7)——SideTables, SideTable, weak_table, weak_entry_t
转自:掘金 奉孝
https://juejin.cn/post/6956144382906990623
- EOF -
看完本文有收获?请分享给更多人
关注「 iOS大全 」加星标,关注 iOS 动态
点赞和在看就是最大的支持❤️