ARC梳理

内存的引用

  计算机是按地址访问数据,如果一块内存被使用,就必须让外界知道它在哪儿,反过来讲,要访问一个数据就必须知道其地址,无论是直接知道其地址,还是通过计算等方式间接知道。进一步讲,如果外界没有任何访问某个地址数据的办法,那么这个数据就应该被废弃和回收,否则就是内存泄露;同样的,如果某个地址的数据有很多访问需求,那么怎么管理这个数据也变得十分困难。比如在C/C++中,在堆上分配的内存需要开发者手动释放,而且经常会被多次使用,还可以作为函数返回值返回,甚至是跨线程访问,所以什么时候调用free释放内存变得十分复杂,可能导致其他的指针的非法访问或者内存泄漏。

iOS中的引用(reference)

  C++认为对象的引用是该对象的一个别名,只能在初始化的时被赋值一次(其底层是用指针实现的),直观上看引用和指针不同之处就是一个用“.”访问成员,一个用“->”访问成员。

对照iOS的引用,例如

@interface User : NSObject {
@public
NSString *name;
}
@property (nonatomic, copy) NSString *phone;
@end
@implementation User
@end
User *user = [[User alloc] init];//创建
user.phone = @"abc";//通过“.”访问属性的setter给变量赋值
user->name = @"zhangsan";//通过“->”直接访问变量,必须要是public
user = nil;//对引用赋值

可以看到user这个引用其实更多的偏向于指针的功能。

iOS中RC(reference count,内存对象引用计数)

  苹果RC机制认为如果当前分配的内存空间是有效的,那么它至少应该存在一个引用,否则将无法访问这段内存(就是内存泄漏),因此以是否存在引用来判定该内存的有效性是一种简单且可信的手段;其二,内存经常会被多次引用,还会被不同的函数和线程引用,这时候内存什么时候有效,什么时候该被释放将更加复杂,如果反向保存这些引用当前内存的引用,内存开销比较大,而且容易非法访问,所以苹果定义了内存引用计数器来标记内存被引用的数量,并不记录内存具体被谁引用了,从而将内存操作中的newfree转变成allocretaincopyrelease的操作上来,这样就很好的解决了内存有效性管理。

MRC:

  手动引用计数是建立在RC机制的基础上,通过开发者在适当的时候主动调用会改变retainCount的关键词(allocretainrelease等)。其中比较大的局限是开发者本身需要承担繁重的内存管理操作,而且在比较复杂的环境或者开发者大意时,手动管理容易造成一些内存问题。MRC已经过时,也就不做过多讨论。

ARC:

  为了减轻开发者繁重的内存管理操作,苹果设计了ARC机制,通过栈的生命周期来管理所有引用,又通过引用的生命周期去管理对象的生命。该机制作用于编译时,编译器会在“适当的时候”帮助开发者插入retain,release,autorelease操作。

那么什么是“适当的时候”呢?一般认为:

  1. 对于retain,当调用allocnewcopymutableCopy关键字的时候就会导致retain操作。
  2. 对于release,我们知道对象是分配在堆上的,但是指向这个对象的引用却是分配在栈上的,那么一般情况下我们认为release是在引用(指针)生命周期结束的时候,例如:函数体结束时,for(;;){}结束时或者匿名作用域“{}”结束时。
  3. 除了1中情况产生的返回值会被autorelease,在autoreleasepool释放的时候 release。

  以上只是一种简单的认识,但在开发中,我们遇到的往往是混合场景,ARC机制又是怎么样的呢?接下来我们从Autoreleasepool开始

Autoreleasepool

首先我们假设这样一种情况:

@interface User : NSObject
+ (User *)anUser;
@end

@implementation User
+ (User *)anUser {
    User *user = [User new];
    return user;
}
@end

- (void)viewDidLoad {
    User *user1 = [User anUser];
} 

  这是一种常见的写法,我们知道在viewDidLoad中的引用user1肯定是有值的,而且似乎没有内存问题(后面我们会聊到)。
  假设没有Autoreleasepool,在anUser方法中,使用[User new]创建了一个对象(假设内存地址为A),user指向这个地址,并在ARC机制下RC+1,当这个函数执行完后,user生命周期完结,这时ARC会插入release,导致RC-1,此时A已经被废弃(需要注意的是,当RC-1=0时,RC并不会真正去减-1,而是将当前内存A设置为待回收),然后返回user指针,viewDidLoaduser1引用的A是个待回收的内存,使用user1很可能导致crash。如果考虑return的情况不做 RC-1,那么[User anUser]后会得到一个 retainCount=1的对象,赋值给user1,那么RC+1,user1生命结束,RC-1,对象得不到释放,内存泄漏。
  由此可见在这种有返回值的时候光靠插入retain,release无法满足要求,这时候需要autoreleasepool了。

Autoreleasepool 长啥样?

在main函数中写一个

@autoreleasepool{}

使用clang -rewrite-objc main.m命令会得到

{
    __AtAutoreleasePool __autoreleasepool;
}

  注意:外面的大括号会形成一个匿名作用域,也就是说__autoreleasepool这个变量只存在于大括号里面。
  搜索__AtAutoreleasePool,发现这货在构造函数中调用了objc_autoreleasePoolPush()生成了一个指针赋值给自己的成员变量atautoreleasepoolobj ,在析构函数中调了objc_autoreleasePoolPop(atautoreleasepoolobj)这一下就明亮了。
算了贴个代码吧:

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

所以根据局部变量特性:

@autoreleasepool {
      NSString *a = [NSString stringWithFormat:@“ddd-%d%d”,1];
}

Clang会将其转变成:

{
    __AtAutoreleasePool __autoreleasepool;//objc_autoreleasePoolPush
    NSString *a = ((NSString *(*)(id, SEL, NSString *, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_46_r4htdnq177b9g_x75q2qfl3r0000gn_T_main_3a69a9_mi_0, 1);
    //objc_autoreleasePoolPop
}

可见a这个变量被objc_autoreleasePoolPushobjc_autoreleasePoolPop包裹了。

我们去下载runtime的源码(Mac版)
  接下搜索objc_autoreleasePoolPushobjc_autoreleasePoolPop发现其调用了AutoreleasePoolPagepushpop
  其中AutoreleasePoolPage结构如下(省略不重要的变量和方法)

{
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
}

分别做说明:

  1. id是个指针,next是个二级指针,可以认为记录当前栈顶的位置,而栈中记录都是id类型的数据,这个信息很重要表明其管理的是引用。
  2. thread记录线程PID,因为AutoreleasePoolPage和线程一一对应。
  3. 双向连表,指向父对象。
  4. 双向连表,指向子对象。

然后再看下面的宏
X86下:

#define I386_PGBYTES 4096  
#define PAGE_SIZE I386_PGBYTES  
#define PAGE_MAX_SIZE PAGE_SIZE  

对应的arm64下:

#define PAGE_MAX_SHIFT 14  
#define PAGE_MAX_SIZE (1 << PAGE_MAX_SHIFT)  //16384

  这里我们发现AutoreleasePoolPage的大小是跟随系统的Page大小的,i386是一个4K大小的内存空间,我写了个mac的demo通过sysctlbyname()调用发现新机器依然是4K(iMac late2015 + mac10.13.4);而在arm64下,这个大小是16K(验证iPhone6s + iOS11.3是16K,其他的暂时未验证)。现代操作系统都分页的,将AutoreleasePoolPage设置为一个Page可以更高效的利用内存。iOS Page比Mac的大,这个现象我猜测是ARM CPU对功耗敏感,为了提高效率,对内存对齐要求很高,所以占用的内存较多,因此提高了Page的大小。

PUSH & POP

依次找主要的调用栈
AutoreleasePoolPage::push()->
AutoreleasePoolPage::autoreleaseFast(POOL_BOUNDARY)->
AutoreleasePoolPage::add(id)…(这里还有其他两个方法)
add(id)(就是添加对象的到autoreleasepool具体实现方法)

id *add(id obj) {        
        assert(!full());
        unprotect();  //获取写权限
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
}
  1. 注意*next=obj,将obj写入(*next)obj并没有做nil校验,所以可以接收nil做为参数,而POOL_BOUNDARY就是nil,是用来作为标记位的,所以这里的push操作中,只是压入了一个nil,并没有添加真正的引用记录,表示新开启一个autoreleasepool。
  2. 查找add函数的Call Hierarchy,反向查找调用函数。
    AutoreleasePoolPage::autoreleaseFast()//static inline
    AutoreleasePoolPage::autorelease()//static inline
    [NSObject rootAutorelease2]
    注意:在这个函数中,将this指针传递给了autorelease(this),这个信息表明除了push的时候压入POOL_BOUNDARYnil外,其他的时候都不会是nil,在AutoreleasePoolPage以外的地方保证了其安全性(或许这不是一个好的做法吧)。
    [NSObject rootAutorelease]
    [NSObject autorelease]
    追踪到这里已经就已经无法再追踪了。
  3. 呵呵,对于这个方法写过MRC的同学应该不陌生,在MRC下创建autorelease对象都是要主动调用autorelease,到了ARC时代,这个已经由编译器插入了,好了一切又明朗了。

  需要说明一下,ARC插入的objc_autorelease或类似调用,其通过objc_msgSend 调用了[NSObject autorelease];retain,release的插入也类似,最后都是通过 objc_msgSend 调用的。

  说完push和添加引用到autoreleasepool,再说一下pop,处理后的结果如下:

static inline void pop(void *token)
{
        AutoreleasePoolPage *page;
        id *stop;
        pop所有的AutoreleasePoolPage后return
        检测*token是否为POOL_BOUNDARY,不合法则return,也就是说每次 pop 必须遇到POOL_BOUNDARY才结束,其他都不合法

        page->releaseUntil(stop);//这里*stops是nil
        处理page后续操作,如:切换到父page,释放page
}

void releaseUntil(id *stop)
{
        while (this->next != stop) {
            AutoreleasePoolPage *page = hotPage();
          while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }
            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }
        setHotPage(this);
}

循环移除引用,同时调用objc_release

小结:

  1. ARC下,对象是release还是autorelease在编译时就已经决定了。
  2. autoreleasepool包裹的范围大小决定其中对象的生命周期,所以这个范围的大小是值得商榷和优化的地方。比如:一种常见的场景就是在循环中临时创建大量的对象,如果不使用autoreleasepool包裹这个小范围,这些对象会大量累积无法及时释放。
  3. 最重要的一个结论:ARC机制将对象生命周期的管理转换成引用(指针)的生命周期管理,而引用(指针)因为分配在栈上(autoreleasepool也类似,只不过是AutoreleasePoolPage这种特殊的全局栈,独立于函数调用栈以外),生命周期直接被栈管理,因此只要处理好栈的 Push 和 Pop就可以间接管理对象。(这是一种 perfect的设计,将问题转化,并利用和模仿现有的机制去解决。这个跟iOS中GCD将多线程问题转换成dispatch_queue有异曲同工之妙,都是让人惊叹的设计。)
  4. release机制利用函数调用期间引用(指针)的局部生命周期比较精确的控制对象的生命周期,而autorelease则在此基础上自己实现了一个记录引用的栈,并在适当的时机压栈和出栈完成对引用的生命周期的管理进而管理对象的生命周期,特别是仅在 runloop 的参与下,所以生命周期并不精确。
  5. 两种机制都依赖RC,相辅相成,互不干扰。同一个对象同时可以被两者管理,也可以被多个autoreleasepool管理。
接下来讨论一个问题:autorelease是否可以替代release?

  autorelease通过全局AutoreleasePoolPage代为管理的对象的生命周期,本身机制不复杂,AutoreleasePoolPage本身结构占用的内存也极少,如果将autoreleasepool创建的粒度更细些,更多的去触发autorelease,比如每一个方法都套一个,那么应该就可以代替release了,这也是在ARC时代,无法主动调用release来精确管理对象生命的替代做法。autorelease确实可以替代release,但事实上这么做没有必要,因为release能更好的在函数等局部范围管理引用,而且更简单快捷。所以使用release和runtime的autorelease就是为了替代MRC,而显式创建autoreleasepool则是为了延续MRC的精确管理的优点。

隐式autoreleasepool

  前面讨论了autorelease机制是如何管理对象生命周期的和其管理的对象的范围,但其还有一种隐藏使用方式,这种方式的引入使我们对对象生命的管理大大简化了。
这里引入一个经典的例子:

__weak id weakString = nil;
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"zhangwei_%d",1];
    weakString = string;
    NSLog(@"string is : %@", string);//zhangwei_1
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"string is : %@", weakString);//zhangwei_1
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"string is : %@", weakString);//null
}
*TaggedPointer

  注意: [NSString stringWithFormat:@“zhangwei_%d”,1]这句很有讲究,首先不能是一个完全的字面常量字符串,这可能导致编译器直接优化为常量,导致weakString一直有值;其二,字符串“zhangwei_1”需要大于等于10,否则也会导致weakString一直有值,~,没搞懂这是啥情况!!!只能猜测可能是被编译器搞成常量了或者就是搞成了TaggedPointer(但似乎又不够装这个10byte,毕竟ARM64指针也只有64bit),一直都在内存里面(如果是TaggedPointer,那么weakString里面就已经有具体的值了),所以weakString会一直有值。
  这个问题对我们的研究影响较大,为了说明这个问题,我们引入另外一种情况坑爹的情况!!!

