内存管理解析

前言

今天我们大致分析下内存管理相关的底层原理等知识点,分为包括内存布局内存管理方案两大块,其中内存管理方案会重点分析引用计数相关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。

三、其它内存管理方案

内存管理方案除了前文提及的MRCARC,还有以下三种

  1. Tagged Pointer:专门用来处理小对象,例如NSNumber、NSDate、小NSString等。
  2. Nonpointer_isa:非指针类型的isa,主要是用来优化64位地址。(不做过多的介绍)
  3. 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的两种初始化】👇

  1. WithString 或 @"xxx"
  2. 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种

  1. __NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区

  2. __NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆区

  3. 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完整的二进制👇

  1. 最高位(第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。

  1. 接着第60到62的索引对应的值是010,转换成十进制是2,表示是OBJC_TAG_NSString,即是NSString类型。
  2. 第4到第59的索引对应的值是01100010,转换成十进制是98,ASCII即b

还不信,自己可以再次验证NSNumberNSDate类型的十六进制对应的值。

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的执行流程如下👇

  1. taggedPointer时,直接返回原对象。

  2. 非taggedPointer,retian操作时,先将新isa拷贝当前isa对象,因为copy不影响旧值

  3. 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(使用散列表)标记为truetranscribeToSideTable(转移给散列表)标记为true
  4. do-while结束,判断transcribeToSideTable(转移给散列表)的标记位,给散列表添加extra_rc最大容量的一半进行计数sidetable_addExtraRC_nolock(RC_HALF)

  5. 返回,结束流程。

至此,我们清楚了引用计数的增加的底层实现策略及流程,那么,问题来了

  1. 散列表为什么在内存是一张还是多张?最多有多少张?
  2. 引用计数溢出时,为何采用散列表去存储,而不用数组或者链表?

散列表有多少张,这个得看散列表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以外,常用的还有releaseretainCount操作。

3.3.1 release

同理,来到rootRelease👇

  1. retry流程👇
  1. underflow流程👇

综上,release比retain稍微多了一个dealloc的触发流程,详细步骤如下👇

  1. 如果是小对象taggedPointer,则直接返回该对象,不需要处理引用计数。
  2. 不管是retry还是underflow,都是先将isa的位域信息copy给newisa
  3. retry代码块:(do-while循环,监测多线程环境下,当前isa是否变化)
    3.1 如果不支持指针优化,直接操作散列表进行release
    3.2 尝试给isa的extra_rc - 1,如果失败,跳转underflow
  4. 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的流程如下👇

  1. 判断:优化指针,且无弱引用表,无关联对象,无析构函数、无散列表,直接free。
  2. 其他情况,依次检查:
    有析构函数:调用析构函数
    有关联对象:移除关联对象
    非指针优化:直接清除散列表
    散列表中有弱引用表:直接清除
    使用引用计数表:移除散列表内的引用计数表
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。接下来我们重点分析一下weakstrong的底层实现原理。

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

接下来我们看看两个核心的流程处理:

  1. weak_unregister_no_lock
  2. weak_register_no_lock

在分析这两个流程之前,我们先看看weak_entry_t的内部结构👇

  • weak_unregister_no_lock底层流程
  • weak_register_no_lock底层流程

综上,我们清楚了弱引用weak的底层实现流程,需要注意以下几点细节👇

  1. weak是使用weakTable弱引用表进行存储信息,这个弱引用表其实就是上面讲的sideTable散列表(哈希表)中的成员变量
  2. 弱引用表中存储的元素所对应的结构体是weak_entry_t,将referent引用计数加入到weak_entry_t的成员变量数组inline_referrers中。
  3. 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个问题:

  1. 为什么无法释放?
  2. 怎样才能正常释放?
4.2.1 强引用

之前我们在Block底层中分析过循环引用,一般是某个对象被其它对象强引用了,导致其无法释放。那么,NSTimer的初始化方法是否存在强引用的情况呢?我们可以查询官方文档👇

果然,定时器会维持对这个对象的强引用直到它(定时器)失效

那么,我们只要打破这个强引用关系,就能解除循环引用了。针对这个timer:self -> timer -> 加入weakself -> self,但是,真实情况是这样吗?仔细看代码,我们可以发现👇

  1. 当前timer除了被self持有,还被加入[NSRunLoop currentRunLoop]
  2. 当前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虚基类(推荐使用

NSProxyNSObject同级,是个抽象类,内部什么都没有,但是可以持有对象,并将消息全部转发给对象。示例代码👇

@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,以及面试中经常问到的强弱引用的底层实现原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容