iOS-OC-底层原理速记

127 阅读25分钟

1. 一个NSObject对象占用多少内存?

从C++实现可以看出,它只包含一个Class类型的isa指针,在64位架构中,占用64位,8个字节。 如果有成员变量,就在内存中的isa指针后面放置。

内存对齐:最宽的基本数据类型作为对齐模数(我理解最多128位,也就是16字节)。

数据成员对齐规则:每个数据成员存储的起始位置要从该成员大小(或者它的子成员大小)的整数倍开始。

如果一个结构体有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

结构体的总大小,必须是其内部最大成员的整数倍,不足的要补齐。

64位系统环境的OC对象最小分配内存为16字节。iOS系统在分配内存(堆)时,也有内存对齐的概念,为16的倍数。

排列对象属性顺序时,合理排列可以优化内存。

2. OC的类信息放在哪里?

实例(instance)对象 类(class)对象 元类(meta-class)对象

实例对象包括isa指针和其他成员变量。

类对象包括isa指针、superclass指针、类的属性信息(@property)、类的成员变量信息(ivar)、类的对象方法信息、类的协议信息

object-getClas([NSObject class])得到元类对象。

调用类对象的class,无论调用多少次,都是类对象。

class_isMetalClass(objectMetaClass)

元类对象包括isa指针、superclass指针、类的类方法信息(class method)

isa和superclass的指向总结如下:

instance的isa指向class

class的isa指向meta-class

meta-class的isa指向基类的meta-class,基类的isa指向自己

class的superclass指向父类的class,如果没有父类,则为nil

meta-class的superclass指向父类的meta-class。基类的meta-class的superclass指向基类的clas

instance调用对象方法:isa找到class,方法不存在,通过superclass继续找父类

class调用类方法:isa找meta-class,方法不存在,就通过superclass找父类

控制台通过p/x打印地址来证明。

3. Class的本质

底层都是objc_class结构体的指针,内存中就是结构体。

typedef struct objc_class *Class

objec2的objc_class继承自objc_object

类中的方法列表、属性列表、协议列表放在了class_rw_t(可读可写,运行时写入)中。

成员变量信息存储在class_rw_t中的class_ro_t(只读,编译时期确定,必要时可以清除)里了。class_ro_t里还有instanceSize等。

4. KVO本质

当一个对象使用了KVO监听,runtime会修改这个对象的isa指针,改为指向一个全新的通过runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath监听方法

要想手动触发KVO。需要自己调用willChangeValueForKey和didChangeValueForKey

5. 消息转发机制(避免崩溃、模拟多继承?)和方法缓存

OC的函数调用,最终会转化为C语言的objc_msgSend函数的调用。objc_msgSend(转发者(id),选择器(SEL),参数,参数) 先是快速查找,从缓存中找,这块逻辑也适用汇编写的,O(1)的时间复杂度。

然后是慢速寻找,通过isa和superclass找,在方法列表遍历查找或者二分法查找(如果已经排序了),如果没找到,就会进入消息转发阶段。分三个小阶段

第一个阶段(动态解析):resolveInstanceMethod、resolveClassMethod

可以通过runtime动态添加特定方法(class_addMethod),返回YES,重新找。如果返回NO,继续

第二个阶段(快速转发):-(id)forwardingTargetForSelector:(SEL)aSelector

如果返回特定的对象,流程转给这个对象。如果返回self或者nil,继续

第三个阶段(完整消息转发):methodSignatureForSelector和forwardInvocation

先对消息重签名,然后转发。该流程可以转发给多个对象,也可以直接return,不调用doesNot RecognizeSelector。(避免闪退)

在这个过程中,会有个缓存机制。调用过的方法,会放在cache_t中,里面有三个主要的变量

struct bucket_t *_buckets; // 缓存数组,即哈希桶

mask_t _mask; // 缓存数组的容量临界值 mask_t

_occupied; // 缓存数组中已缓存方法数量

selector和mask做&操作求得哈希桶的下标。索引值是hash运算的,因此它是无序的。

如果冲突了,往上一个位置找,如果还没有,就去后面找。

为了高效,容量到达3/4的时候,会做扩容操作。扩容时,丢弃旧的桶,开辟新的桶。此时mask容量会变化,我理解旧的缓存的索引也会发生变化了。新的桶开辟后,把新的bucket缓存进去,然后把旧桶的所有元素缓存进去,使用LRU算法?

6. 自动释放池

简介

app的事件循环(RunLoop)的每次循环开始时,会在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease对象。

