×

iOS知识点总结

96
Kk太阳
2016.03.14 18:33* 字数 7756

内容均转自标哥的技术博客 只是按照自己的习惯进行简单的整理

1、对数组中的元素去重复

NSArray *array = @[@"12-11", @"12-11", @"12-11", @"12-12", @"12-13", @"12-14"];

  • 1.第一种方法:开辟新的内存空间,然后判断是否存在,若不存在则添加到数组中,得到最终结果的顺序不发生变化。效率分析:时间复杂度为O ( n2 ):
NSMutableArray *resultArray = [[NSMutableArray alloc] initWithCapacity:array.count];
// 外层一个循环
for (NSString *item in array) {

   // 调用-containsObject:本质也是要循环去判断,因此本质上是双层遍历
   // 时间复杂度为O ( n^2 )而不是O (n)
    if (![resultArray containsObject:item]) {
      [resultArray addObject:item];
    }
}
NSLog(@"resultArray: %@", resultArray);

//补充:原来集合操作可以通过valueForKeyPath来实现的,去重可以一行代码实现:
array = [array valueForKeyPath:@"@distinctUnionOfObjects.self"];
NSLog(@"%@", array);

但是返回的结果是无序的,与原来的顺序不同。大家可以阅读:Collection Operators

  • 2.第二种方法:利用NSDictionary去重,字典在设置key-value时,若已存在则更新值,若不存在则插入值,然后获取allValues。若不要求有序,则可以采用此种方法。若要求有序,还得进行排序。效率分析:只需要一个循环就可以完成放入字典,若不要求有序,时间复杂度为O(n)。若要求排序,则效率与排序算法有关:
NSMutableDictionary *resultDict = [[NSMutableDictionary alloc] initWithCapacity:array.count];
for (NSString *item in array) {
    [resultDict setObject:item forKey:item];
}
NSArray *resultArray = resultDict.allValues;
NSLog(@"%@", resultArray);
//需要结果有序的话  就用快速枚举排序
  • 3.利用集合NSSet的特性(确定性、无序性、互异性),放入集合就自动去重了。但是它与字典拥有同样的无序性,所得结果顺序不再与原来一样。如果不要求有序,使用此方法与字典的效率应该是差不多的。效率分析:时间复杂度为O (n):
NSSet *set = [NSSet setWithArray:array];
NSArray *resultArray = [set allObjects];
NSLog(@"%@", resultArray);

//使用过有序集合
NSOrderedSet *set = [NSOrderedSet orderedSetWithArray:array];
NSLog(@"%@", set.array);

2、说说以下元素的特性和作用

NSArray NSSet NSDictionary与NSMutableArray NSMutableSet NSMutableDictionary

特性:

  • NSArray表示不可变数组,是有序元素集,只能存储对象类型,可通过索引直接访问元素,而且元素类型可以不一样,但是不能进行增、删、改操作;NSMutableArray是可变数组,能进行增、删、改操作。通过索引查询值很快,但是插入、删除等效率很低。
  • NSSet表示不可变集合,具有确定性、互异性、无序性的特点,只能访问而不能修改集合;NSMutableSet表示可变集合,可以对集合进行增、删、改操作。集合通过值查询很快,插入、删除操作极快。
  • NSDictionary表示不可变字典,具有无序性的特点,每个key对应的值是唯一的,可通过key直接获取值;NSMutableDictionary表示可变字典,能对字典进行增、删、改操作。通过key查询值、插入、删除值都很快。
    作用
  • 数组用于处理一组有序的数据集,比如常用的列表的dataSource要求有序,可通过索引直接访问,效率高。
  • 集合要求具有确定性、互异性、无序性,在iOS开发中是比较少使用到的,笔者也不清楚如何说明其作用
  • 字典是键值对数据集,操作字典效率极高,时间复杂度为常量,但是值是无序的。在ios中,常见的JSON转字典,字典转模型就是其中一种应用。

3、简单描述一下XIB与Storyboards,说一下他们的优缺点。

