前言:面试题是参考网上不同文章,汇总而成的,有什么不足往见谅
1.KVO的实现原理
1.1:KVO也就是键值观察是一种能让对象观察到其他对象属性变化的通知
1.2:KVO是基于runtime机制的,当某个类的属性对象第一次被观察的时候,系统就会在运行期间动态的创建该类的一个派生类,这个派生类会重写这个类的任何被观察属性的setter方法,派生类会在被重写的setter方法中实现真正的通知机制.
1.3:每一个类的属性对象中都有一个isa指针来指向当前类,当一个类的属性(对象)被第一次观察的时候,系统会偷偷的将isa指针指向给动态生成的派生类,在给被监控属性赋值的时候执行的是派生类的setter方法
1.4:通知机制(键值观察)是依赖于NSObject的两个方法:willChangValueForkey: 和didChangevalueForKey: 在被观察者属性发生变化之前willChangValueForkey方法被调用,上一次的值就会被记录,当发生改变之后didChangevalueForKey的方法被调用,observeValueForKey:ofObject:change:context:也会被调用 就实现了通知的作用.
2.KVC的实现原理
2.1:KVC也就是键值编码就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性。而不是在编译时确定.
2.2:当通过kvc调用对象的时候,例如:[self valueForKey:@” someKey”]时,程序会自动通过以下几种不同的方法来解析这个调用:首先查找对象是否有 hello 这个方法,如果没找到,会继续查找对象是否带有 someKey 这个实例变量, 如果还没找到的话,程序会继续调用-(id)valueForUnderfinedKey: 这个方法,如果这个方法还是没有被实现的话,程序就会抛出一个NSUndefinedKeyException异常错误
2.3KVC查找方法的时候,不仅仅会查找someKey这个方法,还会查找getsomeKey这个方法,前面加一个get,或者_someKey以_getsomeKey这几种形式。同时,查找实例变量的时候也会不仅仅查找someKey这个变量,也会查找_someKey这个变量是否存在。
3.消息转发机制的原理
oc中调用方法其实就是像对象发送消息.
例如:
[person run];
这实际上就是在给person这个对象发送run消息.
那么他是怎么发送的消息的呢:当这个方法被调用的时候,系统会产看这个类中有没有定义这个方法或者实现这个方法,如果没有的话那就会用到了消息转发了.(这个是系统给予的补救机会,防止我们的程序crash,如果这个也行不通的话程序就会crash)
我们来写个列子进行说明
Person* p = [Person alloc]init];
[p run];
注意:run 方法是没有实现的
1.动态方法解析: 当外部调用run 方法的时候,由于run方法没有实现,这个时候系统会调用resolveInstanceMethod(当然,如果这个方法是一个类方法,就会调用resolveClassMethod)在这个方法中我们可以为person类动态的添加一个run方法的实现.
2.备用接受者: 现在我新建了一个Car类,并实现了一个run方法
如果我在方法1的resolveInstanceMethod这个里面没有动态的实现run方法,直接调用父类的方法,当程序运行的时候就会在car类中的run方法进行运行
3.重新制作方法签名:如果上面2个方法都没实现的话,我们可以利用系统提供的方法methodSignatureForSelector和forwardInvocation
methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。程序之所以crash的原因就是由于没有找到run对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。
4.weak的实现原理
Runtime维护了一个weak表,用来存储指向一个对象的所有的weak指针,这个weak表其实就是一个hash(哈希)表,key是所指对象的地址,value是weak指针的地址数组.
当初始化的时候,runtime会调用objc_initWeak函数,初始化一个新的weak指针来指向对象的地址;添加引用时,调用objc_storeWeak函数来更新指针指向,创建对应的weak表;释放的时候会调用clearDeallocating函数,这个函数首先是根据对象的地址获取weak指针的数组,然后遍历这个数组把对应的数据设置为nik,然后从weak表中删除,清楚对象记录.
5.main()开始之前还有那些过程
1)dyld 开始将程序二进制文件初始化
2)交由ImageLoader 读取 image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP)
3)由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4)runtime 接手后调用map_images做解析和处理
5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法
6)至此 所有的信息都被加载到内存中
7)最后dyld调用真正的main函数
6.字典实现原理
字典的底部原理就是一个哈希表,利用哈希表来实现key和value之间的存储和映射.哈希表的本质是一个数组,数组中的每一个元素称为箱子,箱子中存放的是键值对.
哈希表的存储过程
1.根据key来计算出它的哈希值h.
2.假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中。
3.如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突。
在使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。
哈希表还有一个重要的属性: 负载因子(load factor),它用来衡量哈希表的空/满程度,一定程度上也可以体现查询的效率,计算公式为:
负载因子 = 总键值对数 / 箱子个数
负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将自动扩容。
哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。
哈希表的扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。
5.ios中的响应链的工作原理
能够响应事件的对象都是UIResponder的子类对象,响应链的传递是系统通过不断查找下一个响应者来响应点击事件而形成的.一般传递的顺序是UIButton---->UIView----->UIViewControll---->UIWindow----->UIApplication----->AppDelagate 如果到达AppDelagate事件还没有被处理的话那么这个事件就会被废弃.
事件的传递的过程是和响应链的顺序是相反的,是通过hitTest:withEvent和pointInside:withEvent:这两个api来判断是否可以处理事件,hitTest:withEvent是来判断是否可以处理事件,pointInside:withEvent是来判断用户点击是否在该视图内,如果在该试图内的话会进一步的循环遍历该视图的子视图.这样就可以找到事件的第一响应者.
注意:控件不能响应的情况
1.userInteractionEnabled = NO
2.hidden = YES
3.透明度 alpha 小于等于0.01
4.子视图超出了父视图区域
子视图超出父视图,不响应的原因:因为父视图的pointInside:withEvent:方法返回了NO,就不会遍历子视图了。可以重写pointInside:withEvent:方法解决此问题。
6.对于Run Loop的理解
Run Loop 实际上就是一个对象,这个对象管理需要处理的事件和消息,提供一个入口函数,来进行相关的工作.在ios系统下,提供了两个对象: NSRunLoop和CFRunLoopRef.
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的.
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
6.1 :RunLoop,是多线程的法宝,即一个线程一次只能执行一个任务,执行完任务后就会退出线程。主线程执行完即时任务时会继续等待接收事件而不退出。非主线程通常来说就是为了执行6.2:某一任务的,执行完毕就需要归还资源,因此默认是不运行RunLoop的;
6.3:每一个线程都有其对应的RunLoop,只是默认只有主线程的RunLoop是启动的,其它子线程的RunLoop默认是不启动的,若要启动则需要手动启动;
6.4:在一个单独的线程中,如果需要在处理完某个任务后不退出,继续等待接收事件,则需要启用RunLoop;
6.5:NSRunLoop提供了一个添加NSTimer的方法,可以指定Mode,如果要让任何情况下都回调,则需要设置Mode为Common模式;
6.6:实质上,对于子线程的runloop默认是不存在的,因为苹果采用了懒加载的方式。如果我们没有手动调用[NSRunLoop currentRunLoop]的话,就不会去查询是否存在当前线程的RunLoop,也就不会去加载,更不会创建。
总结:线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
- SDWebImage实现原理
7.1:根据setImageWithURL:placeholderImage:options这个方法会先把placeholderImage图片展示出来,然后SDWebImageManager根据URL去处理图片.
7.2:根据SDWebImageManagerdownloadWithURL,先从SDImageCache缓存中查找图片是否已经存在,如果已经存在的话就给SDWebImageManager回调然后展示出来
7.3:如果内存中没有的话,就添加到队列开始中硬盘中进行查找图片,根据URLKey在硬盘缓存目录下尝试读取图片文件,如果查找到了图片就会添加到内存中进行缓存并展示
7.4:如果硬盘中也没有的话,就开始下载图片了,图片下载由 NSURLConnection 来做,下载完成后就展示,然后内存缓存和硬盘进行同时保存.
8.AFNetworking的实现原理
AFNetworking3.0是基于NSURLSession做了一系列的封装,他大致有5个功能模板.其中核心模板是网络通信模块(AFURLSessionManager、AFHTTPSessionManger).其中AFHTTPSessionManager是继承于AFURLSessionManager的,我们一般做网络请求都是用这个类,但是它本身是没有做实事的,只是做了一些简单的封装,把请求逻辑分发给父类AFURLSessionManager或者其它类去做。而其余的四个模块,均是为了配合网络通信或对已有UIKit的一个扩展工具包。