OC底层原理三十七:内存管理(autorelease & runloop)

OC底层原理 学习大纲

  • 👉 上一节 ,详细介绍了weakstrong强引用解决方案。本节,我们将介绍:
  1. autorelease自动释放池
  2. runloop

准备工作:


1.autorelease自动释放池

  • autorelease自动释放池: 自动管理作用域内对象引用计数池子

面试题1:临时变量什么时候释放
面试题2:简述自动释放池原理
面试题3:自动释放池能否嵌套使用?

1.1 初探autorelease

  • APP的入口函数main,包含了@autoreleasepool:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}
  • 使用clangmain.m编译后输出main.cpp,在cpp文件中,可以看到:
    image.png
image.png
  1. @autoreleasepool编译期转化为了__AtAutoreleasePool结构体。
  2. __AtAutoreleasePool构造函数创建了自动释放池对象
  3. __AtAutoreleasePool析构函数释放了自动释放池对象
  • 仿__AtAutoreleasePool实现一个构造析构函数,观察生命周期:
    image.png

利用结构体构造析构函数,有效的匹配作用域

1.2 源码分析

  • 定位源码: (libobjc库)
  • main.m文件的@autoreleasepool处加上断点,打开汇编模式运行代码:

    image.png

  • 加入objc_autoreleasePoolPush符号断点,运行代码,发现源码libobjc

    image.png

  • 打开objc4源码,搜索objc_autoreleasePoolPush
    image.png
1.2.1 自动释放池结构
image.png
image.png
1.2.2 push自动释放池
image.png
1.2.3 pop自动释放池
image.png

1.3 代码验证:

必须在MRC环境下,才可以使用autorelease

image.png

#import <objc/runtime.h>
#import <malloc/malloc.h>

// 声明外部实现
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        for (int i = 0 ; i<505; i++) {
            NSObject * objc = [[NSObject alloc] autorelease];
        }
        // 打印自动释放池的结构信息
        _objc_autoreleasePoolPrint();
    }
    return 0;
}
  • 打印结果:


    image.png

    image.png
  • 数据较多,只截取了第一页开头第二页数据

1.4 autorelease的嵌套

#import <objc/runtime.h>
#import <malloc/malloc.h>

// 声明外部实现
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        for (int i = 0 ; i<5; i++) {
            NSObject * objc = [[NSObject alloc] autorelease];
        }
        
        @autoreleasepool {
            for (int i = 0 ; i<3; i++) {
                NSObject * objc = [[NSObject alloc] autorelease];
            }
            // 打印自动释放池的结构信息
            _objc_autoreleasePoolPrint();
            printf("\n-----------------------------\n");
        }
        // 打印自动释放池的结构信息
        _objc_autoreleasePoolPrint();
    }
    return 0;
}
image.png
  • autoRelease嵌套,并没有结构上进行嵌套。而是利用哨兵作用,直接多插入一个哨兵
  • 因为每次遇到哨兵(pop出栈时),都表示一个autorelease释放

2. runloop