参考答案:

笔者倾向于纯代码开发,所以所提供的参考答案可能具有一定的个人感情,不过还是给大家说说笔者的想法。

优点:

  • XIB:在编译前就提供了可视化界面,可以直接拖控件,也可以直接给控件添加约束,更直观一些,而且类文件中就少了创建控件的代码,确实简化不少,通常每个XIB对应一个类。
  • Storyboard:在编译前提供了可视化界面,可拖控件,可加约束,在开发时比较直观,而且一个storyboard可以有很多的界面,每个界面对应一个类文件,通过storybard,可以直观地看出整个App的结构。

缺点:

  • XIB:需求变动时,需要修改XIB很大,有时候甚至需要重新添加约束,导致开发周期变长。XIB载入相比纯代码自然要慢一些。对于比较复杂逻辑控制不同状态下显示不同内容时,使用XIB是比较困难的。当多人团队或者多团队开发时,如果XIB文件被发动,极易导致冲突,而且解决冲突相对要困难很多。
  • Storyboard:需求变动时,需要修改storyboard上对应的界面的约束,与XIB一样可能要重新添加约束,或者添加约束会造成大量的冲突,尤其是多团队开发。对于复杂逻辑控制不同显示内容时,比较困难。当多人团队或者多团队开发时,大家会同时修改一个storyboard,导致大量冲突,解决起来相当困难。

4、请把字符串2015-04-10格式化日期转为NSDate类型

参考答案:

NSString *timeStr = @"2015-04-10";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd";
formatter.timeZone = [NSTimeZone defaultTimeZone];
NSDate *date = [formatter dateFromString:timeStr];
// 2015-04-09 16:00:00 +0000
NSLog(@"%@", date);

5、在App中混合HTML5开发App如何实现的。在App中使用HTML5的优缺点是什么?

参考答案:

在iOS中,通常是通常UIWebView来实现,当然在iOS8以后可以使用WKWebView来实现.有以下几种实现方法:

  • 通过实现UIWebView的代理方法来拦截,判断scheme是否是约定好的,然后iOS调用本地相关API来实现:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
 

优缺点:

  • iOS加入H5响应比原生要慢很多,体验不太好,这是缺点。
  • iOS加入H5可以实现嵌入别的功能入口,可随时更改,不用更新版本就可以上线,这是最大的优点

6、请描述一下同步和异步,说说它们之间的区别。

参考答案:

首先,我们要明确一点,同步和异步都是在线程中使用的。在iOS开发中,比如网络请求数据时,若使用同步请求,则只有请求成功或者请求失败得到响应返回后,才能继续往下走,也就是才能访问其它资源(会阻塞了线程)。网络请求数据异步请求时,不会阻塞线程,在调用请求后,可以继续往下执行,而不用等请求有结果才能继续。

区别:

  • 线程同步:是多个线程访问同一资源时,只有当前正在访问的线程访问结束之后,其他线程才能开始访问(被阻塞)。
  • 线程异步:是多个线程在访问竞争资源时,可以在空闲等待时去访问其它资源(不被阻塞)。

7、请简单描述一下队列和多线程的使用原理。

参考答案:

在iOS中队列分为以下几种:

  • 串行队列:队列中的任务只会顺序执行
dispatch_queue_t q = dispatch_queue_create("...", DISPATCH_QUEUE_SERIAL);
  • 并行队列: 队列中的任务通常会并发执行
 
dispatch_queue_t q = dispatch_queue_create("......", DISPATCH_QUEUE_CONCURRENT);
  • 全局队列:是系统的,直接拿过来(GET)用就可以;与并行队列类似
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  • 主队列:每一个应用程序对应唯一一个主队列,直接GET即可;在多线程开发中,使用主队列更新UI
dispatch_queue_t q = dispatch_get_main_queue();

上面这四种是针对GCD来讲的,串行队列中的任务只能一个个地执行,在前一个没有执行完毕之前,下一个只能等待。并行队列可以并发地执行任务,因此多个任务之间执行的顺序不能确定,当添加一个新的任务时,交由GCD来判断是否要创建新的新的线程。

