iOS 面试题三

题目:

1. 讲一下你对iOS 内存管理的理解
2. KVO实现原理
3. 观察者模式
4. 如果让你实现 NSNotificationCenter,讲一下思路
5. 如果让你实现 GCD线程池,讲一下思路
6.Category 的实现原理,以及 Category 为什么只能加方法不能加实例变量。
7. swiftstructclass的区别
8. 在一个HTTPS连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么
9. 在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么
10. main()之前的过程有哪些?
11. 消息转发机制原理?
12. 说说你理解weak属性?
13. 遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?
14. UIViewCALayer区别联系
15. 什么是离屏渲染,为什么会触发离屏渲染,离屏渲染的危害

一. 讲一下你对 iOS 内存管理的理解

Objective-C的内存管理中,其实就是引用计数(reference count)的管理。内存管理就是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放。如果程序员内存资源使用不当,有时不仅会造成内存资源浪费,甚至会导致程序crach

memory management from apple document.png

1. 引用计数(Reference Count)

为了解释引用计数,我们做一个类比:员工在办公室使用灯的情景。

引用Pro Multithreading and Memory Management for iOS and OS X的图.png
  • 第一个人进入办公室时,他需要使用灯,于是开灯,引用计数为1

  • 另一个人进入办公室时,他也需要灯,引用计数为2;每当多一个人进入办公室时,引用计数加1

  • 有一个人离开办公室时,引用计数减1,当引用计数为0时,也就是最后一个人离开办公室时,他不再需要使用灯,关灯离开办公室

2. 内存管理规则
从上面员工在办公室使用灯的例子,我们对比一下灯的动作与Objective-C对象的动作有什么相似之处:

灯的动作Objective-C对象的动作.png

因为我们是通过引用计数来管理灯,那么我们也可以通过引用计数来管理使用Objective-C对象。

引用Pro Multithreading and Memory Management for iOS and OS X的图.png

Objective-C对象的动作对应有哪些方法以及这些方法对引用计数有什么影响?

image.png

当你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对象

引用Pro Multithreading and Memory Management for iOS and OS X的图.png

4. ARC管理方法
iOS/OS X内存管理方法有两种:手动引用计数(Manual Reference Counting)自动引用计数(Automatic Reference Counting)

自动引用计数(Automatic Reference Counting)简单来说,它让编译器来代替程序员来自动加入retainrelease方法来持有放弃对象所有权

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(属性)

image.png

详见: iOS/OS X内存管理(一):基本概念与原理

二. 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: 也会被调用。

KVO原理图.png

三.观察者模式

观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新

iOS中典型的观察者模式是:NSNotificationCenterKVO

1. NSNotificationCenter

NSNotificationCenter.png
  • 观察者Observer,通过NSNotificationCenteraddObserver: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_classclass_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连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么

