阅读 2047

iOS探索 内存管理篇

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

一个优秀的app必然是对内存"精打细算"的,本文就来探索一下内存管理中的一些门道。如果你看到了这篇文章,请仔细看下去,尤其是NSTimer部分的内容介绍了NSTimerBlock中的循环引用存在的差异

一、内存布局

1. 五大区

接下来我从内存中的低地址往高地址依次介绍五大区

  • 代码段(.text)
    • 存放着程序代码,直接加载到内存中
  • 初始化区域(.data)
    • 存放着初始化的全局变量、静态变量
    • 一般以 0x1 开头
  • 未初始化区域(.bss)
    • bss段存放着未初始化的全局变量、静态变量
    • 一般以 0x1 开头
  • 堆区(.heap)
    • 堆区存放着通过alloc分配的对象、block copy后的对象
    • 堆区速度比较慢
    • 一般以 0x6 开头
  • 栈区(.stack)
    • 栈区存储着函数、方法以及局部变量
    • 栈区比较小,但是速度比较快
    • 一般以 0x7 开头

在这里提一句关于函数在内存中的分布:函数指针存在栈区,函数实现存在堆区

除了五大区之外,内存中还有保留字段内核区

  • 内核区:在4gb内存中只用到了3gb,还有1gb给内核区处理
  • 保留字段:保留一定的区域给保留字段,进行一些存储

平时在使用App过程中,栈区就会向下增长,堆区就会向上增长

接下来就用LLDB来打印一下堆区和栈区中的一些内容

  • 堆区访问对象的顺序是先拿到栈区的指针,再拿到指针指向的对象,才能获取到对象的isa、属性方法等
  • 栈区访问对象的顺序是直接通过寄存器访问到对象的内存空间,因此访问速度快

2. 内存布局面试题

  1. 全部变量和局部变量在内存中是否有区别?
  • 全局变量存放在相应的全局储存区,而局部变量存放在栈区
  • 两者访问的权限不一样
  1. block是否可以修改全局变量 可以,因为全局变量的作用域很大(可以理解为公共区域),block可以访问到

  2. 关于全局静态变量的误区

  • 全局静态变量是可变的
  • 全局静态变量的值只针对文件而言,不同文件的全局静态变量的内存地址是不一样的
    - 假设有个`全局静态变量num`=10
    - vc中修改`num=20`并在vc中打印会输出`20`
    - view中修改`num=num+1`并在view中会输出`11`
    - model中修改`num=0`并在model中会输出`0`
    - 在model的分类中修改`num=1000`并在model分类中会输出`1000`
    复制代码

总结: 全局静态变量只针对文件而言,无论别的文件怎么修改,本文件使用时都拿原有值/本文件修改后的值

二、内存管理方案

1. taggedPointer

1.1 taggedPointer初探

分别调用下面两种方法,哪个会崩溃?为什么?

@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t  queue;
@property (nonatomic, strong) NSString *name;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.queue = dispatch_queue_create("com.felix", DISPATCH_QUEUE_CONCURRENT);
    [self testNormal];
    [self testTaggedPointer];
}

- (void)testNormal {
    for (int i = 0; i < 10000; i++) {
        dispatch_async(self.queue, ^{
            self.name = [NSString stringWithFormat:@"1234567890-"];
            NSLog(@"%@", self.name);
        });
    }
}

- (void)testTaggedPointer {
    for (int i = 0; i < 10000; i++) {
        dispatch_async(self.queue, ^{
            self.name = [NSString stringWithFormat:@"F"];
             NSLog(@"%@", self.name);
        });
    }
}
复制代码

经过运行测试之后,会发现testNormal会崩溃,而testTaggedPointer方法正常运行

首先来分析下为什么会崩溃的原因?其实是多线程和setter、getter操作造成的

  • 调用setter方法objc_retain(newValue)+objc_release(oldValue)
  • 但是加上多线程就不一样了——在某个时刻线程1对旧值进行relese(没有relese完毕),同时线程2对旧值进行relese操作,即同一时刻对同一片内存空间释放多次,会造成野指针问题(访问坏的地址)

但是为什么testNormal会崩溃,而testTaggedPointer方法正常运行?

  • testNormal中的对象为NSCFString类型

  • testTaggedPointer中的对象为NSTaggedPointerString类型

