OC--Autorelease

不解之谜1,NSLog(@"2222=====%@",obj2); // crash 野指针

@implementation TestObj
- (instancetype)init
{
    self = [super init];
    if (self) {
        __unsafe_unretained NSObject *obj1 = [TestObj getObj];
        NSLog(@"1111=====%@",obj1); // 运行OK
        __unsafe_unretained NSObject *obj2 = [TestObj getObj2];
        NSLog(@"2222=====%@",obj2); // crash 野指针
    }
    return self;
}

+ (id)getObj {
    return [NSObject new];
}
+ (id)getObj2 {
    return [NSObject new];
}

不解之谜2,ViewController的viewDidLoad里面,NSLog不一样

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak NSObject *obj1 = [ViewController getObj];
    NSLog(@"=====%@",obj1); // nil
    __weak NSObject *obj2 = [ViewController getObj];
    NSLog(@"=====%@",obj2);// 输出对象
}
+ (id)getObj {
    return [NSObject new];
}

Autorelease与Autoreleasepool

参考:
ARC环境下编译器到底对autorelease对象做了怎样的优化
黑幕背后的Autorelease
自动释放池的前世今生 ---- 深入解析 Autoreleasepool
Objective-C 小记(8)autorelease

autoreleasepool

1、自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
2、当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
3、调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
4、新建线程会第一个autorelease对象时候,新建AutoreleasePool,线程销毁AutoreleasePool释放对象且销毁
5、每个AutoreleasePoolPage对象大小为4096, 对象本身信息占 56 个字节, 所以 begin() 需要排除这 56 个字节, 真正用于存储 autorelease 对象地址的内存量为 end() - begin(), 共有 4040 个字节, 可存储 505 个 autorelease 变量(一个对象8个字节).

AutoreleasePoolPage

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

对应字段意义:
1、magic:这个变量的类型是 magic_t,是用来检查 AutoreleasePoolPage 的内存没有被修改的,放在第一个也就是这个原因,防止前面地址有内容溢过来。
2、next:类型是 id *,存放的是下一个被 autorelease 的对象指针存放的地址。
3、thread:对应的线程,这说明了自动释放池是对应线程的。
4、parent 和 child:用来保存前一个 AutoreleasePoolPage 和后一个 AutoreleasePoolPage,就是一个双向链表,毕竟一个 AutoreleasePoolPage 能存放的对象是有限的。
5、depth:很明显是这个链表有多深。
6、hiwat:一个在 DEBUG 时才有用的参数,表示最高有记录过多少对象(hi-water)。

@autoreleasepool{}
@autoreleasepool {
       __autoreleasing NSObject *obj = [NSObject new];
    }

伪代码

    // 获取哨兵POOL_SENTINEL
    void * atautoreleasepoolobj = objc_autoreleasePoolPush();
    {
        __autoreleasing NSObject *obj = [NSObject new];
    }
    // 就是release哨兵之后的autorelease对象。
    objc_autoreleasePoolPop(atautoreleasepoolobj);

autorelease调用栈

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └─ id objc_object::rootAutorelease2()
       └─ static id AutoreleasePoolPage::autorelease(id obj)
          └─ static id AutoreleasePoolPage::autoreleaseFast(id obj)
             ├─ id *add(id obj)
             ├─ static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
             │  ├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
             │  └─ id *add(id obj)
             └─ static id *autoreleaseNoPage(id obj)
                ├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                └─ id *add(id obj)

方法流程图


autorelease流程图
各方法解析
1、id *add(id obj):解锁加锁,将 obj 存到 next 的位置,并将 next 加 1,典型的入栈操作
    id *add(id obj)
    {
        assert(!full());
        unprotect(); // 解锁
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();// 加锁
        return ret;
    }