for (int i = 0; i < 1000000000; i++) {
        NSString *str = [NSString stringWithFormat:@"%d  ", i];
}

  运行的时候,开始内存占用很稳定,但是后面突然开始内存飙升,这个现象和TaggedPointer指针对象有关系,开始的时候i还比较小,可以被优化成一个TaggedPointer,占用内存很小,并且是在栈上,这时ARC机制已经不起作用了(只有指针,指针里面已经存了相应的数据,也就没有对象了),被栈接管了;当i很大的时候,就只能被当做常规的对象分配在堆上,所以导致内存飙升。~我一直以为字符串不会被优化成TaggedPointer,当时被这货折腾得怀疑人生了。
补充实验:
  使用NSLog(@"string is : %p %p %@",weakString, string, string);输出一下
当使用“zhangwei_%d”时,多次运行,俩指针存的是类似于0x1c02215c00x102d58eb8这样的正常地址,所以autoreleasepool起作用了。
当使“zhangwei%d”时,多次运行,里面装的都是0xa395482da90005e9,64个bit,很明显iPhone肯定不存在这么大的一个内存地址,所以其装的应该包含数据,我尝试反编码这个数据 ,呃,没搞出来,不知道苹果是怎么编码这个数据的。

  另外TaggedPointer的引入确实提高了小对象的读写效率,但毕竟不是个真正的对象,没有了isa指针,破坏了 OC 语言的机制,所以苹果不得不去填这个坑,所以就会看到源码里面大量的 if(isTaggedPointer())的判断来特殊处理。

  回到正题,在viewDidLoadviewWillAppearviewDidAppear中各来一个断点
断点1堆栈

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000102282618 ADemo`-[ViewController viewDidLoad](self=0x0000000145d0c7c0, _cmd="viewDidLoad") at ViewController.m:22
    frame #1: 0x000000018d932ee0 UIKit`-[UIViewController loadViewIfRequired] + 1020
    frame #2: 0x000000018d932acc UIKit`-[UIViewController view] + 28
    frame #3: 0x000000018d923d60 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 136
    frame #4: 0x000000018d922b94 UIKit`-[UIWindow _setHidden:forced:] + 272
    frame #5: 0x000000018d9b06a8 UIKit`-[UIWindow makeKeyAndVisible] + 48
    frame #6: 0x000000018d9262f0 UIKit`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3660
    frame #7: 0x000000018d8f365c UIKit`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1680
    frame #8: 0x000000018df23a0c UIKit`__111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 784
    frame #9: 0x000000018d8f2e4c UIKit`+[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 160
    frame #10: 0x000000018d8f2ce8 UIKit`-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 240
    frame #11: 0x000000018d8f1b78 UIKit`-[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 724
    frame #12: 0x000000018e58772c UIKit`__82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 296
    frame #13: 0x000000018d8f1268 UIKit`-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 432
    frame #14: 0x000000018e36c9b8 UIKit`__125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 220
    frame #15: 0x000000018e4baae8 UIKit`_performActionsWithDelayForTransitionContext + 112
    frame #16: 0x000000018d8f0c88 UIKit`-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 248
    frame #17: 0x000000018d8f0624 UIKit`-[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 368
    frame #18: 0x000000018d8ed65c UIKit`-[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 540
    frame #19: 0x000000018d8ed3ac UIKit`-[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 364
    frame #20: 0x0000000186554470 FrontBoardServices`-[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 364
    frame #21: 0x000000018655cd6c FrontBoardServices`__56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 224
    frame #22: 0x000000010253d220 libdispatch.dylib`_dispatch_client_callout + 16
    frame #23: 0x0000000102549850 libdispatch.dylib`_dispatch_block_invoke_direct + 232
    frame #24: 0x0000000186588878 FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 36
    frame #25: 0x000000018658851c FrontBoardServices`-[FBSSerialQueue _performNext] + 404
    frame #26: 0x0000000186588ab8 FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 56
    frame #27: 0x0000000183cff404 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #28: 0x0000000183cfec2c CoreFoundation`__CFRunLoopDoSources0 + 276
    frame #29: 0x0000000183cfc79c CoreFoundation`__CFRunLoopRun + 1204
    frame #30: 0x0000000183c1cda8 CoreFoundation`CFRunLoopRunSpecific + 552
    frame #31: 0x0000000185bff020 GraphicsServices`GSEventRunModal + 100
    frame #32: 0x000000018dbfd78c UIKit`UIApplicationMain + 236
    frame #33: 0x00000001022827ec ADemo`main(argc=1, argv=0x000000016db839e8) at main.m:14
    frame #34: 0x00000001836adfc0 libdyld.dylib`start + 4

断点2堆栈

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x00000001022826b0 ADemo`-[ViewController viewWillAppear:](self=0x0000000145d0c7c0, _cmd="viewWillAppear:", animated=NO) at ViewController.m:29
    frame #1: 0x000000018d9b26d8 UIKit`-[UIViewController _setViewAppearState:isAnimating:] + 616
    frame #2: 0x000000018d9b2448 UIKit`-[UIViewController __viewWillAppear:] + 140
    frame #3: 0x000000018d9b2330 UIKit`-[UIViewController viewWillMoveToWindow:] + 704
    frame #4: 0x000000018d91795c UIKit`-[UIView(Hierarchy) _willMoveToWindow:withAncestorView:] + 584
    frame #5: 0x000000018d8fe6c0 UIKit`-[UIView(Internal) _addSubview:positioned:relativeTo:] + 424
    frame #6: 0x000000018d924008 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 816
    frame #7: 0x000000018d922b94 UIKit`-[UIWindow _setHidden:forced:] + 272
    frame #8: 0x000000018d9b06a8 UIKit`-[UIWindow makeKeyAndVisible] + 48
    frame #9: 0x000000018d9262f0 UIKit`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3660
    frame #10: 0x000000018d8f365c UIKit`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1680
    frame #11: 0x000000018df23a0c UIKit`__111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 784
    frame #12: 0x000000018d8f2e4c UIKit`+[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 160
    frame #13: 0x000000018d8f2ce8 UIKit`-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 240
    frame #14: 0x000000018d8f1b78 UIKit`-[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 724
    frame #15: 0x000000018e58772c UIKit`__82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 296
    frame #16: 0x000000018d8f1268 UIKit`-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 432
    frame #17: 0x000000018e36c9b8 UIKit`__125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 220
    frame #18: 0x000000018e4baae8 UIKit`_performActionsWithDelayForTransitionContext + 112
    frame #19: 0x000000018d8f0c88 UIKit`-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 248
    frame #20: 0x000000018d8f0624 UIKit`-[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 368
    frame #21: 0x000000018d8ed65c UIKit`-[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 540
    frame #22: 0x000000018d8ed3ac UIKit`-[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 364
    frame #23: 0x0000000186554470 FrontBoardServices`-[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 364
    frame #24: 0x000000018655cd6c FrontBoardServices`__56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 224
    frame #25: 0x000000010253d220 libdispatch.dylib`_dispatch_client_callout + 16
    frame #26: 0x0000000102549850 libdispatch.dylib`_dispatch_block_invoke_direct + 232
    frame #27: 0x0000000186588878 FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 36
    frame #28: 0x000000018658851c FrontBoardServices`-[FBSSerialQueue _performNext] + 404
    frame #29: 0x0000000186588ab8 FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 56
    frame #30: 0x0000000183cff404 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #31: 0x0000000183cfec2c CoreFoundation`__CFRunLoopDoSources0 + 276
    frame #32: 0x0000000183cfc79c CoreFoundation`__CFRunLoopRun + 1204
    frame #33: 0x0000000183c1cda8 CoreFoundation`CFRunLoopRunSpecific + 552
    frame #34: 0x0000000185bff020 GraphicsServices`GSEventRunModal + 100
    frame #35: 0x000000018dbfd78c UIKit`UIApplicationMain + 236
    frame #36: 0x00000001022827ec ADemo`main(argc=1, argv=0x000000016db839e8) at main.m:14
    frame #37: 0x00000001836adfc0 libdyld.dylib`start + 4