其实之前在objc源码的方法中有看到过类似的身影——objc_retainobjc_release的对象如果是isTaggedPointer类型就直接返回(不操作)

__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
复制代码

其实taggedPointer的优化不仅仅如此,在read_images阶段就对taggedPointer进行了处理

1.2 taggedPointer深入

在推出iPhone 5s(iPhone首个采用64位架构)的时候,为了节省内存和提高执行效率,同时也提出了taggedPointer

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}
复制代码

底层也做了对objc_debug_taggedpointer_obfuscator进行异或的操作(两次异或同一个数相当于编码解码)

extern uintptr_t objc_debug_taggedpointer_obfuscator;

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
复制代码

我们也可以通过类似的方法对taggedPointer进行解码

extern uintptr_t objc_debug_taggedpointer_obfuscator;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *str1 = @"F";
    NSString *str2 = [NSString stringWithFormat:@"F"];
    NSString *str3 = [NSString stringWithFormat:@"F"];
    NSNumber *number1 = @10;
    NSNumber *number2 = [NSNumber numberWithInt:10];
    NSNumber *number3 = [NSNumber numberWithFloat:10];
    NSNumber *number4 = [NSNumber numberWithDouble:10];
    NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:0];
    NSLog(@"%@  %p  %lx", str1, str1, _objc_decodeTaggedPointer_(str1));
    NSLog(@"%@  %p  %lx", str2, str2, _objc_decodeTaggedPointer_(str2));
    NSLog(@"%@  %p  %lx", str3, str3, _objc_decodeTaggedPointer_(str3));
    NSLog(@"%@  %p  %lx", number1, number1, _objc_decodeTaggedPointer_(number1));
    NSLog(@"%@  %p  %lx", number2, number2, _objc_decodeTaggedPointer_(number2));
    NSLog(@"%@  %p  %lx", number3, number3, _objc_decodeTaggedPointer_(number3));
    NSLog(@"%@  %p  %lx", number4, number4, _objc_decodeTaggedPointer_(number4));
    NSLog(@"%@  %p  %lx", indexPath, indexPath, _objc_decodeTaggedPointer_(indexPath));
}

uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

--------------------输出结果:-------------------
F  0x1065591c0  7527f27d4e64a912
F  0xd527f27c48313cb3  a000000000000461
F  0xd527f27c48313cb3  a000000000000461
10  0xc527f27c48313870  b0000000000000a2
10  0xc527f27c48313870  b0000000000000a2
10  0xc527f27c48313876  b0000000000000a4
10  0xc527f27c48313877  b0000000000000a5
<NSIndexPath: 0x8dec1fd169f9f89d> {length = 1, path = 0}  0x8dec1fd169f9f89d  c00000000000000e
--------------------输出结果:-------------------
复制代码

这里还是有细节点,读者可以自行去尝试一下:

  • taggedPointer的指针是由:标志位+值+值类型三种组成的
    • number1就可以看出:a其实是10的16进制
    • 最后一位的规律是:NSString为1、Int为2、long为3、float为4、double为5...
  • NSString用字面量初始化会是__NSCFString而不是NSTaggedPointerString
  • NSString不能超过9位,否则会使用__NSCFConstantString存储

最后再介绍一下taggedPointer中的标志位

enum objc_tag_index_t : uint16_t
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};
复制代码

可是明明NSString的标志位是a呀,这里怎么显示的是2呢?

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
    // They are reversed here for payload insertion.

    // ASSERT(_objc_taggedPointersEnabled());
    if (tag <= OBJC_TAG_Last60BitPayload) {
        // ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        // ASSERT(tag >= OBJC_TAG_First52BitPayload);
        // ASSERT(tag <= OBJC_TAG_Last52BitPayload);
        // ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}
复制代码

上面这段代码针对不同tag进行了不同的位运算最后将标志位+值+值类型格式的taggedPointer呈现出来,但是调用此处的代码并未开源

1.3 taggedPointer总结
  • taggedPointer专门用来存储小的对象,例如NSNumberNSDate
  • taggedPointer指针的值不再是地址了,而是真正的值。所以他不再是一个对象了,他只是一个"披着对象外皮"的普通变量而已——它的内存并不在堆中,也不需要mallocfree
  • taggedPointer不像地址指针一样,直接从指针中拿到值——编译读取的时候更加直接了
  • 在内存读取上有着3倍的效率,创建时比以前快106倍

关于taggedPointer并没有深入下去,苹果提出的这层内存优化还是有很多神秘之处等待着被发现(部分未开源)网上也有很多大佬进行了更深层次的研究

2. nonpointer_isa

nonpointer_isaisa章节已经有提到过了,这是苹果优化内存的一种方案: isa是个8字节(64位)的指针,仅用来isa指向比较浪费,所以isa中就掺杂了一些其他数据来节省内存

3. SideTable

3.1 为什么有多张散列表
  • 安全性能低——若所有对象全存在一张散列表中,某个对象需要处理时就对散列表进行unlock,表中其他对象的安全性无法得到保障
  • 优化加锁、解锁速度——对于散列表的操作频率较高,分为多表可以提高性能
3.2 散列表结构
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    ...
}
复制代码

