设计模式
- 设计模式
设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的事情。
1). MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。
2). MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。
3). 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。
4). 观察者模式:KVO是典型的通知模式,观察某个属性的状态,状态发生变化时通知观察者。
5). 委托模式:代理+协议的组合。实现1对1的反向传值操作。
6). 工厂模式:通过一个类方法,批量的根据已有模板生产对象。
- MVC 和 MVVM 的区别
MVC是一种架构模式,M表示Model,V表示视图View,C表示控制器Controller:
Model负责存储、定义、操作数据;
View用来展示给用户,并且和用户进行交互;
Controller是Model和View的协调者,Controller把Model中的数据拿过来给View使用。Controller可以直接与Model和View进行通信,而View不能与Controller直接通信。,当有数据更新时,Model也要与Controller进行通信,这个时候就要用Notification和KVO,这个方式就像发广播一样,Model发信号,Controller设置接收监听信号,当有数据更新是就发信号给Controller,Model和View不能直接通信,这样违背MVC设计原则。View与Controller通信需要利用代理协议的方式,Controller可以直接根据Model决定View的展示。View如果接受响应事件则通过delegate,target-action,block等方式告诉Controller的状态变化。Controller进行业务的处理,然后再控制View的展示。
那这样Model和View就是相互独立的。View只负责页面的展示,Model只是数据的存储,那么也就达到了解耦和重用的目的。
MVVM就是帮忙分担一下controller里面的部分业务逻辑。
这个时候,controller将不再直接和真实的model进行绑定了,而通过ViewModel,viewModel进而持有真实的Model。
使用MVVM你会发现VC里面已经省去了不少的代码。一切都和viewModel进行交流。这里我只是展示一个最简单的数据展示,如果有其他响应事件,是需要viewModel开放方法来进行处理的,并要通知VC处理结果的。
关于MVVM的优点:
1.方便测试
在MVC下,Controller基本是无法测试的,里面混杂了个各种逻辑,而且分散在不同的地方。有了MVVM我们就可以测试里面的viewModel,来验证我们的处理结果对不对(Xcode7的测试已经越来越完善了)。
2.便于代码的移植
比如iOS里面有iPhone版本和iPad版本,除了交互展示不一样外,业务逻辑的model是一致的。这样,我们就可以以很小的代价去开发另一个app。
3.兼容MVC
MVVM是MVC的一个升级版,目前的MVC也可以很快的转换到MVVM这个模式。VC可以省去一大部分展示逻辑。
缺点:
1.类会增多,每个VC都附带一个viewModel,类的数量*2
2.viewModel会越来越庞大
我们把逻辑给了viewModel,那势必Model也会变得很复杂,里面的属性和方法越来越多。可能重写的方法比较多,因为涉及到一些数据的转换以及和controller之间的通信。
3.调用复杂度增加
由于数据都是从viewModel来,想想突然来了一个新人,一看代码,不知道真实的模型是谁。比如常用tableview的数据源,一般都是一个数组,如果不断的通过viewModel去取,沟通上没有那么直接。况且每封一层,意味着要写很多代码去融合他们的转换。
基础语法
import跟 #include 有什么区别,@class呢,#import<> 跟 #import””有什么区别?
(1)#import指令是Object-C针对@include的改进版本,能确保引用的文件只会被引用一次,不会陷入递归包含的问题中;
(2)@import与@class的区别:#import会链入该头文件的全部信息,包括实体变量和方法等;二@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类如何定义的,暂时不用考虑。在头文件中,一般只需要知道被引用的类的名称就可以了,不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称;而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import类包含这个被引用类的头文件。@class还可以解决循环包含的问题
(3)import<>跟#import""的区别:
import<>用来包含系统自带的文件,#import""用来包含自定义的文件属性readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?
strong和copy区别 https://www.jianshu.com/p/07cff6feace6
readwrite:是可读可写特性,同时生成get方法和set方法的声明和实现(补充:默认属性,将生成不带额外参数的getter和setter方法(setterff只有一个参数))
readonly:只读特性,只会生成get方法的声明和实现;不希望属性在类外改变
assign:是赋值特性,set方法的实现是直接赋值,用于基本数据类型;仅设置变量时
retain:表示持有特性,set方法将传入参数先保留,再赋值,传入参数的retaincount会+1;
copy:表示拷贝特性,set方法的实现是release旧值,copy新值,用于NSString、block等类型(set方法将传入的对象复制一份;需要完全一份新的变量时使用);
nonatomic:非原子操作,决定编译器生成的setter getter是否是原子操作,atomic表示多线程安全,如果写atomic这个时候生成的setter方法的代码就会被加上一把线程安全锁.一般使用nonatomic,代码中维护锁week详解
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。Objective-C的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方式用继承好还是分类好?为什么?
答:Objective-C的类不可以多重继承;可以实现多个接口(协议);Category是类别;一般情况用分类好,用Category去重写类的方法,仅对本Category有效,不会影响到其他类与原有类的关系。@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的
@property 的本质是什么?
@property = ivar + getter + setter;
“属性” (property)有两大概念:ivar(实例变量)、getter+setter(存取方法)
“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。Category(类别)、 Extension(扩展)和继承的区别
分类有名字,类扩展没有分类名字,是一种特殊的分类。
分类只能扩展方法(属性仅仅是声明,并没真正实现),类扩展可以扩展属性、成员变量和方法。
继承可以增加,修改或者删除方法,并且可以增加属性。
*Extension是Category的一个特例,没有分类名字,可以扩展属性,成员变量和方法。常用的扩展是在.m文件中声明私有属性和方法,基本上我们天天都在用。KVC的底层实现?
当一个对象调用setValue方法时,方法内部会做以下操作:
1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。ViewController生命周期
按照执行顺序排列:
initWithCoder:通过nib文件初始化时触发。
awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
loadView:开始加载视图控制器自带的view。
viewDidLoad:视图控制器的view被加载完成。
viewWillAppear:视图控制器的view将要显示在window上。
updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
viewDidAppear:视图控制器的view已经展示到window上。
viewWillDisappear:视图控制器的view将要从window上消失。
viewDidDisappear:视图控制器的view已经从window上消失。OC中的反射机制?简单聊一下概念和使用
1). class反射
通过类名的字符串形式实例化对象。
Class class = NSClassFromString(@"student");
Student *stu = [[class alloc] init];
将类名变为字符串。
Class class =[Student class];
NSString *className = NSStringFromClass(class);
2). SEL的反射
通过方法的字符串形式实例化方法。
SEL selector = NSSelectorFromString(@"setName");
[stu performSelector:selector withObject:@"Mike"];
将方法变成字符串。
NSStringFromSelector(@selector*(setName:));
调用方法有两种方式:
利用performSelector 和NSInvocation来调用
相同点:父类都是NSObject不同点:performSelector最多传两个参数,使用比较简单
内存泄漏产生的原因一般是三种情况:
分配完内存之后忘了回收;
程序Code有问题,造成没有办法回收;
某些API函数操作不正确,造成内存泄漏。内存中的五大区域及其垃圾回收
栈: 局部变量. 当局部变量的作用域被执行完毕之后,这个局部变量就会被系统立即回收.
堆: OC对象.使用C函数申请的空间.
BSS段: 未初始化的全局变量、静态变量. 一旦初始化就回收 并转存到数据段之中.
数据段: 已经初始化的全局变量、静态变量. 直到程序结束的时候才会被回收.
代码段: 代码. 程序结束的时候,系统会自动回收存储在代码段中的数据.
栈、BSS段、数据段、代码段存储在它们中的数据的回收,是由系统自动完成的.不需要我们干预.懒加载?
懒加载就是只在用到的时候才去初始化。也可以理解成延时加载。
我觉得最好也最简单的一个例子就是tableView中图片的加载显示了, 一个延时加载, 避免内存过高,一个异步加载,避免线程堵塞提高用户体验。谓词?
谓词就是通过NSPredicate给定的逻辑条件作为约束条件,完成对数据的筛选。
//定义谓词对象,谓词对象中包含了过滤条件(过滤条件比较多)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];
isa指针问题
isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。
深入浅出Cocoa之类与对象https://blog.csdn.net/kesalin/article/details/7211228block的注意点
1). 在block内部使用外部指针且会造成循环引用情况下,需要用__week修饰外部指针:
__weak typeof(self) weakSelf = self;
2). 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下。
__strong typeof(self) strongSelf = weakSelf;
3). 如果需要在block内部改变外部栈区变量的话,需要在用__block修饰外部变量。BAD_ACCESS在什么情况下出现?
这种问题在开发时经常遇到。原因是访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。Instruments里面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
3). Allocations:用来检查内存,写算法的那批人也用这个来检查。(还没用过)
4). Leaks:检查内存,看是否有内存泄露。GCD 和 NSOperation 都是用于实现多线程:
GCD 基于C语言的底层API,GCD主要与block结合使用,代码简洁高效。
NSOperation 属于Objective-C类,是基于GCD更高一层的封装。复杂任务一般用NSOperation实现。GCD group
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
// 当并发队列组中的任务执行完毕后才会执行这里的代码
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
- GCD 栅栏dispatch_barrier_async
函数定义:dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
作用:
1.在它前面的任务执行结束后它才执行,它后面的任务要等它执行完成后才会开始执行。
2.避免数据竞争
// 1.创建并发队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
// 2.向队列中添加任务
dispatch_async(queue, ^{ // 1.2是并行的
NSLog(@"任务1, %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务2, %@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"任务 barrier, %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{ // 这两个是同时执行的
NSLog(@"任务3, %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务4, %@",[NSThread currentThread]);
});
// 输出结果: 任务1 任务2 ——》 任务 barrier ——》任务3 任务4
// 其中的任务1与任务2,任务3与任务4 由于是并行处理先后顺序不定。
- OC中创建线程的方法是什么?切换到主线程中执行代码?
// 创建线程的方法
-[NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]
-[self performSelectorInBackground:nil withObject:nil];
-[[NSThread alloc] initWithTarget:nil selector:nil object:nil];
-dispatch_async(dispatch_get_global_queue(0, 0), ^{});
-[[NSOperationQueue new] addOperation:nil];
// 主线程中执行代码的方法
-[self performSelectorOnMainThread:nil withObject:nil waitUntilDone:YES];
-dispatch_async(dispatch_get_main_queue(), ^{});
-[[NSOperationQueue mainQueue] addOperation:nil];
Runtime
OC是动态语言,每个方法在运行时会被动态转为消息发送
Runtime 是OC的底层实现,是OC的幕后执行者Method Swizzle
1). 在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法 Method Swizzle。
2). Method Swizzle 指的是改变一个已存在的选择器对应的实现的过程。OC中方法的调用能够在运行时通过改变,通过改变类的调度表中选择器到最终函数间的映射关系。
3). 在OC中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用OC的动态特性,可以实现在运行时偷换selector对应的方法实现。
4). 每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的方法实现。
5). 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP。
6). 我们可以利用 class_replaceMethod 来修改类。
7). 我们可以利用 method_setImplementation 来直接设置某个方法的IMP。
8). 归根结底,都是偷换了selector的IMP。_objc_msgForward 函数是做什么的,直接调用它将会发生什么?
_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。-
消息转发
通信底层原理(OSI七层模型)
OSI采用了分层的结构化技术,共分七层:
物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。XMPP
XMPP是一种以XML为基础的开放式实时通信协议。
简单的说,XMPP就是一种协议,一种规定。就是说,在网络上传东西,XMM就是规定你上传大小的格式。tableView的重用机制?
UITableView 通过重用单元格来达到节省内存的目的: 通过为每个单元格指定一个重用标识符,即指定了单元格的种类,当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队列中,等待被重用,当有新单元格从屏幕外滑入屏幕内时,从重用队列中找看有没有可以重用的单元格,如果有,就拿过来用,如果没有就创建一个来使用。UIView不规则点击事件
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
//返回一个view来响应事件 (我们如果不想影响系统的事件传递链,在这个方法内,最好调用父类的这个方法)
- (nullableUIView *)hitTest:(CGPoint)point withEvent:(nullableUIEvent *)event;
//返回的值可以用来判断是否继续遍历子视图(返回的根据是触摸的point是否在view的frame范围内)
- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent *)event;
//重写该方法后可以让超出父视图范围的子视图响应事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
if (view == nil) {
for (UIView *subView in self.subviews) {
CGPoint tp = [subView convertPoint:point fromView:self];
if (CGRectContainsPoint(subView.bounds, tp)) {
view = subView;
}
}
}
return view;
}
-
响应链
如何实行cell的动态的行高
如果希望每条数据显示自身的行高,必须设置两个属性,1.预估行高,2.自定义行高。
设置预估行高 tableView.estimatedRowHeight = 200。
设置定义行高 tableView.estimatedRowHeight = UITableViewAutomaticDimension。
如果要让自定义行高有效,必须让容器视图有一个自下而上的约束。main()之前的过程有哪些?
1)dyld 开始将程序二进制文件初始化
2)交由ImageLoader 读取 image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP)
3)由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4)runtime 接手后调用map_images做解析和处理
5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法
6)至此 所有的信息都被加载到内存中
7)最后dyld调用真正的main函数锁
性能从高到低排序
1、os_unfair_lock
2、OSSpinLock
3、dispatch_semaphore
4、pthread_mutex
5、dispatch_queue(DISPATCH_QUEUE_SERIAL)
6、NSLock
7、NSCondition
8、pthread_mutex(recursive)
9、NSRecursiveLock
10、NSConditionLock
11、@synchronized
详解:https://juejin.im/post/5bf21d935188251d9e0c2937iOS开发中静态库和动态库区别:
静态库和动态库是相对编译期和运行期的:
静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
- 静态库 好处:
模块化,分工合作,提高了代码的复用及核心技术的保密程度
避免少量改动经常导致大量的重复编译连接
也可以重用,注意不是共享使用 - 动态库 好处:
使用动态库,可以将最终可执行文件体积缩小,将整个应用程序分模块,团队合作,进行分工,影响比较小
使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
应用插件化
软件版本实时模块升级
- IOS程序启动过程
系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。 - 动态链接库的加载步骤具体分为5步:
1.load dylibs image 读取库镜像文件
2.Rebase image
3.Bind image
4.Objc setup
5.initializers
详解:
- step 1. load dylibs image
在每个动态库的加载过程中, dyld需要:
1.分析所依赖的动态库
2.找到动态库的mach-o文件
3.打开文件
4.验证文件
5.在系统核心注册文件签名
6.对动态库的每一个segment调用mmap()
通常的,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载。
针对这一步骤的优化有:
1.减少非系统库的依赖
2.合并非系统库
3.使用静态资源,比如把代码加入主程序 - step 2.rebase/bind
由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。
rebase修复的是指向当前镜像内部的资源指针;
而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。
bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。
//通过命令行可以查看相关的资源指针:
xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp
优化该阶段的关键在于减少__DATA segment中的指针数量。
可以优化的点有:
1.减少Objc类数量, 减少selector数量
2.减少C++虚函数数量
3.转而使用swift stuct(其实本质上就是为了减少符号的数量)
step 3.Objc setup
这一步主要工作是:
1.注册Objc类 (class registration)
2.把category的定义插入方法列表 (category registration)
3.保证每一个selector唯一 (selctor uniquing)
4.由于之前2步骤的优化,这一步实际上没有什么可做的。step 4.initializers
以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有:
1.Objc的+load()函数
2.C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
3.非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
Objc的load函数和C++的静态构造函数采用由底向上的方式执行,来保证每个执行的方法,都可以找到所依赖的动态库。
1).dyld 开始将程序二进制文件初始化
2).交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
3).由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
4).runtime 接手后调用 mapimages 做解析和处理,接下来 loadimages 中调用 callloadmethods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法至此
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)。
整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存, 动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
https://www.jianshu.com/p/7096478ccbe7Autoreleasepool
根据苹果官方文档中对 NSAutoreleasePool 的描述,我们可知,在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。我们上面提到的场景 1 中创建的 autoreleased 对象就是被系统添加到了这个自动创建的 autoreleasepool 中,并在这个 autoreleasepool 被 drain 时得到释放。详解
持续化CI
- fastlane
- jenkins
组件化
架构相关
第三方
未完待续.....