内存管理和runloop、autoreleasePool之间的关系

109 阅读10分钟

现在的iOS开发中都是ARC的内存管理,所谓ARC的内存管理其实就是编译器+runtime共同合作管理内存

其实所谓管理,就是管理释放的时机,因为不释放的话,会造成内存一直飙升,那么我们开发代码时,就要求我们在合适的时间去插入release代码及时释放,

1、在非ARC时代,对象的持有和释放,需要程序员自己进行retain和release,但是在ARC时代,编译器会自动的在合适时机插入retain和release代码,这样开发者无需自己手动书写retain和release相关代码

2、只有编译器自己可以完全管理的了内存吗?ARC的编译器规则是扫描到创建对象的方法名称是以alloc/new/copy/mutcopy开头的话,就会自动的插入retain和release方法来管理引用计数器,但是这样会遇到一个问题如果工厂类方法返回的对象,如果按照ARC的机制,在出了方法作用域后,直接插入了release就释放了,外界调用的地方根本没有机会持有,所以只有编译器管理不太行,还需要一个延迟释放的时机,给外界一个持有的机会

3、都是strong状态修饰符修饰的变量,容易引起循环引用的问题,这时候需要weak去解决循环引用的问题,ARC是会插入retain和release,但是weak变量指向的地址不会变,需要有一个时机去给weak变量赋值空,再次访问weak变量的话,不会有问题

综上所述,内存管理是由ARC和自动释放池共同管理的,控制这对象的引用计数的变化,

1、编译器管理的对象

原则:能有编译器管理的尽量使用编译器管理,因为编译器管理内存释放更早,对内存是有优化的

  • 编译器会根据对象初始化的方法名,由alloc/new/copy/mutablecopy等创建的方法会由编译器管理在合适的时间插入retain和release
  • 由strong变量持有的对象的也是直接由编译器管理的
NSString *str = [NSString stringWithFormat:@"Hello"]; // 类工厂方法,本应放入自动释放池
self.myProperty = str; // 被强引用持有后,进不放入自动释放池
  • swift中的对象,ARC对swift中的对象更倾向于直接使用retain和release,默认是由编译器管理,但是与OCAPI交互时是放入自动释放池的

2、自动释放池管理的对象

  • 由非alloc/new/copy/mutableCopy创建并直接返回的对象,会被直接放入自动释放池
// 以下对象会被加入自动释放池(ARC隐式处理)
NSString *str = [NSString stringWithFormat:@"Hello, %@", name];
NSArray *array = [NSArray arrayWithObjects:obj1, obj2, nil];
  • 方法返回值未被立即持有的对象,这就是一个方法返回一个对象没有被strong类型变量持有,会放入自动释放池,如果不放入自动释放池的话,会被立即释放,调用方没有机会持有
    • 当一个方法返回对象,且该对象未被调用方的代码直接强引用持有时,ARC 可能通过自动释放池延长其生命周期。
- (NSString *)generateName {
    return [NSString stringWithFormat:@"User_%d", rand()]; // 返回的对象可能被加入自动释放池
}
  • 显示调用@autoreleasePool代码块包裹的临时对象
//如果不加autorelasePool代码块的话,大量的tmp对象被加入外层自动释放池,释放的时机是主线程的runloop循环结束的时候
// 在循环期间大量对象残留形成内存峰值
for (int i = 0; i < 10000; i++) {
    @autoreleasepool {
        NSString *temp = [NSString stringWithFormat:@"Temp_%d", i]; // 放入当前池
        // temp 在池结束时释放
    }
}

3、如何管理对象知道了,编译器管理的无需我们管,那么自动释放池管理的对象需要研究下

自动释放池管理的对象,什么是自动释放池,以及自动释放池的创建时机,以及自动释放池中的对象什么时候销毁呢?

先讲清楚什么是自动释放池,然后在看自动释放池跟线程的关系,自动释放池是一个结构体对象,调用@autoreleasePool{}, 内部回调用autoreleasePoolPush,autoreleasePoolPush会判断下当前是否有aureleasePoolPage,autoreleasePoolPage对象之间是以链表的形式链接,单个aureleasePoolPage是一个结构体单个是栈,包含next指针指向下一个节点,parent指针,child指针,thread当前所在线程,还有一个看门的boundary,他指向栈顶,调用autoreleasePoolPush的时候,会判断page是否存在,存在的话,判断当前page是否满了,满的话调用autoreleaseFullPage方法,这个方法新建一页,并且设置为hotpage,调用push压入栈中,不满的话直接push压入栈中,page不存在的话,直接autoreleaseNoPage方法去创建新的一页,设置为hotPage,并且push入栈,autoreleasePool中的对象会在自动释放池销毁的时候,里面所有的对象出栈,对象的引用计数会减1,那么autoreleasepool这个对象是什么时候创建的呢?跟runloop的关系呢?

4、runloop

