零散的记录, 摘抄与网络,具体在收集的博客那篇文章里
RunTime相关
- methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。
- 编译器会根据情况在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 或 objc_msgSend_fpret 五个方法中选择一个来调用。如果消息是传递给超类,那么会调用 objc_msgSendSuper 方法,如果消息返回值是数据结构,就会调用 objc_msgSendSuper_stret 方法,如果返回值是浮点数,则调用 objc_msgSend_fpret 方法
- 对象关联允许开发者对已经存在的类在 Category 中添加自定义的属性
- 要取出被关联的对象使用 objc_getAssociatedObject 方法即可,要删除一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可
objc_removeAssociatedObjects 方法将会移除源对象中所有的关联对象. - unrecognized selector sent to instance 0x7fd0a141afd0 但是在程序crash之前,Runtime会给我们动态方法解析的机会,消息发送的步骤大致如下:
- 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数了
- 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,如果 cache 找不到就找一下方法分发表,完了找得到就跳到对应的函数去执行,
- 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止 如果还找不到就要开始进入消息转发了
消息转发阶段
-
进入 resolveInstanceMethod: 方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕。
-
resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector: 方法,这是 Runtime 给我们的第二次机会,用于指定哪个对象响应这个 selector。返回nil,进入下一步(消息重定向),返回某个对象,则会调用该对象的方法。
-
若 forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector: 来指定方法签名,返回nil,表示不处理,若返回方法签名,则会进入下一步。
-
当第 methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等。
Objective-C 考虑method swizzling特殊情况
static inline void af_swizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
if (class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
复制代码
- class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
Adds a new method to a class with a given name and implementation. class_addMethod will add an override of a superclass's implementation, but will not replace an existing implementation in this class. To change an existing implementation, use method_setImplementation. - BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); Replaces the implementation of a method for a given class. This function behaves in two different ways: If the method identified by name does not yet exist, it is added as if class_addMethod were called. The type encoding specified by types is used as given. If the method identified by name does exist, its IMP is replaced as if method_setImplementation were called. The type encoding specified by types is ignored.
RunLoop相关
- CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
- CFRunLoopModeRef代表的是RunLoop的运行模式。
- 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
- 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
- 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
- kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes //这是一个占位用的Mode,不是一种真正的Mode
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
- kCFRunLoopEntry = (1UL << 0), //即将进入Runloop
- kCFRunLoopBeforeTimers = (1UL << 1), //即将处理NSTimer
- kCFRunLoopBeforeSources = (1UL << 2), //即将处理Sources
- kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
- kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
- kCFRunLoopExit = (1UL << 7), //即将退出runloop
- kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变};
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
复制代码
// 从字典中获取子线程的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 把当前子线程和对应的runloop保存到字典中
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
//线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 里
//CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
复制代码
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
-
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
-
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
-
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
runLoop与AutoReleasePool这篇文章有点看不懂
-
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
-
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
-
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
Call out
在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
复制代码
RunLoop休眠
- 其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。RunLoop的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件Darwin中的Mach来完成的(Darwin是开源的)。可以从下图最底层Kernel中找到Mach:
- Mach是Darwin的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在Mach中,进程、线程间的通信是以消息的方式来完成的,消息在两个Port之间进行传递(这也正是Source1之所以称之为Port-based Source的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数(事实上苹果提供的Mach API很少,并不鼓励我们直接调用这些API):
及简单内核知识
OS X的进化史
- OS X是Mac OS Classic和NeXTSTEP的融合。
- Darwin是操作系统的类UNIX核心,由kernel、XNU和运行时组成,Mach是Darwin的核心,是OS X和iOS的重要组成部分,OS X的Darwin是开源的,除OS X10.0对应Darwin 1.3.x之外,其他版本都符合:if (OSX.version == 10.x.y) Darwin.version = (4+x).y 从10.3(Panther)开始,苹果开发了Safari替代IE for Mac;从10.4.4(Tiger)开始,支持Intel x86架构;10.5(Leopard)有了Objective-C 2.0;10.6(Snow Leopard)开始完整支持64位,提供GCD,完全抛弃PPC架构。
iOS和OS X对比: - iOS基于ARM架构,而OS X基于Intel i386和x86_64。
- iOS内核代码依然闭源,OS X内核XNU则是开源的。
- iOS内核的编译稍有不同,关注的是嵌入式特性和一些新的API。
- iOS的系统GUI是SpringBoard,OS X为Aqua。
- iOS的内存管理要紧凑得多,因为移动设备没有几乎无穷的交换空间可以使用。
- iOS应用程序不允许访问底层UNIX API(即Darwin),也没有root访问权限,而且只能访问自己的目录内数据。
内核架构
- 内核架构设计类型有巨内核(UNIX、Linux)、微内核(Mach)、混合内核(XNU、Windows)。
- 内核态、用户态转换分为自愿转换、非自愿转换。
- 自愿转换:使用内核服务,即系统调用。
- 非自愿转换:发生异常、中断或处理器陷阱时。
- XNU中系统调用有4种:UNIX、MACH、MDEP(机器相关调用)、DIAG(诊断调用) 32位系统下,UNIX系统调用编号为正数,MACH为负数,64位则都为正数,最高位字节包含调用类型。
BSD层
- OS X有UNIX03认证,达到源码级兼容,即提供与UNIX统一的API。
- BSD层在Mach层之上,提供了POSIX API。但XNU的BSD不是完整的BSD,即移植部分BSD内容,如VFS和网络架构。 BSD的进程和线程都是在Mach提供原语的基础上进行了封装,BSD进程和线程对应有Mach的任务和线程。
- XNU中内核线程都是Mach线程,没有对应的BSD线程,同样内核任务kernel_task也没有对应的进程(因此其pid为0,表示没有进程pid)。
- UNIX模型中,进程不能被“创建”出来,只能通过fork()系统调用复制出来。vfork、fork、posix_spawn系统调用,底层都是由fork1()实现,只是传入参数不同。 除了DTrace,XNU在BSD层还提供了其他UNIX具有的ptrace,但功能大大缩水,如不能读写其他进程内存。
- Mach通过异常机制处理底层的陷阱,BSD则在异常机制之上构建了信号处理机制。操作系统和用户产生的信号先被Mach转换为异常,然后再由BSD产生信号。
- Mach-O,是Mach object文件格式的缩写,是一种可执行文件、目标代码、共享程序库、动态加载代码和核心DUMP。
多线程相关Pthread、NSThread、GCD、NSOperationQueue
- Objective-C 类属性 摘自 Xcode 8正式版中的说明:
Objective-C now supports class properties, which interoperate with Swift type properties.
They are declared as: @property (class) NSString *someStringProperty;.
They are never synthesized. (23891898)
复制代码
翻译如下:
Objective-C 现在支持类属性了,与OC 中的类属性对应的是Swift的类型属性。
它们是这样声明的:@property (class) NSString *someStringProperty;
类属性永远不会被自动合成。
复制代码
- GCD Grand Central Dispatch。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核
- NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列
- 使用NSOperation子类的方式有3种
- NSInvocationOperation
- NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应main的方法封装操作
- 自定义 Operation。自定义 Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑
NSOperation
- NSOperation可以调用start方法来执行任务,但默认是同步执行的 如果 将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
- 只要NSBlockOperation封装的操作数 >1,就会异步执行操作
NSOperationQueue
- alloc/init的NSOperatinoQueue队列默认就是并发,maxConcurrentOperationCount 默认等于 -1, 代表不限制, 可以创建N多线程
- 如果想实现串行, 那么就设置maxConcurrentOperationCount = 1,注意: 最大并发数, 不能设置为0, 否则任务不会被执行
网络相关
- HTTP本质上是一种协议,全称是Hypertext Transfer Protocol,即超文本传输协议。从名字上可以看出该协议用于规定客户端与服务端之间的传输规则,所传输的内容不局限于文本(其实可以传输任意类型的数据)。
- HTTP请求在iOS中用NSURLRequest与NSMutableRequest表示;HTTP响应用NSHTTPURLResponse表示。
- HTTP请求所必备的几大要素:请求行、请求头(headerField)、请求体(body);同理,响应也有状态行、响应头、实体内容。
- 请求行
请求行包含请求方法(Method)、请求统一资源标识符(URI)、HTTP版本号 - 请求头
- Host: 目标服务器的网络地址
- Accept: 让服务端知道客户端所能接收的数据类型,如text/html /
- Content-Type: body中的数据类型,如application/json; charset=UTF-8
- Accept-Language: 客户端的语言环境,如zh-cn
- Accept-Encoding: 客户端支持的数据压缩格式,如gzip
- User-Agent: 客户端的软件环境,我们可以更改该字段为自己客户端的名字,比如QQ music v1.11,比如浏览器Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/600.8.9 (KHTML, like Gecko) Maxthon/4.5.2
- Connection: keep-alive,该字段是从HTTP 1.1才开始有的,用来告诉服务端这是一个持久连接,“请服务端不要在发出响应后立即断开TCP连接”。关于该字段的更多解释将在后面的HTTP版本简介中展开。
- Content-Length: body的长度,如果body为空则该字段值为0。该字段一般在POST请求中才会有。
- POST请求的body请求体也有可能是空的,因此POST中Content-Length也有可能为0
- Cookie: 记录者用户信息的保存在本地的用户数据,如果有会被自动附上
-
请求体
真正需要发给服务端的数据 -
响应状态行
状态行是服务端返回给客户端的状态信息,包含HTTP版本号、状态码、状态码对应的英文名称。
HTTP/1.1 200 OK
复制代码
- 1XX:信息提示。不代表成功或者失败,表示临时响应,比如100表示继续,101表示切换协议
- 2XX: 成功
- 3XX: 重定向
- 4XX:客户端错误,很有可能是客户端发生问题,如亲切可爱的404表示未找到文件,说明你的URI是有问题的,服务器机子上该目录是没有该文件的;414URI太长
- 5XX: 服务器错误,比如504网关超时
NSURLSession
NSURLSession对象是一个会话,你可以把他当成是生产网络请求任务的工厂。NSURLSession有三种工作模式:
- Shared Session:获取单例对象。使用全局共享的会话,并且不能调整cache、cookie和证书等;也不能使用代理方法,因此将不能检测下载进度一类事件。
- Default Session:默认会话配置使用基于磁盘持久缓存,将凭据保存在用户的钥匙串中,默认将cookie存储在与NSURLConnection相同的共享cookie存储中。通过调用NSURLSessionConfiguration类中的defaultSessionConfiguration方法创建默认会话。
- Ephemeral Session:会把cacehe、cookie和用户凭据保存在RAM中而不是用户磁盘中,只有在你主动保存某个URL的内容到某个文件时才会保存数据,因此最大好处是可以保护用户隐私,适用于无痕浏览。可以缓存的数据大小取决于剩余RAM大小,当会话销毁后,所有临时数据均会清除。另外,在iOS中,应用程序挂起时保存在内存中的数据不会被清除,但在应用程序终止或系统内存不足时会被清除。通过调用NSURLSessionConfiguration类中的ephemeralSessionConfiguration创建Ephmeral Session。
- Background Session:允许在后台执行HTTP和HTTPS上传或下载任务。backgroundSessionConfigurationWithIdentifier:配置的会话由系统在单独的进程中控制数据传输。在iOS中,backgroundSessionConfigurationWithIdentifier:配置的会话可以使应用程序挂起或终止后依然进行传输数据。如果app是由系统终止并重新启动,app可以使用相同标志符创建新配置对象和会话,并恢复到终止时的传输状态;如果用户从多任务屏幕中终止app,系统会取消所有后台传输任务,此外,系统也不会重新启动app,必须由用户启动app后传输任务才会继续。通过调用NSURLSessionConfiguration类中的backgroundSessionConfigurationWithIdentifier:创建Background Session。
- 这些工作模式是由NSURLSessionConfiguration决定的。以下三种创建方式一一对应NSURLSession的三种工作模式,其实本质上还是NSURLSessionConfiguration内部属性所决定,也省得我们一一去设置。
- 从指定可用网络,到 cookie,安全性,缓存策略,再到使用自定义协议,启动事件的设置,以及用于移动设备优化的几个新属性
- NSURLSession 在初始化时会把配置它的 NSURLSessionConfiguration 对象进行一次 copy,并保存到自己的 configuration 属性中,而且这个属性是只读的。因此之后再修改最初配置 session 的那个 configuration 对象对于 session 是没有影响的。也就是说,configuration 只在初始化时被读取一次,之后都是不会变化的。
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
+ (NSURLSessionConfiguration *)backgroundSessionConfiguration:(NSString *)identifier;
复制代码
- SSL Pinning
- Https对比Http已经很安全,但在建立安全链接的过程中,可能遭受中间人攻击。防御这种类型攻击的最直接方式是Client使用者能正确鉴定Server发的证书【目前很多浏览器在这方面做的足够好,用户只要不在遇到警告时还继续其中的危险操作】,而对于Client的开发者而言,一种方式保持一个可信的根证书颁发机构列表,确认可信的证书,警告或阻止不是可信根证书颁发机构颁发的证书。
- SSL Pinning其实就是证书绑定,一般浏览器的做法是信任可信根证书颁发机构颁发的证书,但在移动端【非浏览器的桌面应用亦如此】,应用只和少数的几个Server有交互,所以可以做得更极致点,直接就在应用内保留需要使用的具体Server的证书。对于iOS开发者而言,如果使用AFNetwoking作为网络库,那么要做到这点就很方便,直接证书作为资源打包进去就好,AFNetworking会自动加载,具体代码就不贴了,nsscreencast已经有很好的tutorial。
afn
- 网络通信模块(AFURLSessionManager、AFHTTPSessionManger)
- 网络状态监听模块(Reachability)
- 网络通信安全策略模块(Security)
- 网络通信信息序列化/反序列化模块(Serialization)
- 对于iOS UIKit库的扩展(UIKit)
- https 服务器会返回一个包含公钥的受保护空间(也成为证书)
- 如果你的app服务端安装的是SLL颁发的CA,可以使用系统方法直接实现信任SSL证书,关于Apple对SSL证书的要求请参考:苹果官方文档CertKeyTrustProgGuide
这种方式不需要在Bundle中引入CA文件,可以交给系统去判断服务器端的证书是不是SSL证书,验证过程也不需要我们去具体实现。 - 基于AFNetWorking的SSL特定服务器证书信任处理
- NSURLSession调用代理didReceiveChallenge,对CA文件进行验证,并建立信任连接。
https
- HTTPS一般使用的加密与HASH算法如下:
- 非对称加密算法:RSA,DSA/DSS
- 对称加密算法:AES,RC4,3DES
- HASH算法:MD5,SHA1,SHA256
- 说明:其中非对称加密算法用于在握手过程中加密生成的密码,对称加密算法用于对真正传输的数据进行加密,而HASH算法用于验证数据的完整性。
传输过程使用服务器的公钥对在客户端随机生成的对称加密密钥加密,这样服务端能够获取对称加密密钥
- 幂等 :在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
架构相关
- 控制反转与依赖注入模式
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给外部容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是组件对象的控制权转移了,从程序代码本身转移到了外部容器。
如果Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式的new一个B的对象。采用依赖注入技术之后,A的代码只需要定义一个私有的B对象,不需要直接new来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。 - 组合设计模式
组合模式的主要意图是让树形结构中的每个节点具有相同的抽象接口。这样整个结构可作为一个统一的抽象结构使用,而不暴漏其内部表示。对每个节点(叶节点或组合体)的任何操作 - casa的博客view层
/**
view 代码层规定
---------
关于private methods,正常情况下ViewController里面不应该写
不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。对的,正常情况下ViewController里面一般是不会存在private methods的,这个private methods一般是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他做成一个模块,哪怕这个模块只有一个函数也行。
ViewController基本上是大部分业务的载体,本身代码已经相当复杂,所以跟业务关联不大的东西能不放在ViewController里面就不要放。另外一点,这个private method的功能这时候只是你用得到,但是将来说不定别的地方也会用到,一开始就独立出来,有利于将来的代码复用。
-----------
*/
#pragma mark - lifeCyle
//每一个delegate都把对应的protocol名字带上,delegate方法不要到处乱写,写到一块区域里面去
//例如#pragma mark - UITableViewDelegate
#pragma mark - delegate
#pragma mark - eventResponse
//getter和setter全部都放在最后
#pragma mark - getters&setters
复制代码
是否有必要让业务方统一派生ViewController?
答案是没有必要
我们可以使用OOP的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。
--
解决办法是使用aop
**iOS黑魔法-Method Swizzling 方法交换+利用category + runtime + 异常的捕获**
我们先给UIViewController添加一个**Category**,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。
由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
--
复制代码
- 关于MVC、MVVM等一大堆思想
其实这些都是相对通用的思想,万变不离其宗的还是在开篇里面我提到的那三个角色:数据管理者,数据加工者,数据展示者
关于胖Model和瘦Model
1. 什么叫胖Model?
胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。
2. 什么叫瘦Model?
瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。
瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。举个例子:
ViewModel本质上算是Model层(因为是胖Model里面分出来的一部分),所以View并不适合直接持有ViewModel,那么View一旦产生数据了怎么办?扔信号扔给ViewModel,用谁扔?ReactiveCocoa。
在MVVM中使用ReactiveCocoa的第一个目的就是如上所说,View并不适合直接持有ViewModel。第二个目的就在于,ViewModel有可能并不是只服务于特定的一个View,使用更加松散的绑定关系能够降低ViewModel和View之间的耦合度。
View <-> C <-> ViewModel <-> Model,所以使用MVVM之后,就不需要Controller的说法是不正确的。严格来说MVVM其实是MVCVM。
Controller夹在View和ViewModel之间做的其中一个主要事情就是将View和ViewModel进行绑定。
在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel,然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系,所以叫Controller/控制器就是这个原因。
其实归根结底就是一句话:在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。
然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,因为苹果本身并没有提供一个比较适合这种情况的绑定方法。
iOS领域里KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。
为什么会出现面向切片编程?
你针对每一个切片的间隙,塞一些代码进去,在程序正常进行1,2,3,4步的间隙可以跑到你塞进去的代码,那么你写这些代码就是面向切片编程。
你要想做到在每一个步骤中间做你自己的事情,不用AOP也一样可以达到目的,直接往步骤之间塞代码就好了。
但是事实情况往往很复杂,直接把代码塞进去,主要问题就在于:塞进去的代码很有可能是跟原业务无关的代码,在同一份代码文件里面掺杂多种业务,这会带来业务间耦合。
为了降低这种耦合度,我们引入了AOP。
复制代码
- 布局相关
2017-10-04 16:59:59.594811+0800 XXX[15662:803767] Begin pushViewController to [<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c07b643b0>]
viewDidLoad()---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
willMove(toParentViewController:)---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
viewWillDisappear---Optional("rootVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewWillAppear---Optional("pushVC")---UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
viewSafeAreaInsetsDidChange()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewWillLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewWillLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidLayoutSubviews()---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidAppear---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
viewDidDisappear---Optional("rootVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
didMove(toParentViewController:)---Optional("pushVC")---UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
2017-10-04 16:59:59.604563+0800 XXX[15662:803767] Did PushViewController [<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c0790d170>]->[<_TtCC8XXXTests27ContainerViewControllerTest20MockUIViewController: 0x7f9c07b643b0>] time = [0.009772]
--------------
可以看到,viewSafeAreaInsetsDidChange调用时机很早,在viewWillAppear后,这是为什么出现多余动画的原因。
并且“pushVC”的safeAreaInsets直到viewSafeAreaInsetsDidChange调用前,都是UIEdgeInsetsZero,之后才是正确的UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
并且viewSafeAreaInsetsDidChange后面会调用两次viewDidLayoutSubviews,所以我们应该把改变高度或布局的代码都写在viewDidLayoutSubviews里,这样就不会有多余的动画效果了。
需要注意viewDidLayoutSubviews可能会由别的操作频繁触发,所以如果调整safeArea布局的代码比较耗时,可以考虑加上一个状态标记,只在didChange后执行一次布局调整
最后的代码应该长这样
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
UIEdgeInsets safeAreaInsets = sgm_safeAreaInset(self.view);
CGFloat height = 44.0; // 导航栏原本的高度,通常是44.0
height += safeAreaInsets.top > 0 ? safeAreaInsets.top : 20.0; // 20.0是statusbar的高度,这里假设statusbar不消失
if (_navigationbar && _navigationbar.height != height) {
_navigationbar.height = height;
}
复制代码
动画相关
layer是通过树形结构组织起来的,共有三种树形结构
model layer tree (模型层树)
presentation tree (表示层树)
render tree (渲染层树)
当改变layer的值的时候,model layer tree的值会马上改变,通过render tree渲染,presentation tree以动画的形式展现layer的某个属性值的渐变过程。
model layer tree中的layer是我们通常意义说的layer,当我们修改layer的属性的时候,就会立刻修改model layer tree。
layer动画运行的过程:
其实我们给一个视图添加layer动画时,真正移动并不是我们视图本身,而是presentation layer 的一个缓存,
动画开始的时候presentationlayer开始移动,原始layer隐藏,动画结束时,presentation layer从屏幕上移除原始layer显示,这就解释了为什么我们的视图在结束后又回到了原的状态,因为它根本没动过。
复制代码
quartz2D
Quartz 2D能够提供的强大功能如下:
1. 透明层(transparency layers)
2. 阴影 基于path的绘图(path-based drawing)
3. 离屏渲染(offscreen rendering)
4. 复杂的颜色处理(advanced color management)
5. 抗锯齿渲染(anti-aliased rendering)
6. PDF创建,展示,解析(这部分不在这个系列之中) 配合Core Animation, OpenGL ES,UIKit完成复杂的功能 画板-The Graphics Context
7. 而Quartz 2D的容器就是CGContextRef数据模型。这种数据模型是C的结构体,存储了渲染到屏幕上需要的一切信息。
复制代码
事件处理
//作用:去寻找最适合的View
//什么时候调用:当一个事件传递给当前View,就会调用.
//返回值:返回的是谁,谁就是最适合的View(就会调用最适合的View的touch方法)
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//1.判断自己能否接收事件
if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
//2.判断当前点在不在当前View.
if (![self pointInside:point withEvent:event]) {
return nil;
}
//3.从后往前遍历自己的子控件.让子控件重复前两步操作,(把事件传递给,让子控件调用hitTest)
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
//取出每一个子控件
UIView *chileV = self.subviews[i];
//把当前的点转换成子控件从标系上的点.
CGPoint childP = [self convertPoint:point toView:chileV];
UIView *fitView = [chileV hitTest:childP withEvent:event];
//判断有没有找到最适合的View
if(fitView){
return fitView;
}
}
//4.没有找到比它自己更适合的View.那么它自己就是最适合的View
return self;
}
//作用:判断当前点在不在它调用View,(谁调用pointInside,这个View就是谁)
//什么时候调用:它是在hitTest方法当中调用的.
//注意:point点必须得要跟它方法调用者在同一个坐标系里面
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return YES;
}
复制代码
JavaScriptCore
JavaScriptCore 和 JavaScriptCore 框架
首先要区分JavaScriptCore 和 JavaScriptCore 框架(同后文中的JSCore)
JavaScriptCore框架 是一个苹果在iOS7引入的框架,该框架让 Objective-C 和 JavaScript 代码直接的交互变得更加的简单方便。
而JavaScriptCore是苹果Safari浏览器的JavaScript引擎,或许你听过Google的V8引擎,在WWDC上苹果演示了最新的Safari,
据说JavaScript处理速度已经大大超越了Google的Chrome,这就意味着JavaScriptCore在性能上也不输V8了。
JavaScriptCore框架其实就是基于webkit中以C/C++实现的JavaScriptCore的一个包装,在旧版本iOS开发中,很多开发者也会自行将webkit的库引入项目编译使用。
现在iOS7把它当成了标准库。
复制代码