一、内存管理
1. 引用计数
- OC类中实现了引用计数器,对象知道自己当前被引用的次数。
- 对象初始化时计数器为1,每次操作对象都会引起相应的计数器变化。
档引用计数器为0时,给对象发送dealloc
消息销毁对象。
1.1 黄金法则
- 凡是通过
alloc
、init
、copy
、mutableCopy
、retain
进行创建的对象,都要使用Release
或Autorelease
进行释放。 - 自己生成的对象,自己持有。不是自己生成的对象,自己也能持有。
- 不再需要持有的对象时释放,非自己持有的对象不需要释放。
1.2 引用计数存放
-
isa:
- 从64bit开始,对象的引用计数存放在优化过的isa指针中,也可能存放在SideTable中。
- 当优化过的isa指针中,引用计数过大存放不下时,就会将引用计数存放到SideTable中。
-
SideTable:
- 是一个哈希表,Key为对象的指针,Value为对象内容具体存放的SideTable
- SideTable包含自旋锁,引用计数表,弱引用表,由Runtime维护。
- 查获或修改引用计数时是要加锁的,方便多个对象同时操作。
2. MRC
- MRC(Manual Reference Counting)手动内存管理
- iOS5之前,需要开发者手动去管理内存。
- 需要引用对象时,发送
retain
消息,对象的引用计数+1 - 不需要引用对象时,发送
release
消息,对象的引用计数-1 - 当引用计数为0时,自动调用对象的
dealloc
方法销毁对象,释放内存,不能再使用release
和其他方法。
3. ARC
- ARC(Automatic Reference Counting)自动内存管理
- ARC也是基于引用计数,只是编译器在编译时期自动在已有代码中插入适合的内存管理代码(
retain
、release
、copy
、autorelease
、autoreleasePool
)以及在Runtime做一些优化。 - 简单来说就是代码中自动加入了
retain
、release
,原先 MRC 中需要手动添加的用来管理引用计数的代码都由编译器帮我们完成了。
4. Strong
- 强引用,指向一个对象时,对象引用计数+1;
- 当对象没有一个强引用指向它时,他才会被释放。
- 如果在声明引用时不加修饰符,默认为
strong
。
5. Weak
- 弱引用,不会引起对象的引用计数变化;
- 当对象被释放时,所有指向它的弱引用指针会自动被置为nil,防止野指针。
- 给nil发送消息时,会直接renturn,不会调用方法,也不会crash。
weak原理:
- 对象的SideTable中有一个weak表,以对象的内存地址为key,value则是所有指向该对象的弱引用指针的数组。
- 当对象销毁时,通过对象内存地址找到所有指向它的弱引用指针,置为nil并删除。
6. Assign
- assign指针纯粹指向对象,不会引起对象的引用计数发生变化。
- 当对象被释放时,指针依然指向对象原来的内存地址,不会自动被置为nil,容易造成野指针。
- 所以一般assign都用来修饰基本数据类型如
int
、float
、struct
等值类型。 - 值类型会被放入栈中,遵循先进后出的原则,由系统负责管理栈内存。
- 引用类型会被放入堆中,需要我们自己手动管理内存(MRC)或通过 ARC 管理。
7. Copy、MutableCopy
浅拷贝:
copy出来的对象与源对象地址一致,对拷贝对象做修改会影响源对象。
深拷贝:
copy出来的对象与源对象地址不一致,开辟新的内存空间存放拷贝对象,对拷贝对象做修改不会影响源对象。
- 对于不可变类型的对象,copy为浅拷贝,对象引用计数+1。
- 对于可变类型的对象,copy为深拷贝,拷贝对象也变为不可变类型。
- 对对象做mutableCopy操作,都为深拷贝,拷贝对象也会变为可变类型。
- 对于容器类型(
NSArray
、NSDictionary
等),深拷贝也仅是拷贝容器本身,对容器里面的元素只做浅拷贝。
声明NSString类型的属性,用copy还是strong修饰更好?
考虑多态的原因,NSString
类型的属性,最终可能指向的是NSMutableString
,为了防止源字符串的修改引起变化,最好是采用copy来修饰。
如何自定义cpoy操作?
- 遵循copy协议
<NSCopying, NSMutableCopying>
- 重写
copyWithZone
、mutableCopyWithZone
方法。
8. Atomic、Nonatomic
对atomic修饰的属性的setter、getter方法添加了原子锁,保证set、get操作的完整性。也就是下一次的setter、getter操作必须等到上一次的set、get操作完成之后才能执行。
因为atomic添加了原子锁,会增加开销,运行速度更慢,在不需要保证setter、getter操作的完整的情况下,所以一般都使用nonatomic。
-
atomic不能完全保证线程安全
- 只能保证setter、getter操作的完整性,当开启多个线程执行多个setter、getter操作时,无法保证执行的顺序。
- 另外如数组除了setter、getter操作外,还有remove的操作。
9. 内存泄漏
是指申请的内存空间用完之后未释放,在ARC下根本原因是循环引用(在ViewController中没有正确的使用NStimer
、delegate
、block
)引起的。
10. 循环饮用
- 两个对象之间相互强引用,引用计数都依赖于对方,导致对象无法释放。
- 最容易产生循环引用的两种情况就是
delegate
和block
,所以才引入了弱引用。 - 持有对象,但不增加引用计数,这样就避免了循环引用的产生。
11. 内存溢出
通俗的讲就是内存不够用了,程序申请内存空间时,没有足够的内存空间可供使用。
二、Runtime
1. Runtime:
Runtime(运行时)是一个C语言的库,提供API创建类、添加方法、删除方法、交换方法等。
运行时:用户可以运行编译过的程序,程序运行的过程。
编译时:源代码被编译成机器可以识别的代码的过程。
-
OC是一门动态语言:
- OC的动态性由Runtime支持的
- 动态语言是指程序可以在运行时可以改变其结构:添加新的函数、属性,删除已有的函数、属性等结构上的变化,在运行时做类型的检查。
2. id、instanceType
-
id:
-
id
是任意类型 - 使用
id
修饰的对象是动态类型,只是简单的声明了指向对象的指针。 - 编译时不做类型检查,可以发送任何信息给
id
类型的对象。
-
-
instanceType:
- 表示某个方法返回未知类型的OC对象,非关联类型的方法返回所在类的类型。
-
instancetype
可以返回和方法所在类相同类型的对象,id
只能返回未知类型的对象。 -
instancetype
只能作为返回值,不能像id
一样作为参数。
-
关联返回类型:
- 1.类方法中,以
alloc
、new
开头 - 2.实例方法中,以
autorelease
、init
、retain
、或self
开头 - 当方法的返回值为
id
类型,方法不会返回一个类型不明的对象,会返回一个方法所在类类型的对象。
- 1.类方法中,以
-
非关联返回类型:
- 1.类方法中,不以
alloc
、new
开头 - 2.实例方法中,不以
autorelease
、init
、retain
、或self
开头 - 当方法的返回值为
id
,方法会返回一个类型不明的对象; - 可以用
instancetype
作为方法的返回值的类型,返回一个方法所在类类型的对象。
- 1.类方法中,不以
-
NSObject *:
- 声明类指向
NSObject
类型对象的指针,编译时要做类型检查。 -
NSObject
是OC中的基类,绝大多数类都继承与NSObject
- 声明类指向
-
id <NSObject>:
- 也是一个指针,要求指向的类型要实现NSObject protocol
-
NSObject
、NSProxy
类实现了NSObject接口,id<NSObject>
可以指向它们
-
OC对象的本质:
- OC对象本身是一个结构体,这个结构体只有一个isa指针
- 任何数据结构,只要在恰当的位置有个指针指向一个
class
,那么它就可以被认为是一个对象。
-
NSObject对象内存大小:
- 64bit下
bool
、signed char
、unsigned char
占1个字节; -
short
、unsigned short
占2个字节; -
int
、unsigned int
、float
占4个字节; -
long
、unsigned long
、long long
、double
占8个字节。 -
NSObject
占8个字节; - 结构体内成员按自身长度自对齐;
- 对象内存申请的时候按8字节对齐,开辟内存时按16字节对齐。
- 64bit下
3. isa(is a what?)
- 实例对象isa指向类对象;
- 类对象isa指向元类;
- 类对象
superClass
指向父类指向的类对象; - 所有元类isa指向
NSObject
对象的元类(根元类); - 根元类isa指向自己;
- 根元类的
superClass
指向NSObject
的类对象; - 元类的
superClass
指向对应父类的元类;
4. 消息发送机制
在OC中,对象调用方法其实是对象接收消息,消息的发送采用“动态绑定”的机制,具体调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码
OC对象调用方法在运行时会被转化为 void objc_msgSend(id self, SEL cmd...)
SEL:方法名
IMP:指向方法实现的函数指针
-
消息发送流程:
- 1.根据消息接收者的isa确定自己所属的类,先在类的 _x001D_cache 和 MethodLists 中从上向下查找IMP;
- 2.如果本类中没有找到,则会根据本类的superClass指针,沿着继承体系继续向上查找(向父类查找)
- 3.如果向父类查找都没有找到,则会进入「消息转发流程」
-
消息转发流程:
-
1.动态解析:
- 类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveInstanceMethod:(SEL)selector;
- 实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveIClassMethod:(SEL)selector;
- 类方法未找到时调起,可于此添加类方法实现
-
2.备用接收者:
- 消息接收者重定向(返回一个实例对象、或类)
- (id)forwardingTargetForSelector:(SEL)selector;
- 消息接收者重定向(返回一个实例对象、或类)
3.消息重定向:
- (void)forwardInvocation:(NSInvocation *)anInvocation; - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- 4.抛出异常:
最后消息未能处理的时候,还会调用- (void)doesNotRecognizeSelector:(SEL)aSelector
抛出异常,程序也就崩溃了。
-
5. Merhod Swizzing(方法交换)
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换。
先给要替换的方法的类添加一个Category,然后在Category中的
+(void)load
方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。Swizzling应该总在
+load
中执行,load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。Swizzling应该总是在
dispatch_once
中执行,避免被多次执行。-
Method Swizzing应用:
- 页面统计:交换Controller View的生命周期方法
- 事件统计、防止按钮短时间内重复点击:交换UIControl
sendAction:to:forEvent
方法 - 交换delegate的方法:hook setDelegate方法,最好加上是否实现了delegate的方法的判断
- 防止数组越界崩溃:hook objectAtIndex,需要获取到类簇的真身
6. Category、Extension
Category
在程序运行时实例方法整合到主类中、类方法整合到元类中、协议同时整合到主类和元类中在类的+laod方法中可以调用category里声明的方法吗?
可以,因为附加Category到类的工作优先于+load
方法的执行类和category的+load方法调用的顺序
先类,后category。而各个category的+load
方法按照编译的顺序执行关联对象存在哪?
所有的关联对象都由AssociationsManager管理,AssociationsManager里面由一个静态AssociationsHashMap来存储所有的关联对象的-
在category里可以添加属性吗?
- category中只能添加方法,不能添加实例变量。类的内存大小是在编译时确定的,而category是在运行时被添加的,此时再添加实例变量会破坏内存结构。
- 在category中添加属性,通过关联对象实现setter、getter方法。
-
类和category的同名方法调用的顺序
- category并不是完全替换掉主类的同名方法,只是类的方法列表中会出现名字一样的方法且category的方法会排在前面,多个category中的同名方法按编译的顺序排。runtime查找方法按照顺序,一旦找到就return
- 遍历类的方法列表,列表里最后一个同名的方法,即是原方法。
-
category与extension区别:
-
category:
- 运行时决议
- 有单独的
.h
和.m
文件 - 可以为系统类添加分类
- 看不到源码的类可以添加分类
- 只能添加方法,不能添加实例变量
-
extension
- 编译时决议
- 以声明的方式存在,寄生于主类.m文件
- 不可以为系统类添加 extension
- 没有
.m
源码的类不可以 extension - 可以添加方法,可添加实例变量,默认为
@private
-
7. 归档解档(NSCoding)
归档与解档是iOS中一种「序列化」与「反序列化」的方式。
对象要实现 序列化 需要遵循
NSCoding
协议。通过
class_copyIvarList
获得对象的属性列表通过
ivar_getName(ivar)
获取到属性的C字符串名称转成对应的OC名称
NSString *key = [NSString stringWithUTF8String:name];
利用KVC进行归档
[[corder encodeObject: [self valueForKey:key] forKey: key];
解档
id value = [coder decodeObjectForKey];
利用KVC进行赋值
[self setValue:value ForKey:key];
8. KVC、KVO
8.1 KVC
是一种可以通过key来访问类属性的机制。而不是通过调用Setter、Getter方法访问。可以在运行时动态访问和修改对象的属性。
-
setValue:ForKey:
- 按照setKey、_setKey的顺序查找方法,找到了就传递参数,调用方法
- 如果没找到,则查看accessInstanceVariableDirectly方法的返回值,如果为NO(默认是YES)就不再继续往下执行,直接调用
setValue:forUndefinedKey
抛出NSUnknownKeyException
异常 - 如果返回值为YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到了就直接赋值
- 如果没找到,则调用
setValue:forUndefinedKey
抛出异常
-
valueForKey:
- 按照getKey、_getKey的顺序查找方法,找到了就直接调用方法
- 如果没找到,则查看accessInstanceVariableDirectly方法的返回值,如果为NO(默认是YES)就不再继续往下执行,直接调用
value:forUndefinedKey
抛出NSUnknownKeyException
异常 - 如果返回值为YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到了就直接取值
- 如果没找到,则调用
value:forUndefinedKey
抛出异常
8.2 KVO
KVO(Key Value Observing):键值监听,可以监听对象某个属性值的变化
- 给对象添加监听
- 通过runtime动态创建一个子类,修改对象的isa指向子类
- 子类重写set方法,内部执行顺序
willChangeValueForKey
[super setKey]
didChangeValueForKey
- 在
didChangeValueForKey
中调用KVO的回调方法:observeValueForKeyPath:ofObject:change:context:
三、Block
1. Block原理
- block是一个结构体,通常包含两个成员变量:__block_impl、__block_desc和一个构造函数。
- block本质上也是一个OC对象,因为它内部有isa指针,block封装了函数调用以及函数调用环境的OC对象
- block实际上就是OC对于闭包的实现,闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量
2. Block类型
全局Block(NSGlobalBlock):
没有访问外部auto变量(我们平时写的局部变量,默认就有自动变量(Auto),离开作用域就销毁),访问外部static
或者全局变量还是全局Block栈Block(NSStackBlock):
访问了外部auto变量(在ARC下没有强引用指向这个block,而是直接打印出来)-
堆Block(NSMallocBlock):
- 栈block调用了copy(在ARC下访问了auto变量且有强引用指向该block或作为函数的返回值,就会自动将栈block copy到堆上)
- 全局block调用copy还是全局block,堆block调用copy引用计数+1
3. 变量捕获
局部变量为什么要捕获?
考虑作用域的问题,在block里属于跨函数来访问局部变量,所以需要捕获-
auto变量值传递,static变量指针传递
- auto变量可能会销毁,内存可能会消失,不采用指针访问;
-
static
变量一直保存在内存中,采用指针访问
-
全局变量不需要捕获,直接访问
- block里访问self会捕获self
- OC函数转成C++函数时,self和_cmd作为函数默认传的参数,是局部变量,所以要捕获。
- block里访问成员变量是先捕获self,然后通过self访问成员变量
4. __block、__weak
__block
不管是MRC、还是ARC下都可以使用,可以修饰对象,也可以修饰基本数据类型;__weak
只能在ARC下使用,只能修饰对象,不能修饰基本数据类型;__block
对象可以在block中被重新赋值,__weak
不可以解决循环引用:在ARC下使用
__weak
,在MRC下使用__block
-
修改变量:
-
__block
修饰的变量在block中保存的是变量的地址,使用__block
修饰之后的变量类似于static
变量 -
__block
不能修饰全局变量、静态变量(static
)
-
5. weakSelf
如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象;
如果block在堆空间,如果外部强引用,block内部也是强引用,如果外部弱引用,block内部也是弱引用
__weak typedof(self) weakSelf = self;
- block是controller的属性,如果内部没有使用weakSelf会造成内存泄漏
- block不是controller的属性,内部使用self不会造成内存泄漏
- 当使用类方法有block作为参数使用时,block内部使用self不会造成内存泄漏
四、多线程
1. 多线程
-
什么是多线程?
- 多线程是指实现多个线程并发执行的技术,进而提升整体处理性能。
- 同一时间,CPU 只能处理一条线程,多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换),如果 CPU 调度线程的时间足够快, 就造成了多线程并发执行的假象
进程:
当一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能。-
线程:
- 线程是进程中的一个执行单元,负责当前进程中程序的之心,一个进程至少有一个线程,一个进程中可以有多个线程;
- 对于CPU单一个核心而言,某个时刻只能执行一个线程,而CPU在多个线程之间切换的速度相对我们的感觉要快,看上去就是在同一时刻运行。
- 多线程并不能提高程序的运行速度,但能提高运行效率
- 单线程程序:若有多个任务只能一次执行
- 多线程程序:若有多个任务,可以同时执行
2. 队列(dispatch queue)
执行任务的等待队列,即用来存在任务的队列。队列是一种特殊的线性表,采用FIFO(first in first out)的原则。新的任务总是被插到队列的末尾,读取任务总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
串行队列(serial dispatch queue)
只开启一个线程,每次只能执行一个任务,一个任务执行完毕后才能执行下一个任务。并发队列(concurrent dispatch queue)
可以让多个任务并发(同时)执行,可以开启多个线程,并同时执行任务。并行队列的并发功能只能在异步下才有效主队列:是一个特殊的串行队列,添加到主队列的任务只能在主线程中执行
全局队列:也是并发队列
3. 任务
执行操作的意思,就是要在线程中执行的代码
-
同步执行(sync)
- 同步添加任务到队列中,队列在任务结束之前会一直等待,直到任务完成之后再继续执行
- 只能在当前线程中执行任务,不具备开启新线程的能力
-
异步执行(async)
- 异步添加任务到队列中,队列不会等待,可以继续执行其他任务。
- 可以在新的线程中执行任务,具备开启线程的能力,但不一定开启新线程。
4. 线程安全
在多线程中运行得到的结果与在单线程中运行得到的结果一致,即为线程安全
-
GCD信号量:
- 保持线程同步,将异步执行转换为同步执行
- 保证线程安全,为线程加锁
-
自旋锁:
- 如果资源被占用,等待的线程以死循环的方式一直处于忙等状态,一旦资源释放,立马执行
-
互斥锁:
- 如果资源被占用,等待的线程会进入休眠状态,直到等待的资源被解锁才被唤醒
-
iOS八大锁:
-
NSLock
互斥锁 -
NSCondition
互斥锁(条件锁) -
NSConditionLock
互斥锁(条件锁) -
pthread_mutex
互斥锁 -
NSRecursiveLock
递归锁 -
@synchronized
递归锁 -
OSSpinLock
自旋锁 -
dispatch_semaphore
:是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥
-
5. GCD
-
什么是GCD?
- GCD(Grand Central Dispatch)是 Apple 开发的一个多核编程的较新的解决方法
- 它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统
- 它是一个在线程池模式的基础上执行的并发任务
-
为什么要使用GCD?
- GCD 可用于多核的并行运算
- GCD 会自动利用更多的 CPU 内核(比如双核、四核)
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
-
队列(Dispatch Queue:
- 串行队列(Serial Dispatch Queue)
- 并发队列(Concurrent Dispatch Queue)
-
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。
- 同步执行(sync)
- 异步执行(async)
-
常用函数:
dispatch_barrier_async:
栅栏函数,等待前面加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中dispatch_after:
延时执行,不是在指定时间之后才开始执行任务,而是在指定时间之后将任务追加到主队列中。严格来讲,这个时间并不是绝对准确的dispatch_once:
只执行一次,常用于创建单例,在多线程的环境下,也能保证线程安全dispatch_apply:
快速迭代,可以在多线程中同时(异步)遍历dispatch_group_notify:
监听group中任务的完成状态,当所有任务都执行完后,追加任务到group中并执行dispatch_group_wait:
阻塞当前线程,等待指定的group中的任务执行完成后,才继续往下执行dispatch_group_enter:
表示一个任务追加到group中dispatch_group_leave:
表示一个任务离开groupdispatch_semaphore:
信号量dispatch_semaphore_create:
创建信号量并初始化信号总量dispatch_semaphore_signal:
信号量+1-
dispatch_semaphore_wait:
- 信号量-1
- 信号量<0时会一直阻塞所在线程,反之就可以正常执行
6. NSOperation、NSOperationQueue
NSOperation 操作(任务):
即GCD中的任务,将要在线程中执行的代码片段-
NSOperationQueue 操作队列:
- 不同于GCD的队列先进先出(FIFO)的原则。
- 对于添加到队列中的操作(任务),首先进入准备就绪的状态(就绪状态取决于操作(任务)之间的依赖关系)
- 然后就绪状态的操作(任务)的开始执行顺序由操作(任务)之间的优先级决定
没有依赖关系的操作(任务)先进入就绪状态,根据优先级决定执行顺序;当前操作(任务)依赖的操作(任务)执行完毕,当前操作(任务)进入就绪状态。
通过设置最大并发操作(任务)数来控制并发和串行,默认为 -1,不做限制,可进行并发执行;==1是串行队列,串行执行;>1是并发队列,并发执行。
被添加到队列的操作(任务),默认是异步执行的;主队列运行在主线程;自定义队列同时包含串行、并发的功能,运行在其他线程。
通过设置队列的 isSuspended 属性,可实现队列的暂停与继续的效果,正在执行的操作(任务)不受影响;可以取消队列中所有的操作(任务),也可取消单个操作(任务),只对未执行的操作(任务)有效;操作(任务)已经在执行中,系统不会强制停止这个操作(任务),只会标记 cancelled 属性为 true。
7. NSThread
NSThread
是苹果官方提供的,使用起来比PThread
更加面向对象,简单易用,可以直接操作线程对象。NSThread
的对象就代表一条线程,轻量级的线程操作,生命周期需要程序员控制,当任务执行完毕之后被释放掉。
五、RunLoop
1. 什么是RunLoop?
- 顾名思义,RunLoop就是一直运行着的循环;
- RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说touch事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;
- Runloop在没有事件处理时,会让线程进入休眠,只有在接收到事件时才会被唤醒,然后再做出相应的处理;
- 一条线程对应一个RunLoop对象,主线程的RunLoop对象由系统自动创建
2. RunLoop Mode
NSDefaultRunLoopMode:
app默认Mode,通常主线程就在该Mode下运行NSTrackingRunLoopMode:
界面跟踪mode,用于scrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响UIInitializationRunLoopMode:
在刚启动app时进入的第一个Mode,启动完成后就不再使用GSEventReceiveRunLoopMode:
接收系统事件的内部Mode,通常用不到NSRunLoopCommonModes:
这是一个占位Mode,不是一个真正的Mode,会同时处理默认模式和UI模式中的事件
3. RunLoop Source
- 定时源,基于时间的触发器,基本上就是 NSTimer;
- NSTimer 默认被加入RunLoop的
Default Mode
下; - source0:非基于Port的源,基本就是应用层事件,比如点击事件
- source1:基于Port的源,通过内核和其他线程通信,接收、分发系统事件
- 我们点击app内的某个按钮,先触摸到屏幕(硬件),屏幕表面的事件会先封装成Event传递给source1(mach_port),source1唤醒runloop,让后将Event分发给source0处理
4. RunLoop Observer
- CFRunLoopObserverRef,监听RunLoop的状态改变
- 即将进入Loop:1
- 即将处理Timer:2
- 即将进入source:4
- 即将进入休眠:3、2
- 即将从休眠中唤醒:6、4
- 即将从Loop中退出:1、2、8
5. RunLoop运行逻辑
- 1.通知观察者即将进入RunLoop。
- 2.通知观察者即将处理Timer。
- 3.通知观察者即将处理source0。
- 4.处理source0。
- 5.如果有source1,跳至第9步。
- 6通知观察者线程即将进入休眠状态。
- 7.线程进入休眠,等待直到以下任一事件发生唤醒线程。
- (1)某一事件到达基于端口的源;
- (2)定时器启动;
- (3)RunLoop设置的时间超时;
- (4)RunLoop被显式唤醒;
- 8.通知观察者线程即将被唤醒
- 9.处理唤醒时收到的事件。
- 1.如果用户定义的定时器启动,处理定时器事件并重启RunLoop,跳回步骤2;
- 2.如果输入源启动,传递相应消息;
- 3.如果RunLoop被显式唤醒且时间未超时,重启RunLoop,跳回步骤2;
- 10.通知观察者,即将退出RunLoop。
6. 如何实现常驻线程
如果经常会在子线程中做一些耗时操作(比如下载文件、后台播放音乐等),最好能让这条线程常驻内存。
- 为当前线程添加一个RunLoop
- 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环
- 启动该RunLoop
六、性能优化
1. 卡顿优化
解决卡顿的主要思路就是尽可能的减少CPU与GPU资源的消耗
1.1针对CPU
- 尽量用轻量级的对象,如:不用处理事件的UI控件可以考虑使用CALayer;
- 不要频繁的调用UIView的相关属性,如:frame、bounds、transfor等;
- 尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改;
- AutoLayout会比直接设置frame消耗更多的CPU资源;
- 图片size和UIImageView的size保持一致;
- 控制线程的最大并发数;
- 耗时操作放入子线程,如:文本的尺寸计算、绘制,图片的解码、绘制等。
1.2 针对GPU
- 尽量避免短时间内显示大量的图片
- GPU能处理的尺寸最大纹理尺寸为4096*4096,超过这个尺寸就会占用CPU资源,所以纹理不能超过这个尺寸
- 尽量减少透明视图的数量与层次
- 减少透明的视图(alpha<1),不透明就设置opaque为YES视
- 尽量避免离屏渲染,以下操作会导致离屏渲染
- 光栅化,
layer.shouldRasterize = YES
layer.mask
-
layer.maskToBounds = YES
,layer.cornerRadius > 0
- 阴影未设置
layer.shadowPath
- 光栅化,
2. 耗电优化
- 尽可能降低CPU、GPU功耗;
- 少用定时器;
- 定位优化:
- 如果只是需要快速确定用户位置,用
CLLocationManager
的requestLocation方法定位,定位硬件会自动断电; - 若不是导航应用,尽量不要实时更新位置,定位结束就关掉定位服务尽量降低定位精度,如不使用精度最高的KCLLocationAccuracyBest;
- 需要后台定位时,尽量设置pauseLocationUpdatesAutomatically为YES,若用户不怎么移动的时候,系统会自动暂停位置更新。
- 如果只是需要快速确定用户位置,用
3. 启动优化
3.1 热启动
app进程还在系统中,无需开启新进程的启动过程
3.2 冷启动
- app不在系统进程中,用户再点击启动app的过程,这时需要创建一个新进程分配给app;
- app启动最佳速度是400ms内,因为从点击app图标启动,然后Launch Screen出现再消失的时间就是400ms;
- app启动最慢不能>20s,否则app进程会被系统杀死;
- 冷启动的整个过程是指从用户唤起app开始到
AppDelegate
中的;didFinishLaunchWithOptions
方法执行完毕,并以执行main()
函数的时机为分界点,分为pre-main和main()阶段。
3.3 Mach-O
-
Mach-O文件:
- Mach Object File Format:是一种用于记录可执行文件、共享库、动态加载代码和内存转储的文件格式。是OSX和iOS上主要的可执行文件格式,类似于Windows系统上的exe。
- app编译生成的二进制可执行文件就是Mach-O格式的,iOS工程所有的类编译后会生成对应的目标文件.o文件,而这个可执行文件就是这些.o文件的集合。
.ipa(iPhone Application):
实际上只是一种变相的zip压缩包;dylib:
动态链接库,是运行时加载的,可以被多个app进程共用。分为系统dylib和内嵌dylib(即开发者手动引入的动态库);dyld:
动态链接器,一个专门用来加载dylib的库;dyld shared cache:
动态库共享缓存,当需要加载的动态库非常多时,相互依赖的符号也更多了,为了节省解析处理符号的时间,OSX和iOS上动态链接器使用了共享缓存;images:
镜像,每个app都是以images为单位进行加载的;executable:
应用的二进制可执行文件;bundle:
资源文件,属于不能被链接的dylib,只能在运行时通过dlopen()加载;framework:
可以是动态库,也可以是静态库,是一个包含dylib、bundle和头文件的文件夹。