通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。

原理分析

AutoreleasePoolPage::push()

AutoreleasePoolPage::pop(ctx)

它继承自AutoreleasePoolPageData

POOL_BOUNDARY nil // 编辑对象(哨兵对象)

自动释放池是由若干个AutoreleasePoolPage组成的双向链表结构,AutoreleasePoolPage中拥有parent和child指针,分别指向上一个和下一个page。

当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的page中。

另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

为什么多页?

一、操作过程需要加锁解锁,如果所有页面都在一页,操作非常复杂,一个对象进行操作其他的都得等待。 二、已满的页面都不进行操作了,只对没满的那个进行操作,效率比较高。 三、不用非得是一片连续的内存。

504*8 + 56(成员变量大小) + 8(边界对象) = 4096

每当调用objc_autoreleasePoolPush方法时,会将POOL_BOUNDARY放到当前page的栈顶(新页),并且返回这个边界对象。

而在调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

_objc_autoreleasePoolPrin可以打印相关信息

push逻辑

push函数中调用了autoreleaseFast函数,函数中又分三种情况:

当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置。

当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中。

当前page不存在时,调用autoreleaseNoPage创建一个hotPage,先将边界对象(POOL_BOUNDARY)添加至page的栈中,再调用page->add(obj) 方法将对象添加至page的栈中。

pop逻辑

先根据传入的边界对象地址找到边界对象所处的page,然后选择当前page中最新加入的对象一直向前清理,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;

另外,清空page对象还会遵循一些原则:

如果当前的page中存放的对象少于一半,则子page全部删除;

如果当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

自动释放池可以嵌套

autorelease的释放有两种,一是RunLoop管理。而是自己创建@atuoreleasepool管理

注意:如果不用autorelease,那就跟autoreleasepool没啥关系了。

与RunLoop的关系

  1. kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。

  2. kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()。

  3. kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()。

  4. __AtAutoreleasePool对象应该就是push时返回的哨兵对象吧。

7. OC的alloc流程

alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone->instanceSize(要开辟多少内存)、caloc(申请内存)、initInstanceIsa(将类对象和isa指针关联)

malloc与calloc区别

两者都是动态分配内存。主要的不同是malloc不初始化分配的内存,已分配的内存中可以是任意的值。calloc 初始化已分配的内存为0。

8. 内存5大区

从高地址到低地址如下:

栈区、堆区、全局/静态区、常量区、代码区。

栈区:

  1. 栈是一块连续的内存区域从从高地址向低地址进行存储,遵循先进后出(FILO)原则。

  2. 栈的地址空间在 iOS 中是以0X7开头。

  3. 栈区一般在运行时分配,内存空间由系统管理,申明的变量过了作用域范围后内存便会自动释放。

  4. 函数内部定义的局部变量、方法的参数(方法中默认参数:self、cmd),都存放在栈区。

优缺点

优点:栈是由系统自动分配并释放的,不会产生内存碎片,所以快速高效。

缺点:栈的内存大小有限制,数据不灵活,iOS 主线程栈大小是1MB,其他线程是512KB,MAC 只有 8M,信息来自 官方文档。

堆区:

  1. 堆是不连续的内存区域从低地址向高地址进行存储,类似于链表结构(便于增删,不便于查询),遵循先进先出(FIFO)原则。

  2. 堆的地址空间在iOS中是以0x6开头,其空间的分配总是动态的。

  3. 开发人员需要关注变量的生命周期,如果不及时释放,会造成内存泄漏,只有等程序结束时由系统统一回收。

  4. OC中使用alloc或者new开辟空间创建对象。

  5. C语言中使用malloc、calloc、realloc分配的空间,需要free释放。

优缺点:

优点:获得空间灵活,分配内存较大。

缺点:需手动管理,速度慢、容易产生内存碎片。

全局/静态区

该区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。

未初始化的全局变量和静态变量,即BSS区(.bss)。

已初始化的全局变量和静态变量,即数据区(.data)。

全局变量和全局静态变量的区别:

首先,并不是说全局变量在定义时加了static关键字才是静态存储,不加static就是动态存储,不是的。不管加不加static,全局变量都是存储在静态存储区的,都是在编译时分配存储空间的,两者只是作用域不同,全局变量默认具有外部链接性,作用域是整个工程,全局静态变量的作用域仅限本文件,不能在其他文件中引用。

常量区(即.rodata)

该区是编译时分配的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。

存放常量:整型、字符型、浮点、字符串等。

验证 

借助MachOView查看常量区:

代码区

该区是编译时分配的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。

程序运行时的代码会被编译成二进制,存进内存的代码区域。

9. tagged pointer(有标记的指针)

  1. tagged pointer专门用来存储小的对象,例如NSNumber,NSDate,NSString。

  2. tagged pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。

  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

arm64下tagged pointer结构从63位到0位分别如下:

taggedPointer结构图.png

__NSCFString(在堆上,会有多线程问题,加锁或者atomic解决)

NSTaggedPointerString(一个值,不需要retain和release,不会崩溃)

10.SideTables(散列表)

  1. SideTables的本质就是个hash表,并通过StripedMap进行了封装,每个元素是SideTable表,真机时开辟表的最大个数是8个,其他情况下开辟表的最大个数是64个。

  2. 为什么是多张SideTable,而不是一张呢,因为如果所有的对象公用一张表,我们使用表的时候要做开锁和解锁操作,这样性能消耗就会比较大。

每一个SideTable又包含如下:

slock是自旋锁,refcnts是引用计数表,weak_table是弱引用表

__weak typeof(id) weakObjc = objc

调用了objc_initWeak.流程如下:

  1. 在SideTables中先获取obj所在的SideTable。

  2. 在SideTable中获取到弱引用表:weak_table_t。

  3. 在weak_table_t中查找obj对应的weak_entry_t(对象地址、弱引用指针的地址列表)

3.1 如果有,将weak指针地址直接添加到weak_entry_t中。

3.2 如果没有

3.2.1 新创建一个weak_entry_t,并将obj和weak指针地址添加进去。

3.2.2 检测存储空间如果大于3/4进行扩容操作。

3.2.3 将新建的weak_entry_t插入到weak_table中。

weak_entry_t中有对象地址,还有2个结构体的联合类型。一个结构体包括弱引用指针列表、引用的数量、mask、hash元素上限阈值等;还有一个结构体包括固定大小的数组(inline_referrers)。此处也是使用了空间优化的方式。数量不多,使用inline。数量多的话,使用地址列表。

11.dealloc流程

  1. 判断 5 个条件

    1.isa为nonpointer;2.没有弱引用;3.没有关联对象;4.没有C++的析构函数;5.没有额外采用SideTabel进行引用计数存储;

    如果这 5 个条件都成立,直接调用free函数销毁对象,否则调用object_dispose做一些释放对象前的处理;

  2. 调用object_dispose进行相关处理

    1.如果有C++的析构函数,调用object_cxxDestruct;

    2.如果有关联对象,调用_object_remove_assocations函数,移除关联对象;

    3.调用weak_clear_no_lock将指向该对象的弱引用指针置为nil;

    4.调用table.refcnts.erase从引用计数表中擦除该对象的引用计数(如果isa为nonpointer,还要先判断isa.has_sidetable_rc)

  3. 调用free函数销毁对象。

12. 关联对象

objc_setAssociatedObject

objc_getAssociatedObject

objc_removeAssociatedObjects

如果只想移除给定对象的某个key的关联,可以使用objc_setAssociatedObject的给参数value传值nil

原理

AssociationsManager

AssociationsHashMap

ObjectAssociationMap

ObjcAssociation

关联对象图示.png

策略有5个

Assign retain(atomic与否) copy(atomic与否)

13. 如何统计页面加载/渲染时长

  1. hook vc的生命周期来统计,比如viewDidLoad开始,到viewDidAppear开始
  2. 利用runtime的kvo动态生成子类机制。hook vc的初始化方法,在新的实现中,手动触发kvo生成动态子类,然后利用runtime给动态子类添加对应的生命周期方法,比如viewDidLoad,在自己实现的方法中第一行和最后一行统计时间,中间调用原方法(动态子类的父类)的viewDidLoad实现。

14. Category(分类)的本质

问: Category的实现原理,以及Category为什么只能加方法不能加属性?

答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法在程序运行时将分类的成员变量添加到实例对象的结构体中。因此分类中不可以添加成员变量。

load 和 initialize

Load方法会在程序启动就会调用,当装载类信息的时候就会调用。

优先调用类的load方法之后调用分类的load方法,不过调用类的load方法之前会保证其父类已经调用过load方法。

我们知道当类第一次接收到消息时,就会调用initialize,相当于第一次使用类的时候就会调用initialize方法。调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会先调用分类的方法。但是load方法并不会被覆盖,首先我们来看一下initialize的源码。

initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现。

通过源码看到+load的实现是直接拿到load方法的内存地址直接调用方法,不再是通过消息发送机制调用。

问:Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法。

问:load、initialize的区别,以及它们在category重写的时候的调用的次序。
答:区别在于调用方式和调用时刻
调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用
调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

调用顺序:先调用类的load方法,先编译哪个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

实测:一个OC类的+initialize会调用,如果对它添加了kvo,生成了动态子类,它的+initialize又被调用了,调用了多次。

15. 闭包的原理

block本质上也是一个oc对象,他内部也有一个isa指针。block是封装了函数调用以及函数调用环境的OC对象。

block块中写下的代码在__main_block_func_0中

func_0的函数地址保存在了__main_block_impl_0结构体中

block捕获了值变量,就跟外部的原始值变量不一样的地址了。

__main_block_impl_0还有Desc,保存了block对象占用的内存大小。

__main_block_impl_0的第一个属性是__block_impl,里面有isa(简单理解为存储着block类对象的地址)指针和*FuncPtr

block底层实现.png

变量捕获

局部变量(出作用域会销毁,而block又想捕获,所有值传递会拷贝一份值)前面自动添加auto,基础类型是值传递

static变量,是指针传递

全局变量 不捕获,直接就可以访问

block有3种类型,在内存中的位置如下:

__NSGlobalBlock_在数据段。__NSMallocBlock_在堆上。__NSStackBlock_在栈上

block如何定义其类型呢?

没有访问auto变量在数据段。访问了auto变量在栈上。栈block做了copy,就是堆block了。

block调用copy时,global的block什么都不做。栈的block变为堆的block。堆的block引用计数+1

什么情况下ARC会自动将block进行一次copy操作?

  1. Block作为函数返回值时
  2. 将block赋值给__strong指针时
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时
  4. block作为GCD API的方法参数时

捕获对象变量

__weak修饰的变量,在生成的__main_block_impl_0中也是使用__weak修饰。

__main_block_copy_0 和 __main_block_dispose_0

当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copy和dispose函数

小总结:

一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会多出两个参数copy和dispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retain操作,因此一旦block捕获的变量是对象类型就会会自动生成copy和dispose来对内部引用的对象进行内存管理。

当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用。

如果block被拷贝到堆上。copy函数会调用_Block_object_assign函数,根据auto变量的修饰符(__strong,__weak,unsafe_unretained)做出相应的操作,形成强引用或者弱引用

如果block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto变量。

block内部想修改捕获的int局部变量,使用static修饰,或者__block

编译器会将__block修饰的变量包装成一个对象

被__block修饰的age变量声明变为名为age的__Block_byref_age_0结构体

__block将变量包装成对象,然后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

如果捕获的对象类型,只使用该对象的地址,不更改对象地址,那不加__block就没问题。

上面提到过__block修饰的age变量在编译时会被封装为结构体,那么当在外部使用age变量的时候,使用的是__Block_byref_age_0结构体呢?还是__Block_byref_age_0结构体内的age变量呢?

是外面的age,苹果为了隐藏Wrapper的实现,比如KVO的NSKVONotifying_Person的class也是重写了。

__block内存管理?

16.共用体和位域

Isa指针是使用isa_t共用体(union)来实现的,其中有两个变量 Class cls;和uintptr_t bits;

其中最高位nonpointer如果是0,代表普通的指针;如果是1,代表优化后的使用位域存储更多的信息。如果是普通指针,就使用cls,如果是优化后的,就使用uintptr_t来获取各个bit的详细信息。

17. 锁

锁的性能比较.png

  1. 自旋锁(OSSpinLock iOS10弃用)

响应速度快:当一个线程试图获取一个被其他线程持有的锁是,它不是陷入休眠状态,它会一直循环等待(自旋),直到锁被释放为止。这种忙等待机制会导致高优先级的任务一直在忙等待而被低优先级的任务抢占CPU时间片,从而造成优先级反转。

  1. os_unfair_lock

通过优先级继承(提高持有锁的低优先级线程的优先级)和线程休眠机制,既避免了CPU资源的浪费,又动态调整线程优先级,确保高优先级任务不会被长时间阻塞。

  1. pthread_mutex

该锁是POSIX线程库(pthread)的一部分,支持iOS、macOS、Linux等多个平台。经过苹果优化后,性能不错。支持递归,支持优先级继承。直接调用系统级API,减少了内存管理和隐式操作的开销。

  1. pthread_mutex(recursive)

递归锁允许同一个线程在未释放其拥有的锁时,反复对该锁进行加锁操作

  1. NSLock

