一些小的tips
网络七层图
HTTPS建立连接
- 步骤 1: 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)
- 步骤 2: 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。
- 步骤 3: 之后服务器发送 Certificate 报文。报文中包含公开密钥证书。
- 步骤 4: 最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的 SSL 握手协商部分结束。
- 步骤 5: SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密。
- 步骤 6: 接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。
- 步骤 7: 客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。
- 步骤 8: 服务器同样发送 Change Cipher Spec 报文。
- 步骤 9: 服务器同样发送 Finished 报文。
- 步骤 10: 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP 请求。
- 步骤 11: 应用层协议通信,即发送 HTTP 响应。
- 步骤 12: 最后由客户端断开连接。断开连接时,发送 close_notify 报文。上图做了一些省略,这步之后再发送 TCP FIN 报文来关闭与 TCP 的通信。
- 在以上流程中,应用层发送数据时会附加一种叫做 MAC(Message Authentication Code)的报文摘要。MAC 能够查知报文是否遭到篡改,从而保护报文的完整性。
常量和预处理指令
- 如果用预处理指令定义常量,定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找和替换操作,即使有人重新定义了常量值编译器也不会产生警告信息这将导致应用程序中的常量值不一致
- 在实现文件中使用static const来定义,只在编译单元类可见的常量值,这种常量没有在全局符号表中
- 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中。所以名称要加以区分通常使用类名做前缀
Controller的生命周期
- loadView:加载view
- viewDidLoad:view加载完毕
- viewWillAppear:控制器的view将要显示
- viewWillLayoutSubviews:控制器的view将要布局子控件
- viewDidLayoutSubviews:控制器的view布局子控件完成
这期间系统可能会多次调用viewWillLayoutSubviews 、 viewDidLayoutSubviews 俩个方法 - viewDidAppear:控制器的view完全显示
- viewWillDisappear:控制器的view即将消失的时候
这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法 - viewDidDisappear:控制器的view完全消失的时候
用枚举表示状态,选项,状态码
- 用枚举表示状态机的状态,传递给方法的选项,以及状态码等值,给这些值起个易懂的名字
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可以同时使用,那么就将各个选项定义成2的幂,以便通过按位或操作将其组合起来
- 使用 NS_ENUM 与 NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型,这样做可以确保枚举使用开发者所选择的底层数据类型实现出来的,而不会采用编译器所选的类型
- 在处理枚举类型的switch语句中不要实现default分支。这样的话加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有的枚举
精简initialize 与 load 的实现代码
- 在加载阶段,如果类实现了load方法,那么系统就会调用到它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。
- 首次使用某个类之前,系统会像其发送initialize消息。由于此方法遵从普通的覆写规则,所有通常应该在里面判断当前要初始化的是哪个类。
- load 与 initialize 方法都应该实现的精简一些,这里有助于保持应用程序的响应能力,也能减少引入依赖环的几率。
- 无法在编译期设定的全局常量,可以放在initialize方法里初始化
NSTimer会保留其目标对象
- NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完成任务之后也会失效。
- 反复执行任务的计时器,很容易引起循环引用,如果这种计时器的目标对象有保留了计时器本身,肯定会导致循环引用,这种循环引用可能是直接发生的也可能是通过对象图里的其他对象发生的。
- 可以扩充NSTimer的其他功能,用块来打破循环引用,(目前公共接口已经提供,但是该函数创建的定时器没有自动加入到runlooph中)
给category添加属性,使用关联对象
- 可以使用objc_setAssocaiatedObject(id object,void *key,id value,objc_AssociationPolicy)该方法以给定的键和策略为某对象设置关联对象
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的拥有关系与非拥有关系
- 只有在其他做法不可行的时候才应选用关联对象,因为这种做法通常会引入难以查找的bug
对象的isa指针
- 所有对象父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
- 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的(对象方法列表,成员变量列表,属性列表...),类对象中也有一个isa指针指向元对象(meta class),元对象的内部存放的是类方法列表,类对象的内部还有一个superclass指针,指向它的父类对象
- 根对象就是NSObject,它的superclass指针指向nil
- 类对象既然称为对象,那它也是一个实例,类对象中也有一个isa指针指向他的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向的是自己,根元类的superclass指针指向的是NSObject类,如图:
[图片上传失败...(image-6564c9-1515825443606)]
objc_msgSend的作用
- 在Objective-C中,如果向某个对象传递消息,会使用动态绑定机制决定需要调用的方法,对象收到消息之后调用那个方法完全由运行时期决定的,[someObject messageName:parameter], someObject叫做接收者,messageName叫做选择子,选择子和参数一起称为消息(message),编译器看到这个消息的时候,将其转换成一条标准的C语言函数调用 void objc_msgSend(id self,SEL cmd,...),第一个参数是接收者,第二个参数是选择子,后面的参数是消息中的那些参数
- objc_msgSend函数会根据接收者与选择子的类型来调用适当的方法,objc_msgSend函数会根据选择子去查询接受者所属的类中查询方法列表(methods list),找到就跳转到实现代码,并缓存在快速映射表中(每个类都有快速映射表这样一块缓存),如果找不到,沿着继承体系向上查找,等找到合适的方法之后在跳转。如果最终还是找不到相符的方法,就执行消息转发(messageforwarding)操作。
- 方法列表(methods list)是一个映射表,key是选择子,value是函数的内存地址objc_msgSend函数正是通过这张表格寻找到应该执行的方法,并跳转到其实现的
- 如果要给父类发消息,例如[super messageName:parameter],就会被编译器转换为objc_msgSendSuper函数
消息转发机制
- 对象在收到无法解读的消息后,首选将调用所属类的 +(BOOL)resolveInstanceMethod:(SEL)selector; 这个方法的参数就是那个未知的选择子。在继续执行转发机制之前这个类有一个新增加一个处理这个选择子的方法,如果未实现的方法不是对象方法,而是类方法,那么运行时期的系统会调用+(BOOL)resolveClassMethod:(SEL)selector,这个方法和resolveInstanceMethod类似;使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插入带类里面就可以。
- 当前接收者还有第二次机会处理未知的选择子,运行期系统会问对象能不能把这条消息转给其他接收者处理,会调用对象的 - (id)forwardingTargetForSelector:(SEL)selector,方法参数是未知的选择子,接收者可以找到一个备援对象,将其返回,如果返回nil或者self,会启用完整的消息转发。
- 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
- 完整的消息转发会创建一个NSInvocation对象,把未处理的那条消息有关的全部细节都封装于其中,包含选择子,目标对象,和参数。传给目标对象的这个方法:-(void)forwardInvocation:(NSInvocation *)invocation;这个方法用来改变调用目标,或者不需要处理,但是会提前创建一个NSInvocation对象,开销更大一些,与备援接收者所处理的方案类似。
- 如果对象也没有实现-(void)forwardInvocation:(NSInvocation *)invocation;这个函数,会在继承体系中继续查找直到NSObject,如果最后调用了NSobject类的这个函数,会调用NSObject类的- (void)doesNotRecognizeSelector:(SEL)aSelector;这个函数抛出一个异常,表示选择子没有能得到有效的处理。
如图:
- 接收者每一步中均有机会处理消息,步骤越往后,处理的消息代价越大。最好在第一部完成,如果第一次完成,运行时的系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名的选择子,就不用在重新启动消息转发流程。若想在第三步里把消息转给备援的接收者,还不如把转发操作提前到第二步,应为第三步只是修改了调用目标,放在第二步会更简单一些,第三步会创建完整的NSInvocation对象。
method swizzling 方法交换
- 在运行期,可以向类中新增或者替换选择子所对应的方法实现
- 可以通过方法交换为已经有的方法增加新的功能,编写一个方法,在此方法中实现所需要的附加功能,并调用原有的方法实现。
- 一般来说只有调试程序的时候才需要在运行期修改方法实现,这种方法不宜乱用。
runloop和线程的关系
- 总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上runloop和线程是紧密相连的,可以这样说runloop是为了线程而生,没有线程它就没有存在的必要。runloop是线程的基础架构部分。Cocoa和CoreFundation都提供了runloop对象方便配置和管理线程的runloop,每个线程,包括主线程都有与之对应的runloop对象。
- 主线程的runloop是默认启动的,iOSAPP启动后会走UIApplicationMain()函数,这个函数会为mainThread设置一个runloop对象,所以iOSApp可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
- 对于其他的线程来说runloop默认是没有启动的,如果需要和用户交互,可以手动配置启动runloop,如果线程只是执行一个长时间的已经确定的任务,则不需要启动runloop。
- Cocoa中的NSRunLoop类并不是线程安全的,CoreFundation中的不透明类CFRunLoopRef是线程安全的,两种类型的runloop可以混合使用.
- runloop同时也负责autorelease pool的创建和释放,每当一个运行循环开始的时候,它会创建它都会自动创建autorelease pool,每当一个运行循环结束的时候它都会释放销毁autorelease pool(autorelease pool销毁的时候会对autorelease pool中所有的对象做一次release操作)
runloop的mode作用
作用:线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:ScrollView滑动时
- UIInitializationRunLoopMode:启动时
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的 Mode 有两个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
runloop的(input source)和(timer source)
runloop接收时间来自于两种不同的源输入源(input source)和定时源(timer source),两种源都使用程序的某一特定的处理例程来处理到达的事件 如图:
- 输入源(input source)
- 基于端口的输入源,由内核自动发送。Cocoa和Core Foundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到run loop。端口对象会自己处理创建和配置输入源。
在Core Foundation,你必须人工创建端口和它的run loop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。 - 自定义输入源,自定义输入源需要人工从其他的线程发送。创建自定义输入源必须使用Core Fundation里面的CFRunLoopSourceRef类型相关的函数来创建。可以使用回调函数来配置自定义输入源。Core Fundation会在配置源的不同地方调用回调函数,处理输入事件,在源从run loop移除的时候清理它。除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。
- Cocoa上的Selector源,Cocoa定义了自定义的输入源,允许你在任何线程执行selector方法,和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完后会自动从run loop里面移除。当在其他线程上面执行selector时,目标线程须有一个活动的run loop。对于你创建的线程,这意味着线程在你显式的启动run loop之前是不会执行selector方法的,而是一直处于休眠状态。NSObject类提供了类似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray *)array;
2 .定时源(timer source)
- 定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定的时间或者重复的时间间隔,定时源直接传递消息给处理例程,不会立即退出runloop,需要注意的是,尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的run loop的特定模式相关。如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到run loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次run loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不启动。