每一张SideTable主要是由三部分组成:自旋锁、引用计数表、弱引用表

  • spinlock_t:自旋锁,用于加锁/解锁散列表
  • RefcountMap:用来存储OC对象的引用计数的哈希表
    • 仅在未开启isa优化或在isa优化情况下isa的引用计数溢出时才会用到
  • weak_table_t:存储对象弱应用指针的哈希表
3.3 散列表的数据结构

散列表本质上是一张哈希表,综合了数组链表的优势

  • 数组:通过下标查询数据快(查询快);增删改慢
  • 链表:需要通过一个个节点才能找到目标(查询慢);增删改快

三、ARC&MRC

面试中常常会问到ARCMRC,其实这两者在内存管理中才是核心所在

  • ARCLLVMRuntime配合的结果
  • ARC中禁止手动调用retain/release/retainCount/dealloc
  • ARC新加了weakstrong关键字

1. alloc

之前已经对alloc流程有了一个详细的介绍

2. retain

retain会在底层调用objc_retain

  1. objc_retain先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->retain()
  2. objc_object::retain通过fastpath大概率调用rootRetain(),小概率通过消息发送调用对外提供的SEL_retain
  3. rootRetain调用rootRetain(false, false)
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}
复制代码
  1. rootRetain内部实现其实是个do-while循环:
    • 先判断是否为nonpointer_isa(小概率事件)不是的话则对散列表中的引用技术表进行处理
      • 找到对应的散列表进行+=SIDE_TABLE_RC_ONE,其中SIDE_TABLE_RC_ONE是左移两位找到引用计数表
    • 正常情况下为nonpointer_isa会调用addc函数进行引用计数的处理,并用carry记录引用计数是否超负荷
      • isa中的第45位(RC_ONEarm64中为45)extra_rc进行操作处理
    • 超负荷情况下,将extra_rc的一半引用计数存入引用计数表中,并标记isa->has_sidetable_rc为true
      • 这里为什么优先考虑使用isa进行引用计数存储是因为对isa操作的性能强,操作引用计数表需要进行加锁解锁操作

3. release

releaseretain相似,会在底层调用objc_release

  1. objc_release先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->release()
  2. objc_object::release通过fastpath大概率调用rootRelease(),小概率通过消息发送调用对外提供的SEL_release
  3. rootRelease调用rootRelease(true, false)
  4. rootRelease内部实现也有个do-while循环
    • 先判断是否为nonpointer_isa(小概率事件)不是的话则对散列表中的引用技术表进行处理
    • 正常情况下为nonpointer_isa会调用subc函数进行引用计数的处理,并用carry记录引用计数是否超负荷
    • 超负荷情况下会来到underflow分支
      • 如果isa中的has_sidetable_rc为true时就开始着手处理引用计数,否则就将isa中的deallocating标记为true准备释放
      • 如果isa中的extra_rc减到只剩一半时会清空存放在引用计数表中的值,重新放回到extra_rc中,返回retrydo-while循环

4. retainCount

前面说了这么多引用计数,那么我们来看看retainCount和引用计数有什么关系呢?

NSObject *objc = [[NSObject alloc] init];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));
复制代码

上述代码会输出1,然而在alloc流程中并没有看到任何与retainCount相关的内容,这又是怎么一回事呢?接下来就来看看retainCount的底层实现

  1. retainCount会调用rootRetainCount
  2. rootRetainCount的具体实现
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();
}
复制代码
  • 先判断是否为isTaggedPointer
  • 再判断是否为nonpointer,如果是的话,当前引用计数=1+extrac_rc
    • 这行代码就能说明alloc出来的对象引用计数为0,是苹果人员为了不给开发人员造成引用计数为0时就销毁造成错觉才默认加一
  • 接着判断has_sidetable_rc是否有额外的散列表
    • 有的话引用计数再加上引用计数表中的数量
  • 所以引用计数=1 + extrac_rc + sidetable_getExtraRC_nolock

