UITouch表示了触摸在屏幕上的位置、移动、大小、压力。
UIResponder抽象了响应和处理事件的接口。
当iPhone接受到一个触摸事件时,处理过程如下:
UIWindow开始使用逆序深度优先遍历算法,查找到最合适的Responsder。响应过程是顺着事件传递过程的路径反向进行的。view的nextResponder可能是控制器,也可能是view。
离屏渲染是指GPU在当前屏幕缓冲区(FrameBuffer)以外开辟一块新的缓冲区(Off-ScreenBuffer)进行渲染工作。在当前屏幕缓冲区之外的渲染称之为离屏渲染。创建新的缓冲区是会消耗CPU和GPU资源的,而且创建和删除缓冲区都需要CPU和GPU同步,这会造成GPU渲染流水线停顿。离屏渲染需要两次渲染工作,一次在当前屏幕缓冲区另一次在离屏缓冲区,这意味着GPU需要做更多的工作。触发离屏渲染时会从FrameBuffer切换到Off-ScreenBuffer,渲染完毕后再切换回FrameBuffer,这一过程需要来回切换上下文,因此对性能有一定的影响。
以下方式会造成离屏渲染:
实际上可以理解为该视图需要多个图层合并的情况下。
离屏渲染并不一定是不好的,合理使用离屏渲染也是提高app性能的一种方式。比如在视图包含图片切比较复杂的情况下,开启光栅化虽然会造成离屏渲染,但是系统会将这一次渲染结果进行保存,下次需要渲染的时候之间就可以拿过来使用了,从而在一定程度上提高了性能。需要注意的是,该缓存只有100ms,且大小不得超过屏幕像素数据的2.5倍。
UITableView常用的优化思路如下:
可以从进入UIViewController和退出两个方面阐述。
当进入一个视图控制器时:
当退出一个视图时:
实例对象的底层实际上是一个结构体,比如一个NSObject的实例对象结构如下:
structNSObject_IMP{Classisa;};2.2类对象的本质类对象的本质也是一个结构体。结构体里面保存了isa、superclass指针、方法缓存、类信息等。实例对象的isa指向类对象。类对象的isa指向元类对象。类对象和元类对象的结构是一样的。isa从64位以后是以共用体形式存在的。下面是简化版的结构:
structcategory_t{constchar*name;classref_tcls;structmethod_list_t*instanceMethods;structmethod_list_t*classMethods;structprotocol_list_t*protocols;structproperty_list_t*instanceProperties;structproperty_list_t*classProperties;method_list_t*methodsForMeta(boolisMeta){if(isMeta)returnclassMethods;elsereturninstanceMethods;}property_list_t*propertiesForMeta(boolisMeta,structheader_info*hi);}后编译的分类方法会被插入到类的方法列表前面,因此会先调用分类方法。
load方法调用顺序:
initialize的方法调用顺序:
两者的区别是load方法的调用是直接通过函数地址调用,而initialize方法是消息发送。initialize方法如果子类没有实现,那么父类的方法会被调用多次。
KVO是OC里面的对类的属性变化监听的技术。其原理用到了OC的runtime底层技术,体现了OC动态语言的强大之处。具体实现是这样的:当我们给一个实例对象XXX添加观察者调用addObserver:forKeyPath:方法后,该方法内部会进行一系列的处理。其内部会生成一个XXX的子类NSKVONotifing_XXX,并且XXX的isa会指向新生成的子类,因此在调用方看来还是跟之前一样。这个新派生的类重写了基类NSObject的class、_isKVOA、dealloc和父类被观察属性的setter方法。
被观察属性的setter方法被重写后,里面会调用_NSSetXValueAndNotifiy(),其内部会调用[supersetXX:xx]且之前之后分别插入[selfwillChangeValueForKey]和[selfdidChangeValueForKey]。后者会调用观察者实现的observerValueForKeyPath:ofObject:change:context:方法;使用KVO的时候应该注意,在合适的时机移除观察者,否则会触发NSRangeException异常;KVC会触发KVO,但是直接给成员变量赋值是不会触发的;
KVC,说的官方一点就是“键值编码”,通过KVC这种技术可以给一个类的私有属性进行赋值,比如UITextField的placholderLabel修改文字颜色等等。使用方法是setValue:forKey:或者是setValue:ForKeyPath:。其中value可以传nil。可以提一下的是,字典的方法setObject:forKey:中的两个参数都不可以为空。
下面谈谈KVC的实现原理。先去看有没有实现上面提到的两个赋值方法,如果有就直接赋值,如果没有就调用accessInstanceVariablesDirectly方法,如果返回了NO,就调用valueForUndefineKey:方法,并抛出异常。如果是返回了YES,说明可以直接访问成员变量,按照_key,_isKey,key,isKey的顺序找成员变量,如果找到了就赋值,反之就调用valueForUndefineKey:并抛出异常。
KVC的取值跟上面的类似,查找顺序是getKey,key,isKey,_Key,如果找到了就返回,没有找到也是调用accessInstanceVariableDirectly方法判断是否能够直接访问成员变量,如果可以的话,就按照_key,_isKey,key,isKey这个顺序查找。
通知中心维护了一个table,table里面包含了named表、nameless表、wildcard链表这三个数据结构;当我们调用addObserver:selector:name:object:方法时,其内部大概是这样实现的:1.构造一个Observation对象,该对象里面保存着object和selector,可以看做是一个链表的节点。2.判断传入的name是否为空。如果name不为空,以name为key从named的字典中取出一个n字典,然后从n字典里面以object为key取出observation,再然后把observation对象存入链表。3.判断传入的object是否为空。如果object不为空,以object为key从namedless字典中取出observation链表,将observation对象存入;4.如果name和object都为空,则将Observertion对象存入wildcard链表中。
发送通知的过程是先判断object,再判断name。name的优先级高于object。
同步的,会调用performSelector:withObject。但是有种情况可以不实时发送通知,而是在合适的时机发送,并没有开启线程,这种说法是指使用NSOperationQueue,指定发送时机,可以依赖Runloop等到下一次循环开始时发送。
是的,发送消息在哪个线程,接收消息就在哪个线程。
没有异步发送一说,只是利用了Runloop可以选择触发时机。
前者依赖后者。比如指定postStyle的时候NSPostWhenIdle表示在Runloop空闲的时候发送。此外还有NSPostASAP,尽可能快发送,NSPostNow多个相同的通知合并后马上发送。
使用block方式注册通知,在主队列响应。或者是在主线程注册machPort,这是负责线程通信的,当异步线程收到通知后,给machport发送消息。还可以在通知的回调方法里面,使用GCD主队列调度方法。
iOS9之后不会了,通知中心对Observer是弱引用的。
多次添加会多次响应。移除没事儿。
当给某个实例发送一个消息找不到方法时,就会进入所谓的消息转发流程。消息转发流程是这样的:
虽然是通过super关键字调用方法,本质上还是给当前对象发送消息,只不过是方法查找的起点是从父类开始:objcMsgSendSuper2:这么一个函数调用。该函数接收两个参数,第一个是一个结构体:
structobjc_super2{idreceiver;//消息接收者,也就是selfClasscurrent_class;//方法查找起点,也就是父类}第二个参数是SEL。像下面这种调用方式肯定会死循环的:
@implementation**B-(void)a{[superperformSelector:@selector(a)];}@end3.Block的本质block本质也是一种OC对象,其内部也有isa,是封装了函数调用和函数调用环境的OC对象。有三种类型block,即NSGlobalBlock、NSStackBlock、NSMallocBlock,这三种block都继承自NSBlock。没有访问auto变量的block属于Global类型的,保存在数据区。在ARC环境下,block作为返回值、usingBlock:方法传入的block、block被__strong指针指向、GCD里面的block,这些情况下编译器会自动识别并且会调用copy方法拷贝到堆上。
block的变量捕获:auto类型的变量是值捕获,static修饰的变量是指针捕获,全局变量不捕获。
Runtime是OC的基石,没有Runtime支持就没有OC、也没有OC的动态特性。通过Runtime的api可以实现很多功能,比如方法交换、动态生成类(KVO)、获取类的成员变量方法列表等信息、字典转模型、关联对象等等。常用api:
Runloop是APP运行的保证,事件处理、NStimer、autoreleasePoll内存管理、GCD回到主队列的执行、网络请求、perforSelector、屏幕刷新都是基于Runloop的,Runloop的底层实际上就是一个做了很多事情的while循环,前面说的那些事情就是在循环内部实现的。Runloop和线程是一一对应关系,每个线程都可以获取一个Runloop。
OC里面有可以通过NSRunloop来使用Runloop,也可以使用CFRunloopRef这套C语言的api使用。
Runloop和线程是以一一对应关系,保存在全局字典里面,线程对象作为key,Runloop为值。Runloop会在第一次获取的时候创建。子线程的Runloop默认是不开启的。Runloop会在线程结束后销毁。
CFRunloopRef是个结构体指针,结构体里面有三个集合分别是_commonModes、_commonModeItems、_modes,还有_currentMode和pthread。
每个mode里面包含source0、source1、timers、observers,如果没有这些东西,Runloop会退出。observer可以监听Runloop的六种状态,进入、退出、处理source、处理timer、开始等等、结束等待,枚举值分别为kCFRunloopEntry、kCFRunloopExit、kCFRunloopBeforeSources、kCFRunloopBeforeTimers、kCFRunloopBeforeWating、kCFRunlooopAfterWating。
Runloop的运行逻辑:
自从iOS5开始,OC的内存管理由MRC升级到了ARC,即自动引用计数。ARC是编译器、Runtime共同作用下完成的。OC的内存管理是通过引用计数来实现的,当一个对象的引用计数为0时,该对象就会被释放。引用计数是被存储在isa指针或者是SideTable里面,当isa指针提供的19位不够使用时就会存到SideTable。
SideTable是内存管理的方案之一,包含了引用计数表和弱引用表。__weak修饰的对象,就会被添加到弱引用表里面。一个iOS项目会全局维护一个SideTables,SideTables里面又有多个SideTable(真机情况下是8个)。SideTable里面有线程锁、RefcountMap、weak_table_t三个核心属性。
AutoreleasePool实际用途:
iOS里面常用的线程方案有NSThread、NSOperation、GCD。NSThread是基于p_thread的高层次封装,NSOperation是基于GCD的封装。NSOpeartion不能够直接使用,通常情况下我们会使用NSBlockOperation和NSInvocationOperation,前者的回调是block形式的,后者是方法。我们还可以使用NSOperationQueue控制并发数、添加依赖、取消任务等等多种操作。GCD是面向C语言的接口,通过block的形式执行任务,使用起来代码更加聚合。
使用GCD的时候需要注意死锁。使用sync函数向当前串行队列中添加任务时就会产生死锁。
TCP建立连接需要经过所谓的“三次握手”。
之所以是三次握手而不是两次,是因为双方都需要确定对方有发送消息和接收消息的能力,如果只有两次的话服务端是不知道客户端是否有接收消息的能力的。