首先什么是runloop,runloop的作用是什么,runloop与线程什么关系,runloop如何创建,runloop结构长什么样,runloop在实际开发中的应用

  • 什么是runloop, runloop是一个对象,iOS系统中提供了连个runloop对象,一个是NSRunloop(OC对象)和CFRunloopRef是一个C对象,是一个结构体
  • runloop的作用是什么,正常的情况,一个线程执行一次任务后就会退出,这样的话,就会造成在现有新的事件的时候,需要重新开启线程执行事件,事件执行完后退出,如果我们想要一个机制,就是线程不退出,有事件来了就可以随时处理,这样的一种机制应该怎么做呢,就是写一个do while循环不退出,一直从队列里面去事件,有事件就执行,这种模型叫EventLoop,这个模型的关键在于如何管理消息,另外一个就是如何保证线程在没有消息的时候休眠避免一直占用CPU资源,有消息时唤醒执行事件,runloop这个对象就管理了消息,并提供了一个类似的loop函数,所以runloop的作用是为了保证线程不退出
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}
  • runloop和线程的关系,从上面可知runloop的作用就是为了保证线程不退出,为了说明线程和runloop的关系,可以通过runloop的创建说明,runloop的创建苹果没有提供对应的API可以直接创建,只提供了连个可以获取的函数,一个是获取主线程的roopCFRunLoopGetMain(), 获取其他线程runloop的CFRunloopGetCurrent(),函数内部的逻辑,由代码逻辑可只runloop和线程是一一对应的关系,并且存储在一个全局的字典中,从代码中可以看到runloop默认是没有创建的,只有第一次获取的时候才会创建懒加载机制。主线的runloop的开启是在UIApplicationMain函数内部
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}
  • runloop长什么样,runloop中有modes数组,currentMode当前mode类型,commonModes, modeItem有各种ModeItem,因为runloop必须运行在某个Mode下,每个mode中都有对应的事件源(input Source)、Timer Source(定时器源)、observer观察者(为了通知线程处理事件),runloop有几种mode,每个mode中有很多的observe的集合,timers的集合,source1的集合,source0集合
    • KCFRunloopDefaultMode, App的默认运行模式,主线程是在这个运行模式下运行的
    • UITrackingRunLoopMode: 用户交互事件的mode,滚动的时候用这个
    • UIInitializationRunloopMode, 刚启动app时进入的第一个mode
    • GSEventRecieverRunloopMode,接收系统内部事件,平常用不到
    • KCFRunLoopCommonModes,伪模式,一个组合可以将default,uitracking放入进去
  • runloop内部的timer、source、observe
    • timer我们平常使用的timer,如果不手动添加到runloop中,默认也会加入到runloop中的,它是作为事件源被加入到runloop中,timer会造成循环引用,原因是timer与VC相互循环引用,如何解决循环引用使用NSProxy将target传给NSProxy,NSProxy弱引用target,重写methodSignatureForSelector和forwardInvocation可以将消息转发到VC中
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //将定时器加入到默认运行模式中(一旦用户交互就不会响应)
    NSTimer *timer1 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
    
    //将定时器加入到交互运行模式中(一旦停止交互就不会响应)
    NSTimer *timer2 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInTrackingMode) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
    
    //将定时器加入到伪模式中(无论是否交互都可以响应)
    NSTimer *timer3 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInCommonMode) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes];
}

- (void)runInDefaultMode {
    NSLog(@"我只有在默认模式下运行!");
}

- (void)runInTrackingMode {
    NSLog(@"我只有在交互模式下运行!");
}

- (void)runInCommonMode {
    NSLog(@"我在默认模式和交互模式下都能运行!");
}
    • FCRunloopSourceRef 分为source0 和source1
      • source0中只包含了一个函数指针,不能主动触发runloop的唤醒,
      • source1中包含了一个mach_port和一个函数指针,mach_port可以主动唤醒runloop,
    • CFRunloopObserverRef, 是观察者,观察runloop的状态,然后发送通知给线程去处理事件,runloop有几种状态呢
      • KCFRunloopEntry 即将进入runloop
      • KCFRunloopBeforeTimers 即将处理timer
      • KCFRunloopBeforeSources 即将处理source
      • KCFRunloopBeforeWaiting 即将进入休眠
      • KCFRunloopAfterWaiting 即将从休眠中唤醒
      • KCFRunloopExit 即将退出runloop
    • runloop的运行机制原理、
1、通知observe即将进入runloop--》2、通知observe即将处理timer--> 3、处理timer--》
4、通知observe即将处理source--> 5、处理source0--> 6、处理blocks-->
7、判断是否有souce1(有的话跳转)-->8、通知observe即将进入休眠-->9、休眠等待被唤醒--》
10、通知observe即将被唤醒--》11、处理唤醒事件 然后跳到2步 --》12、通知observe即将退出

5、runloop与aureleasePool的关系

app启动后,苹果在主线程runloop里注册了两个observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。

  • 第一个observe监听runloop的即将进入状态,回调中会调用objcAutoreleasePoolPush()创建自动释放池,这个优先级最高,保证释放池的创建发生在其他所有回调之前,这样才能保证所有对象的创建都被自动释放池包裹
  • 第二个observe监听了两个事件
    • beforeWaiting事件 这时候会调用objcAutoreleasePoolPop和objcAutoreleasePoolPush,释放旧的自动释放池并创建新的自动释放池
    • exit事件,这个时候会调用objcAutoreleasePoolPop()