5. autorealese

在后续讲到

6. dealloc

在第4小节已经提到了dealloc——在release底层会判断当前extra_rc引用计数表是否都为0,如果满足条件就会通过消息发送调用dealloc

dealloc在底层会调用_objc_rootDealloc

  1. _objc_rootDealloc调用rootDealloc
  2. rootDealloc
    • 判断是否为isTaggedPointer,是的话直接返回,不是的话继续往下走
    • 判断isa标识位中是否有弱引用、关联对象、c++析构函数、额外的散列表,有的话调用object_dispose,否则直接free
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);
    }
}
复制代码
  1. object_dispose
    • 先判空处理
    • 接着调用objc_destructInstance(核心部分)
    • 最后再free释放对象
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
复制代码
  1. objc_destructInstance
    • 判断是否有c++析构函数关联对象,有的话分别调用object_cxxDestruct_object_remove_assocations进行处理
    • 然后再调用clearDeallocating
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;
}
复制代码
  1. objc_destructInstance
    • 判断是否是nonpointer,是的话调用sidetable_clearDeallocating清空散列表
    • 判断是否有弱引用额外的引用计数表has_sidetable_rc,是的话调用clearDeallocating_slow进行弱引用表和引用计数表的处理

最后附上一张dealloc流程图

四、弱引用

1. weak原理

笔者在之前的iOS探索 runtime面试题分析中已经讲过了

2. NSTimer中的循环引用

众所周知使用NSTimer容易出现循环引用,那么我们就来分析并解决一下

static int num = 0;

@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"TimerViewController";
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome {
    num++;
    NSLog(@"current - %d",num);
}

- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s", __FUNCTION__);
}
复制代码

这段代码运行起来所发生的问题就是当前VCpop到前一页时不会触发dealloc函数

刚才我们已经看到了release在引用计数为0时会调用dealloc消息发送,此时没有触发dealloc函数必然是出现了循环引用,那么循环引用出现在哪个环节?其实是NSTimer的API是被强持有的,直到Timer invalidated

那么能不能像block一样使用弱引用来解决循环引用呢?答案是不能的!

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
复制代码

之前在Block篇章说的是使用弱引用__weak typeof(self) weakSelf = self; 不处理引用计数,使用弱引用表管理,怎么在这里就不好使了呢?

关键在于weakSelfself指向的都是VC对象,但是weakSelfself的指针并不相同——两者并不是一个东东,只是指向同一个VC对象

block持有的是weakSelf指针地址timer持有的是weakSelf指针指向的对象,这里间接持有了self,所以仍然存在循环引用导致释放不掉

3. 解决NSTimer的循环引用

  • 方案一:提前invalidate

    • 既然dealloc不能来,就在dealloc函数调用前解决掉这层强引用
    • 可以在viewWillDisappearviewDidDisappear中处理NSTimer,但这样处理效果并不好,因为跳转到下一页定时器也会停止工作,与业务不符
    • 使用didMoveToParentViewController可以很好地解决这层强引用
      -(void)didMoveToParentViewController:(UIViewController *)parent {
          if (parent == nil) {
              [self.timer invalidate];
              self.timer = nil;
          }
      }
      复制代码
  • 方案二:中介者模式

    • 使用其他全局变量,此时timer持有全局变量self也持有全局变量,只要页面pop,self因为没有被持有就能正常走dealloc,在dealloc中再去处理timer

    • 此时的持有链分别是runloop->timer->target->timer、self->target、self->timer

      -(void)intermediary {
          self.target = [[NSObject alloc] init];
          class_addMethod([NSObject class], @selector(timerFire), (IMP)fireFunc, "v@:");
          self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(timerFire) userInfo:nil repeats:YES];
      }
      
      void fireFunc() {
          num++;
          NSLog(@"current - %d",num);
      }
      
      -(void)dealloc {
          [self.timer invalidate];
          self.timer = nil;
          NSLog(@"%s", __FUNCTION__);
      }
      复制代码
  • 方案三:使用包装者

    • 类似于方案二,但是在使用更便捷
    • 如果传入的响应者target能响应传入的响应事件selector,就使用runtime动态添加方法并开启计时器
    • fireWapper中如果如果wrapper.target,就让wrapper.target(外界响应者)调用wrapper.aSelector(外界响应事件)
    • fireWapper中没有了wrapper.target,意味着响应者释放了(无法响应了),此时定时器也就可以休息了(停止并释放)
    • 持有链分别是runloop->timer->FXProxy、vc->FXProxy-->vc
  • 方案四:使用虚基类——NSProxy有着NSObject同等的地位,多用于消息转发

    • 使用NSProxy打破NSTimer的对vc的强持有,但是强持有依然存在,需要手动关闭定时器
    • 持有链分别是runloop->timer->FXTimerWrapper->timer、vc->FXTimerWrapper-->vc

