阅读 104

底层探索-内存管理

当提到App的优化时,优化内存必然是会被提到的,本篇章就来探索下内存管理。

一:内存布局

关于内存的分区,在 底层探索-多线程基础已经做了相关介绍,在此进行一些补充。除了常说的五大区,还有内核区和保留区域。

内核区:交给系统进行处理的内核区域。
保留区:预留给系统进行一些处理的区域

Tips:栈区就会向下增长,堆区就会向上增长

image.png

二:内存管理方案

Tagged Pointer

概述和示例

在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。

使用Tagged Pointer之前,如果声明一个NSNumber *number = @10;变量,需要一个占8字节的指针变量number,和一个占16字节的NSNumber对象,指针变量number指向NSNumber对象的地址。这样需要耗费24个字节内存空间。

而使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。直接将数据10保存在指针变量number中,这样仅占用8个字节。

但是当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。

下面来看代码

- (void)taggedPointerDemo1 {
    self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"wj"]; 
             NSLog(@"%@",self.nameStr);
        });
    }
}

- (void)taggedPointerDemo2 {
    self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"wj_十五的月亮十六圆哈哈"];
            NSLog(@"%@",self.nameStr);
        });
    }
}复制代码

taggedPointerDemo1taggedPointerDemo2中的代码几乎一样,但是实际运行结果却不一样,taggedPointerDemo1能够正常运行,而taggedPointerDemo2却崩溃了。

  • 其实崩溃的原因很简单,当调用self.nameStr赋值的时候,实际上会频繁的调用setter,因而retainrelease也会被频繁调用,因为多线程的影响,在某个瞬间会对同一个对象做多次释放,导致过度释放而造成崩溃。

那么taggedPointerDemo1凭什么能正常运行呢?我们通过断点的方式来看看!

image.png

image.png

taggedPointerDemo1中对象类型是NSTaggedPointerString
taggedPointerDemo2中对象类型是NSCFString

objc源码中对于retainrelease方法定义如下:

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

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

系统判断如果对象类型是isTaggedPointer类型就执行返回了,不做其他操作,因此taggedPointerDemo1中的对象已经被优化了,压根不会去调用setter

源码解读

在前文的他不就中read_image,其实就包含对Tagged Pointer的处理initializeTaggedPointerObfuscator()

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;

}复制代码

举个例子来看打印结果

- (void)viewDidLoad {

    [super viewDidLoad];

    NSString *str1 = [NSString stringWithFormat:@"a"];

    NSString *str2 = [NSString stringWithFormat:@"b"];

    NSLog(@"%p-%@",str1,str1);

    NSLog(@"%p-%@",str2,str2);

    // NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
    
}

uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}复制代码

image.png打印结果其实并非真正的指针地址,这个地址是经过混淆的,我们需要_objc_decodeTaggedPointer解码来获取真正的地址,将上述代码中注释部分打开就能得到真正的指针地址。

image.png

_objc_makeTaggedPointerimage.png

_objc_makeTaggedPointer中会传入一个标志位tag进行了不同的位运算最后将标志位+值+值类型格式的taggedPointer呈现出来。

下面是系统定义的各种Tagged Pointer标志位。

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
};复制代码

总结

  • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumberNSDateNSString等小对象存储

  • 在没有使用Tagged Pointer之前,NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

  • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中,Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要mallocfree

  • 在内存读取上有着3倍的效率,创建时比以前快106倍。不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

  • 这是一个特别的指针,不指向任何一个地址

  • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

NONPOINTER_ISA

nonpointer_isa,在之前 对象原理之isa的本质探究 已经做了介绍,它也是内存管理方案之一,isa是个8字节(64位)的指针,仅用来isa指向比较浪费,所以isa中就掺杂了一些其他数据来节省内存。

image.png

SideTable

先看下SideTable结构

struct SideTable {

    spinlock_t slock; 

    RefcountMap refcnts;

    weak_table_t weak_table;

    ...... 省略

};复制代码

slock: 开解锁
refcnts: 引用计数表
weak_table: 弱引用表

在全局其实散列表是SideTables

static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}复制代码

为什么不放在一张表呢?

  • 安全性能低——若所有对象全存在一张散列表中,某个对象需要处理时就对散列表进行unlock,表中其他对象的安全性无法得到保障。

  • 优化加锁、解锁速度——对于散列表的操作频率较高,分为多表可以提高性能。

三:MRC & ARC

alloc

对象原理之alloc & init 探究

retain

  • objc_retain先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->retain()

  • objc_object::retain通过fastpath大概率调用rootRetain(),小概率通过消息发送调用对外提供的SEL_retain

  • 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;
        ???? 判断是否是nonpointer
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            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;
        ???? 对extra_rc进行操作,处理引用计数
        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;
}复制代码

