原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、内存布局
- 1、内存五大区域的介绍
- 2、Demo演示
- 二、Tagged Pointer
- 1、引入原因
- 2、特点
- 3、原理
- 4、面试题
- 三、NONPOINTER_ISA
- 1、isa_t 结构
- 2、对象引用计数保存在哪里
- 四、SideTables 散列表
- 1、简介
- 2、MRC
- 3、ARC
- 五、循环引用问题
- 1、产生循环引用问题的原因
- 2、循环引用场景:NSTimer
- 3、探索NSTimer循环引用的解决方案
- 4、使用Proxy中介者解决Timer的循环引用问题
一、内存布局
1、内存五大区域的介绍
栈区
- 创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等
- 在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用
- 和堆一样,用户栈在程序执行期间可以动态地扩展和收缩,向下增长
堆区
- 通过
new
、alloc
等分配,block
经过copy
后的对象。 - 堆可以动态地扩展和收缩,向上增长。
它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会销毁该内存区域对象)。
一般一个 new
就要对应一个release
。在ARC
下编译器会自动在合适位置为OC
对象添加release
操作,会在当前线程Runloop
退出或休眠时销毁这些对象。
NSString
的对象就是stack
中的对象,NSMutableString
的对象就是heap
中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的。两类对象的创建方法也不同,前者直接创建NSString * str1=@"welcome";
,而后者需要先分配再初始化NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];
。
bss(静态区)
未初始化的全局变量和静态变量,程序运行过程内存的数据一直存在,程序结束后由系统释放。
data(常量区)
已初始化的全局变量、静态变量、常量,在程序结束后由系统释放。
text(代码区)
用于存放程序运行时的代码,是被编译成二进制的程序代码。
2、Demo演示
- 0x1:静态常量
- 0x7:栈区
- 0x6:堆区
- 如何找到这个对象:指针 (栈)——>堆区内存空间——>读取到值
// 局部变量
int money = 10;
NSLog(@"小学生的零花钱:%p",&money);
NSLog(@"小学生的年龄:%lu",sizeof(money));
// 对象
NSObject *student = [NSObject new];
NSLog(@"中学生:%@,中学生的姓名:%p",student,&student);
NSLog(@"中学生的年龄:%lu",sizeof(&student));
// 数组
NSArray *class = [[NSArray alloc] init];
NSLog(@"班级:%@,几年级几班:%p",class,&class);
// 静态常量
NSLog(@"int == \t%p",&pureInt);
NSLog(@"static int == \t%p",&staticInt);
NSLog(@"static NSString == \t%p",&staticNSString);
输出结果为:
2021-03-01 17:26:46.721875+0800 内存管理[92080:4361603] 小学生的零花钱:0x7ffee3da905c
2021-03-01 17:26:46.721991+0800 内存管理[92080:4361603] 小学生的年龄:4
2021-03-01 17:26:46.722072+0800 内存管理[92080:4361603] 中学生:<NSObject: 0x600001e60590>,中学生的姓名:0x7ffee3da9050
2021-03-01 17:26:46.722135+0800 内存管理[92080:4361603] 中学生的年龄:8
2021-03-01 17:26:46.722222+0800 内存管理[92080:4361603] 班级:(
),几年级几班:0x7ffee3da9048
2021-03-01 17:26:46.722295+0800 内存管理[92080:4361603] int == 0x10be5c798
2021-03-01 17:26:46.722366+0800 内存管理[92080:4361603] static int == 0x10be5c7a0
2021-03-01 17:26:46.722425+0800 内存管理[92080:4361603] static NSString == 0x10be5c7a8
二、Tagged Pointer
内存管理方案
-
Tagged Pointer:存储
NSNumber
、NSDate
等小对象 -
NONPOINTER_ISA:非指针型
isa
-
SideTable散列表:
spinlock
自旋锁、refcountMap
引用计数表,weak_table_t
弱引用表
1、引入原因
通常我们创建对象,对象存储在堆中,对象的指针存储在栈中,如果我们要找到这个对象,就需要先在栈中,找到指针地址,然后根据指针地址找到在堆中的对象。
这个过程比较繁琐,当存储的对象只是一个很小的东西,比如一个字符串,一个数字。去走这么一个繁琐的过程,无非是耗费性能的,所以苹果就搞出了TaggedPointer
这么一个东西。
假设我们要存储一个NSNumber
对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger
的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。如果没有Tagged Pointer
对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber
、NSDate
一类的对象所占用的内存会翻倍。
苹果提出了Tagged Pointer
对象。由于NSNumber
、NSDate
一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648
,另外1位作为符号位),对于绝大多数情况都是可以处理的。
2、特点
TaggedPointer
专门用来存储小的对象,针对NSNumber
、NSDate
以及部分NSString
的内存优化方案。
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮囊的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
。
Tagged Pointer
指针中包含了当前对象的地址、类型、具体数值。
Tagged Pointer
指针在内存读取上有着3倍的效率,创建时比普通需要malloc
跟free
的类型快106倍,因为避免了从栈和堆读取值的过程。
3、原理
a、实验代码
Tagged Pointer
通过在其最后一个 bit
位设置一个特殊标记,用于将数据直接保存在指针本身中。因为Tagged Pointer
并不是真正的对象,我们在使用时需要注意不要直接访问其isa
变量。
为了存储和访问一个 NSNumber
对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
int intNumber = 1;
float floatNumber = 2.5;
long longNumber = 3;
double doubleNumber = 4.9;
NSNumber *number0 = @(intNumber);
NSNumber *number1 = @(2);
NSNumber *number2 = @(floatNumber);
NSNumber *number3 = @(longNumber);
NSNumber *number4 = @(doubleNumber);
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number0 pointer is %p", number0);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"number4 pointer is %p", number4);
NSLog(@"numberffff pointer is %p", numberFFFF);
// 最终输出结果
number0 pointer is 0xb000000000000012
number1 pointer is 0xb000000000000022
numberFFFF pointer is 0xb0000000000ffff2
可以看到苹果确实是将值直接存储到了指针本身里面。我们还可以猜测,数字最末尾的 2
以及最开头的 0xb
是否就是苹果对于Tagged Pointer
的特殊标记呢?实际上最末尾的数字(这里是2)表示的是数值类型,比如int
、float
等,而倒数第二个数字表示的是Tagged Pointer
的值(Tagged Pointer
不是对象而是值),所以Tagged Pointer
存储的小对象的指针不再是一个简单的地址,而是包括了具体值、类型、长度。
我们尝试放一个长于8 字节的整数到NSNumber
实例中,对于这样的实例,由于Tagged Pointer
无法将其按上面的压缩方式来保存,那么应该就会以普通对象的方式来保存。
NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);
// 输出
bigNumber pointer is 0x10921ecc0
验证了我们的猜测,bigNumber
的地址更像是一个普通的指针地址。可见,当 8 字节可以承载用于表示的数值时,系统就会以Tagged Pointer
的方式生成指针,如果 8 字节承载不了时,则又用以前的方式来生成普通的指针。
b、源码解析
对Tagged Pointer
对象进行编码(添加特殊标识,说明为Tagged Pointer
)和解码(还原,不影响使用)。通过两次异或操作返回原值。
static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t _objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
return value ^ objc_debug_taggedpointer_obfuscator;
}
在_objc_msgSend
函数中Tagged Pointer
对象会直接返回,而不会去查询imp
。
ENTRY _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
4、面试题
为什么第二个for
会崩溃?答:taggedpointer
。
- (void)taggedPointerBug
{
self.queue = dispatch_queue_create("com.xiejiapei", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<100000; I++)
{
dispatch_async(self.queue, ^{
// 文字够短属于小对象符合taggedPointer的存储标准,故未崩溃
self.nameStr = [NSString stringWithFormat:@"xjp"];
});
}
}
- (void)touchTaggedPointer
{
for (int i = 0; i<100000; i++) {
dispatch_async(self.queue, ^{
// 文字太长不符合taggedPointer的存储标准,采用其他方式存储,生成100000个临时变量超负荷故崩溃
self.nameStr = [NSString stringWithFormat:@"dsggkdashjksda"];
});
}
}
执行了objc_release(id obj)
,由于大量的循环,导致了线程问题,使引用计数<=-1
。但是由于第一个循环中的obj
是taggedpointer
类型的string
,会直接return obj
,并不会release
。
这里release
,那retain
的时候咋办呢,引用计数是一直往上增吗?并不是,在objc_retain(id obj)
中,同样判断了obj->isTaggedPointer
,如果是true
,就直接return obj
。
TaggedPointer
对象避免了不断retain
和release
操作(引用计数处理)。异步并发中可能导致在开辟的多个线程中一直执行retain
操作,未能及时release
操作,(或反之,引用计数减为0后再继续减为负数)。
void objc_release(id obj)
{
// 如果是TaggedPointer对象则直接返回
if (obj->isTaggedPointerOrNil()) return;
// 否则进行release操作
return obj->release();
}
id objc_retain(id obj)
{
// 如果是TaggedPointer对象则直接返回
if (obj->isTaggedPointerOrNil()) return obj;
// 否则进行retain操作
return obj->retain();
}
三、NONPOINTER_ISA
1、isa_t 结构
struct objc_object {
isa_t isa;
...
}
在OC的类结构中出现的第一个成员变量就是联合体isa_t
,其保存了很多有用的信息,以主流的arm64
为例,主要包含了:
-
arm64:在
arm64
、x86_64
等不同的架构下,isa
的存储结构是不同的 -
nonpointer:表示是否对
isa
指针开启指针优化。0表示纯isa
指针,1表示isa
中包含了类对象地址、类信息、对象的引用计数等 - has_assoc:关联对象标识位。0:没有关联对象 1:存在关联对象
-
has_cxx_dtor:该对象是否有
C++
或者Objc
的析构器,如何有析构函数则需要做析构处理,如果没有则可以更快地释放对象 -
shiftcls:存储类指针的值。开启指针优化的情况下,在
arm64
架构中有33位用来存储类指针 - magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced:对象是否被指向或者曾经指向一个弱引用变量,没有弱引用的对象可以更快地释放
- unused:标识对象是否正在释放内存
- has_sidetable_rc:当对象引用计数大于10的时候则需要借助该变量存储进位
-
extra_rc:表示该对象的引用计数值,实际上是引用计数-1的值。例如,如果对象的引用计数为10,那么
extra_ _rc
为9。如果引用计数大于10, 则需要使用到上面的has_ sidetable_ rc
# if __arm64__
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
2、对象引用计数保存在哪里
引用计数的存储
extra_rc
和has_sidetable_rc
两个标志就是用来存储对象引用计数的。引用计数的增加主要通过retain()
函数来实现。在不支持nonpointer
的对象中,引用计数只存储在sidetable
中。可以看出在支持nonpointer
的情况下:在isa
的位域extra_rc
足以存储的情况,引用计数会优先选择存储在isa
的extra_rc
位域中。
若isa
位域extra_rc
溢出,则会选择将将引用计数的RC_HALF
(如果extra_rc
占据8bit
,则RC_HALF=2^7
)保存在isa
中,另一半RC_HALF
叠加保存在sidetable
中。之所以这样选择是因为isa_t
的存取理论上会比sidetable
的操作效率上快很多,这样做既可以使超出extra_rc
存储范围的引用计数得到有效存储,又可以确保引用计数的增减足够快速(存取都以extra_rc
优先)。
引用计数的获取
因为对象的引用计数主要存储在isa
位域extra_rc
和散列表中,所以要获取对象的引用计数也需要从两个位置进行获取。在开启nonpointer
的对象中,对象的引用计数包括两部分:存储在isa
中的引用计数;存储在sidetable
中的引用计数。在未开启nonpointer
的对象中,对象的引用计数全部存储在sidetable
中,只需要从sidetable
中获取就可以。
其实在创建对象的过程中(例如alloc
和copy
),存储的引用计数并没有进行+1
操作,而是默认对象的最小引用计数为1。在release
操作时才会判断引用计数-1
是否为0,如果为0则进行释放,否则进行引用计数-1
。
四、SideTables 散列表
1、简介
SideTables
是一个64个元素长度的hash
数组,里面存储了SideTable
。SideTables
的hash
键值就是一个对象obj
的address
。因此可以说,一个obj
对应了一个SideTable
,但是一个SideTable
,会对应多个obj
。因为SideTable
的数量只有64个,所以会有很多obj
共用同一个SideTable
。
a、什么是 Spinlock_t 自旋锁?
自旋锁若已被其他线程获取,则当前线程会不断探索该锁是否被释放,如果释放则第一时间获取,适用于轻量访问的情况。
b、为什么是多张SideTable而不是一张哈希表?
一张表的引用计数或者弱引用需要顺序操作,这就需要等前一个解开锁才能继续工作,存在效率问题。所以通过分离锁打散成多张表(共8个),支持并发操作,提高访问效率。
因为改变属性的引用计数需要对属性进行加锁和解锁操作,如果只有一张哈希表那么会由于操作过于频繁而影响性能。自旋锁由于忙等的特性,如果只有一张哈希表就会由于等待耗时影响性能,而如果有多张哈希表则可以分离锁互不影响以减少耗时。
c、怎样实现快速分流,即通过指针快速定位到属于哪一张SideTable表?
SideTable
本质是一个哈希表,给定值是对象内存地址,目标值是数组下标索引,哈希查找通过取余存储值和取值,内存地址均匀分布。哈希函数是一个均匀散列函数,不需要通过遍历操作,效率很高。
static StripedMap<SideTable>& SideTables()
{
return SideTablesMap.get();
}
class StripedMap
{
enum { StripeCount = 64 };
// 重写下标操作符,用于从SideTables中获取特定SideTable
T& operator[] (const void *p)
{
return array[indexForPointer(p)].value;
}
}
struct SideTable
{
spinlock_t slock;// 自旋锁
RefcountMap refcnts;// 引用计数表
weak_table_t weak_table;// 弱引用表
}
struct weak_table_t
{
weak_entry_t *weak_entries;
};
struct weak_entry_t
{
weak_referrer_t *referrers;
inline_referrers[0] = newReferrer;
}
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
weak_entry_t new_entry(referent, referrer);
weak_entry_insert(weak_table, &new_entry);
}
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
weak_entries[index] = *new_entry;
weak_table->num_entries++;
}
2、MRC
alloc
retain +1
release -1
retainCount 引用计数值
autorelease 结束时候调用release释放
dealloc
a、MRC通过引用计数管理对象生命周期
NSNumber *i = [NSNumber numberWithInteger:2];
[i retainCount];//1
NSMutableArray *array = [NSMutableArray array];
[array addObject:i];
[i retainCount];//2
NSNumber *j = i;//不获得i的所有权
[i retainCount];//2
NSNumber *j = [i retain];//获得
[i retainCount];//3
[i release];//2
[array removeObjectAtIndex:0];
[i retainCount];//1
[i release];//销毁
//释放动态分配的内存和关闭文件
- (void)dealloc{[super dealloc];}
b、alloc实现
经过一系列调用,最终调用了C函数的calloc
,但此时并没有将引用计数+1
。
c、retainCount实现
刚alloc
对象引用计数值为0,之所以 retainCount
能获取到1,是因为添加了局部变量size_t refcnt_result
令其值为1,再让其实现相加操作。
SideTable &table = sideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find(this);
refcnt_resount+= it->second>>SIDE_TABLE_RC_SHIFT
d、retain实现
经过两次哈希查找,第一次从SideTables
中查找到对象所在的SideTable
,第二次从引用计数表中查找到该对象的引用计数值实现+1
(实际是通过地址偏移量来实现的)。
SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage += SIZE_TABLE_RC_ONE
如果溢出就将启用散列表中的引用计数,并将extra_rc
大小置为满载的一半。
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
// 是否溢出
uintptr_t carry;
// extra_rc中的引用计数+1
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
// 如果溢出就将启用散列表中的引用计数,并将extra_rc大小置为满载的一半
if (slowpath(carry))
{
// 将extra_rc大小置为满载的一半,另外一半存到散列表中
newisa.extra_rc = RC_HALF;
// 启用散列表中的引用计数
newisa.has_sidetable_rc = true;
}
}
e、release实现
经过两次哈希查找,第一次从SideTables
中查找到对象所在的SideTable
,第二次从引用计数表中查找到该对象的引用计数值实现-1
(实际是通过地址偏移量来实现的)。
SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage -= SIZE_TABLE_RC_ONE;
如果extra_rc
下溢出了,就会从散列表中去减少一些引用计数。
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
// 是否向下溢出(多减为负)
uintptr_t carry;
// 将extra_rc中的引用计数-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
// 如果extra_rc下溢出了,就会从散列表中去减少一些引用计数
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
// 调用析构函数
if (performDealloc)
{
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
}
f、内存泄漏问题
Analyze
(product-->Analyze
)是静态分析工具 , Instruments
(product-->profile
)是动态分析工具( Leaks
和Allocations
)。静态分析方法能发现大部分的问题,但是仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,因为有的内存泄露是在运行时,用户操作时才产生的,所以我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments
中的Leaks
方法进行排查。
系统工具analyze:设置为YES后会在编译期开启内存泄露的检查
instrument工具里的leak
// 创建两个对象a和b
MemoryObject *a = [MemoryObject new];
MemoryObject *b = [MemoryObject new];
// 互相引用对方
a.obj = b;
b.obj = a;
点击左上角的红色圆点,这时项目开始启动了,由于leaks
是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
- 监控内存分布情况,随着时间的变化内存使用的折线图,有红色菱形图标 O 出现 , 则有内存泄漏
- 显示泄漏的对象,列出了它们的内存地址 、 占用字节 、 所属框架和响应方法等信息
-
RetCt
是引用计数列,最后的引用计数不为零 ,这说明该对象内存没有释放 - 点击右边的跟踪栈信息按钮,可以定位到泄露点在项目中的代码位置
调用者没有这个对象的所有权而释放它 ,都会造成过度释放,从而产生僵尸对象(Zombies
分析模板),试图调用僵尸对象 ,则会崩溃(应用直接跳出),并抛出EXC_BAD_ACCESS
异常,僵尸对象的引用计数变化是 : 1 (创建) --->0 (释放)--->-1(僵尸化)。
3、ARC
什么是ARC?
ARC
是LLVM
和Runtime
协同作用的结果,在这种模式下,程序员不需要清楚地了解获得/放弃对象所有权的时机,ARC 会自动在适当的位置插入retain
、release
或autorelease
函数,这样程序员能将更多的精力用于开发程序的核心功能。
ARC有什么好处?
- 不需要再在意对象的所有权
- 可以删除程序中内存管理部分的大部分代码,使程序看起来更清爽
- 可以避免手动内存管理时的错误(内存泄漏等)
- 可以使多线程环境下的编程更简单。例如:不用担心不同的线程之间可能出现的所有权冲突
ARC有什么特点?
- ARC会禁止调用引用计数的相关函数
retain
、release
、autorelease
、retainCount
- 可以重写
dealloc
方法,但是不能显示调用super.dealloc
- ARC中新增
weak
、strong
属性关键字
五、循环引用问题
1、产生循环引用问题的原因
当双方都在等待对方释放的时候,就形成了循环引用,结果两个对象都不会被释放,只有打破循环引用关系才能够释放内存。ARC代码中的内存泄漏多半是由于强引用循环引起的 ,从而导致一些内存无法释放,这就会导致dealloc()
方法无法被调用, Leaks
模板提供了查看引用循环视图,选择Cycles & Roots
菜单项即可查看。通过使用弱引用既可以防止生成循环引用,实例对象被释放后自动变成nil
,又可以防止对象被释放后形成野指针。
2、循环引用场景:NSTimer
a、NSTimer的创建
方法一:使用timerWithTimeInterval,但需要手动加入Runloop
__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:NSDefaultRunLoopMode];
方法二:使用scheduledTimerWithTimeInterval,会自动加入Runloop
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:weakSelf
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
调用的方法
- (void)fireHome
{
num++;
NSLog(@"hello word - %d",num);
}
b、NSTimer循环引用分析
发现页面消失之后,定时器并没有停止输出,deinit(swift)
方法也没有执行,这就是循环引用了。这个Timer
的addTarget
和UIButton
的addTarget
有什么不同呢?button
对target
是弱引用,Timer
对target
是强引用。我们能直接看出来self
强持有timer
,target
方法中timer
对self
对象进行了强持有,因此造成了循环引用。
3、探索NSTimer循环引用的解决方案
a、weakSelf
按照惯例用weakSelf
去打破强引用的时候,发现weakSelf
没有打破循环引用,timer
仍然在运行。变成了self -> timer -> weakSelf -> self
。timer
之所以无法打破循环关系是因为timer
创建时target
是对weakSelf
的对象强持有操作,而weakSelf
和self
虽然是不同的指针但是指向的对象是相同的,也就相当于间接的强持有了self
,所以weakSelf
并没有打破循环引用关系。
那么block
使用weakSelf
为什么可以打破循环引用呢?
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object; // 实际上是指针赋值
在.cpp
文件中我们可以看到如上代码段,虽然weakself
对象传入进来,但是内部实际操作的是对象的指针,也就是weakself
的指针,我们知道weakself
和self
虽然内存地址相同,但指针地址是不一样的,也就是block
中并没有直接持有self
,而是通过weakSelf
指针操作,所以就打破了self -> block -> weakSelf -> self
中self
这一层的循环引用,变成了self -> block -> weakSelf
(临时变量的指针地址)来打破循环引用。
b、使用weak关键字修饰
一说到循环引用很容易就想到weak
,但是这里用weak
不行。这要从weak
修饰的对象的释放时机说起,用了weak
关键字修饰之后,系统会在一个hash
表中增加一对key value
,key
就是这个对象的hash
值,value
就是这个对象的指针地址。
我们都知道每一个runloop
都有自己的一个autorelease pool
,在一次runloop
结束之后会销毁这个autorelease pool
。在释放这个autorelease pool
的时候,也会到hash
表中找到weak
的对象把它和它的指针都释放掉。那么问题来了,这个timer
是在runloop
里面重复执行的,换而言之,这个runloop
是一直在执行的,所以这个池子根本不会释放。
c、使用invalidate结束timer运行
我们第一时间想到[self.timer invalidate]
不就可以了吗?当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear
还是viewDidDisAppear
?实际上在我们实际操作中,如果当前页面有push
操作而并没有pop
即还在栈里面,这时候我们释放timer
肯定是错误的,所以这时候我们可以用到下面的方法。但发现无论push
进来还是pop
出去计时器都能正常运行,就算继续push
到下一层再pop
回去计时器还是能够继续运行。为什么不在deinit(swift)
方法里面释放?因为denint
方法都不执行了,写了也没用。
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (parent == nil)
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
4、使用Proxy中介者解决Timer的循环引用问题
a、使用Timer时的循环引用问题
❶ 使用selector的方式
func timerCircularReference()
{
self.timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
RunLoop.current.add(self.timer!, forMode: .common)
}
@objc func timerFire()
{
print("火箭🚀发射")
}
deinit
{
print("\(self) 界面销毁了")
}
我们进入了详情界面后启动了计时器会一直打印火箭🚀发射
,这时候点击返回按钮进入列表页面,发现计时器并没有停止,会继续输出火箭🚀发射
,详情界面整个过程中也没有被销毁掉,这就是计时器的循环引用问题。
火箭🚀发射
火箭🚀发射
......
造成计时器的循环引用问题的原因:self -> timer -> self
。self.timer
会让self
详情界面强持有timer
,而target: self
会让timer
强持有详情界面,这就造成了第一重循环引用。还有一点也会导致timer
无法释放,就是RunLoop.current -> timer
,即RunLoop
强持有timer
,让timer
一直存活。
❷ 使用block的方式
func timerCircularReference()
{
self.timer = Timer.init(timeInterval: 1, repeats: true, block:
{ (timer) in
print("火箭🚀发射 \(timer)")
})
RunLoop.current.add(self.timer!, forMode: .common)
}
使用block
闭包的方式因为没有了target: self
,timer
不会再强持有详情界面,打破了第一重循环引用。但是从详情界面返回到列表界面的时候还是会持续打印火箭🚀发射
,说明计时器仍然存活着,但是详情界面却顺利被销毁掉了。这是因为RunLoop
还强持有timer
,让timer
一直存活。况且,我虽然想要解决循环引用问题,但是我不喜欢使用闭包的方式,我更倾向于selector
的方式,这又该如何解决呢?
火箭🚀发射 <__NSCFTimer: 0x6000015246c0>
火箭🚀发射 <__NSCFTimer: 0x6000015246c0>
<RxSwiftSourceCodeAnalysis.ViewController: 0x7fcaa8e0bd60> 界面销毁了
<RxSwiftSourceCodeAnalysis.Proxy: 0x60000206ec80> 销毁了
火箭🚀发射 <__NSCFTimer: 0x6000015246c0>
......
b、创建Proxy中介者工具类
class Proxy: NSObject
{
// target要实现NSObjectProtocol协议里面的isProxy方法
weak var target: NSObjectProtocol?
// 调用的方法
var selector: Selector?
// 创建计时器
var timer: Timer? = nil
......
}
}
c、工具类提供给外界使用的计时器方法
func scheduledTimer(timeInterval interval: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
{
// 在Proxy里创建计时器,计时器的target为aTarget(vc)
self.timer = Timer.init(timeInterval: interval, target: aTarget, selector: aSelector, userInfo: userInfo, repeats: yesOrNo)
RunLoop.current.add(self.timer!, forMode: .common)
// 将外界传入的aTarget赋给Proxy中的target
self.target = aTarget as? NSObjectProtocol
self.selector = aSelector
// self.target虽然是Proxy中的target,但其实是外界传入的aTarget
// 所以如果外界的Target对象能够响应selector方法则可以调用,否则直接返回
guard self.target?.responds(to: self.selector) == true else
{
return
}
}
d、工具类的使用方式
现在的引用关系变成了:vc -> proxy -weak-> target -> vc
,通过weak var target
打破了对target
对象的强引用。
func proxySolveCircularReference()
{
self.proxy.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerFire), userInfo: nil, repeats: true)
}
但是运行时发现从详情界面返回到列表界面的时候还是会持续打印火箭🚀发射
,说明计时器仍然存活着,同样详情界面也没有被销毁掉。什么鬼?为什么还会有这样的问题?
火箭🚀发射
火箭🚀发射
火箭🚀发射
...
仔细分析引用关系发现仍然存在着循环引用的怪圈,就像马尔克斯的《百年孤独》里面历史的循环怪圈一样难以打破。虽然proxy
对target
对象是弱引用了,但是在timer
中仍然对target
对象产生了强引用,这就是说除了上面的vc -> proxy -> target -weak-> vc
这条引用关系路线外,还存在着另外一条引用关系路线:vc -> proxy -> timer -> target -> vc
,这条路线导致了循环引用问题仍然存在。
self.timer = Timer.init(timeInterval: interval, target: aTarget......
e、将timer的target对象变为proxy
计时器本来应该调用外界的selector(timerFire)
,但是经过runtime
的方法实现交换之后调用了Proxy
里面的proxyTimerFire
方法。
func scheduledTimer(timeInterval interval: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
{
// 在Proxy里创建计时器,计时器的target为self(Proxy)
self.timer = Timer.init(timeInterval: interval, target: self, selector: aSelector, userInfo: userInfo, repeats: yesOrNo)
RunLoop.current.add(self.timer!, forMode: .common)
// 将外界传入的aTarget赋给Proxy中的target
self.target = aTarget as? NSObjectProtocol
self.selector = aSelector
// self.target虽然是Proxy中的target,但其实是外界传入的aTarget
// 所以如果外界的Target对象能够响应selector方法则可以调用,否则直接返回
guard self.target?.responds(to: self.selector) == true else
{
return
}
// 将外部的selector和Proxy里面的#selector实现进行交换
let method = class_getInstanceMethod(self.classForCoder, #selector(proxyTimerFire))
class_replaceMethod(self.classForCoder, self.selector!, method_getImplementation(method!), method_getTypeEncoding(method!))
}
当外界传入的aTarget
为空的时候将计时器销毁掉,存在的时候就调用外界的selector(timerFire)
。
@objc fileprivate func proxyTimerFire()
{
if self.target != nil
{
self.target!.perform(self.selector)
}
else
{
self.timer?.invalidate()
self.timer = nil
}
}
Proxy
销毁的时候调用。
deinit
{
print("\(self) 销毁了")
}
输出结果为:
火箭🚀发射
火箭🚀发射
火箭🚀发射
<RxSwiftSourceCodeAnalysis.ViewController: 0x7fca5cc0c210> 界面销毁了
<RxSwiftSourceCodeAnalysis.Proxy: 0x6000025837c0> 销毁了
计时器和详情界面都成功被销毁掉了。
f、针对NSSelectorFromString找不到Selector导致奔溃问题的特殊处理
找到selector
就正常响应,否则给出错误提示并可以进行处理(比如消息二次转发或者添加方法)。
func proxySolveCircularReference()
{
let selector = NSSelectorFromString("timerFire")
self.proxy.scheduledTimer(timeInterval: 1, target: self, selector: selector, userInfo: nil, repeats: true)
}
override func forwardingTarget(for aSelector: Selector!) -> Any?
{
if self.target?.responds(to: self.selector) == true
{
return self.target
}
else
{
print("老大,老大,你不要为难我呀,我不想浪费时间给你填坑呀!")
return super.forwardingTarget(for: aSelector)
}
}
找到selector
正常响应时候的输出结果为:
火箭🚀发射
火箭🚀发射
火箭🚀发射
<RxSwiftSourceCodeAnalysis.ViewController: 0x7fca5cc0c210> 界面销毁了
<RxSwiftSourceCodeAnalysis.Proxy: 0x6000025837c0> 销毁了
找不到selector
给出崩溃提示的时候输出结果为:
老大,老大,你不要为难我呀,我不想浪费时间给你填坑呀!
2021-01-20 11:37:18.157544+0800 RxSwiftSourceCodeAnalysis[22879:1193311] -[RxSwiftSourceCodeAnalysis.Proxy timerFir]: unrecognized selector sent to instance 0x600003a96620
Demo
Demo在我的Github上,欢迎下载。
BasicsDemo