第一种较为简便,第二种合理使用中介者但是很拉胯,第三种适合装逼,第四种更适合大型项目(定时器用的较多) 详细代码

五、自动释放池

1. autoreleasepool结构

通过clang命令对空白的main.m输出一份main.cpp文件来查看@autoreleasepool的底层结构

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 0;
}
复制代码

objc源码中有一段对AutoreleasePool的注释

/***********************************************************************
   Autorelease pool implementation

   A thread's autorelease pool is a stack of pointers. 
   Each pointer is either an object to release, or POOL_BOUNDARY which is 
     an autorelease pool boundary.
   A pool token is a pointer to the POOL_BOUNDARY for that pool. When 
     the pool is popped, every object hotter than the sentinel is released.
   The stack is divided into a doubly-linked list of pages. Pages are added 
     and deleted as necessary. 
   Thread-local storage points to the hot page, where newly autoreleased 
     objects are stored. 
**********************************************************************/
复制代码

从中可以得出几点:

  • 自动释放池是一个以栈为节点的结构,拥有栈的特性——先进后出
  • 自动释放池的节点可以是对象(可以被释放)也可以是POOL_BOUNDARY(边界/哨兵对象)
  • 自动释放池的数据结构是双向链表
  • 自动释放池tls/线程是有关系的

自动释放池中有个相关的对象AutoreleasePoolPage是继承于AutoreleasePoolPageData

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};
复制代码
  • magic用来检验AutoreleasePoolPage的结构是否完整
  • next指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
  • thread指向的的当前线程
  • parent指向父节点,第一个节点的parent值为nil
  • child指向子节点,最后一个节点的child值为nil
  • depth代表深度,从0开始,往后递增1
  • hiwat代表high water mark——最大入栈数量标记

parentchild两个属性的存在就能证明双向链表的结构,而begin()又是什么呢?

AutoreleasePoolPage中有个属性begin 这个指针地址为什么要加上56呢?这个56是哪里来的呢?其实就是AutoreleasePoolPage中的固有属性 分析:AutoreleasePoolPageData中的指针和对象都占8字节,uint占4字节,只有magic_t未知(因为不是个指针,所以需要看具体类型);magic_t是个指针,由于静态变量的存储区域在全局段,所以magic_t占用4*4=16字节

接着使用_objc_autoreleasePoolPrint函数来打印一下自动释放池的相关信息

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] autorelease];
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

objc[18648]: ##############
objc[18648]: AUTORELEASE POOLS for thread 0x1000d3dc0
objc[18648]: 2 releases pending.
objc[18648]: [0x10081c000]  ................  PAGE  (hot) (cold)
objc[18648]: [0x10081c038]  ################  POOL 0x10081c038
objc[18648]: [0x10081c040]       0x100641de0  NSObject
objc[18648]: ##############
复制代码

POOLPAGE的地址相差38(16进制),相当于56(10进制)

那么是否可以无限往AutoreleasePool添加对象呢?答案是不能!

AutoreleasePoolPage中有个SIZE=4096字节=56字节+4040字节=56字节+8*505个对象

#define PAGE_MIN_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096
复制代码

将i的上限改为505会输出

objc[18750]: ##############
objc[18750]: AUTORELEASE POOLS for thread 0x1000d3dc0
objc[18750]: 506 releases pending.
objc[18750]: [0x103809000]  ................  PAGE (full)  (cold)
objc[18750]: [0x103809038]  ################  POOL 0x103809038
objc[18750]: [0x103809040]       0x102937230  NSObject
...
objc[18750]: [0x10380e000]  ................  PAGE  (hot) 
objc[18750]: [0x10380e038]       0x10293c1c0  NSObject
objc[18750]: ##############
复制代码