【前言】
关于runloop,我看了一些资料,越看越把我看晕。 停下来稍微理一下
(ps:runloop版本号是我虚构的,辅助理解

  • 【使命】runloop官方文档是在thread线程板块中,他只是线程的一个辅助方式

    很简单的理解:一个线程,我们创建后,执行任务,它就释放了。 那每次使用,我都这样创建->执行->释放,岂不是很麻烦

    我也不知道我啥时候会用到,我就希望有一个线程,在我需要在这个线程执行任务时候直接把任务丢过去就可以了。很开心,runloop满足你。

  • 【实现】runloop,顾名思义,就是一个(run)运行(loop)循环。它的作用上面说了,就是让线程一直保持可用状态(保活),如何保持一直在线?

    第一想法是,给个do-while(1)循环,循环内可以接受外部函数,我们每次要执行任务时,给他一个函数就可以了。聪明!runloop1.0版本已经开发了👍。

  • 【小结】runloop本身就是一个函数函数内创建do-while循环,一直持有当前线程(在哪个线程调用它,它就一直持有着哪个线程)。

  • 【优化】按照上面使用do-while(1)循环,我们会发现cpu激增,因为它一直运行。那有人就有想法了,能不能我需要时候它就运行,我不用时候,它就休息不要占用我的cpu资源。我真不想要这个线程的时候,线程循环都给我销毁。可以不可以呀?

    要求挺多呀,但挺合理的。😂 于是runloop2.0版本满足你😃。

    【第一个要求:支持销毁
    runloopdo-while加上条件,你不要了就把这条件设置不满足就OK啦。

    【第二个要求:支持休眠唤醒

    首先,runloop得知道它到底还有没有干完的活。怎么界定呢? 简单,runloop列个业务清单,打今儿起,我就只接几种业务做完了会回调你。(像不像去抽血,1,2,3号抽血,请在4,5,6窗口等结果。血液检测完了,你就可以拿到结果了 😂)。 如果所有业务都处理完了,就进入休眠状态(没活了可以玩会手机,休息下)

    啥时候唤醒?怎么唤醒呢?

    runloop直接使用系统内核mach port的消息机制mach_msg(),当接收业务时,系统会(通过source1)直接唤醒runloop,去执行现在当前接收到的任务
    每一次循环,都会查询活有没有干完有没有其他活在排队没有了就休息收到系统消息就接着干活

总之,目前我就是这么理解runloop的,总结一下:

  • 作用:为线程保活(所以一个线程一个runloop一一对应)
  • 实现:线程内的一个函数,弄个do-while循环让这个线程一直在线。可以处理几类事务回调处理结果。支持休眠被唤醒,也支持销毁

相关链接:
👉 RunLoop 官方文档
👉 逻辑教育kody老师的公开课
👉 ibireme大神的Runloop分析
👉 RunLoop 源码阅读

  • 至此,我想你内心runloop已经有了一个大体认知
    (一开始就一头扎进源码的我,可没这么幸运😂)

  • 现在,我们来正式了解runloop

2.1 runloop是什么

runloop:

    1. 使用一个循环,保持程序持续运行
    1. 一个线程对应一个runloop,负责处理APP各种事件(触摸、定时器、performSelector)
    1. 节省cpu资源。(无任务自动休眠被唤醒继续工作)
  • 经典runloop流程图:


    image.png

2.1.1 runloop的循环

  • 简单循环案例,会占用cpu

    image.png

  • runloop循环闲时不会占用cpu
    (app启动就会启动主线程主线程内就维持一个runloop,一直给程序保活。)

    image.png

  • 我们下载runloop源码,将CFRunLoop.cCFRunLoop.h文件拖入demo文件夹,搜索void CFRunloopRun:

    image.png

  • 发现runloop[do-while]循环在stopfinished时,会结束

ps: 验证runloop1.02.0过渡😃

2.2 runloop线程保活

上面我们说了,一个runloop对应一个线程作用就是线程保活

2.2.1 原始线程(用完即销毁):
//MARK: - HTThread
// 继承NSThread,为了打印dealloc - 线程释放
@interface HTThread : NSThread
@end
@implementation HTThread
-(void)dealloc{ NSLog(@"%@ %s",[HTThread currentThread].name,__func__); }
@end

//MARK: - ViewController
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 调用完,直接销毁
    HTThread * thread = [[HTThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    thread.name = @"ht_Thread";
    [thread start];
}

- (void)threadTest {
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);
}
@end
image.png
  • 可以看到,名为ht_thread的线程,执行完任务(threadTest函数)后,就被销毁了。
2.2.2 runloop线程保活:
@interface HTThread : NSThread
@end
@implementation HTThread
-(void)dealloc{ NSLog(@"%@ %s",[HTThread currentThread].name,__func__); }
@end

//MARK: -ViewController
@interface ViewController ()
@property(nonatomic, strong) HTThread * thread;
@property(nonatomic, strong) NSRunLoop * runloop; // 常驻线程

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self createThread];
}