rootRetain内部实现其实是个do-while循环:

  • 先判断是否为nonpointer_isa(小概率事件)不是的话则对散列表sideTable中的引用技术表进行处理。

    • 找到对应的散列表进行+=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操作的性能强,操作引用计数表需要进行加锁解锁操作。

简单来说就是非nonpointer_isa直接操作散列表,是nonpointer_isa判断是否在释放,未释放的话操作extra_rc,如果extra_rc超负荷,那么满状态的一半会存入extra_rc,另一半存入散列表。

release

releaseretain非常类似。

  • objc_release先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->release()

  • objc_object::release通过fastpath大概率调用rootRelease(),小概率通过消息发送调用对外提供的SEL_release

  • rootRelease调用rootRelease(true, false)

  • rootRelease内部实现也有个do-while循环

    • 如果isa中的has_sidetable_rc为true时就开始着手处理引用计数,否则就将isa中的deallocating标记为true准备释放

    • 如果isa中的extra_rc减到只剩一半时会清空存放在引用计数表中的值,重新放回到extra_rc中,返回retrydo-while循环。

    • 先判断是否为nonpointer_isa(小概率事件)不是的话则对散列表中的引用技术表进行处理

    • 正常情况下为nonpointer_isa会调用subc函数进行引用计数的处理,并用carry记录引用计数是否超负荷

    • 超负荷情况下会来到underflow分支

retainCount

看下面这段代码

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

打印结果为1,init只是构造方法,并没有对引用计数进行操作,alloc我们也研究过,也未看到相关代码,那这个1是怎么出现的呢?

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) {
        ???? alloc创建的对象其实引用计数为0,在这里进行了默认的+1 操作
        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

autorelease

autorelease可以将对象加入自动释放池中。

autorelease最终会调用rootAutorelease2 -> autorelease() -> autoreleaseFast()

autoreleaseFast下文会做介绍

dealloc

  • 先调用_objc_rootDealloc

  • _objc_rootDealloc调用rootDealloc

  • rootDealloc

    • 判断是否为isTaggedPointer,是的话直接返回,不是的话继续往下走

    • 判断isa标识位中是否有弱引用、关联对象、c++析构函数、额外的散列表,有的话调用object_dispose,否则直接free

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);
    }
}复制代码

object_dispose

  • 先判空处理

  • 接着调用objc_destructInstance(核心部分)

  • 最后再free释放对象

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}复制代码

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;
}复制代码

clearDeallocating

  • 判断是否是nonpointer,是的话调用sidetable_clearDeallocating清空散列表

  • 判断是否有弱引用额外的引用计数表has_sidetable_rc,是的话调用clearDeallocating_slow进行弱引用表和引用计数表的处理

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会对弱引用表进行处理

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();
}复制代码

完整流程图:

image.png

四:强弱引用

weak原理

之前已经探究过 底层探索 - weak的实现原理

NSTimer中的循环引用

static int num = 0;

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

@implementation ViewController

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 无论push 进来 还是 pop 出去 正常跑
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

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

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

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

以上代码肯定是会造成循环引用的,我们看下官方文档,scheduledTimerWithTimeInterval中的target:self,这个API会对传入的target对象进行强引用。

image.png

那我们使用__weak typeof(self) weakSelf = self能否像Block那样解决循环引用呢?

答案是 不能

image.png

控制台中,selfweakSelf都是0x12af05570,也就是两者指向了同一个对象,但是我们打印selfweakSelf指针的地址,两者却不相同,也就是说selfweakSelf是两个完全不同指针,但是指向了相同的对象,而官方文档也说了NSTimer会对传入的对象强引用,因而传入weakSelf也无法解决循环引用。

而在Block中,持有的是指针地址,为了解决循环引用,我们通常传入的是weakSelf,而Block中拿到的就是weakSelf,而非真正的对象。

image.png

循环引用解决

方案一:使用Block形式创建Timer

最简单的方案

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer fire - %@",timer);
    }];复制代码

方案二:提前invalidate

  • 既然dealloc不能来,就在dealloc函数调用前解决掉这层强引用

  • 可以在viewWillDisappearviewDidDisappear中处理NSTimer,但这样处理效果并不好,因为跳转到下一页定时器也会停止工作,与业务不符

  • 使用didMoveToParentViewController可以很好地解决这层强引用。

完整代码

static int num = 0;

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

@implementation ViewController

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 无论push 进来 还是 pop 出去 正常跑
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

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

- (void)fire {
    num++;
    NSLog(@"current - %d",num);
}复制代码

方案三:中介者模式

使用中介者来实例化Timer,通过判断target是否存在来决定是否需要执行selector

#import "LGTimerWapper.h"
#import <objc/message.h>
@interface LGTimerWapper()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation LGTimerWapper

- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    if (self == [super init]) {
        self.target     = aTarget; // vc
        self.aSelector  = aSelector; // 方法 -- vc 释放
        if ([self.target respondsToSelector:self.aSelector]) { 
            Method method    = class_getInstanceMethod([self.target class], aSelector);
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
            self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

// 一直跑 runloop
void fireHomeWapper(LGTimerWapper *warpper){
    if (warpper.target) { // vc - dealloc
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
    }else{ // warpper.target
        [warpper.timer invalidate];
        warpper.timer = nil;
    }
}

- (void)lg_invalidate{
    [self.timer invalidate];
    self.timer = nil;
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end复制代码

方案四:使用NSProxy

NSProxy是专门用来做消息转发,和NSObject同级。

@interface MJProxy : NSProxy 
+ (instancetype)proxyWithTarget:(id)target; 
@property (weak, nonatomic) id target; 
@end 
#import "MJProxy.h" 
@implementation MJProxy 
+ (instancetype)proxyWithTarget:(id)target { 
// NSProxy对象不需要调用init,因为它本来就没有init方法 
    MJProxy *proxy = [MJProxy alloc]; 
    proxy.target = target; 
   return proxy; 
} 
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel]; 
}
- (void)forwardInvocation:(NSInvocation *)invocatio{
    [invocation invokeWithTarget:self.target]; 
 } 
@end复制代码

VC使用

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];复制代码

五:自动释放池

通过clang命令对空白的main.m输出一份main.cpp

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}



struct __AtAutoreleasePool {
  ???? 构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} 
  ???? 析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} 
  void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    }
    return 0;
}复制代码

自动释放池底层就是一个__AtAutoreleasePool结构体,里面有构造函数和析构函数。

看下objc源码中对其的解释:

image.png

  • 自动释放池是一个以栈为节点的结构,拥有栈的特性——先进后出

  • 自动释放池的节点可以是对象(可以被释放)也可以是POOL_BOUNDARY(边界/哨兵对象)

  • 自动释放池的数据结构是双向链表

  • 自动释放池tls/线程是有关系的

整体结构如下所示:image.png

objc_autoreleasePoolPushobjc_autoreleasePoolPop都涉及到一个类AutoreleasePoolPage

AutoreleasePoolPage结构

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
	magic_t const magic;// ????16字节
	__unsafe_unretained id *next; // ????8字节
	pthread_t const thread;//8字节
	AutoreleasePoolPage * const parent;// ????8字节
	AutoreleasePoolPage *child;// ????8字节
	uint32_t const depth;// ????4字节
	uint32_t hiwat;// ????4字节

	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——最大入栈数量标记

官方注释中还提到了哨兵对象,那么一个自动释放池包含多少哨兵对象呢?其实只有一个。

#   define POOL_BOUNDARY nil复制代码

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

  • 根据传入的哨兵对象地址找到哨兵对象所在的page。

  • 在当前page中,将晚于哨兵对象插入的所有autorelese对象都发送一次release消息,并移动next指针到正确位置。

  • 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵对象所在的page。

那么一个AutoreleasePoolPage能存储多少对象呢?

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

根据源码我们知道一个AutoreleasePoolPage4096个字节,除去本身属性占用的56字节,剩余4040的空间可以使用,由于在初始化自动释放池的时候,POOL_BOUNDARY哨兵对象会被push到栈顶,因此第一页实际只能存放(4040 -8)/8 = 504对象,从第二页开始可以存储505个对象

进栈

objc_autoreleasePoolPush

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}复制代码

push()

static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            // ???? 创建新的page
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }复制代码

autoreleaseNewPage()

static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }复制代码

autoreleaseNewPage()会进行相关的初始化,最终会调用以下这个函数

image.png

找到真正调用的地方,但值得注意的是,本来应该传入的是*next,但是真正调用的地方传入的却是begin()

image.png

begin()

id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }复制代码

我们用代码跑起来看看

image.png

这个56是什么呢?原因在于AutoreleasePoolPage本身对象里面就包含属性,总共56字节,因此对象压栈的时候要从56字节之后开始。

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的栈中,将next指针平移并指向下一个位置

  • hotPage但page没有满(当前页尚未存满)

    • 调用page->add(obj)将对象添加至AutoreleasePoolPage的栈中

  • hotPage且page已满(当前页已存满)

    • 调用autoreleaseFullPage初始化一个新page

    • 调用page->add(obj)将对象添加至AutoreleasePoolPage的栈中

autoreleaseFullPage

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

image.png

出栈

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

这里需要传入一个ctxt,其实是哨兵对象atautoreleasepoolobj,也就是下面构造函数中objc_autoreleasePoolPush()的返回值,进出栈一一对应。

struct __AtAutoreleasePool {
  ???? 构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} 
  ???? 析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} 
  void * atautoreleasepoolobj;
};复制代码

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

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

  • 然后开始page->kill(),用于销毁当前的pagepage->child->kill()等,最后setHotPage(nil)

与RunLoop的关系

  • kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。

  • kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()

  • kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()


作者:云先生
链接:https://juejin.cn/post/7016937951670042638


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