2、id *autoreleaseNoPage(id obj)
    id *autoreleaseNoPage(id obj)
    {
        assert(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // (1)当前page是一个占位符EMPTY_POOL_PLACEHOLDER,需要add(POOL_BOUNDARY)
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // 不使用autoreleasePool测试
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // (2)AutoreleasePoolPage的push,第一次autoreleaseFast(POOL_BOUNDARY)
            return setEmptyPoolPlaceholder();
        }

        // (3)
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        if (pushExtraBoundary) {
            // (4)add(标记位)
            page->add(POOL_BOUNDARY);
        }
        
        // (5)add(obj)
        return page->add(obj);
    }

三种执行情况:
1、AutoreleasePoolPage的push时候:并没有生成 AutoreleasePoolPage 对象,只是执行(2)步骤--将 hot page 设置为 EMPTY_POOL_PLACEHOLDER占位符。
2、当执行push之后,第一个autorelease对象时候:执行(1)(3)(4)(5)步骤。
3、当没有执行push时,第一个autorelease对象时候:执行(3)(5)步骤,线程销毁的时候会调用 pop。

3、 id *autoreleaseNewPage(id obj)

检查 page 有没有还没满的 child(顺链表往下查),没有的话就新建一个,再使用 add 函数将 obj 记录

    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *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);
    }
