本章将对ios开发技术底层实现的总结,其实关于ios开发中各种底层的实现,网上相关文章多到数不过来,而不且非常不错,我也没有自信我能比他们做的更好,因为毕竟每个人专研的东西不一样,本文主要正对三类用户!
- 资深的ios开发者,对底层做过专门研究,但是没有一个系统整理,或者说不能很清楚的表达。
- ios开发初学者,没有专门研究过底层或者相关源码的初学者,但是不太建议一开始就看,因为如果你没有过一点接触,看了也看不懂,或者看了也白看,最多就是留个印象在脑子了,对初学者来说,切记不能靠背或者了解,而且细细研究每一个技术点,再慢慢深入挖掘。
- ios开发待业程序员(面试)专用,不管你有没有接触过ios开发相关的底层,只要你是在准备找工作的程序员,我相信你看了绝对有用,但是并不能正面你就真的理解了,所以希望这对你来说只是短暂的,后续得花大量时间去专门研究才能在这条路上走得更远,不然你永远只是个码农!
好了,废话也不多说了,我们开始吧。。。。。
系统篇
- 内存管理
- Runtime
事件篇
- 事件传递
- 事件响应
代码篇
- Block
- __Block
实战
- KVO
- KVC
高级
- GCD
全栈篇
- JSPatch
- React Native
必备篇
- 多线程
- 网络
- 数据持久化
通用篇
- 数组
- 字典
- 集合
#写在最后
系统篇
内存管理
-
黄金法则
- 如果一个对象使用了alloc,[mutable] copy,retain,那么你必须使用相应的release或autonrelease
MRC:
手动管理内存(retain, release, autorelease,不多说) 持有对象,retain +1 ,引用计数加1, 释放对象:release -1, 引用计数减1,当引用计数为0时,会自动释放内存. autorelease对象内存的管理放到autoreleasepool中, 当pool drain时,回收内存. (这是基于 objective-c的运行时特性和垃圾回收机制)
ARC:
手动管理内存, 这是xcode4.x版本的特性,(4.1及以前没有,我从4.6开始的), 原理是:在编译代码的时候为你自动在合适的位置插入release 和 autorelease, (运行时处理垃圾回收就如何MRC一样).
总结: ARC机制拥有和MRC一样的效率, ARC通过在部分优化和在最合适的地方完成引用计数的维护,所以支持使用ARC.
规则
规则:
-
1、Objective-C类中实现了引用计数器,对象知道自己当前被引用的次数
-
2、最初对象的计数器为1
-
3、如果需要引用对象,可以给对象发送一个retain消息,这样对象的计数器就加1
-
4、当不需要引用对象了,可以给对象发送release消息,这样对象计数器就减1
-
5、当计数器减到0,自动调用对象的dealloc函数,对象就会释放内存
-
6、计数器为0的对象不能再使用release和其他方法
Runtime
一套纯低层的C语言库 平时我们编写的OC代码都会转成Runtime去执行
特性:
动态类型:程序直到执行时才能确定所属的类。
动态绑定:程序直到执行时才能确定实际要调用的方法。
动态加载:根据需求加载所需要的资源
Runtime消息机制
首先通过obj的isa指针找到obj对应的class。
- 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
- 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
- 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,
- 在Class中先去cache中 通过SEL查找对应函数method,找到就执行对应的实现。
- 若cache中未找到,再去methodList中查找,找到就执行对应的实现。
- 若methodlist中未找到,则取superClass中查找(重复执行以上两个步骤),直到找到最根的类为止。
- 若任何一部能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
- 如果以上都不能找到,则会开始进行消息转发
消息转发
- 1.动态方法解析:向当前类发送 resolveInstanceMethod: 信号,检查是否动态向该类添加了方法。(迷茫请搜索:@dynamic)
- 2.快速消息转发:检查该类是否实现了 forwardingTargetForSelector: 方法,若实现了则调用这个方法。若该方法返回值对象非nil或非self,则向该返回对象重新发送消息。
- 3.标准消息转发:runtime发送methodSignatureForSelector:消息获取Selector对应的方法签名。返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出
总结就是: 在一个函数找不到时,OC提供了三种方式去补救:
- 1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
- 2、调用forwardingTargetForSelector让别的对象去执行这个函数
- 3、调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。 如果都不中,调用doesNotRecognizeSelector抛出异常。
事件篇
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view
事件的传递和响应的区别:
- 事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
事件响应
响应者链的事件传递过程:
1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4>如果UIApplication也不能处理该事件或消息,则将其丢弃
事件传递
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
4.最合适的view会调用自己的touches方法处理事件
5.touches默认做法是把事件顺着响应者链条向上抛。
代码篇
Block的底层实现
一句话:
栈地址和对地址值的拷贝
block就是一个里面存储了指向函数体中包含定义block时的代码块的函数指针,以及block外部上下文变量等信息的结构体。
Block结构体中含有isa指针,这就证明了Block其实就是对象,并具有一般对象的所有功能。这个isa指针被初始化为 NSConcreteStackBlock 或者 NSConcreteGlobalBlock 类的地址。在没有开启ARC的情况下,如果Block中包含有局部变量则isa被初始化为前者,否则就被初始化为后者。而当ARC开启后,如果Block中包含有局部变量则isa被初始化为 NSConcreteMallocBlock ,否则就被初始化为 NSConcreteGlobalBlock 。invoke是一个函数指针,它指向的是Block被转换成函数的地址。最后的imported variables部分是Block需要访问的外部的局部变量,他们在编译就会被拷贝到Block中,这样一来Block就是成为一个闭包了。
__block底层实现
一句话:传值 和传址
block打印C++源码可以看到Block_byref_a_0结构体,这个结构体中含有isa指针,所以也是一个对象,它是用来包装局部变量a的。当block被copy到堆中时,Persontest_block_impl_0的拷贝辅助函数Persontest_block_copy_0会将__Block_byref_a_0拷贝至堆中,所以即使局部变量所在堆被销毁,block依然能对堆中的局部变量进行操作
这样做是为了保证操作的值始终是堆中的拷贝,而不是栈中的值。(处理在局部变量所在栈还没销毁,就调用block来改变局部变量值的情况,如果没有__forwarding指针,则修改无效)
实战篇
KVO/KVC
VC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NSObject类实现的NSKeyValueCoding/NSKeyValueObserving非正式协议的形式被定义为基础框架的一部分。从协议的角度来说,KVC/KVO本质上是定义了一套让我们去遵守和实现的方法。 当然,KVC/KVO实现的根本是Objective-C的动态性和runtime,这在后文的原理部分会有详述。 另外,KVC/KVO机制离不开访问器方法的实现,这在后文中也有解释。
1、KVC简介
全称是Key-value coding,翻译成键值编码。顾名思义,在某种程度上跟map的关系匪浅。它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制。
2、KVO简介
全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。
KVO的底层实现(基于KVC-》运行时)
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源
isa指针指向的其实是类的元类,如果之前的类名为:Person,那么被runtime更改以后的类名会变成:NSKVONotifying_Person。 新的NSKVONotifying_Person类会重写以下方法: 增加了监听的属性对应的set方法,class,dealloc,_isKVOA
KVC的底层实现
KVC运用了一个isa-swizzling技术。isa-swizzling就是类型混合指针机制。KVC主要通过isa-swizzling,来实现其内部查找定位的。isa指针,如其名称所指,(就是is a kind of的意思),指向维护分发表的对象的类。该分发表实际上包含了指向实现类中的方法的指针,和其它数据。
一个对象在调用setValue的时候,
- (1)首先根据方法名找到运行方法的时候所需要的环境参数。
- (2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。
- (3)再直接查找得来的具体的方法实现。
高级
GCD底层实现
GCD内部是怎么实现的
- 1 IOS和OS X的核心是XNU内核,GCD是基于XUN内核实现的
- 2 GCD的API全部在libdispatch库中
-
3 GCD的底层实现主要有Dispatch Queue 和Dispatch Source
- Dispatch Queue :管理block操作
- Dispatch Source :处理事件(比如线程间通信)
NSOperationQueue 和GCD的区别和类似的地方
- 1 GCD是纯C语言的API, NSOperationQueue是基于GCD的OC版本封装
- 2 GCD只支持FIFO的队列,NSOperationQueue可以很方便地调整执行顺序 设置最大并发数量
- 3 NSOperationQueue 可以轻松地在operation 间设置依赖关系,而GCD需要写很多的代码
- 4 NSOperationQueue支持KVO,可以监测operation是否正在执行(is Executed),是否结束(is finished),是否取消( is canceld);
- 5 GCD的执行速度比NSOperationQueue快
全栈篇
JSpatch底层实现
JSPatch 能做到通过 JS 调用和改写 OC 方法最根本的原因是 Objective-C 是动态语言,OC 上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
也可以替换某个类的方法为新的实现:
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");
还可以新注册一个类,为类添加方法:
Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
对于 Objective-C 对象模型和动态消息发送的原理已有很多文章阐述得很详细,这里就不详细阐述了。理论上你可以在运行时通过类名/方法名调用到任何 OC 方法,替换任何类的实现以及新增任意类。所以 JSPatch 的基本原理就是:JS 传递字符串给 OC,OC 通过 Runtime 接口调用和替换 OC 方法。这是最基础的原理,实际实现过程还有很多怪要打,接下来看看具体是怎样实现的。
总结:
使用JS利用OC的动态特性,执行我们想要执行的代码
React Native
RN主要的通信在于java与js之间,平常我们写的jsx代码最终会调用到原生的View。上一篇博客我们也了解到了要新建一个原生模块需要在java层和js层分别写一个Module
特点:
-
可以基于 React Native使用 javascript 编写应用逻辑,UI 则可以保持全是原生的。这样的话就没有必要就 html5 的 UI 做出常见的妥协;
-
React 引入了一种与众不同的、略显激进但具备高可用性的方案来构建用户界面。长话短说,应用的 UI 简单通过一个基于应用目前状态的函数来表达。
RN总共分为三层,java层,C++层,js层
- Java层:java层就是app原生代码,它通过启动C++层的javascript解析器javascriptCore来执行js代码,从而构建原生UI等。java层依赖于众多优秀开源库,在图片处理使用的是Fresco,网络通信使用的是okhttp,Json解析工具用jackson,动画库用NineOldandroids等,在java层原生的功能均封装为Module,如Toast和Log等。
- C++层:c++层最主要是封装了JavaScriptCore,它是一个全新的支持ES6的webKit。Bridge连接了java与js之间的通信。解析js文件是通过JSCExectutor进行的。
- Js层:主要处理事件分发及UI Layout,平常开发最常用的。通用jsx来写业务代码,通过flexbox来实现布局。不依赖DOM。由于react有 DOM diff这个算法,所以它的效率非常高。 通信机制
在Java层与Js层的bridge分别存有相同一份模块配置表,Java与Js互相通信时,通过将里配置表中的moduleID,methodID转换成json数据的形式传递给到C++层,C++层传送到js层,通过js层的的模块配置表找到对应的方法进行执行,如果有callback,则回传给java层。这里只是大概介绍。
总结:
- 在程序启动的时候,首先会调用ReactActivity的onCreate函数中,我们会去创建一个ReactInstanceManagerImpl对象。通过ReactRootView的startReactApplication方法开启整个RN世界的大门。
- 在这个方法中,我们会通过一个AsyncTask去创建ReactContext
- 在创建ReactContext中,我们把我们自己注入和CoreModulesPackage通过processPackage方法将其中的各个modules注入到了对应的Registry中。最后通过CatalystInstanceImpl中的ReactBridge将NativeModule和JSModule注册表通过jni传输到了JS层。
- java调用js时,会在ReactApplicationContext创建的时候存入注册表类JavaScriptModuleRegistry中,同时通过动态代理生成代理实例,并在代理拦截类JavaScriptModuleInvocationHandler中统一处理发向Javascript的所有通信请求。
- JSCExecutor将所有来自Java层的通信请求封装成Javascript执行语句。
- 接着在js层中的MessageQueue里匹配ModuleId和MethodId。找到调用模块。
- 如果是js层调用java层,js最终都会调用_nativeCall方法,通过flushedQueue将this.queue返回给Bridger。
- C++层调用PlatformBridgeCallback对象的onCallNativeModules方法,执行makeJavaCall方法,里面最终通过env->CallVoidMethod调用了Java层的方法。
- 调用Java层NativeModulesReactCallback的call方法,通过moduleID从保存在其内部的NativeModule映射表,匹配到需要被执行的NativeModule对象,再通过methodID匹配到所要调用的方法。通过invoke反射方式执行NativeModule的方法。