HTTPS加密流程.png
  1. 客户端打包请求。包括url,端口,你的账号密码等等。账号密码登陆应该用的是Post方式,所以相关的用户信息会被加载到body里面。这个请求应该包含三个方面网络地址,协议,资源路径。注意,这里是HTTPS,就是HTTP + SSL / TLS,在HTTP上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙

  2. 服务器接受请求。一般客户端的请求会先发送到DNS服务器DNS服务器负责将你的网络地址解析成IP地址,这个IP地址对应网上一台机器。这其中可能发生Hosts HijackISP failure的问题。过了DNS这一关,信息就到了服务器端,此时客户端会和服务器的端口之间建立一个socket连接socket一般都是以file descriptor的方式解析请求。这个过程相当于是服务器端分析是否要向客户端发送钥匙模板

  3. 服务器端返回数字证书服务器端会有一套数字证书(相当于是个钥匙模板),这个证书会先发送给客户端。这个过程相当于是服务器端客户端发送钥匙模板

  4. 客户端生成加密信息。根据收到的数字证书(钥匙模板)客户端会生成钥匙,并把内容锁上,此时信息已经加密。这个过程相当于客户端生成钥匙并锁上请求。

  5. 客户端发送加密信息。服务器端会收到由自己发送出去的数字证书加锁的信息。 这个时候生成的钥匙也一并被发送到服务器端。这个过程是相当于客户端发送请求。

  6. 服务器端解锁加密信息。服务器端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端`解锁请求、生成、加锁回应信息。

  7. 服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于服务器端客户端发送回应。

  8. 客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。

HTTPS加密过程详解请去https原理:证书传递、验证和数据加密、解密过程解析

详见: 答卓同学的iOS面试题

九. 在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么

响应链大概有以下几个步骤:

  • 设备将touch到的UITouch和UIEvent对象打包, 放到当前活动的Application的事件队列中

  • 单例的UIApplication会事件队列中取出触摸事件并传递给单例UIWindow

  • UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view

(备注:UIResponderUIView的父类,UIViewUIControl的父类。)
RunLoop这边我大概讲一下

  • 主线程的RunLoop被唤醒
  • 通知Observer,处理TimerSource 0
  • Springboard接受touch event之后转给App进程中
  • RunLoop处理Source 1Source1就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。
  • RunLoop处理完毕进入睡眠,此前会释放旧的autorelease pool并新建一个autorelease pool

详见: 答卓同学的iOS面试题

十. main()之前的过程有哪些?

1)dyld 开始将程序二进制文件初始化

2)交由ImageLoader 读取image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP

3)由于runtimedyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理

4)runtime 接手后调用map_images解析和处理

5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class+load和其他Category+load方法

6)至此 所有的信息都被加载内存

7)最后dyld调用真正的main函数

注意:dyld缓存上一次把信息加载内存的缓存,所以第二次第一次启动快一点

十一. 消息转发机制原理?

  • 动态方法解析

  • 备用接受者

  • 完整转发

消息转发.png

举个 :

新建一个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所指对象的地址Valueweak指针的地址(这个地址的值是所指对象的地址)数组

  • 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址

  • 添加引用时:objc_initWeak函数会调用 objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表

  • 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entryweak表中删除,最后清理对象的记录。

追问的问题一:

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的时候,都会先去缓冲池中寻找可循环利用cell,如果没有再重新创建cell

  • 避免cell的重新布局
    cell布局填充等操作 比较耗时,一般创建时就布局好
    如可以将cell单独放到一个自定义类,初始化时就布局好

  • 提前计算并缓存cell属性内容
    当我们创建cell数据源方法时,编译器并不是先创建cell再定cell的高度
    而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入屏幕都会计算高度提前估算高度告诉编译器编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样的方式不用浪费时间去计算显示以外的cell

  • 减少cell中控件的数量
    尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件
    不适用的可以先隐藏

  • 不要使用ClearColor无背景色透明度也不要设置为0,因为渲染耗时比较长

  • 使用局部更新
    如果只是更新某组的话,使用reloadSection进行局部更新

  • 加载网络数据,下载图片,使用异步加载并缓存

  • 少使用addViewcell动态添加view

  • 按需加载cellcell滚动很快时,只加载范围内的cell

  • 不要实现无用的代理方法,tableView只遵守两个协议

  • 缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可

  • 不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制imagetext,然后再调用绘制方法

  • 预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;

  • 使用正确的数据结构存储数据

十四. UIViewCALayer的区别和联系

联系:

  • 每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView尺寸样式都由内部CALayer所提供。两者都有树状层级结构layer内部有SubLayersUIView内部有SubViews,但是CALayerUIView多了个AnchorPoint;

  • View显示的时候,UIView做为CALayerCALayerDelegateView的显示内容由内部的CALayerdisplay

  • CALayer是默认修改属性支持隐式动画的,在给UIViewLayer 做动画的时候,View 作为Layer 的代理,Layer 通过 actionForLayer:forKey:View请求相应的 action(动画行为)

  • layer内部维护者三份layer tree,分别是presentLayer Tree(动画树)、modeLayer Tree(模型树)、Render Tree(渲染树),在做iOS动画的时候,我们修改动画的属性,其实是LayerpresentLayer的属性值,而最终展示在界面上的其实是提供viewmodelLayer

区别:

  • UIViewCALayer最大的区别是UIView可以接受并处理触摸事件,而CALayer不可以。

详见:详解 CALayer 和 UIView 的区别和联系

详见: 详解 CALayer 和 UIView 的区别和联系

十五. 什么是离屏渲染,为什么会触发离屏渲染, 离屏渲染的危害

1. 什么是离屏渲染:

GPU渲染机制:

CPU 计算好显示内容提交到GPUGPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

GPU屏幕渲染有以下两种方式:

  • On-Screen Rendering
    意为当前屏幕渲染,指的是GPU渲染操作是在当前用于显示的屏幕缓冲区中进行。

  • Off-Screen Rendering
    意为离屏渲染,指的是GPU当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

  • 特殊的“离屏渲染”
    如果将不在GPU当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU渲染

如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPUApp内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。

2. 为什么会触发离屏渲染

设置了以下属性时,都会触发离屏绘制:

- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- cornerRadius, 如果能够只用 cornerRadius 解决问题,不设置masksToBounds,则不会引起离屏渲染,如果既设置了cornerRadius,又设置了masksToBounds,就会触发离屏渲染

因为当设置锯齿,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。

这句话的意思是当你给一个控件设置锯齿,阴影,遮罩的时候,这时候控件需要去混合各个图层的像素来找出各个图层的正确显示效果,所以触发离屏渲染

3. 离屏渲染的危害

相比于当前屏幕渲染离屏渲染的代价是很高的,主要体现在两个方面:

  • 创建新缓冲区

要想进行离屏渲染,首先要创建一个新的缓冲区

  • 上下文切换

离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境切换是要付出很大代价的。

详见:离屏渲染学习笔记

推荐阅读更多精彩内容