将i的上限改为505+505会输出

objc[19082]: ##############
objc[19082]: AUTORELEASE POOLS for thread 0x1000d3dc0
objc[19082]: 1011 releases pending.
objc[19082]: [0x10480c000]  ................  PAGE (full)  (cold)
objc[19082]: [0x10480c038]  ################  POOL 0x10480c038
objc[19082]: [0x10480c040]       0x101c3e9d0  NSObject
...
objc[19082]: [0x10480e000]  ................  PAGE (full)  
objc[19082]: [0x10480e038]       0x101c43960  NSObject
...
objc[19082]: [0x104810000]  ................  PAGE  (hot) 
objc[19082]: [0x104810038]       0x101c458f0  NSObject
复制代码

由于自动释放池在初始化时会讲POOL_BOUNDARY哨兵对象push到栈顶,所以第一页只能存放504个对象,接下来每一页都能存放505个对象

2.哨兵对象

#   define POOL_BOUNDARY nil
复制代码

哨兵对象本质上是个nil,它的作用主要在调用objc_autoreleasePoolPop时体现:

  • 根据传入的哨兵对象地址找到哨兵对象所在的page
  • 在当前page中,将晚于哨兵对象插入的所有autorelese对象都发送一次release消息,并移动next指针到正确位置
  • 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵对象所在的page

3.进栈/出栈

objc_autoreleasePoolPushobjc_autoreleasePoolPop分别代表进栈和出栈

objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
复制代码

接下来看看进栈时的操作:push->autoreleaseFast

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}
复制代码

autoreleaseFast中分为三条分支:(hotPage可以获取当前的AutoreleasePoolPage

  • 无当前页(刚创建,意味着池子尚未被push)
    • 调用autoreleaseNoPage创建一个hotPage
    • 调用page->add(obj)将对象添加至AutoreleasePoolPage的栈中
  • hotPage但page没有满(当前页尚未存满)
    • 调用page->add(obj)将对象添加至AutoreleasePoolPage的栈中
  • hotPage且page已满(当前页已存满)
    • 调用autoreleaseFullPage初始化一个新page
    • 调用page->add(obj)将对象添加至AutoreleasePoolPage的栈中
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    ASSERT(page == hotPage());
    ASSERT(page->full()  ||  DebugPoolAllocation);

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}
复制代码

autoreleaseFullPage通过遍历当前页的子页,如果存在继续遍历,如果不存在就开辟新的AutoreleasePoolPage并设为HotPage

接下来看看出栈操作 objc_autoreleasePoolPop传入的ctxt是一个指针,回顾上文中的clang文件就可以得知传入的是哨兵对象atautoreleasepoolobj

AutoreleasePoolPage::pop(ctxt)->pop->popPage最终会调用到popPage popPage其中

  • 通过page->releaseUntil(stop)通过一个while循环next指针来不断遍历调用objc_release(obj)释放对象,直到next指针指向栈顶才停止循环
  • 然后开始page->kill()page->child->kill()等,最后setHotPage(nil)

4.提出疑问

  • 临时变量什么时候释放?
    • 每一次运行循环执行后,也就是每当事件被触发时都会创建自动释放池。在程序执行的过程中,所有autorelease的对象在出了作用域之后会被添加到最近创建的自动释放池中。运行循环结束前会释放自动释放池,还有池子满了也会销毁
  • 自动释放池原理
    • 自动释放池被销毁或耗尽时会向池中的所有对象发送release消息,释放所有autorelease对象
  • 自动释放池能否嵌套使用?
    • MRCautoreleasepool需要手动添加
    • @autoreleasepool与线程关联,不同的线程的autoreleasepool是不一样的
    • @autoreleasepool嵌套时只会创建一个page,但是有两个哨兵
      • 为什么只有一个page?因为page的创建和线程有关,一个线程对应一个autoreleasepool的空间
      • 为什么有两个哨兵?因为有两个作用域

写在后面

最近的打算是先将iOS探索系列完结,然后开始着手整理其他系列,本系列后续还有Runloop、启动优化、性能优化、页面优化、组件化、项目架构,敬请期待!

喜欢作者的文章可以点赞+收藏iOS探索系列支持一下