断点3堆栈

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x000000010228273c ADemo`-[ViewController viewDidAppear:](self=0x0000000145d0c7c0, _cmd="viewDidAppear:", animated=NO) at ViewController.m:35
    frame #1: 0x000000018d9b27b8 UIKit`-[UIViewController _setViewAppearState:isAnimating:] + 840
    frame #2: 0x000000018dc0347c UIKit`__64-[UIViewController viewDidMoveToWindow:shouldAppearOrDisappear:]_block_invoke + 44
    frame #3: 0x000000018da08814 UIKit`-[UIViewController _executeAfterAppearanceBlock] + 92
    frame #4: 0x000000018dd0e9c4 UIKit`_runAfterCACommitDeferredBlocks + 564
    frame #5: 0x000000018dd0498c UIKit`_cleanUpAfterCAFlushAndRunDeferredBlocks + 384
    frame #6: 0x000000018dd156c0 UIKit`__34-[UIApplication _firstCommitBlock]_block_invoke_2 + 152
    frame #7: 0x0000000183cff2bc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 20
    frame #8: 0x0000000183cfea7c CoreFoundation`__CFRunLoopDoBlocks + 264
    frame #9: 0x0000000183cfc7b0 CoreFoundation`__CFRunLoopRun + 1224
    frame #10: 0x0000000183c1cda8 CoreFoundation`CFRunLoopRunSpecific + 552
    frame #11: 0x0000000185bff020 GraphicsServices`GSEventRunModal + 100
    frame #12: 0x000000018dbfd78c UIKit`UIApplicationMain + 236
    frame #13: 0x00000001022827ec ADemo`main(argc=1, argv=0x000000016db839e8) at main.m:14
    frame #14: 0x00000001836adfc0 libdyld.dylib`start + 4

控制台输出:

2018-04-08 19:31:17.031524+0800 Ademo[2412:1210361] string: zhangwei_1
2018-04-08 19:31:17.418043+0800 Ademo[2412:1210361] string: zhangwei_1
2018-04-08 19:31:17.828153+0800 Ademo[2412:1210361] string: (null)

  可以发现weakstring这个弱引用在viewDidLoadviewWillAppear都有值,则证明这个对象还没有释放,那么这就有两种可能性,一种是对象可能已经被标记为将释放,但却在其他线程回收,这就会有一个延时,导致依旧可以访问到对象,但我在viewWillAppear里做了一个for循环,让weakstring有足够的时间来清空,但事实上结果如上weakstring还是有值,那就证明对象未被释放依然是有效的。
在此基础上我们在main函数入口加入一个runloop的observer:

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop--kCFRunLoopEntry"); break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"runloop--kCFRunLoopBeforeTimers"); break;
            case kCFRunLoopBeforeSources:
                NSLog(@"runloop--kCFRunLoopBeforeSources"); break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"runloop--kCFRunLoopBeforeWaiting"); break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"runloop--kCFRunLoopAfterWaiting");  break;
            case kCFRunLoopExit:
                NSLog(@"runloop--kCFRunLoopExit");  break;
            default:  break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);

同时修改

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"zhangwei_%d",1];
    weakstring = string;

    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"string5 :%@", weakstring);
    });
    NSLog(@"string1: %@", weakstring);
}

最后输出部分连续日志如下:

2018-04-09 11:06:37.448474+0800 Ademo[2712:1457333] runloop--kCFRunLoopBeforeSources
2018-04-09 11:06:37.518347+0800 Ademo[2712:1457333] string1: zhangwei_1
2018-04-09 11:06:37.518493+0800 Ademo[2712:1457333] string2: zhangwei_1
2018-04-09 11:06:37.522253+0800 Ademo[2712:1457333] string3: (null)
2018-04-09 11:06:37.522306+0800 Ademo[2712:1457333] runloop-kCFRunLoopBeforeTimers
2018-04-09 11:06:37.522322+0800 Ademo[2712:1457333] runloop-kCFRunLoopBeforeSources
2018-04-09 11:06:37.522445+0800 Ademo[2712:1457333] string5 :(null)
2018-04-09 11:06:37.523187+0800 Ademo[2712:1457333] runloop-kCFRunLoopBeforeTimers

我们观察三个堆栈,发现前两个很相似,都是在__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__中回调的,而viewDidAppear调用堆栈和前两者明显不一样,所以我们大胆猜测:viewDidLoadviewWillAppear是在同一个runloop中执行的。

Runloop

runloop内部逻辑图

  先看苹果的runloop内部逻辑图,runloop周期有四次通知Observer分别是2,3,6,8。