大家可以阅读图片多线程,也许更明了:

8、描述一下iOS的内存管理,在开发中对于内存的使用和优化包含哪些方面。我们在开发中应该注意哪些问题。

参考答案:

内存管理准则:谁强引用过,谁就在不再使用时使引用计数减一。

对于内存的使用和优化常见的有以下方面:

  • 重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用。
  • 尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能。
  • 不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多。
  • 选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
  • gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。
  • 延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。
  • 数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。
  • 处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
  • 重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。
  • 避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。
  • 使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存。
  • 正确选择图片加载方式:详情阅读细读UIImage加载方式

9、plist文件是用来做什么的。一般用它来处理一些什么方面的问题。

参考答案:

plist是iOS系统中特有的文件格式。我们常用的NSUserDefaults偏好设置实质上就是plist文件操作。plist文件是用来持久化存储数据的。

我们通常使用它来存储偏好设置,以及那些少量的、数组结构比较复杂的不适合存储数据库的数据。比如,我们要存储全国城市名称和id,那么我们要优先选择plist直接持久化存储,因为更简单。

10、iOS中缓存一定量的数据以便下次可以快速执行,那么数据会存储在什么地方,有多少种存储方式?

参考答案:

11、请简单写出增、删、改、查的SQL语句。

参考答案:

数据库的简单操作,还是会的,大学可没白学。

增:

insert into tb_blogs(name, url) values('aaa','http://www.baidu.com');

删:

delete from tb_blogs where blogid = 1;

改:

update tb_blogs set url = 'www.baidu.com' where blogid = 1;

查:

select name, url from tb_blogs where blogid = 1;

12、在提交苹果审核时,遇到哪些问题被拒绝,对于被拒绝的问题是如何处理的。

参考答案:
·····

13、请写出有多少有方法给UIImageView添加圆角?

1.最直接的方法就是使用如下属性设置:

imgView.layer.cornerRadius = 10;
// 这一行代码是很消耗性能的
imgView.clipsToBounds = YES;

好处是使用简单,操作方便。坏处是离屏渲染(off-screen-rendering)需要消耗性能。对于图片比较多的视图上,不建议使用这种方法来设置圆角。通常来说,计算机系统中CPU、GPU、显示器是协同工作的。CPU计算好显示内容提交到GPU,GPU渲染完成后将渲染结果放入帧缓冲区。

简单来说,离屏渲染,导致本该GPU干的活,结果交给了CPU来干,而CPU又不擅长GPU干的活,于是拖慢了UI层的FPS(数据帧率),并且离屏需要创建新的缓冲区和上下文切换,因此消耗较大的性能。

2.给UIImage添加生成圆角图片的扩展API:

- (UIImage *)hyb_imageWithCornerRadius:(CGFloat)radius {
  CGRect rect = (CGRect){0.f, 0.f, self.size};
  
  UIGraphicsBeginImageContextWithOptions(self.size, NO, UIScreen.mainScreen.scale);
  CGContextAddPath(UIGraphicsGetCurrentContext(),
                   [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
  CGContextClip(UIGraphicsGetCurrentContext());
  
  [self drawInRect:rect];
  UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  
  UIGraphicsEndImageContext();
  
  return image;
}
 
//然后调用时就直接传一个圆角来处理:
gView.image = [[UIImage imageNamed:@"test"] hyb_imageWithCornerRadius:4];

这么做就是on-screen-rendering了,通过模拟器->debug->Color Off-screen-rendering看到没有离屏渲染了!(黄色的小圆角没有显示了,说明这个不是离屏渲染了)

3.在画之前先通过UIBezierPath添加裁剪,但是这种不实用:

- (void)drawRect:(CGRect)rect {
  CGRect bounds = self.bounds;
  [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
 
  [self.image drawInRect:bounds];
}

通过mask遮罩实现
这里不细说了,个人感觉不如第二种好用、通用

14.请描述事件响应者链的工作原理

参考答案:

iOS使用hit-testing寻找触摸的view。 Hit-Testing通过检查触摸点是否在关联的view边界内,如果在,则递归地检查该view的所有子view。在层级上处于lowest(就是离用户最近的view)且边界范围包含触摸点的view成为hit-test view。确定hit-test view后,它传递触摸事件给该view。

官方小例子事件响应者链如下图所示:

  • 触摸点在view A中,所以要先检查子view B和C。
  • 触摸点不在view B中,但在C中,所以检查C的子view D和E。
  • 触摸点不在D中,但在E中。View E是这个层级上处于lowest的view的边界范围包含触摸点,所以它成为了hit-test view。

Hit-test view是处理触摸事件的第一选择,如果hit-test view不能处理事件,该事件将从事件响应链中寻找响应器,直到系统找到一个处理事件的对象。若不能处理,则就有事件传递链了,继续看下面的事件传递链。

事件传递链如下图所示:


左半图:

  • initial view若不能处理事件,则传到其父视图view
  • view若不能处理,则传到其父视图,因为它还不是最上层视图
  • 这里view的父视图是view controller的view,因为这个view也不能处理事件,因此传给view controller
  • 若view controller也不能处理此事件,则传到window
  • 若window也不能处理此事件,则传到app单例对象Application
  • 若UIApplication单例对象也不能处理,则表示无效事件

右半图:

  • initial view一直传递直到最上层view(原话:A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.)
  • topmost view传递事件到它所在的控制器(原话:The topmost view passes the event to its view controller.)
  • view controller传递事件到topmost view的父视图,重复前三步,走到到达root controller(原话:passes the event to its topmost view’s superview. Steps 1-3 repeat until the event reaches the root view controller.)
  • 由root控制器传递事件到window(原话:The root view controller passes the event to the window object.)
  • 若window也不能处理此事件,则传到app单例对象Application
  • 若UIApplication单例对象也不能处理,则表示无效事件
    为了解答这个小题目,翻阅了官方文档,由于内容较多,这里不说那么多,若要了解更多,参考官方文档吧:Event Handling Guide for iOS

15如何避免使用block时发生循环引用

参考答案:

关于block循环引用问题是非常常见的,但是很多人没有深入研究过,xcode没有提示警告就以为没有形成循环引用了。笔者也见过很多高级iOS开发工程师的同事,使用block并不会分析是否形成循环引用。

推荐阅读iOS Block循环引用精讲

16、请比较GCD与NSOperation的异同

参考答案:

  • 相同点:GCD和NSOperation都是苹果提供的多线程实现方案。
  • 不同点:GCD是轻量级的纯C写的多线程实现方案,使用起来非常方便,在开发中大量使用,但是对于取消和暂停线程就比较麻烦些。而NSOperation是面向对象的,兼容KVO,对于取消和暂停任务是非常容易的。
    更详细地,推荐阅读:iOS图解多线程

17、请写出NSTimer使用时的注意事项(两项即可)

说到NSTimer这个定时器类,要使用好它,还得了解Run Loop,因为在不同的run loop mode下,定时器不都会回调的。

mode主要是用来指定事件在运行循环中的优先级的,分为:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态

  • UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode

  • UIInitializationRunLoopMode:run loop启动时,会切换到该mode

  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
    苹果公开提供的Mode有两个:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

  • NSRunLoopCommonModes(kCFRunLoopCommonModes)
    如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。

如果想更深入地了解RunLoop,请参考iOS之Run Loop详解

如果想要销毁timer,则必须先将timer置为失效,否则timer就一直占用内存而不会释放。造成逻辑上的内存泄漏。该泄漏不能用xcode及instruments测出来。 另外对于要求必须销毁timer的逻辑处理,未将timer置为失效,若每次都创建一次,则之前的不能得到释放,则会同时存在多个timer的实例在内存中。

参考答案:

注意timer添加到runloop时应该设置为什么mode
注意timer在不需要时,一定要调用invalidate方法使定时器失效,否则得不到释放

18、说说Core Animation是如何开始和结束动画的

19、线程和进程的区别不正确的是

  • A 进程和线程都是由操作系统所提供的程序运行的基本单元
  • B 线程之间有单独的地址空间
  • C 进程和线程的主要差别在于它们是不同的操作系统资源管理方式
  • D 线程有自己的堆栈和局部变量

参考答案:B

这是学习操作系统知识的时候经常会考试的内容,但是在工作中经常会遇到多线程处理问题。通常来说,一个进程就代表着一个应用程序,而操作系统为了更好的利用资源,提供了线程用于处理并发。线程之间没有有单独的地址空间,处理完成之后还得回到主线程,所以,一个线程死掉就等于整个进程死掉。进程和线程都是操作系统的基本单元,只是分工不同,是两种不同的资源管理方式。线程所需要的资源都来自于进程,它没有自己独立的资源,也就没有自己的堆栈和局部变量。

修正:这里参考答案与描述不符合的问题。

20、堆和栈的区别正确的是

  • A 对于栈来讲,我们需要手工控制,容易产生memory leak
  • B 对于堆来说,释放工作由编译器自动管理,无需我们手工控制
  • C 在Windows下,栈是向高地址扩展的数据结构,是连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的
  • D 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低

参考答案:D

栈是由编译器管理的,不是我们手动控制,但是栈所能分配的内存是比较少的,如果要处理大数据,则需要在堆上分配,因此在栈上比较容易出现Memory Leak;
对于堆,需要我们自己申请内存,同时也需要我们自己手动释放,否则会造成内存泄露;对于堆,如果过多地申请内存空间,会导致内存空间不连接,从而造成内存碎片,使程序效率降低。

21、下列回调机制的理解不正确的是

  • A target-action:当两个对象之间有⽐较紧密的关系时,如视图控制器与其下的某个视图。
  • B delegate:当某个对象收到多个事件,并要求同一个对象来处理所有事件时。委托机制必须依赖于某个协议定义的⽅法来发送消息。
  • C NSNotification:当需要多个对象或两个无关对象处理同一个事件时。
  • D Block:适⽤于回调只发⽣生一次的简单任务。

参考答案:B

对于Target-Action机制,要求两个对象之间有比较紧密的联系,比如在控制器与cell之间,可通过设置target为控制器对象,而action则为控制器中的某个回调方法;
对于Delegator机制,它是苹果提供的标准回调机制,通常会提供一个标准的协议,然后由代理类遵守协议,最常用的用法是反向传值,比如打开蓝牙后要反馈给前一个界面蓝牙的开关状态;
对于通知,通常是多对多的关系,它并不关心是谁要处理消息,任意对象都可以注册通知到通知中心,当发送通知时,所有注册了该通知的对象都可以收到消息。最常用的场景是跨模块,比如登录模块与其它模块有着非常紧密的联系,但是登录成功后各个地方可能需要做一些处理,因此通常会在登录成功或者登出成功后发送通知,以便各个需要处理的模块得到正确的处理;
对于Block是相当简单的,它只适用于一对一的关系,比如在做某个操作成功或者失败后回调。

22、对于runloop的理解不正确的是

  • A 每一个线程都有其对应的RunLoop
  • B 默认非主线程的RunLoop是没有运行的
  • C 在一个单独的线程中没有必要去启用RunLoop
  • D 可以将NSTimer添加到runloop中

参考答案:C

说到RunLoop,它可是多线程的法宝。通常来说,一个线程一次只能执行一个任务,执行完任务后就会退出线程。但是,对于主线程是不能退出的,因此我们需要让主线程即时任务执行完毕,也可以继续等待接收事件而不退出,那么RunLoop就是关键法宝了。但是非主线程通常来说就是为了执行某一任务的,执行完毕就需要归还资源,因此默认是不运行RunLoop的。

每一个线程都有其对应的RunLoop的,只是默认只有主线程的RunLoop是启动的,其它子线程的RunLoop默认是不启动的,若要启动则需要手动启动。
在一个单独的线程中,如果需要在处理完某个任务后不退出,继续等待接收事件,则需要启用RunLoop。
NSRunLoop提供了一个添加NSTimer的方法,可以指定Mode,如果要让任何情况下都回调,则需要设置Mode为Common模式。
实质上,对于子线程的runloop默认是不存在的,因为苹果采用了懒加载的方式。如果我们没有手动调用[NSRunLoop currentRunLoop],就不会去查询是否存在当前线程的RunLoop,也就不会去加载,更不会创建。


// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
  t = pthread_main_thread_np();
}
 
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
  __CFSpinUnlock(&loopsLock);
  CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
  CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
  CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
  if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
    CFRelease(dict);
  }
  CFRelease(mainLoop);
  __CFSpinLock(&loopsLock);
}
 
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
  CFRunLoopRef newLoop = __CFRunLoopCreate(t);
  __CFSpinLock(&loopsLock);
  loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  if (!loop) {
    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
    loop = newLoop;
  }
  // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
  __CFSpinUnlock(&loopsLock);
  CFRelease(newLoop);
}
 
