题目:
1. 讲一下你对
iOS
内存管理的理解
2.KVO
实现原理
3. 观察者模式
4. 如果让你实现NSNotificationCenter
,讲一下思路
5. 如果让你实现GCD
的线程池
,讲一下思路
6.Category
的实现原理,以及Category
为什么只能加方法不能加实例变量。
7.swift
中struct
和class
的区别
8. 在一个HTTPS
连接的网站里,输入账号密码
点击登录
后,到服务器
返回这个请求前,中间经历了什么
9. 在一个app
中间有一个button
,在你手触摸屏幕
点击后,到这个button
收到点击事件,中间发生了什么
10.main()
之前的过程有哪些?
11.消息转发
机制原理?
12. 说说你理解weak
属性?
13. 遇到tableView
卡顿嘛?会造成卡顿
的原因大致有哪些?
14.UIView
和CALayer
的区别
和联系
15. 什么是离屏渲染
,为什么会触发离屏渲染
,离屏渲染
的危害
一. 讲一下你对 iOS
内存管理的理解
在Objective-C
的内存管理中,其实就是引用计数(reference count)
的管理。内存管理
就是在程序需要时程序员
分配一段内存空间
,而当使用完之后将它释放。如果程序员
对内存资源
使用不当,有时不仅会造成内存资源
浪费,甚至会导致程序crach
。
1. 引用计数(Reference Count)
为了解释引用计数,我们做一个类比:员工在办公室使用灯的情景。
当
第一个人
进入办公室
时,他需要使用灯,于是开灯,引用计数为1
当
另一个人
进入办公室
时,他也需要灯,引用计数为2
;每当多一个人进入办公室时,引用计数加1
当
有一个人
离开办公室
时,引用计数减1
,当引用计数为0
时,也就是最后一个人
离开办公室
时,他不再需要使用灯,关灯离开办公室
。
2. 内存管理规则
从上面员工在办公室使用灯
的例子,我们对比一下灯的动作与Objective-C对象
的动作有什么相似之处:
因为我们是通过引用计数
来管理灯,那么我们也可以通过引用计数
来管理使用Objective-C
对象。
而Objective-C
对象的动作对应有哪些方法以及这些方法对引用计数
有什么影响?
当你alloc
一个对象objc
,此时RC=1
;在某个地方你又retain
这个对象objc
,此时RC加1
,也就是RC=2
;由于调用alloc/retain
一次,对应需要调用release
一次来释放对象objc
,所以你需要release对象objc
两次,此时RC=0
;而当RC=0
时,系统会自动调用dealloc
方法释放对象
。
3. Autorelease Pool
在开发中,我们常常都会使用到局部变量
,局部变量
一个特点就是当它超过作用域
时,就会自动释放
。而autorelease pool
跟局部变量
类似,当执行代码超过autorelease pool
块时,所有放在autorelease pool
的对象都会自动调用release
。它的工作原理如下:
创建一个
NSAutoreleasePool
对象在
autorelease pool
块的对象调用autorelease
方法释放
NSAutoreleasePool
对象
4. ARC管理方法
iOS/OS X
内存管理方法有两种:手动引用计数(Manual Reference Counting)
和自动引用计数(Automatic Reference Counting)
。
自动引用计数(Automatic Reference Counting)
简单来说,它让编译器
来代替程序员来自动加入retain
和release
方法来持有
和放弃对象
的所有权
。
在ARC内存管理
机制中,id
和其他对象类型变量
必须是以下四个ownership qualifiers
其中一个来修饰:
所以在管理Objective-C对象内存
的时候,你必须选择其中一个
,下面会用一些列子来逐个解释它们的含义以及如何选择它们。
__strong:被它修饰的变量持有对象的所有权(默认,如果不指定其他,编译器就默认加入)
__weak: 被它修饰的变量都不持有对象的所有权,而且当变量指向的对象的RC为0时,变量设置为nil。
__unsafe_unretained:被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址;这样的话,对象有可能已经释放,但继续访问,就会造成非法访问(Invalid Access)。
__autoreleasing:相比之前的创建、使用和释放NSAutoreleasePool对象,现在你只需要将代码放在@autoreleasepool块即可。你也不需要调用autorelease方法了,只需要用__autoreleasing修饰变量即可。
5.Property(属性)
二. KVO实现原理
KVO
基本原理:
1.
KVO
是基于runtime
机制实现的2.当
某个类的属性对象第一次被观察
时,系统就会在运行期
动态地创建该类的一个派生类
,在这个派生类
中重写基类
中任何被观察属性的setter
方法。派生类
在被重写的setter
方法内实现真正的通知机制
3.如果
原类
为Person
,那么生成的派生类
名为NSKVONotifying_Person
4.每个类对象中都有一个
isa指针
指向当前类
,当一个类对象
的第一次被观察
,那么系统
会偷偷将isa指针
指向动态生成的派生类
,从而在给被监控属性
赋值时执行的是派生类的setter
方法5.
键值观察通知
依赖于NSObject
的两个方法:willChangeValueForKey:
和didChangevlueForKey:
;在一个被观察属性
发生改变之前,willChangeValueForKey:
一定会被调用,这就 会记录旧的值
。而当改变发生后,didChangeValueForKey:
会被调用,继而observeValueForKey:ofObject:change:context:
也会被调用。
三.观察者模式
观察者模式(Observer Pattern)
:定义对象间的一种一对多
依赖关系,使得每当一个对象状态
发生改变时,其相关依赖对象皆得到通知并被自动更新
。
在iOS
中典型的观察者模式是:NSNotificationCenter
和KVO
。
1. NSNotificationCenter
观察者Observer
,通过NSNotificationCenter
的addObserver:selector:name:object
接口来注册对某一类型通知感兴趣。在注册时候一定要注意,NSNotificationCenter
会对观察者
进行引用计数+1
的操作,我们在程序中释放观察者
的时候,一定要去从center
中将其移除。通知中心NSNotificationCenter
,通知的枢纽
。被观察的对象
,通过postNotificationName:object:userInfo:
发送某一类型通知,广播改变。通知对象
NSNotification
,当有通知来的时候,Center
会调用观察者注册的接口
来广播通知,同时传递存储着更改内容的NSNotification
对象。
2. KVO
KVO
的全称是Key-Value Observer
,即键值观察
。是一种没有中心枢纽
的观察者模式
的实现方式。一个主题对象管理所有依赖于它的观察者对象
,并且在自身状态发生改变
的时候主动通知观察者对象
。
- 注册观察者
[object addObserver:self forKeyPath:property options:NSKeyValueObservingOptionNew context:]。
更改
主题对象
属性的值,即触发发送更改
的通知。在制定的
回调函数
中,处理收到的更改通知。注销观察者
[object removeObserver:self forKeyPath:property]
。
四. 如果让你实现 NSNotificationCenter,讲一下思路
NSNotificationCenter
是一个单例NSNotificationCenter
内部使用可变字典NSMutableDictionary
来存储,以通知名称postName
作为key
,以数组NSAray
作为值,该数组存储着每个观察者
的信息:观察者对象、观察者处理方法、通知名称等
。当发送通知时,以
通知名称为key
去获取相应的观察者信息数组
,然后遍历这个数组,取出观察者对象
和相对应处理方法
,进行实例方法调用
。
五. 如果让你实现 GCD 的线程池,讲一下思路
线程池
包含如下8
个部分:
线程池管理器(ThreadPoolManager)
:用于创建并管理线程池
,是一个单例工作线程(WorkThread)
:线程池中线程
任务接口(Task)
:每个任务必须实现的接口,以供工作线程
调度任务的执行。任务队列
:用于存放没有处理的任务。提供一种缓冲机制
。corePoolSize核心池的大小
:默认情况下,在创建了线程池
后,线程池
中的线程数
为0,当有任务来之后,就会创建一个线程去执行任务,当线程池
中的线程数目
达到corePoolSize
后,就会把到达的任务放到缓存队列
当中;maximumPoolSize线程池最大线程数
:它表示在线程池
中最多能创建多少个线程
;存活时间keepAliveTime
:表示线程
没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池
中的线程数大于corePoolSize
时,keepAliveTime
才会起作用,这是如果一个线程空闲
的时间达到keepAliveTime
,则会终止直到线程池
中的线程数
不大于corePoolSize
.
具体流程:
当通过
任务接口
向线程池管理器
中添加任务时,如果当前线程池管理器
中的线程数目
小于corePoolSize
,则每来一个任务,就会通过线程池管理器
创建一个线程
去执行这个任务;如果当前
线程池
中的线程数目
大于等于corePoolSize
,则每来一个任务,会尝试将其添加到任务缓存队列
当中,若添加成功,则该任务会等待空闲线程
将其取出去执行;若添加失败(一般来说是任务缓存队列
已满),则会尝试创建新的线程
去执行这个任务;如果
当前线程池中
的线程数目
达到maximumPoolSize
,则会采取任务拒绝策略
进行处理;如果
线程池中
的线程数量
大于corePoolSize
时,如果某线程空闲时间
超过keepAliveTime
,线程
将被终止,直至线程池
中的线程数目
不大于corePoolSize
;
六.Category
的实现原理
,以及Category
为什么只能加方法
不能加实例变量
。
详见: iOS 面试题一
category
是可以添加属性
,不能添加实例变量
!之所以不能添加实例变量
,是因为一个类
的实例变量
在编译阶段
,就会在objc_class
的class_ro_t
这里进行存储
和布局
,而category
是在运行时
才进行加载的,
然后在加载 ObjC
运行时的过程中在 realizeClass
方法中:
// 从 `class_data_bits_t `调用 `data` 方法,将结果从 `class_rw_t `强制转换为 `class_ro_t `指针
const class_ro_t *ro = (const class_ro_t *)cls->data();
// 初始化一个 `class_rw_t` 结构体
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
// 设置`结构体 ro` 的值以及 `flag`
rw->ro = ro;
// 最后设置正确的` data`。
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
运行时
加载的时候class_ro_t
里面的方法、协议、属性
等内容赋值给class_rw_t
,而class_rw_t
里面没有用来存储相关变量
的数组
,这样的结构
也就注定实例变量
是无法在运行期
进行填充.
七. swift 中 struct和class的区别
swift
中,class
是引用类型
,struct
是值类型
。值类型
在传递
和赋值
时将进行复制
,而引用类型
则只会使用引用对象
的一个"指向"
。所以他们两者之间的区别就是两个类型
的区别
。
class
有这几个功能struct
没有的:
class
可以继承
,这样子类
可以使用父类
的特性
和方法
类型转换
可以在runtime
的时候检查
和解释
一个实例的类型
可以用
deinit
来释放资源
一个类
可以被多次引用
struct
也有这样几个优势:
结构较小,适用于
复制
操作,相比于一个class
的实例被多次引用更加安全。无须担心内存
memory leak
或者多线程
冲突问题
详见: 答卓同学的iOS面试题
八.在一个HTTPS
连接的网站里,输入账号密码
点击登录后,到服务器返回这个请求前,中间经历了什么
客户端
打包请求。包括url,端口
,你的账号密码等等。账号密码
登陆应该用的是Post方式
,所以相关的用户信息
会被加载到body
里面。这个请求应该包含三个方面
:网络地址,协议,资源路径
。注意,这里是HTTPS
,就是HTTP + SSL / TLS
,在HTTP
上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙
。服务器接受请求
。一般客户端
的请求会先发送到DNS服务器
。DNS服务器
负责将你的网络地址
解析成IP地址
,这个IP地址
对应网上一台机器。这其中可能发生Hosts Hijack
和ISP failure
的问题。过了DNS
这一关,信息就到了服务器端
,此时客户端
会和服务器
的端口之间建立一个socket连接
,socket
一般都是以file descriptor
的方式解析请求
。这个过程相当于是服务器端
分析是否要向客户端
发送钥匙模板
。服务器端
返回数字证书
。服务器
端会有一套数字证书(相当于是个钥匙模板)
,这个证书会先发送给客户端
。这个过程相当于是服务器端
向客户端
发送钥匙模板
。客户端
生成加密信息
。根据收到的数字证书(钥匙模板)
,客户端
会生成钥匙
,并把内容锁上
,此时信息已经加密
。这个过程相当于客户端
生成钥匙
并锁上请求。客户端
发送加密信息
。服务器端
会收到由自己发送出去的数字证书
加锁的信息。 这个时候生成的钥匙
也一并被发送到服务器端
。这个过程是相当于客户端
发送请求。服务器端
解锁加密信息。服务器端收到加密信息后,会根据得到的钥匙
进行解密
,并把要返回的数据进行
对称加密。这个过程相当于
服务器端`解锁请求、生成、加锁回应信息。服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于
服务器端
向客户端
发送回应。客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。
HTTPS
加密过程详解请去https原理:证书传递、验证和数据加密、解密过程解析
详见: 答卓同学的iOS面试题
九. 在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么
响应链
大概有以下几个步骤:
设备将
touch
到的UITouc
h和UIEvent
对象打包, 放到当前活动的Application
的事件队列中单例的UIApplication会
从事件队列
中取出触摸事件
并传递给单例UIWindow
UIWindow
使用hitTest:withEvent:
方法查找touch
操作的所在的视图view
(备注:UIResponder
是UIView
的父类,UIView
是UIControl
的父类。)
RunLoop
这边我大概讲一下
- 主线程的
RunLoop
被唤醒 - 通知
Observer
,处理Timer
和Source 0
-
Springboard
接受touch event
之后转给App
进程中 -
RunLoop
处理Source 1
,Source1
就会触发回调,并调用_UIApplicationHandleEventQueue()
进行应用内部的分发。 -
RunLoop
处理完毕进入睡眠,此前会释放旧的autorelease pool
并新建一个autorelease pool
详见: 答卓同学的iOS面试题
十. 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
函数
注意:dyld
会缓存
上一次把信息加载内存的缓存
,所以第二次
比第一次
启动快一点
十一. 消息转发机制原理?
动态方法解析
备用接受者
完整转发
举个 :
新建一个HelloClass
的类,定义两个方法:
@interfaceHelloClass:NSObject
- (void)hello;
+ (HelloClass *)hi;
@end
1. 动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法-resolveInstanceMethod:(实例方法)
或者+resolveClassMethod:(类方法)
。在这个方法中,我们有机会为该未知消息新增一个”处理方法”
。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod
函数动态添加到类里面就可以了。
void functionForMethod(id self, SEL _cmd)
{
NSLog(@"Hello!");
}
Class functionForClassMethod(id self, SEL _cmd)
{
NSLog(@"Hi!");
return [HelloClass class];
}
#pragma mark - 1、动态方法解析
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"resolveClassMethod");
NSString *selString = NSStringFromSelector(sel);
if ([selString isEqualToString:@"hi"])
{
Class metaClass = objc_getMetaClass("HelloClass");
class_addMethod(metaClass, @selector(hi), (IMP)functionForClassMethod, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
NSString *selString = NSStringFromSelector(sel);
if ([selString isEqualToString:@"hello"])
{
class_addMethod(self, @selector(hello), (IMP)functionForMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
2. 备用接受者
动态方法解析
无法处理消息,则会走备用接受者
。这个备用接受者
只能是一个新的对象
,不能是self本身
,否则就会出现无限循环
。如果我们没有指定相应的对象来处理aSelector
,则应该调用父类的实现
来返回结果。
#pragma mark - 2、备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 将消息交给_helper来处理 if ([selectorString isEqualToString:@"hello"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
在本类中需要实现这个新的接受对象
@interfaceHelloClass()
{
RuntimeMethodHelper *_helper;
}
@end
@implementation HelloClass
- (instancetype)init{
self = [super init];
if (self){
_helper = [RuntimeMethodHelper new];
}
return self;
}
RuntimeMethodHelper
类需要实现这个需要转发的方法:
#import"RuntimeMethodHelper.h"
@implementationRuntimeMethodHelper
- (void)hello
{
NSLog(@"%@, %p", self, _cmd);
}
@end
3. 完整消息转发
如果动态方法解析和备用接受者都没有处理这个消息,那么就会走完整消息转发:
#pragma mark - 3、完整消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"forwardInvocation");
if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}
/*必须重新这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature)
{
if ([RuntimeMethodHelper instancesRespondToSelector:aSelector])
{
signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
详见: 2018-iOS面试题
十二. 说说你理解weak属性?
weak实现原理:
Runtime
维护了一个weak
表,用于存储指向某个对象的所有weak指针
。weak表
其实是一个hash(哈希)表
,Key
是所指对象的地址
,Value
是weak指针的地址(这个地址的值是所指对象的地址)数组
。
初始化时:
runtime
会调用objc_initWeak
函数,初始化一个新的weak指针指向对象的地址
。添加引用时:
objc_initWeak
函数会调用objc_storeWeak()
函数,objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表
。释放时,调用
clearDeallocating
函数。clearDeallocating函数
首先根据对象地址
获取所有weak指针地址的数组
,然后遍历这个数组
把其中的数据设为nil
,最后把这个entry
从weak表
中删除,最后清理对象的记录。
追问的问题一:
1.实现weak
后,为什么对象释放
后会自动为nil?
runtime
对注册的类
, 会进行布局
,对于weak 对象
会放入一个 hash 表
中。 用 weak 指向的对象内存地址
作为 key
,当此对象的引用计数
为 0
的时候会dealloc
,假如 weak 指向的对象内存地址
是a
,那么就会以a
为键, 在这个 weak 表中搜索,找到所有以a 为键
的 weak 对象
,从而设置为nil
。
追问的问题二:
2.当weak引用指向的对象
被释放时,又是如何去处理weak指针
的呢?
1、调用objc_release
2、因为对象的引用计数
为0,所以执行dealloc
3、在dealloc
中,调用了_objc_rootDealloc
函数
4、在_objc_rootDealloc
中,调用了object_dispose
函数
5、调用objc_destructInstance
6、最后调用objc_clear_deallocating
,详细过程如下:
a. 从weak表中获取废弃对象的地址
为键值
的记录
b. 将包含在记录中的所有附有 weak修饰符变量的地址
,赋值为 nil
c. 将weak表
中该记录删除
d. 从引用计数表
中删除废弃对象的地址
为键值的记录
十三. 遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?
可能造成tableView
卡顿的原因有:
最常用的就是
cell的重用
, 注册重用标识符
如果不重用cell
时,每当一个cell
显示到屏幕上时,就会重新创建一个新的cell
如果有很多数据的时候,就会堆积很多cell
。
如果重用cell
,为cell
创建一个ID
,每当需要显示cell
的时候,都会先去缓冲池
中寻找可循环利用
的cel
l,如果没有再重新创建cell
避免
cell
的重新布局
cell
的布局填充
等操作 比较耗时,一般创建
时就布局好
如可以将cell
单独放到一个自定义类
,初始化时就布局好
提前计算并
缓存cell
的属性
及内容
当我们创建cell
的数据源
方法时,编译器并不是先创建cell
再定cell的高度
而是先根据内容
一次确定每一个cell的高度
,高度确定后,再创建要显示的cell
,滚动时,每当cell
进入屏幕都会计算高度
,提前估算高度
告诉编译器
,编译器
知道高度后,紧接着就会创建cell
,这时再调用高度的具体计算
方法,这样的方式不用浪费时间去计算显示以外的cell
减少
cell
中控件的数量
尽量使cell
得布局大致相同,不同风格的cell
可以使用不用的重用标识符
,初始化时添加控件
,
不适用的可以先隐藏不要使用
ClearColor
,无背景色
,透明度
也不要设置为0
,因为渲染
耗时比较长使用局部更新
如果只是更新某组的话,使用reloadSection
进行局部更新加载网络数据,下载图片,使用
异步加载
,并缓存
少使用
addView
给cell
动态添加view
按需
加载cell
,cell
滚动很快时,只加载范围内的cell
不要实现无用的代理方法,
tableView
只遵守两个协议缓存行高:
estimatedHeightForRow
不能和HeightForRow
里面的layoutIfNeed
同时存在,这两者同时存在才会出现“窜动”
的bug
。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高
就不要写预估方法了,用一个行高的缓存字典
来减少代码的调用次数
即可不要做
多余的绘制
工作。在实现drawRect:
的时候,它的rect参数
就是需要绘制的区域
,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect
判断是否需要绘制image
和text
,然后再调用绘制方法
。预渲染图像
。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context
里先将其画一遍,导出成UIImage
对象,然后再绘制到屏幕;使用正确的
数据结构
来存储数据
。
十四. UIView
和CALayer
的区别和联系
联系:
每个
UIView
内部都有一个CALayer
在背后提供内容的绘制和显示
,并且UIView
的尺寸样式
都由内部
的CALayer
所提供。两者都有树状层级结构
,layer
内部有SubLayers
,UIView
内部有SubViews
,但是CALayer
比UIView
多了个AnchorPoint;
在
View
显示的时候,UIView
做为CALayer
的CALayerDelegate
,View
的显示内容由内部的CALayer
来display
。CALayer
是默认修改属性支持隐式动画
的,在给UIView
的Layer
做动画的时候,View
作为Layer
的代理,Layer
通过actionForLayer:forKey:
向View
请求相应的action(动画行为)
layer
内部维护者三份layer tree
,分别是presentLayer Tree(动画树)、modeLayer Tree(模型树)、Render Tree(渲染树)
,在做iOS动画
的时候,我们修改动画的属性
,其实是Layer
的presentLayer
的属性值,而最终展示在界面上的其实是提供view
的modelLayer
。
区别:
-
UIView
和CALaye
r最大的区别是UIView
可以接受并处理触摸事件
,而CALayer
不可以。
十五. 什么是离屏渲染,为什么会触发离屏渲染, 离屏渲染的危害
1. 什么是离屏渲染:
GPU渲染机制:
CPU
计算好显示内容提交到GPU
,GPU 渲染
完成后将渲染结果
放入帧缓冲区
,随后视频控制器
会按照 VSync 信号
逐行读取帧缓冲区
的数据,经过可能的数模转换
传递给显示器
显示。
GPU屏幕渲染
有以下两种方式:
On-Screen Rendering
意为当前屏幕渲染
,指的是GPU
的渲染操作
是在当前用于显示的屏幕缓冲区
中进行。Off-Screen Rendering
意为离屏渲染
,指的是GPU
在当前屏幕缓冲区
以外新开辟一个缓冲区
进行渲染操作
。特殊的
“离屏渲染”
如果将不在GPU
的当前屏幕缓冲区
中进行的渲染
都称为离屏渲染
,那么就还有另一种特殊的“离屏渲染”
方式:CPU渲染
。
如果我们重写了drawRect
方法,并且使用任何Core Graphics
的技术进行了绘制操作,就涉及到了CPU渲染
。整个渲染过程由CPU
在App
内同步地完成,渲染得到的bitmap
最后再交由GPU
用于显示。
2. 为什么会触发离屏渲染
设置了以下属性时,都会触发离屏绘制:
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- cornerRadius, 如果能够只用 cornerRadius 解决问题,不设置masksToBounds,则不会引起离屏渲染,如果既设置了cornerRadius,又设置了masksToBounds,就会触发离屏渲染
因为当设置锯齿,阴影,遮罩
的时候,图层属性的混合体
被指定为在未预合成
之前不能直接在屏幕中绘制
,所以就需要屏幕外渲染
被唤起。
这句话的意思是当你给一个控件
设置锯齿,阴影,遮罩
的时候,这时候控件需要去混合各个图层
的像素来找出各个图层
的正确显示效果,所以触发离屏渲染
。
3. 离屏渲染的危害
相比于当前屏幕渲染
,离屏渲染
的代价是很高的,主要体现在两个方面:
- 创建新缓冲区
要想进行离屏渲染
,首先要创建一个新的缓冲区
。
- 上下文切换
离屏渲染
的整个过程,需要多次切换上下文环境
:先是从当前屏幕(On-Screen)
切换到离屏(Off-Screen)
;等到离屏渲染
结束以后,将离屏缓冲区
的渲染结果
显示到屏幕
上有需要将上下文
环境从离屏
切换到当前屏幕
。而上下文环境
的切换
是要付出很大代价的。
详见:离屏渲染学习笔记