前言
今天我们大致分析下内存管理相关的底层原理等知识点,分为包括内存布局
和内存管理方案
两大块,其中内存管理方案会重点分析引用计数
相关api的底层源码,以及结合示例分析 weak strong
的底层实现原理。
一、内存布局
我们之前在内存五大分区分析过内存的布局,按照内存地址从高(0xffffffff)到低(0x00000000)的顺序排列,可分为5大分区:栈区 -> 堆区 -> 全局静态区 -> 常量区 -> 代码区
。其实这5大分区归属于内存区
,除了内存区
,内存中还有内核区
和保留区
👇
- 内核区 --> 系统内核处理操作的区域
- 保留区 --> 系统预留处理nil NULL等
以4GB内存手机为例,如下所示,系统将其中的3GB给了五大区+保留区,剩余的1GB给内核区使用👇
二、ARC & MRC
iOS MacOS中的内存管理方案,大致可以分为两类:MRC(手动内存管理)
和ARC(自动内存管理)
。
2.1 MRC
在MRC时代,系统是通过对象的引用计数
来判断一个是否销毁,有以下规则👇
对象被创建时引用计数都为1
当对象被其他指针引用时,需要手动调用[objc retain],使对象的引用计数+1
当指针变量不再使用对象时,需要手动调用[objc release]来释放对象,使对象的引用计数-1
当一个对象的引用计数为0时,系统就会销毁这个对象
所以,在MRC模式下,必须遵守:谁创建,谁释放,谁引用,谁管理
。
2.2 ARC
ARC模式是在WWDC2011和iOS5引入的自动管理机制,即自动引用计数
,是编译器的一种特性。其规则与MRC一致,区别在于,ARC模式下不需要手动
retain、release、autorelease。编译器会在适当的位置
插入release和autorelease。
三、其它内存管理方案
内存管理方案除了前文提及的MRC
和ARC
,还有以下三种
-
Tagged Pointer
:专门用来处理小对象,例如NSNumber、NSDate、小NSString等。 -
Nonpointer_isa
:非指针类型的isa,主要是用来优化64位地址。(不做过多的介绍) -
SideTables
:散列表
,在散列表中主要有两个表,分别是引用计数表
和弱引用表
。
3.1 TaggedPointer
我们先创建一个Demo工程,在ViewController.m
中添加下面的代码,看看输出是什么?
- (void)viewDidLoad {
[super viewDidLoad];
[self taggedPointerDemo];
}
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci_和谐学习不急不躁"];
NSLog(@"%@",self.nameStr);
});
}
}
run👇
再点击屏幕,看看输出是什么?
我们发现,taggedPointerDemo方法单独运行没有问题,当触发touchesBegan方法后。程序会崩溃,崩溃的原因是多条线程同时对一个对象进行释放,导致了过渡释放
,所以崩溃。
我们分别在taggedPointerDemo方法和touchesBegan方法中打断点(touchesBegan中先注释掉异步并发队列),看看self.nameStr是个什么类型?
一个是NSTaggedPointerString
,一个是__NSCFString
。why?
taggedPointerDemo方法中,nameStr在alloc分配时在
堆区
,由于较小
,所以经过iOS的优化
,成了NSTaggedPointerString类型
,存储在常量区
。touchesBegan方法中的nameStr类型是
NSCFString
类型,实实在在的存储在堆区
。
接着,我们再看看一个例子【NSString的两种初始化】👇
- WithString 或 @"xxx"
- WithFormat
- (void)testNSString{
NSString *str1 = @"1";
NSString *str2 = [[NSString alloc] initWithString:@"222"];
NSString *str3 = [NSString stringWithString:@"33"];
NSLog(@"%p-%@ class:%@",str1,str1, [str1 class]);
NSLog(@"%p-%@ class:%@",str2,str2, [str2 class]);
NSLog(@"%p-%@ class:%@",str3,str3, [str3 class]);
NSLog(@"------------分割线------------");
//字符串长度在9以内
NSString *str4 = [NSString stringWithFormat:@"123456789"];
NSString *str5 = [[NSString alloc] initWithFormat:@"123456789"];
NSLog(@"%p-%@ class:%@",str4,str4, [str4 class]);
NSLog(@"%p-%@ class:%@",str5,str5, [str5 class]);
NSLog(@"------------分割线------------");
//字符串长度大于9
NSString *str6 = [NSString stringWithFormat:@"1234567890"];
NSString *str7 = [[NSString alloc] initWithFormat:@"1234567890"];
NSLog(@"%p-%@ class:%@",str6,str6, [str6 class]);
NSLog(@"%p-%@ class:%@",str7,str7, [str7 class]);
}
运行👇
以上发现,NSString的类型分为3种
__NSCFConstantString
:字符串常量,是一种编译时常量
,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区
。__NSCFString
:是在运行时
创建的NSString子类
,创建后引用计数会加1
,存储在堆区
。-
NSTaggedPointerString
:标签指针
,是苹果在64位环境
下对NSString、NSNumber等对象做的优化
。- 当字符串是
由数字、英文字母组合
且长度小于等于9
时,会自动成为NSTaggedPointerString类型
,存储在常量区
。 - 当字符串有
中文
或者其他特殊符号
时,会直接成为__NSCFString类型
,存储在堆区
。
- 当字符串是
NSTaggedPointerString底层
上面的例子,我们打断点👇
看看汇编层👇
发现是调用objc_retain
,看看源码👇
发现,如果是TaggedPointer
小对象,则直接返回。那么对应的release呢?👇
果然,也是一样 --> taggedPointer对象不参与引用计数的计算
。
接着,我们看看isTaggedPointer()
到底是依据什么来判断的?先看看源码👇
inline bool
objc_object::isTaggedPointer()
{
return _objc_isTaggedPointer(this);
}
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
而_OBJC_TAG_MASK定义👇
这里做了一个与掩码
的位运算操作,why?
补充:taggedPointer混淆机制
之前我们分析iOS应用程序加载大致流程分析时,知道App启动,dyld调用_read_images
时,有一个初始化taggedPointer混淆机制
👇
接着看看initializeTaggedPointerObfuscator
源码👇
继续搜索看看objc_debug_taggedpointer_obfuscator
是个什么东西?👇
我们发现,_objc_encodeTaggedPointer
和 _objc_decodeTaggedPointer
都执行一个混淆
的操作,即异或objc_debug_taggedpointer_obfuscator
。
示例验证一下,混淆
到底做了什么?Demo👇
extern uintptr_t objc_debug_taggedpointer_obfuscator;
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
- (void)testTaggedPointer {
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
}
run👇
这是一个解码
的验证,str2解码后是0xa000000000000621
,其中0x62
对应二进制是98,而98的ASCII码
就是b
。
那为什么取末尾的
62
,那么前面的a
和末尾的1
,并且中间位的0
,都代表什么意思呢?
我们先看看0xa000000000000621
对应的完整的二进制代码👇
接着,我们发现,在_objc_encodeTaggedPointer
和 _objc_decodeTaggedPointer
函数的后面,有一个函数_objc_makeTaggedPointer
源码👇
进行了一系列的位运算,函数的第一个入参是objc_tag_index_t
,是个枚举👇
再回来看看0xa000000000000621
完整的二进制👇
- 最高位(第63个索引值)是1,根据
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
define _OBJC_TAG_MASK (1UL<<63)
那么_objc_isTaggedPointer
就是判断最高位
(第63个索引)的值是否是1
--> 答案是1。
- 接着
第60到62的索引
对应的值是010
,转换成十进制是2
,表示是OBJC_TAG_NSString
,即是NSString类型。 -
第4到第59的索引
对应的值是01100010
,转换成十进制是98
,ASCII即b
。
还不信,自己可以再次验证NSNumber
和 NSDate
类型的十六进制对应的值。
Tagged Pointer 小结
用于存储
NSNumber、NSDate、小NSString
,小对象指针不再是简单的地址
,而是地址 + 值
,值直接放入了地址中的某些索引位置,所以可以直接进行读取。优点是占用空间小,节省内存
。Tagged Pointer小对象不会进入retain 和 release,而是直接返回了,那么ARC不需要对其进行管理,所以可以直接被系统自主的释放和回收。
Tagged Pointer的内存实际对应在常量区中,不在堆区,所以也不需要malloc和free,可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右。
所以,综合来说,taggedPointer的内存管理方案,比常规的内存管理,要快很多。
总之:Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值。
优化内存建议:对于NSString来说,当
字符串长度<=9时,建议直接通过@""进行初始化
,因为存储在常量区,可以直接进行读取,会比WithFormat初始化方式
更加快速
。
3.2 SideTables 散列表
根据之前isa结构分析中,我们知道了对象的isa指针结构体
的具体位置分部及作用,其中👇
-
has_sidetable_rc
标记是否有sidetable结构
,用于存储引用计数
。 -
extra_rc
标记对象的引用计数是多少
。(首先会存储在该字段中,当到达上限后,在存入对应的引用计数表
中)
那么这个sidetable
就是我们现在要分析的对象散列表
。
3.2.1 retain
既然是引用计数
的增加,谁负责增加了,众所周知 --> retain
!我们先看看retain的源码(就是我们之前汇编看到的objc_retain
)👇
接着看看retain()👇
继续,rootRetain()👇
显然,核心代码是这个do-while循环
。
3.2.2 引用计数的增加
接着
其中,我们看看散列表sidetable
的结构体👇
其中,
RefcountMap
就类似于关联对象的底层结构,支持递归持有的特性。
接着看看几个关键的散列表操作
- sidetable_tryRetain
- sidetable_retain
- sidetable_addExtraRC_nolock
至此,retain的执行流程如下👇
taggedPointer
时,直接返回
原对象。非taggedPointer
,retian操作时,先将新isa拷贝
当前isa对象,因为copy不影响旧值
。-
do-while循环,是由于
多线程
环境下,当前isa可能在变化,只要变化就需要再次操作。
3.1 如果不支持指针优化!newisa.nonpointer
,直接操作散列表进行计数sidetable_retain
。
3.2. 如果isa记录了正在释放tryRetain && newisa.deallocating
,就不用retain
了。
3.3 引用计数+1,首先尝试在isa的extra_rc
中+1:- 不处理溢出:
递归
retain,再一次do-while循环,进入第3步 - 处理溢出: 表示
extra_rc存储满
了,此时将extra_rc计数减半
,has_sidetable_rc
(使用散列表)标记为true
,transcribeToSideTable
(转移给散列表)标记为true
。
- 不处理溢出:
do-while结束,判断
transcribeToSideTable
(转移给散列表)的标记位,给散列表添加extra_rc最大容量的一半进行计数sidetable_addExtraRC_nolock(RC_HALF)
。返回,结束流程。
至此,我们清楚了引用计数的增加的底层实现策略及流程,那么,问题来了
- 散列表为什么在内存是一张还是多张?最多有多少张?
- 引用计数溢出时,为何采用散列表去存储,而不用数组或者链表?
散列表有多少张,这个得看散列表sideTable是如何创建生成的?我们注意到散列表的几个操作函数中sidetable_tryRetain sidetable_retain sidetable_addExtraRC_nolock
都有👇
SideTable& table = SideTables()[this];
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
而StripedMap定义如下👇
所以,散列表是多张,且最多为8张。
问题2,引用计数溢出操作时,使用散列表的结构来存储的原因:
- 使用
数组(有序表)
时,其特点是查询快,增删慢,即读快而写慢
- 使用
链表
时,增删快,但查询慢,因为要根据头节点一个个的向后或向前查找,即读慢而写快
- 那么
散列表
,一个哈希表结构的无序表
,通过哈希算法确定位置,不论是读还是写,都很快
。
同时,根据之前的锁的分析,我们知道,哈希表可形成一个多条链
的形式,如下图👇
3.3 引用计数的其它操作
引用计数除了retain
以外,常用的还有release
和 retainCount
操作。
3.3.1 release
同理,来到rootRelease
👇
- retry流程👇
- underflow流程👇
综上,release比retain稍微多了一个dealloc的触发流程,详细步骤如下👇
- 如果是小对象
taggedPointer
,则直接返回该对象,不需要处理引用计数。 - 不管是
retry
还是underflow
,都是先将isa的位域信息copy给newisa
。 - retry代码块:(
do-while循环
,监测多线程环境下,当前isa是否变化)
3.1 如果不支持
指针优化,直接操作散列表
进行release
。
3.2 尝试给isa的extra_rc - 1
,如果失败,跳转underflow
- underflow代码块:(表示extra_rc计数
不可以进行-1
,需要去散列表
获取引用计数再操作或直接dealloc)
4.1 如果有散列表(newisa.has_sidetable_rc
)
4.1.1 如果散列表没锁
,上锁完再重新 retry
4.1.2 尝试从散列表
中读取isa.extra_rc
的一半容量
的引用计数
4.1.2.1 读取成功
,同步给oldisa
的extra_rc
4.1.2.2 同步失败多给一次机会
:再次读取当前isa的位域bits信息,然后再同步剩下的一半的引用计数-1
4.1.2.3 如果还是失败
,就将borrowed之前读取一半容量
的引用计数返回给散列表
4.2 散列表
内已经空
了,表示没有引用计数信息时,去到释放dealloc
的流程
4.3 dealloc的流程:
4.3.1 如果正在释放中,则清空当前isa的位域信息,包括引用计数。然后报错:过渡释放
!
4.3.2 否则,将deallocating
置为true
(去释放)
4.3.3 如果引用计数同步
给oldisa失败
,则重新retry
4.3.4 如果指定释放
(入参指定),则消息发送触发dealloc
3.3.2 dealloc
底层是调用_objc_rootDealloc
👇
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
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);
}
}
-
指针优化,无弱引用表,无关联对象,无析构函数,无引用计数散列表
,则直接free
- 否则进入
object_dispose
👇
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
接着看objc_destructInstance
👇
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;
}
- 有析构函数,则调用析构
- 有关联对象,则移除关联
- 接着clearDeallocating()
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());
}
- 非指针优化,则清空并析构散列表
- 有弱引用表或有使用引用计数散列表,则调用
clearDeallocating_slow()
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();
}
- 获取当前isa对应的散列表table
- 散列表中有弱引用表,则清除
- 当前isa有使用引用计数散列表,则清空散列表中的引用计数表
至此,dealloc的流程如下👇
- 判断:优化指针,且无弱引用表,无关联对象,无析构函数、无散列表,直接free。
- 其他情况,依次检查:
有析构函数:调用析构函数
有关联对象:移除关联对象
非指针优化:直接清除散列表
散列表中有弱引用表:直接清除
使用引用计数表:移除散列表内的引用计数表
3.3.3 retainCount
最后,我们来看看retainCount
的一个使用案例👇
// Q:打印的引用计数为多少,alloc、init改变了引用计数吗?
- (void)demo {
NSObject * objc = [NSObject alloc];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef) objc));
objc = [objc init];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef) objc));
}
答案是1 和 1,那么说明,不论是alloc
还是init
,其实都没有触发
引用计数+1,这个1是CFGetRetainCount
读取内存后触发的。why?我们先看看CFGetRetainCount
的源码👇
可以加入符号断点CFGetRetainCount
,然后查看汇编👇
去到objc源码搜索retainCount
👇
沿着调用链来到
rootRetainCount()
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
- 如果是
bits.nonpointer
才会进行引用计数的1 + bits.extra_rc - 否则返回散列表的
sidetable_retainCount()
从源码中可知,只有isa指针支持指针优化
时,系统会给个1+ bits.extra_rc
,如果有散列表,再+散列表的计数
,而真实打印结果是1
,则说明alloc、init是不处理对象引用计数
。
并且,我们注意到,rootRetainCount
只是对引用计数读操作
,并没有写操作
(存入其isa的extra_rc中
或散列表中
),这个系统+1的操作只是为了防止alloc创建的对象被释放
,因为引用计数为0的话会被释放
,而实际上在extra_rc中
的引用计数仍然为0
。
四、weak & strong
上面说完了引用计数
的相关底层实现,其实在我们日常开发中,还会经常碰到另一个关于内存管理的场景:weak & strong
强持有与弱持有,也经常因为某些变量被强持有而导致页面无法dealloc。接下来我们重点分析一下weak
和 strong
的底层实现原理。
4.1 weak底层
4.1.1 找入口
我们先写一句常用的代码,断点👇
查看底层汇编👇
这个objc_initWeak
就是入口。
4.1.2 objc_initWeak
先看源码👇
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
接着看看storeWeak
接下来我们看看两个核心的流程处理:
- weak_unregister_no_lock
- weak_register_no_lock
在分析这两个流程之前,我们先看看weak_entry_t
的内部结构👇
- weak_unregister_no_lock底层流程
- weak_register_no_lock底层流程
综上,我们清楚了弱引用weak的底层实现流程,需要注意以下几点细节👇
-
weak
是使用weakTable弱引用表
进行存储信息
,这个弱引用表其实就是上面讲的sideTable散列表(哈希表)
中的成员变量
。 -
弱引用表
中存储的元素
所对应的结构体是weak_entry_t
,将referent引用计数
加入到weak_entry_t
的成员变量数组inline_referrers
中。 - weak_table可以扩容
weak_grow_maybe
,再把new_entry即weak_entry_t
加入到弱引用表weak_table
中。
以下是weak的底层实现流程图👇
4.2 strong强持有分析
分析strong之前,我们先看一个案例👇
@property (nonatomic, strong) NSTimer *timer;
- (void)createTimer {
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)fireHome{
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
注意:
NSTimer
创建后,需要手动加入到Runloop中
才可以运行,但timer会使得当前控制器不走dealloc方法,导致timer和控制器
都无法释放
。
那么接下来我们就来解决NSTimer
所带来的2个问题:
- 为什么无法释放?
- 怎样才能正常释放?
4.2.1 强引用
之前我们在Block底层中分析过循环引用
,一般是某个对象被其它对象强引用了,导致其无法释放。那么,NSTimer的初始化方法是否存在强引用
的情况呢?我们可以查询官方文档👇
果然,定时器会维持对这个对象的
强引用
,直到
它(定时器)失效
。
那么,我们只要打破这个强引用关系,就能解除循环引用了。针对这个timer:self -> timer -> 加入weakself -> self
,但是,真实情况是这样吗?仔细看代码,我们可以发现👇
- 当前timer除了
被self持有
,还被加入
了[NSRunLoop currentRunLoop]
中 - 当前timer直接
指向self的内存空间
,是对内存
进行强持有
,而不是
简单的指针拷贝
。
总之,如官网所说,currentRunLoop
没结束,timer没失效
,那么timer就不会释放
,self的内存空间
也不会释放
。
4.2.2 解决方案
- 方案1:didMoveToParentViewController手动打断循环
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
- 方案2:不加入Runloop,使用官方闭包API
- (void)createTimer{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}
- 方案3:中介者模式(不使用self)
既然timer会强持有对象(内存空间),我们就给他一个中介者的内存空间
,让timer碰不到ViewController,我们再对中介者操作和释放。
那么就自定义一个NSobject的类,其包含定时器功能,示例代码如下👇
@interface XFTimer : NSObject
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats;
- (void)invalidate;
- (void)fire;
@end
----------------------------------------------分割线---------------------------------------------------
@interface XFTimer ()
@property (nonatomic, strong) NSTimer * timer;
@property (nonatomic, weak) id aTarget;
@property (nonatomic, assign) SEL aSelector;
@end
@implementation XFTimer
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats {
XFTimer * timer = [XFTimer new];
timer.aTarget = aTarget;
timer.aSelector = aSelector;
timer.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:timer selector:@selector(run) userInfo:userInfo repeats:repeats];
[[NSRunLoop currentRunLoop] addTimer:timer.timer forMode:NSRunLoopCommonModes];
return timer;
}
- (void)run {
//如果崩在这里,说明你没有在使用Timer的VC里面的deinit方法里调用invalidate方法
if(![self.aTarget respondsToSelector:_aSelector]) return;
// 消除警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.aTarget performSelector:self.aSelector];
#pragma clang diagnostic pop
}
- (void)fire {
[_timer fire];
}
- (void)invalidate {
[_timer invalidate];
_timer = nil;
}
- (void)dealloc
{
// release环境下注释掉
NSLog(@"计时器已销毁");
}
@end
调用代码👇
@interface TimerViewController ()
@property (nonatomic, strong) XFTimer * timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建
self.timer = [XFTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome{
NSLog(@"hello word" ); // 调用
}
- (void)dealloc{
// 释放
[self.timer invalidate];
NSLog(@"%s",__func__);
}
@end
- 方案4:NSProxy虚基类(
推荐使用
)
NSProxy
与NSObject
同级,是个抽象类,内部什么都没有,但是可以持有对象,并将消息全部转发给对象。示例代码👇
@interface XFProxy : NSProxy
/// 麻烦把消息转发给`object`
+ (instancetype)proxyWithTransformObject:(id)object;
@end
----------------------------------------------分割线---------------------------------------------------
#import "XFProxy.h"
@interface XFProxy ()
@property (nonatomic, weak) id object; // 弱引用object
@end
@implementation XFProxy
/// 麻烦把消息转发给`object`
+ (instancetype)proxyWithTransformObject:(id)object {
XFProxy * proxy = [XFProxy alloc];
proxy.object = object;
return proxy;
}
// 消息转发。 (所有消息,都转发给object去处理)
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
// 消息转发 self.object(可以利用虚基类,进行数据收集)
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
//
// if (self.object) {
// }else{
// NSLog(@"麻烦收集 stack111");
// }
// return [self.object methodSignatureForSelector:sel];
//
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation{
//
// if (self.object) {
// [invocation invokeWithTarget:self.object];
// }else{
// NSLog(@"麻烦收集 stack");
// }
//
//}
-(void)dealloc {
NSLog(@"%s",__func__);
}
@end
调用代码👇
@interface TimerViewController ()
@property (nonatomic, strong) XFProxy * proxy;
@property (nonatomic, strong) NSTimer * timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建虚基类代理
self.proxy = [XFProxy proxyWithTransformObject: self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome{
NSLog(@"hello word" ); // 调用
}
- (void)dealloc{
// 释放
[self.timer invalidate];
NSLog(@"%s",__func__);
}
@end
注意
NSProxy
是抽象类,必须继承再使用。- proxy中是
weak弱引用object
。
总结
本篇文章围绕内存管理
,首先介绍了内存布局
,接着通过内存管理
的几个方案,详细介绍了TaggedPoint
小对象,引用计数
相关的Api,以及面试中经常问到的强弱引用的底层实现原理
。