看日志发现kCFRunLoopBeforeSources,即第3步之后NSLog连续输出string1string2string3,所以得出结论viewDidLoadviewWillAppear在第4步执。
再看第三个堆栈,发现回调函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__,反向查找调用函数__CFRunLoopDoBlocks,结合源码分析,发现这个函数在__CFRunLoopRun(runloop循环处理事件函数)中有三个地方调用,第3步后,第4步后和第9步后,对照log日志,立即排除第3步后(其不可能早于viewWillAppear),所以viewDidAppear的调用只可能在第4或9步后调用的,但不管第4还是第9步但总是跳过了第6、7步,不然会有kCFRunLoopBeforeWaiting日志。对于这两者情况,我更偏向于是在第9步调用的,也就是说runloop在处理完了source0的所有item之前就已经收到了source1(就是视图渲染完成的通知),也就是说这三者有可能还在同一runloop周期,也可能不在,这里需要打一个问号。
如果视图渲染需要更长的时间,那是否意味着viewDidAppear会在不同的runloop周期中回调?我找了一个稍微复杂的页面来做这个实验(视图渲染需要更多的时间),得到以下log:

2018-04-09 17:44:53.651759+0800 shop[3035:1600882] string1: zhangwei_1
2018-04-09 17:44:53.652762+0800 shop[3035:1600882] string2: zhangwei_1
2018-04-09 17:44:53.744252+0800 shop[3035:1600882] runloop-kCFRunLoopAfterWaiting 
2018-04-09 17:44:53.744544+0800 shop[3035:1600882] runloop-kCFRunLoopBeforeTimers 
2018-04-09 17:44:53.744564+0800 shop[3035:1600882] runloop-kCFRunLoopBeforeSources 
2018-04-09 17:44:53.744939+0800 shop[3035:1600882] string5 :(null)
….其他若干observer通知
2018-04-09 17:44:54.082226+0800 shop[3035:1600882] string3: (null)

  我们发现string5在下一个runloop就打印了,而string3打印的时间晚了很多,期间有很多observer事件的log,应该已经过了很多个runloop了,所以可以得出结论:viewDidAppearviewDidLoadviewWillAppear应该已经不在同一runloop周期内调用,结合viewDidAppear中log string5为 null,所以这其间至少调用了一次autoreleasepool的pop操作。

接着往下看:
在这个函数内

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

添加如下代码(因为在此之前iOS会向其中加入相关observer)

CFRunLoopRef r = CFRunLoopGetCurrent();
 NSLog(@"runloop: %@", r);

找到如下log:

observers = (
 "<CFRunLoopObserver 0x1c013d100 [0x1b5f62538]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout =<redacted>(0x18dbe9650), context =<CFArray 0x1c005df40 [0x1b5f62538]>{type = mutable-small, count = 1, values = (\n\t0 : <0x1026c0048>\n)}}",
 "<CFRunLoopObserver 0x1c413c840 [0x1b5f62538]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = 0, callout =<redacted>(0x183c79fe0), context =<CFRunLoopObserver context 0x102210120>}", 
 "<CFRunLoopObserver 0x1c013cc00 [0x1b5f62538]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout =<redacted>(0x18dbe94c4), context =<CFRunLoopObserver context 0x1c00c1c00>}", 
 "<CFRunLoopObserver 0x1c013cfc0 [0x1b5f62538])>valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout =<redacted>(0x18dd1c9fc), context =<CFRunLoopObserver context 0x1038014a0>}",
 "<CFRunLoopObserver 0x1c413d600 [0x1b5f62538]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout =<redacted>(0x187e663f4), context =<CFRunLoopObserver context 0x0>}",
 "<CFRunLoopObserver 0x1c013d060 [0x1b5f62538]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout =<redacted>(0x18dbe94cc), context =<CFRunLoopObserver context 0x1038014a0>}",
 "<CFRunLoopObserver 0x1c013d1a0 [0x1b5f62538]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout =<redacted>(0x18dbe9650), context =<CFArray 0x1c005df40 [0x1b5f62538]>{type = mutable-small, count = 1, values = (\n\t0 : <0x1026c0048>\n)}}"
)

  第1项order=-2147483647和最后一项order=2147483647,这两个观察者是用来autoreleasepool创建和销毁的,然后接着看前者activities = 0x1,后者activities = 0xa0(二进制1010 0000),结合以下定义:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

  我们发现activities=0x1,所以第一个observer只在kCFRunLoopEntry时调用;activities = 0xa0(二进制1010 0000),最后一个observer在kCFRunLoopBeforeWaitingkCFRunLoopExit两种情况下回调用,通过字面的意思不难了解其意义。但是这有一个问题,kCFRunLoopEntrykCFRunLoopExit是成对出现的,但kCFRunLoopBeforeWaiting只调用了后者不是成对的。

接下来去翻源码:
  在CFRunLoopAddObserver中有这么一句:rlm->_observerMask |= rlo->_activities;
  再搜索_observerMask关键字,发现与kCFRunLoopBeforeWaiting有关的调用只在CFRunloopRun中有一个地方:

if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting))
    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

  我们知道这个observer回调优先级最低,是pop autoreleasepool的操作,同时observer回调会将当前observer传回去,所以应该是根据observer的activities状态去判断是否需要push,而这个时机也十分恰当。

头上的那块乌云:
  通过以上分析,相信会让你对整个autorelease的机制有一个详细的了解,但依然有一个问题我没能思考明白,有了解的麻烦解答一下我的疑惑。就是既然除了kCFRunLoopExit就只在kCFRunLoopBeforeWaiting通知中会产生autoreleasepool得pop操作,对照那张图就是第6步,在之前分析中我指出会跳过6,7步,那么这就矛盾了,因为日志表明string3:(null)也就是说调用了pop,走了第6步,却又没有产生kCFRunLoopBeforeWaiting的日志。~

接下来讨论release,autorelease啥时候插入的

原始代码:

int main(int argc, char * argv[]) {
    @autoreleasepool{
        NSString *aString = [[NSString alloc]initWithUTF8String:"aaaaa"];
    }
}

  我们尝试使用clang -rewrite-objc命令将代码构建成C++代码,但是没有发现插入相关release的代码,这就傻眼了,没办法只能撸汇编了。
  我们将代码通过Xcode的Product ->perform action -> assemble XXX.m汇编一下得到以下代码

