1. 怎样减少app启动时间。
1.1 pre-main之前的优化
- 动态库dylib loading:这一阶段 dyld 会分析应用依赖的 dylib,依赖的 dylib 越少越好。优化就是检查是否存在不需要的 dylib,移除不必要的 dylib 。将几个动态库合成为一个动态库,减少动态库数量
- rebase/binding:这一阶段系统主要注册Objc类。指针数量越少越好。定期做项目瘦身,之前的文章有记录方案。juejin.cn/post/684490…
- Objc srtup:这一阶段没有什么特别能优化的地方,如果 rebase/binding 阶段优化的好这步耗时也会很少。
- initializer:这一阶段,dyld 开始运行程序的初始化函数,调用每个 Objc 类和分类的 +load 方法,调用 C/C++ 中的构造器函数。initializer阶段执行完后,dyld 开始调用 main() 函数。在这一步,修改代码中直接在 +load 函数初始化逻辑改为在 +initialize 中。
1.2 main函数之后的优化
- didFinishLaunchingWithOptions:将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 didFinishLaunchingWithOptions 里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。数据处理业务根据实际情况考虑可以放在异步执行。
- 首页渲染:viewWillAppear,网络加载方面。
- 闪屏:定时器和更新UI方面。
2. 为什么会离屏渲染。
2.1 屏幕显示的原理
为了让显示器的显示跟视频控制器同步,当电子枪新扫描一行的时候,准备扫描的时发送一个同步信号(Sync信号),显示器的刷新频率就是Sync信号产生的频率。然后CPU计算好frame等属性,将计算好的内容交给GPU去渲染,GPU渲染好之后就会放入帧缓冲区。然后视频控制器会按照HSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,就显示出来了。
2.2 GPU渲染的两种方式
- On-Screen Rendering (当前屏幕渲染) : 指的是GPU渲染操作是在当前用于显示的屏幕缓冲区进行。
- Off-Screen Rendering (离屏渲染):指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。
当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。但是受当前屏幕渲染的局限因素限制(只有自身上下文、屏幕缓存有限等),当前屏幕渲染有些情况下的渲染解决不了的,就使用到离屏渲染。
2.3 离屏渲染的代价
- 创建新的缓冲区
- 过程需要多次切换上下文:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
- 由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
- 离屏渲染可能产生掉帧,但掉帧问题不在于离屏渲染这个机制,而在离屏渲染的时候, 大量的使用cornerRadius等耗性能的属性会导致计算量过大,导致无法在指定时间内完成绘制,无法提交当前帧,造成卡顿。shapeLayer+贝塞尔曲线方式性能更好更快的完成绘制,就不会出现掉帧了
- 有些效果被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成。图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。
2.4 引起离屏渲染的操作
- 为图层设置遮罩(layer.mask)
- 将图层的layer.masksToBounds / view.clipsToBounds属性设置为true
- 将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
- 为图层设置阴影(layer.shadow *)。
- 为图层设置layer.shouldRasterize=true
- 具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层
- 文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
- 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
2.5 离屏渲染优化
- 圆角优化:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角。使用CAShapeLayer和UIBezierPath设置圆角。
- shadow优化:如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。光栅化,关于光栅化会触发离屏渲染,上面也有所离屏渲染会带来很多开销,但是对于cell图层比较多,合成起来比较费时,并且频繁重用的view来说,进行离屏渲染是很有帮助的。
3. 程序应用中锁的种类
- 互斥锁:保证在任何时候,都只有一个线程访问对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
- 原理:sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
- 自旋锁:与互斥锁有点类似,自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环尝试,直到该自旋锁的保持者已经释放了锁;
- 优点:因为不会引起调用者睡眠,所以效率高于互斥锁;
- 缺点:调用者在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时间内获得锁,会使CPU效率降低。所以自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。而且递归调用时有可能造成死锁。
- 原理:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
- 递归锁:特殊的互斥锁,加了递归功能。
4.iOS中的锁
- @synchronized:是对互斥锁的一种封装,具体点是种特殊的互斥锁->递归锁,内部搭配 nil防止死锁,通过表的结构存要锁的对象,表内部的对象又是通过哈希存储的。
- 在大量线程异步同时操作同一个对象时,因为递归锁会不停的alloc/release,在某一个对象会是nil;而此时 @synchronized (obj) 会判断obj==nil,就不会再加锁,导致线程访问冲突。
- NSLock:NSLock可以解决在大量线程异步同时操作同一个对象的内存安全问题。NSLock是对pthread_mutex的封装;NSLock还有timeout超时控制。
- 当NSLock对同一个线程锁两次,就会造成死锁;即不能实现递归锁。
- NSRecursiveLock:可以处理NSLock不能实现递归锁的问题。也是对pthread_mutex的封装,不同的是加Recursive递归调用功能;也有timeout超时控制。
- NSCondition:也是对pthread_mutex的封装;使用wait信号可以让当前线程处于等待中;使用signal信号可以告诉其他某一个线程不用再等待了,可以继续执行;内部还有一个broadcast(广播)信号,用于发送(signal)信号给其他所有线程。
- NSConditionLock:类似于信号量;对NSCondition+线程数的封装,即NSConditionLock = NSCondition + lock;搭配NSCondition可以达到dispatch_semaphore效果。
- dispatch_semaphore:是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。
dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
-
dispatch_semaphore: 和NSConditionLock类似,都是一种基于信号的同步方式,但NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而dispatch_semaphore能保存发送的信号。dispatch_semaphore的核心是dispatch_semaphore_t类型的信号量。dispatch_semaphore_create(1)方法可以创建dispatch_semaphore_t类型的信号量,设定信号量的初始值为 1。注意,这里的传入的参数必须大于或等于 0,否则dispatch_semaphore_create会返回 NULL。dispatch_semaphore_wait(signal, overTime); 方法会判断signal的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和NSCondition一样直接进入waiting状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当overTime时限到了,也会执行后续任务。- 一个
dispatch_semaphore_wait(signal, overTime);方法会去对应一个dispatch_semaphore_signal(signal);看起来像NSLock的lock和unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock只能同一时间,一个线程访问被保护的临界区,而如果dispatch_semaphore的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区。
-
读写锁:一种特殊的自旋锁;能做到多读单写;实现:并发队列+
dispatch_barrier_async- 创建一个并发队列:
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT); - 读数据:
dispatch_sync(self.concurrent_queue, ^{ obj = [self.dataCenterDic objectForKey:key]; }); - 写数据异步栅栏调用设置数据:
dispatch_barrier_async(self.concurrent_queue, ^{ [self.dataCenterDic setObject:obj forKey:key]; });
- 创建一个并发队列:
5.属性修饰符atomic的理解
atomic在对象get/set的时候,会有一个spinlock_t(一种一直轮询的自旋锁)控制。即当前两个线程A和B,如果A正在执行getter时,B如果想要执行setter,就要等A执行getter完成后才能执行atomic只保证set/get方法安全,但是当多个线程不使用set/get方法访问时,就不再安全;所以atomic和property的多线程安全并没有直接的联系,多线程的安全还是要程序员自己保证。atomic由于使用了自旋锁,性能比nonatomic慢20倍。
6.通知的底层实现
- 观察者模式:解耦一系列需要相互协作的类之间进行通信的对象行为模式。它定义了对象之间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。观察者模式的实现一般分为两个步骤:消费者注册通知消息监听器、生产者发送通知消息。
- 你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前(willChangeValue)和之后(didChangeValue),通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
7.Category的底层实现
Category底层其实就是一个category_t类型的结构体
// 定义在objc-runtime-new.h文件中
struct category_t {
const char *name; // 比如给Student添加分类,name就是Student的类名
classref_t cls;
struct method_list_t *instanceMethods; // 分类的实例方法列表
struct method_list_t *classMethods; // 分类的类方法列表
struct protocol_list_t *protocols; // 分类的协议列表
struct property_list_t *instanceProperties; // 分类的实例属性列表
struct property_list_t *_classProperties; // 分类的类属性列表
};
可以看出内部存储的不仅有方法列表,还有协议列表和属性列表。但没有成员变量列表。
- 我们每创建一个分类,在编译时都会生成这样一个结构体并将分类的方法列表等信息存入这个结构体。在编译阶段分类的相关信息和本类的相关信息是分开的。等到运行阶段,会通过
runtime加载某个类的所有Category数据,把所有Category的方法、属性、协议数据分别合并到一个数组中,然后再将分类合并后的数据插入到本类的数据的前面。 - 从Category的底层结构体category_t可以看出,这个结构体中有方法列表、协议列表和属性列表,但是没有成员变量列表,所以我们可以在Category中定义属性,但是不能定义成员变量。
- 如果我们在分类中定义一个属性
@property (nonatomic , strong) NSString *name;,那编译器会为我们做什么呢?编译器只会帮我们声明- (void)setName:(NSString *)name;和- (NSString *)name;这两个方法,而不会实现这两个方法,也不会定义成员变量。 - 添加关联对象API来实现seter和getter:
objc_setAssociatedObject,objc_getAssociatedObject
8. 数组和字典的底层数据结构
- 字典是使用hash表来实现key和value之间的映射和存储的。内部使用了两个指针数组分别来保存keys和values
- 在CFDictionary的结构体内有keys和values这两个二级指针,可以基本断定为数组结构,由于是两个数组分别存储,因此,key哈希出来的数组下标地址,同样这个地址对应到values数组的下标,就是匹配到的值。因此keys和values这两个数组的长度一致才能保证匹配到数据。内部结构还有个_capacity表示当前通列表的扩充阀域 ,当count数量达到这个长度就扩容。
- key值会根据特定的hash函数算出建立的空桶数组,keys和values同样多,然后存储数据的时候,根据hash函数算出来的值,找到对应的index下标,如果下标已有数据,开放定址法后移动插入,如果空桶数组到达数据阀值,这个时候就会把空桶数组扩容,然后重新哈希插入。
- 根据 key 计算出它的哈希值 h。假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中。如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突。