4、AutoreleasePoolPage的pop方法
    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        // (1)一个pool只是执行push,没有add对象时候
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            if (hotPage()) {
                pop(coldPage()->begin());
            } else {
                setHotPage(nil);
            }
            return;
        }

        // (2)根据token地址计算所在Page
        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) { // 检查toekn是否等于POOL_BOUNDARY
            if (stop == page->begin()  &&  !page->parent) {
            } else {
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        // (3)循环直到到 stop 给每个对象调用 release
        page->releaseUntil(stop);

        // (4)如果现在这个 page 只剩下不到一半的空间了,则多留一个 child
        if (DebugPoolAllocation  &&  page->empty()) {
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
一个autorelease对象在什么时刻释放?

1、手动指定@autoreleasepool {}:当前Autoreleasepool作用域大括号结束时释放;

2、主线程不手动指定@autoreleasepool {}:autorelease对象会被添加到最近一次创建的autoreleasepool中,并在当前的runloop迭代结束时候释放。

3、子线程不手动指定@autoreleasepool {},autorelease对象时候会在当前线程新建一个page(没有哨兵POOL_BOUNDARY,所以在线程结束时候才能 page -> pop 释放对象)

主Runloop对Autoreleasepool管理的流程:

1、当前runloop状态为kCFRunLoopEntry(进入runloop) 时,会调用push,由于此时优先级最高,可以确保创建缓存池在其他回调之前。

2、当前runloop状态为kCFRunLoopBeforeWaiting(runloop即将休眠)时,会先调用pop,再调用push。对应着释放旧池并创建新池,由于优先级最低,这一操作也在其他回调之后。

3、当前runloop状态为kCFRunLoopExit(退出runloop) 时,会调用pop,由于优先级最低,此处可确保在其他回调完成后释放缓存池。

autorelease 进行的非持有方法的优化

1、alloc/new/copy/mutableCopy---持有对象方法
2、其他类方法返回的对象,如果下面的createObj

@implementation BBObject
+ (instancetype)createObj {
    return [self new];
}

需要了解下面的方法:

id objc_autoreleaseReturnValue(id obj)
{
    // prepareOptimizedReturn判断是否可以TSL优化,可以则标记,YES--就不需要调用 objc_autorelease(),优化性能
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}
id objc_retainAutoreleasedReturnValue(id obj)
{
  // 如果之前 objc_autoreleaseReturnValue() 存入的标志位为 ReturnAtPlus1,则直接返回对象,无需调用 objc_retain(),优化性能
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
    return objc_retain(obj);
}


static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
    assert(getReturnDisposition() == ReturnAtPlus0);

    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false;
}

static ALWAYS_INLINE ReturnDisposition 
acceptOptimizedReturn()
{
    ReturnDisposition disposition = getReturnDisposition();
    setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state
    return disposition;
}

TLS 全称为 Thread Local Storage(线程本地存储),是每个线程专有的键值存储,需要调用方与被调用方必须都是ARC的情况下(即全ARC环境下)

在某个线程上的函数调用栈上相邻两个函数对 TLS 进行了存取,这中间肯定不会有别的程序『插手』。
所以 getReturnDisposition() 和 setReturnDisposition() 的实现比较简单,不需要判断考虑是针对哪个对象的 Disposition 进行存取,因为当前线程上下文中只处理唯一的对象,保证不会乱掉。 

static ALWAYS_INLINE void 
setReturnDisposition(ReturnDisposition disposition)
{
    tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}

callerAcceptsOptimizedReturn(__builtin_return_address(0))函数在不同架构的 CPU 上实现也是不一样的。
主要作用:

1、__builtin_return_address(0)获取当前函数返回地址。
2、callerAcceptsOptimizedReturn()方法判断调用方是否紧接着调用了 objc_retainAutoreleasedReturnValue或者 objc_unsafeClaimAutoreleasedReturnValue
如果是就直接当前对象地址,而不执行retain与autorelease操作.

TLS优化标记

总结:

MRC下:对象需要经历方法内部new->内部autorelease->外部retain->外部release这样四步流程
ARC下:对象需要经历方法内部new->外部release两步,省了中间两步“autorelease->retain-”
(TLS优化其实与OC内存管理“谁生产谁销毁谁持有谁释放”的黄金法则有所违背)

ARC 会视情况在调用方法时可能会添加 retain ,在方法内部返回时可能会添加 autorelease ,经过优化后很可能会抵消。
autorelease 进行的优化
1、持有、无引用
- (void)test {
    [BBObject new];
}

编译器编译后的伪代码

- (void)test {
    objc_release([BBObject new]) ;
}
2、持有、局部变量引用

__strong

- (void)test {
    __strong BBObject * obj = [BBObject new];
}

编译器编译后的伪代码

- (void)test {
    id temp = [BBObject new];
    objc_storeStrong(&tmp,nil);//相当于tmp指向对象执行release
}

__weak、__unsafe_unretained

    // 这种写法xcode提示警告
    __weak BBObject * obj = [BBObject new]; 
    __unsafe_unretained BBObject * obj = [BBObject new];
3、持有、外部变量引用
- (void)test {
    self.obj = [BBObject new];
}

编译器编译后的伪代码

- (void)test{
    id temp = [BBObject new];
    [self setObj:temp];//setter方法执行objc_storeStrong
    objc_release(temp);
}
- (void)setObj:(id aObj) {
    objc_storeStrong(&_obj, aObj);
}
4、不持有、无引用
- (void)test {
    [BBObject createObj];
}

编译器编译后的伪代码

+ (instancetype) createObj {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease] 
}
- (void)test { 
    objc_unsafeClaimAutoreleasedReturnValue([BBObject createObj]); 
}
5、不持有、局部变量引用

- (void)test {
    BBObject * obj1 = [BBObject createObj];
}

编译器编译后的伪代码

+ (instancetype) createObj {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease] 
}
- (void)test {
    id obj1 = objc_retainAutoreleasedReturnValue([BBObject createObj]);  
    objc_storeStrong(& obj1,nil); 
}

发现obj1指向的对象不会加入autoreleasepool

6、不持有、外部变量引用
+ (instancetype) createObj {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 系统可能会调用[tmp autorelease] 
}
- (void)test {
    self.obj = [BBObject createObj];
}

编译后的伪代码

- (void)test {
    id tmp = _objc_retainAutoreleasedReturnValue([Foo createFoo]); 
    [self setObj:temp]; // setter方法执行objc_storeStrong
    objc_release(temp);
}

查看autoreleasepool中的对象方法:
1、extern void _objc_autoreleasePoolPrint(void); //extern这个方法,需要查看的地方使用_objc_autoreleasePoolPrint();

2、需要查看的地方打断点,然后po _objc_autoreleasePoolPrint()


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

推荐阅读更多精彩内容