.section __TEXT,__text,regular,pure_instructions
.ios_version_min 11, 3
.file 1 "/Users/zhangwei/Desktop/Ademo" "/Users/zhangwei/Desktop/Ademo/Ademo/main.m"
.globl _main                  ; -- Begin function main
.p2align 2
_main:                                  ; @main
Lfunc_begin0:
.loc 1 11 0                  ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:11:0
.cfi_startproc
; BB#0:
sub sp, sp, #48            ; =48
stp x29, x30, [sp, #32]    ; 8-byte Folded Spill
add x29, sp, #32            ; =32
Lcfi0:
.cfi_def_cfa w29, 16
Lcfi1:
.cfi_offset w30, -8
Lcfi2:
.cfi_offset w29, -16
stur w0, [x29, #-4]
str x1, [sp, #16]
Ltmp0:
.loc 1 12 21 prologue_end    ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:12:21
bl _objc_autoreleasePoolPush
adrp x1, l_OBJC_SELECTOR_REFERENCES_@PAGE
add x1, x1, l_OBJC_SELECTOR_REFERENCES_@PAGEOFF
adrp x30, l_OBJC_CLASSLIST_REFERENCES_$_@PAGE
add x30, x30, l_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF
Ltmp1:
.loc 1 13 24                ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:13:24
ldr x30, [x30]
ldr x1, [x1]
str x0, [sp]        ; 8-byte Folded Spill
mov x0, x30
bl _objc_msgSend
adrp x1, l_.str@PAGE
add x2, x1, l_.str@PAGEOFF
adrp x1, l_OBJC_SELECTOR_REFERENCES_.2@PAGE
add x1, x1, l_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
.loc 1 13 23 is_stmt 0      ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:13:23
ldr x1, [x1]
bl _objc_msgSend
mov x1, #0
add x2, sp, #8              ; =8
.loc 1 13 19                ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:13:19
str x0, [sp, #8]
.loc 1 14 5 is_stmt 1        ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:14:5
mov x0, x2
bl _objc_storeStrong
ldr x0, [sp]        ; 8-byte Folded Reload
bl _objc_autoreleasePoolPop
orr w0, wzr, #0x1
Ltmp2:
.loc 1 15 5                  ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:15:5
ldp x29, x30, [sp, #32]    ; 8-byte Folded Reload
add sp, sp, #48            ; =48
ret
Ltmp3:
Lfunc_end0:
.cfi_endproc
……(以下省略各种常量定义啥的)

  以”.”开始的代码暂时忽略,找到标签_main: Lfunc_begin0:就是函数入口,其就是一个汇编标记而已,有点类似于goto语句的标签,不了解汇编的也不要着急,我会讲的稍微详细些,如果不需要看可以跳过直接看结论。

  1. 堆栈寄存器压栈48byte(栈是向下增长的)
  2. stpx29(也就是FP,寄存器保存栈帧地址,类似于SP),x30LR,也就是返回地址寄存器),这句的意思是将这俩寄存器的值依次存储在sp+32的位置,之后恢复的时候再载入,其中方括号[]是sp+32地址所代表内存数据
  3. 顺便把x29跳转到sp+32的位置,以上三句是在保存前面的调用上下文,以便于最后返回是恢复现场。
  4. sturstr都是写入内存,存储之前的参数,在这里没有太具体的作用
  5. Ltmp0: bl _objc_autoreleasePoolPush 跳转到我们熟悉的autoreleasepool创建入口地址
    adrp x1, l_OBJC_SELECTOR_REFERENCES_@PAGE加载selector所在的Page地址到 x1,然后add x1, x1, l_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF,即将 x1偏移到 l_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF位置,连起来就是加载NSStringallocx1,这里需要说明一下x0-x4存储函数调用的前4个参数,更多的参数则存储在栈上,所以频繁调用的函数参数不要超过4个,可以提高效率。还记得objc_msgSend函数么,其类型是我们所以这里是(void(*)(objc_object *, SEL, …),所以将selector存在x1上。
    再看adrp x30 l_OBJC_CLASSLIST_REFERENCES_这句,将class的引用加载到了x30x30是返回地址寄存器。
  6. ldr x30, [x30]x30的内容加载到x30,那就是NSString引用的地址,同理加载x1;这句str x0, [sp],将x0存储在栈上,但x0还是以前的参数值,所以这句应该只是在挪走x0的数据,后面传参需要用到 x0。将x30的值赋值给x0,就是msgSend的第一个参数。参数等准备完毕,跳转到_objc_msgSend,这里没有这部分汇编,也就看不到具体内容了,感兴趣的可以去看汇编文件objc-msg-arm64.s_objc_msgSend实现是汇编做的。
  7. _objc_msgSend调完,会返回这里,继续加载l_.str("aaaaa")到x1,再赋值给x2,就是msgSend第三个参数,加载l_OBJC_SELECTOR_REFERENCES_.2("initWithUTF8String:")到x1,load x1中的值,再次跳转到_objc_msgSend
  8. mov x1, #0,给x1赋值为立即数0,将sp+8存储在x2中,将x0的值存储在内存sp+8的位置。这里需要注意的是x0是返回值存放的寄存器。结合OC代码x0存储的是aString指向的对象。将x2赋值给x0
  9. 接下来到了我们的重点了,跳转_objc_storeStrong,然后将返回值x0写入内存[sp]
  10. 跳转_objc_autoreleasePoolPop
  11. 最后三句ldp x29, x30, [sp, #32] ;add sp, sp, #48;ret恢复到最开始保存的上下文并返回。

到runtime的源码找到_objc_storeStrong函数

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

  发现其有两个参数,内部调用了objc_retainobjc_release,结合上面汇编分析的8、9两步,发现x0=sp+8是有值的,而x1=0,所以再结合这个函数,得知objc_storeStrong retain了0(就是nil),同时release (sp+8)这个对象。what?和网上很多资料的解释不一样!
  这里需要说明的是_objc_storeStrong第二个参数不为空其实也是可以retain对象的,在运行时方法object_setInstanceVariable存储对象值的时候就会调用到,其相对比_objc_retain是可以在指定的地址和retain对象,而且从名字上看很容易认为其是在作一个强引用。
为了进一步证明我改了一下代码

int main(int argc, char * argv[]) {
    @autoreleasepool{
        NSString *aString = [[NSString alloc]initWithUTF8String:”aaaaa"];
        id aId = aString;
    }
}

汇编如下:

Ltmp1:
.loc 1 13 24                ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:13:24
ldr x30, [x30]
ldr x1, [x1]
str x0, [sp, #8]            ; 8-byte Folded Spill
mov x0, x30
bl _objc_msgSend
adrp x1, l_.str@PAGE
add x2, x1, l_.str@PAGEOFF
adrp x1, l_OBJC_SELECTOR_REFERENCES_.2@PAGE
add x1, x1, l_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
.loc 1 13 23 is_stmt 0      ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:13:23
ldr x1, [x1]
bl _objc_msgSend
.loc 1 13 19                ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:13:19
str x0, [sp, #24]
.loc 1 14 12 is_stmt 1      ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:14:12
ldr x0, [sp, #24]
bl _objc_retain
add x1, sp, #16            ; =16
mov x2, #0
str x0, [sp, #16]
.loc 1 15 5                  ; /Users/zhangwei/Desktop/Ademo/Ademo/main.m:15:5
mov x0, x1
mov x1, x2
bl _objc_storeStrong
mov x0, #0
add x1, sp, #24            ; =24
str x0, [sp]        ; 8-byte Folded Spill
mov x0, x1
ldr x1, [sp]        ; 8-byte Folded Reload
bl _objc_storeStrong
ldr x0, [sp, #8]            ; 8-byte Folded Reload
bl _objc_autoreleasePoolPop
orr w0, wzr, #0x1

我们发现多调用了一次_objc_retain_objc_storeStrong
  我们再次修改代码多定义俩强引用,在NSLog打印一下,发现_objc_retain_objc_storeStrong又多出现了俩次,而后者始终比前者多一次。
但发现没有调用_objc_retainAutoreleasedReturnValue啥的?
再次修改代码:

int main(int argc, char * argv[]) {
    @autoreleasepool{
        NSString *a = [NSString stringWithFormat:@"zhangwei_%@",1];
    }
    return 1;
}

再次汇编之后,出现关键跳转命令 bl _objc_retainAutoreleasedReturnValue
  找一下这个源码,发现这个函数似乎只是调用了一下 retain,有点晕是吧。是因为这个创建的对象要赋值给 a 这个引用。

先做个小结:

  1. strong的引用会被编译成_objc_retain,第一次强引用除外;如果连一个强引用都没有,就会立即插入_objc_release
  2. 编译器会帮我们插入release操作,只不过这里是通过调用_objc_storeStrong并传入nil来实现的,同时其被插入在当前作用域的末尾。
  3. alloc出的对象,autoreleasepool就不管这事儿,所以第一份代码里面虽然有_objc_autoreleasePoolPop调用,但实际上并不会影响aString的生命周期
  4. 如果使用stringWithFormat:这类非allocnewcopy 等方法来创建对象,就会调用_objc_retainAutoreleasedReturnValue来调用retain交给autoreleasepool管理(这个解释略有不妥,后面会有详细解释)。
  5. alloc函数已经帮我们retain,所以这里的汇编_objc_retain_objc_storeStrong少一次调用。
  6. 一般认为allocnewcopy 等创建的对象都是自持有对象(作为返回值超过生命周期时,可以转变成 autorelease 对象),其他的都是autorelease对象。

引用计数的过程:

先做一些准备工作:

  1. 为了方便分析,将 retain 记作+1,autorelease 记作(-1),release,记作 -1
  2. 解释一下下面三个函数
    _objc_autoreleaseReturnValue //直接返回,或者(-1)
    _objc_retainAutoreleaseReturnValue //直接返回,或者先 +1,再 (-1)
    _objc_retainAutoreleasedReturnValue // 直接返回,或者+1
    _objc_unsafeClaimAutoreleasedReturnValue //直接返回,或者调-1
  3. 通过[obj valueForKey:”retainCount”]可以获取对象的引用计数,这个方法的返回值会被自动autoreleasepool管理。

用以下代码做实验:

{
      __unsafe_unretained  NSString *string = [[NSString alloc] initWithUTF8String:@“zhangwei_111111”];
}

  这种情况如果后面再访问string会挂掉,原因是编译器,在这句之后立刻插入了_objc_release,对象已经被释放。去掉__unsafe_unretained后,会发现编译器是在{}结束的时候插入_objc_storeStrong的,而在此之前其retainCount=1

{
      __unsafe_unretained  NSString *string = [NSString stringWithFormat:@“zhangwei_%d”,1111];
      (或者[NSString stringWithFormat:@“zhangwei_%d”,1111];)
}

  程序可以正常运行,string 也有值,汇编后发现调用了 _objc_unsafeClaimAutoreleasedReturnValue,竟然没有挂,retainCount=1,有点奇怪了。
为了了解这其中发生了什么,我们自定义对象 Object

+ (instancetype)anObject {
    (__autoreleasing) Object *o = [Object new];
    return o;
}

在 main函数中做

{
    id a = [Object anObject];
}

汇编一下,发现return时是调用了_objc_autoreleasedReturnValue
  通过id a = [Object anObject];来调用,发现retainCount = 1。如果在前面加上__unsafe_unretained,如果后面访问了a也会挂掉,也就是对象已经被释放了。

  在Object *o = [Object new]前添加__autoreleasing,再汇编一下:

"+[Object anObject]":                    ; @"\01+[Object anObject]"
Lfunc_begin0:
.loc 1 10 0                  ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:10:0
.cfi_startproc
; BB#0:
sub sp, sp, #48            ; =48
stp x29, x30, [sp, #32]    ; 8-byte Folded Spill
add x29, sp, #32            ; =32
Lcfi0:
.cfi_def_cfa w29, 16
Lcfi1:
.cfi_offset w30, -8
Lcfi2:
.cfi_offset w29, -16
stur x0, [x29, #-8]
str x1, [sp, #16]
Ltmp0:
.loc 1 11 33 prologue_end    ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:11:33
adrp x0, l_OBJC_CLASSLIST_REFERENCES_$_@PAGE
ldr x0, [x0, l_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF]
adrp x1, l_OBJC_SELECTOR_REFERENCES_@PAGE
ldr x1, [x1, l_OBJC_SELECTOR_REFERENCES_@PAGEOFF]
bl _objc_msgSend
.loc 1 11 29 is_stmt 0      ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:11:29
bl _objc_autorelease
str x0, [sp, #8]
.loc 1 12 12 is_stmt 1      ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:12:12
ldr x0, [sp, #8]
.loc 1 12 5 is_stmt 0        ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:12:5
ldp x29, x30, [sp, #32]    ; 8-byte Folded Reload
add sp, sp, #48            ; =48
b _objc_retainAutoreleaseReturnValue
Ltmp1:
Lfunc_end0:
.cfi_endproc
                                        ; -- End function
.globl _main                  ; -- Begin function main
.p2align 2
_main:                                  ; @main
Lfunc_begin1:
.loc 1 16 0 is_stmt 1        ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:16:0
.cfi_startproc
; BB#0:
sub sp, sp, #48            ; =48
stp x29, x30, [sp, #32]    ; 8-byte Folded Spill
add x29, sp, #32            ; =32
Lcfi3:
.cfi_def_cfa w29, 16
Lcfi4:
.cfi_offset w30, -8
Lcfi5:
.cfi_offset w29, -16
adrp x8, l_OBJC_SELECTOR_REFERENCES_.2@PAGE
add x8, x8, l_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
adrp x9, l_OBJC_CLASSLIST_REFERENCES_$_@PAGE
add x9, x9, l_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF
stur w0, [x29, #-4]
str x1, [sp, #16]
Ltmp2:
.loc 1 17 12 prologue_end    ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:17:12
ldr x9, [x9]
ldr x1, [x8]
mov x0, x9
bl _objc_msgSend
; InlineAsm Start
mov x29, x29 ; marker for objc_retainAutoreleaseReturnValue
; InlineAsm End
bl _objc_retainAutoreleasedReturnValue
add x8, sp, #8              ; =8
mov x9, #0
str x0, [sp, #8]
.loc 1 18 1                  ; /Users/zhangwei/Desktop/Demo2/Demo2/main.m:18:1
mov x0, x8
mov x1, x9
bl _objc_storeStrong
mov w10, #0
mov x0, x10
ldp x29, x30, [sp, #32]    ; 8-byte Folded Reload
add sp, sp, #48            ; =48
ret
Ltmp3:
Lfunc_end1:

  为了使汇编简单,这里我没有在main里面手动创建autoreleasepool,这不影响编译器得到插入。

先理论分析:Object在调用new的时候产生了一次 +1,然后标记为__autoreleasing(-1),之后 return 的时候再次产生了 +1和(-1),赋值给 a 引用,产生了一次+1,大括号完还有一次-1。,和预设的剧本不一样啊!和之前retainCount=2不一样啊!到底哪个靠谱啊?!没法子,只能再撸源码了。
  事实上在+[Object anObject]中发现其调用了一次_objc_autoreleasereturn 的时候又调了_objc_retainAutoreleaseReturnValue,在赋值给aretainCount=2,似乎和我们想的不一样,有点乱了~,我们需要再仔细撸源码了

// Prepare a value at +0 for return through a +0 autoreleasing convention.
id
objc_retainAutoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;
    // not objc_autoreleaseReturnValue(objc_retain(obj))
    // because we don't need another optimization attempt
    return objc_retainAutoreleaseAndReturn(obj);
}
// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool
prepareOptimizedReturn(ReturnDisposition disposition)
{
    assert(getReturnDisposition() == ReturnAtPlus0);
    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }
    return false;
}

  看第一个函数第一句if,可能是跟这个返回值优化有关系,再看第二个函数,__builtin_return_address是在获取函数调用完成的返回地址(参数0表示当前函数),在看其ALWAYS_INLINE的属性,可以了解,其实是获取了objc_retainAutoreleaseReturnValue调用完成后的返回地址,而这个也是+(instancetype)anObject的最后一句,所以也是它的返回地址。callerAcceptsOptimizedReturn在不同的 CPU 架构下实现是不同的,在 X86_64会通过偏移去计算实际的地址,最后在通过比较

if (*sym != objc_retainAutoreleasedReturnValue  && 
    *sym != objc_unsafeClaimAutoreleasedReturnValue)
{
    return false;
}

来查看外部函数是否有 ARC 插入的这两调用。
  在 ARM64下实现方式不太一样,要简单得多,编译器已经插入了一行没有意义的代码来做标记,上源码

static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
    // fd 03 1d aa    mov fp, fp
    // arm64 instructions are well-aligned
    if (*(uint32_t *)ra == 0xaa1d03fd) {
        return true;
    }
    return false;
}

  看注释// fd 03 1d aa mov fp, fp,前面的数据表示0xaa1d03fd这个数据(小端机器,存储器的低地址存放低字节),表示指令mov fp, fp,呵呵,FP就是 x29寄存器,这句没啥实际作用,仅作为标记。第二条注释解释了,ARM64下数据是对齐的(呵呵,RISC机器就是任性啊),所以不需要像X86_64算偏移,直接对 a 脱指针操作,比较是否是这个指令。源码上看是这么回事,注释也这么说,但实际情况是不是这么回事呢?
  我们将代码汇编一下,找到任意bl objc_retainAutoreleasedReturnValue发现有

; InlineAsm Start
mov  x29, x29  ; marker for objc_retainAutoreleaseReturnValue
; InlineAsm End
bl _objc_retainAutoreleasedReturnValue

和之前的源码中解释的一致。OK,到了这里再回头去看
if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;
可以知道if是 true,直接返回了obj,所以导致理论分析中的_objc_retainAutoreleaseReturnValue产生的+1和(-1)被优化掉了。
  ReturnAtPlus0是一个标记,会存在线程的专用存储空间TLS(Thread Local Storage),用来告诉外层函数收到的返回值是否需要+1,在这里具体来说就是内层函数_objc_retainAutoreleaseReturnValue存储了ReturnAtPlus0,外层函数_objc_retainAutoreleasedReturnValue 取出发现该值与ReturnAtPlus1不一致,就调用objc_retain
  OK,一切明了了,retainCount=2,一个是+[Object anObject]调用new造成的,一个外层强引用造成的。

补充:
  1. 除了使用allocnewcopymutablecopy以及 allocObjectnewObjectcopyObjectmutablecopyObject创建的自持有对象外,其他的对象如果生命周期超出当前区域时都要给autoreleasepool去管理,比如:valueForKeyanObjectstringWithFormat产生的返回值。

  2. 除了返回值还有一种方式会导致对象生命周期超出,就是通过二级以上的指针返回对象,这时编译器会帮我们将二级指针指向的引用对象作为autorelease对象来管理。

  3. 哪些情况需要手动创建autoreleasepool?
    1)需要立即释放对象,比如大量的临时对象,但在使用时注意1中的情况,有些对象不归autoreleasepool管理;
    2)autoreleasepool是框架,至于怎么用则由使用者决定,iOS在系统管理的线程,比如主线程,已经帮我创建好了,在main函数入口也创建了。所以我们自己创建的线程就需要手动创建autoreleasepool。另外不是基于UI框架的程序也需要手动创建。
    3)如果在主队列还未插入autoreleasepool之前,比如main函数里面就有手动创建(但我觉得这里可能是没有必要的,因为UIApplicationMain这个函数执行完就意味着 App 生命结束,这时候内存将被系统回收,也就没有必要再去做autorelease,当然也或许有其他原因),再比如load函数调用的时候。

  这篇文章写了一个多星期,期间各种实验,战线拖得有点长了,还剩下几个问题以后再发文章吧。比如:retainCount怎么存储的,weak reference又是怎么玩的等等。

  本篇我们撸源码,抠汇编总算是把 ARC 的这套东西了解了一部分了,大致清楚了在我们代码的背后 ARC 是怎么管理引用和对象的,也了解了源码的一些实现细节和注意事项,以及一些有趣的现象和背后的原因。

最后总结一下:
  ARC通过函数调用栈生命周期和AutoreleasepoolPage栈的生命周期,管理了引用的生命周期,再通过引用的生命周期管理了对象的生命周期,剩下的东西就是细节处理和实现了。 ARC既有对已有机制的巧妙利用,也有创造新机制,最终的结果简单高效,这才是好的设计。

文中内容虽然已多方论证,但也难免出现疏漏,若有错误,烦请指摘,给后来的读者提供更正确的知识指引,十分感谢。

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

推荐阅读更多精彩内容