if (pthread_equal(t, pthread_self())) {
  _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
  if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
    _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
  }
}
 
return loop;

关键加载过程如下:
1.检查全局字典里是否存在该线程的runLoop,如果有则退出,否则
2.创建一个新的runLoop放到全局字典中

23、Apple用什么方式实现对一个对象的KVO?

Apple 的文档对 KVO 实现的描述:

Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

从Apple 的文档可以看出:Apple 并不希望过多暴露 KVO 的实现细节。不过,要是借助 runtime 提供的方法去深入挖掘,所有被掩盖的细节都会原形毕露:

当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。我画了一张示意图,如下所示:

KVO 确实有点黑魔法:

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。

下面做下详细解释:

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们只在希望能控制回调的调用时机时才会这么做。大部分情况下,改变通知会自动调用。

比如调用 setNow: 时,系统还会以某种方式在中间插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的调用。大家可能以为这是因为 setNow: 是合成方法,有时候我们也能看到人们这么写代码:


- (void)setNow:(NSDate *)aDate {
    [self willChangeValueForKey:@"now"]; // 没有必要
    _now = aDate;
    [self didChangeValueForKey:@"now"];// 没有必要
}

这是完全没有必要的代码,不要这么做,这样的话,KVO代码会被调用两次。KVO在调用存取方法之前总是调用 willChangeValueForKey: ,之后总是调用 didChangeValueForkey: 。怎么做到的呢?答案是通过 isa 混写(isa-swizzling)。第一次对一个对象调用 addObserver:forKeyPath:options:context: 时,框架会创建这个类的新的 KVO 子类,并将被观察对象转换为新子类的对象。在这个 KVO 特殊子类中, Cocoa 创建观察属性的 setter ,大致工作原理如下:

- (void)setNow:(NSDate *)aDate {
    [self willChangeValueForKey:@"now"];
    [super setValue:aDate forKey:@"now"];
    [self didChangeValueForKey:@"now"];
}

这种继承和方法注入是在运行时而不是编译时实现的。这就是正确命名如此重要的原因。只有在使用KVC命名约定时,KVO才能做到这一点。

KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。这在Apple 的文档可以得到印证:

Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

然而 KVO 在实现中使用了 isa 混写( isa-swizzling) ,这个的确不是很容易发现:Apple 还重写、覆盖了 -class 方法并返回原来的类。 企图欺骗我们:这个类没有变,就是原本那个类。。。

但是,假设“被监听的对象”的类对象是 MYClass ,有时候我们能看到对 NSKVONotifying_MYClass 的引用而不是对 MYClass 的引用。借此我们得以知道 Apple 使用了 isa 混写(isa-swizzling)。具体探究过程可参考这篇博文

iOS开发整理总结
Web note ad 1