面向对象,API简单易用,性能肯定比pthread_mutex差些。

  1. NSCondition

有wait、signal和broadcast(唤醒所有等待线程)。遵循NSLocking协议,基于pthread实现。

生产者-消费者模型中,通过wait和signal实现高效协作,避免轮询消耗CPU。也可以做任务依赖管理,跟信号量有点像。简单场景使用NSLock,在需要动态条件判断的场景中,用这个更好。

  1. NSRecursiveLock

支持递归锁使用

  1. NSConditionLock条件锁

有个condition参数

lockWhenCondition、tryLock、tryLockWhenCondition、unlockWithCondition也可以实现任务之间的依赖。

  1. @synchronized条件锁

自动处理加锁和解锁,避免忘记写unlock。支持递归加锁,单例模式的shared就可以用该锁保护,防止多个线程多次初始化。支持隐式异常处理,即使临界区的代码抛出异常,锁仍会被正确释放。iOS12之后,对该锁进行了性能优化(通过优化底层objc_sync_enter和objc_sync_exit),接近NSLock的性能。

18.屏幕显示图像原理(屏幕撕裂的原因)

电子枪会从左到右,从上到下扫描要显示的内容。每换一行,会发出Hsync。一帧扫描完成后,会发出Vsync。

如果只有一个帧缓存区,帧缓存区的读取和刷新都会有比较大的效率问题。因此引入了双缓存机制。但是双缓存机制虽然解决了效率问题,又带来了新的问题。

在视频控制器从帧缓存区读取图像进行显示时,如果当前这一帧的内容读了一半,GPU又提交了新的一帧,并更新了两个帧缓冲区,视频控制器继续读取的剩余一半,其实是另一帧的内容,导致了屏幕撕裂(显卡输出帧的速度比显示器快)。

使用vSync解决的屏幕撕裂问题。虽然解决了撕裂问题,但是需要消耗更多的计算资源,也会带来部分延迟(屏幕卡顿),因为要等vSync了,才更新画面。如果CPU/GPU处理时间长,错过了vSync,就丢弃了。

CPU:负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)

GPU:负责纹理的渲染

CPU计算好显示的内容提交到GPU,GPU渲染完成后将结果放到帧缓冲区,随后视频控制器会按照VSync信号逐行读取帧缓冲区的数据,最终在显示器显示出来。

垂直同步技术:让CPU和GPU在收到vSync信号后再开始准备数据,防止撕裂感和跳帧,通俗来讲,就是保证每秒输出的帧数不高于屏幕显示的帧数。

双缓冲技术:iOS是双缓冲机制,前帧缓存和后帧缓存。当GPU下一帧已经渲染完成放入缓冲区,且视频控制器已经读完前帧,GPU会等待vSync信号发出后,瞬间切换前后帧缓存(GPU会直接将视频控制器的指针指向另一个缓冲区),并让CPU开始准备下一帧数据。

19.iOS应用,从用户点击屏幕,到一个按钮有响应,都经过了哪些流程?

一、硬件交互阶段

  1. 触摸信号捕捉
  2. 硬件驱动处理(通过I/O Kit)将原始数据封装为IOHIDEvent对象(Human Interface Device事件)

二、系统层处理

  1. SpringBoard(主屏幕管理进程)路由事件,通过Mach IPC(进程间通信)将事件传递到目标应用的主线程RunLoop
  2. RunLoop事件分发
  • 主线程的RunLoop处于休眠状态,通过mach_mag()系统调用监听消息端口
  • 事件到达后,RunLoop被唤醒,将事件加入应用的事件队列,屏幕触摸事件会被转换为Source0的UIEvent事件。调用UIApplicationHandleEventQueue()处理。

三、应用内事件传递

  1. UIApplicaiton事件分发
  • UIApplication单例从事件队列中取出UIEvent对象(包含多个UITouch对象,如多点触控)
  • 确定事件的初始响应者:通过Hit-Testing(命中测试)
  1. 命中测试(Hit-Testing) -从根视图(keyWindow的根视图)开始,递归调用hitTest:withEvent:方法: --检查视图的userInteractionEnabled、hidden、alpha是否可交互。 --调用pointInside:withEvent:判断触摸点是否在视图范围内。 --按子视图的倒序(后添加的视图优先)递归查找,最终返回最顶层的符号条件的视图(即按钮) -如果按钮是最终响应者,UIEvent被标记为已处理。

四、响应链(Responder Chain)与事件处理

  1. 触摸事件的生命周期