- (void)createThread {
    _thread = [[HTThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    _thread.name = @"ht_Thread";
    [_thread start];
}

- (void)threadTest {
    
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);

    // @autoreleasepool 对子线程中的临时变量做优化管理。更高效利用空间
    @autoreleasepool {
        // 使用runloop对当前线程保活(当前`threadTest`函数是在`ht_Thread`线程内执行)
        _runloop = [NSRunLoop currentRunLoop];
        [_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 处理主线程,其他线程都需要手动开启runloop (run内绑定了线程与runloop的关系)
        [_runloop run];
    }
    
    // runloop没被释放,就到不来这一行。threadTest函数也一直不会结束
    NSLog(@"runloop释放了 %@ %s",[NSThread currentThread].name, __func__);
    
}

- (void)threadTask {
    NSLog(@"%@ %s",[NSThread currentThread].name, __func__);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 点击,让_thread线程执行任务(`threadTask`函数)
    // 如果疑惑_thread是被self强持有,本身就可执行的话。 可手动注释`@autoreleasepool`内部代码。再点击检验。会发现崩溃了。
    // 因为虽然_thread在,但是没法让它为你工作,runloop是可以帮你管理_thread并让它为你工作的。
    [self performSelector:@selector(threadTask) onThread:_thread withObject:nil waitUntilDone:YES];
}

@end
  • runloop成功的实现线程保活。(我想用的时候,都可以用)
    image.png

2.3 runloop的读取

  • runloop的读取,支持2种方式:
    主线程获取(CFRunLoopGetMain) 和 当前线程获取(CFRunLoopGetCurre)。内部调用_CFRunLoopGet0函数。
    image.png

总结:

  1. 主线程是static全局唯一的,第一次获取创建
  2. 线程不存在,默认使用主线程,并返回主线程的runloop
  3. 首次访问,会创建全局唯一__CFRunLoops字典key线程valuerunloop
    (线程runloop一一对应)
  4. 每次优先__CFRunLoops字典中,通过key(线程),获取value(runloop)。
  5. 如果runloop 不存在,就创建线程对应的runloop,并更新__CFRunLoops字典对应值
  6. 更新TSD(线程私有存储),记录runloop
  7. 返回runloop

面试题: runLoop与线程的关系
一一对应关系。由全局Runloop字典进行记录,其中key线程valuerunloop

2.4 runloop的创建

image.png

总结:

  1. __CFRunLoop为模板,创建Runloop结构体对象
  2. 属性初始化赋值
  3. Mode的获取:
    • 如果通过__kCFRunLoopModeTypeID读取到Modes,并且 Modes中存在kCFRunLoopDefaultMode,就直接返回找到的Mode
    • 否则,创建一个Mode,加入modes中。返回Mode

拓展:

  1. runLoop本质是__CFRunLoop格式的结构体

    记录线程锁port唤醒端口所在线程所有标记为Common的Mode加入CommonMode的item事务当前Mode所有Mode

image.png
  1. 理解commonModesmodes:
    image.png

2.5 runloop的运行原理

image.png
  • 看完源码后,runloop运行周期唤醒方式十分清晰了。现在奉上经典Runloop流程图
    image.png

补充说明:

  1. 【最外层流程】
    kCFRunLoopEntry进入循环 (发通知)
    -> __CFRunLoopRun 运行循环
    -> kCFRunLoopExit退出循环(发通知)

  2. 【循环内部】
    kCFRunLoopBeforeTimers即将处理Timer (发通知)
    -> kCFRunLoopBeforeSources 即将处理Sources0 (发通知)
    __CFRunLoopDoBlocks 处理Blocks)
    -> __CFRunLoopDoSources0 处理Sources0
    __CFRunLoopDoBlocks 处理Blocks)
    -> __CFRunLoopServiceMachPort : 监听Port端口消息(source1),有消息就跳转handle_msg
    -> kCFRunLoopBeforeWaiting: 将进入休眠 (发通知)
    进入休眠,等待唤醒 (内部的Timer到期、gcd都可唤醒)
    -> 线程被唤醒, (发通知)

3.【handle_msg】处理消息:

  • 被Timers唤醒(CFRUNLOOP_WAKEUP_FOR_TIMER): __CFRunLoopDoTimers (发通知)
  • 被gcd唤醒(CFRUNLOOP_WAKEUP_FOR_DISPATCH):__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ (发通知)
  • 被source唤醒(CFRUNLOOP_WAKEUP_FOR_SOURCE): __CFRunLoopDoSource1
    __CFRunLoopDoBlocks 处理Blocks)
  • 检查stopfinish

Timer、dispatch、source等回调函数:

// main  dispatch queue
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

// __CFRunLoopDoObservers
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

// __CFRunLoopDoBlocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

// __CFRunLoopDoSources0
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

// __CFRunLoopDoSource1
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

// __CFRunLoopDoTimers
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

在执行回调Block前,我们可以在堆栈中看到上述回调函数

  • 回调函数检验:
    (每次触发TouchBegin时,所在线程runloop都会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数)
    image.png

至此,完成了runloop基础探索

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

推荐阅读更多精彩内容