一些iOS面试基础题总结
一些iOS面试基础题总结
目录
- 多线程
- AutoLayout
- objc_msgSend
- Runtime
- 消息转发
- Category
- NSObject 与 objc_class
- Runloop
- AutoreleasePool
- iOS系统架构
- App启动过程和优化
- UIScrollView 的代理方法
- 响应链和事件传递
- UIView 和 CALayer 的区别和联系
- 轮播图朴素实现的几种方法
- TableView 和 CollectionView 必选的代理方法
- UITableView 的优化思路
多线程
线程之间同步
- 原子操作 Atomic
- 加锁(互斥锁、递归锁、读写锁)NSLock,OSSpinLock
多线程之间通信
- performSelectorOnMainThread:withObject:waitUntilDone:
如何保证线程安全
- OSSpinLock 自旋锁
- dispatch_semaphore
- NSLock 等各种锁
- @synchronized
多线程的坑
- 常驻线程
常驻线程多了影响CPU效率
AFNetworking2.0因为用的NSURLConnection有缺陷,需要所在线程一直存活,所以保持了个常驻线程,3.0用了NSURLSession,可以指定回调的delegateQueue于是弃用常驻线程。
[runloop run]是常驻线程,[runloop runUntilDate]指定保活时长
- 并发
GCD本着最大化CPU效率的原则会多创建线程,但如果是IO类操作,需要等待数据的空档会继续创建新线程导致内存失控。类似数据库操作尽量用串行队列避免多线程并发导致问题。因为创建线程需要堆栈内存,切换线程也消耗CPU。
- 死锁
串行队列(如主队列)同步操作
AutoLayout
更新屏幕时,Layout Engine从上到下调用layoutSubviews()通过 Cassowary 算法计算各个子视图的位置,算出来后将子视图的 frame 从 Layout Engine 里拷贝出来,接下来的过程就跟手写frame是一样的了。iOS12优化了性能,以前元素多了会导致性能下降,现正不会了
objc_msgSend
- 检查这个selector是不是要忽略的
- 检查target是不是nil
- 如果有相应处理nil的函数就跳转到该函数
- 如果没有就自动清理现场并返回,这就是OC中给nil发消息不会崩溃的原因
- 确定不是给nil发消息后,在该class的缓存中查找方法对应的IMP实现
- 如果找到就跳转进去
- 如果没找到就在方法分发表里继续查找,直到找到NSObject为止
- 如果还没找到就开始消息转发,上述过程就是通过SEL快速查找IMP的过程
Runtime
C语言中,编译期函数的调用就决定调用哪个函数,而OC只有在真正运行时才根据函数名称找到对应函数来调用。需要一个运行时系统来动态地创建类和对象、消息传递和转发
讲一下 OC 的消息机制
- OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
- objc_msgSend底层有3大阶段:消息发送(当前类、父类中查找)、动态方法解析、消息转发
- 当递归地找不到selector时,启动消息转发:resolveInstanceMethod、resolveClassMethod、forwardingTargetForSelector
什么是Runtime?平时项目中有用过么?
- OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
- OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
- 平时编写的OC代码,底层都是转换成了Runtime API进行调用
Runtime 的应用(优点):
- 实现多继承 Multiple Inheritance
- Method Swizzling -> 无侵入埋点 -> 使用Category进行
- Aspect Oriented Prigramming 面向切面编程AOP -> 如日志、身份验证、缓存等模块
- isa Swizzling -> KVO的实现
- Associated Object关联对象(给Category添加属性) objc_set(get)AssociatedObject
- 动态地增加方法
- NSCoding 的自动归档和自动解档
- 字典和模型相互转换!
- 异常保护(保护数组越界问题)
Runtime 的缺点
- Method Swizzling 不是原子操作,放在+load里没问题,放在+initialize里就有问题了
- 重写方法而不调用super方法可能有问题
Runtime 注意事项
- Swizzling应该总在+load中执行
- Swizzling应该总在dispatch_once中执行 -> 因为会改变全局状态所以应该只执行一次
- +load中执行Swizzling时,不要调用[super load] -> 避免偶数次执行Swizzling
- SEL其本身是一个Int类型的地址,地址中存放着方法的名字。
Method Swizzling
几个常见方法
- method_setImplementation
为一个方法名设置IMP(实现)
- method_exchangeImplementations
交换两个方法名的实现,即执行两次 method_setImplementation
- class_addMethod
根据官方注释解释,这个方法用于给指定的类增加方法名和IMP(实现),如果该已经存在这个方法名,不做事,返回NO,如果该类不存在这个方法名(即使父类存在),添加这个方法,返回YES
- class_replaceMethod
根据官方注释解释,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用 class_addMethod 来为该类增加一个新方法。若已存在,则等同于 method_setImplementation 为该方法名替换IMP(实现)
Swizzling 模板
12345678910 | + (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector { Class class = classObject; Method fromMethod = class_getInstanceMethod(class, fromSelector); Method toMethod = class_getInstanceMethod(class, toSelector); // 得到交换类的实例方法 if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) { class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod)); // 进行方法的交换 } else { method_exchangeImplementations(fromMethod, toMethod); // 交换 IMP 指针 }} |
123456789101112131415161718192021222324252627 | + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class aClass = [self class]; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(xxx_viewWillAppear:); Method originalMethod = class_getInstanceMethod(aClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } });} |
消息转发
什么时候会报unrecognized selector的异常?
当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
- Method resolution
objc运行时会调用+resolveInstanceMethod:
或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。
- Fast forwarding
如果目标对象实现了-forwardingTargetForSelector:
,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
- Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
Category
通过Runtime给类添加方法,可以把类的实现分开在几个不同的文件,减少单个文件体积、不同功能组织到不同的Category里、可以由多人开发一个类、可以按需加载想要的category、可以把framework的私有方法公开
与extension不同的是,extension需要有类的源码才能添加,所以无法为系统类添加extension。
而category是运行时添加的。所以,extension可以添加实例变量,而category只能添加实例方法、类方法、协议、属性,但是不能添加实例变量。
category并不是完全替换掉原来类的方法,而是附加到方法列表的前面,而runtime寻找方法是顺着找的,找到category覆盖的方法后就执行了
NSObject 与 objc_class
NSObject 继承自 objc_class
objc_class 继承自 objc_object
objc_object -> objc_class -> NSObject
所以OC中,类也是一个对象。
objc_class中,除了isa,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个这个类的实例方法链表。
每个类都有单独的元类,所以类的superclass指针递归最后指向NSObject,NSObject没有超类所以指向nil。类的isa指向对应唯一的元类,每个元类的isa都指向rootMetaClass,rootMetaClass的superClass指向NSObject,isa指向自己
元类中保存了创建类对象以及类方法所需的所有信息
Runloop
可以先粗看这篇YYKit大神ibireme的文章,大概过一遍,不用纠结源码和看不懂的地方。
然后看这个孙源的线下分享会视频,最后再细看一遍那篇文章
介绍
运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。简单来说就是让软件一直活着。
结构
- 一个线程对应一个
Runloop
,主线程的Runloop默认开启,子线程如果不手动开启就没有。 - 每个Runloop内有多个Mode,但Runloop同一时间只能执行一个Mode,换Mode需要停下切换。
- 每个Mode内有任意多个
Source
、Timer
、Observer
Timer
- 即
CFRunloopTimer
NSTimer
、performSelector:afterDelay:
、CADisplayLink
都是对RunloopTimer
的封装
一些iOS面试基础题总结="Source">Source
- 即
CFRunlopSource
,是Runloop的数据源抽象类 - Source分
Source0
和Source1
Source0
处理App内部事件、App自己负责管理触发、如UIEvent
、CFSocker
Source1
有Runloop和内核管理,Mach Port
驱动,如CFMachPort
、CFMessagePort
Observer
- 向外部报告Runloop当前状态的更改
- 框架中很多机制都由RunLoopObserver触发,如CAAnimation
- 与 AutoreleasePool 的关系:UIKit通过Observer在RunLoop两个Sleep之间对AutoreleasePool进行Pop和Push,将这次Loop中产生的Autorelease对象释放
CFRunLoopMode
- 每个Runloop内有多个Mode,但Runloop同一时间只能执行一个Mode,换Mode需要停止当前Loop切换然后重启Loop。
- 默认Mode:
NSDefaultRunLoopMode
, 空闲状态、普通事件等 - 界面追踪Mode:
UITrackingRunLoopMode
, 滑动时(ScrollView) - 私有Mode:UIInitializationRunLoopMode, App刚启动时, 不重要
NSRunLoopCommonModes
是一个集合(打标签),默认包含上面的1和2两个Mode,可以自己添加Mode进去- 开始滑动时,会从DefaultRunLoopMode切换成UITrackingRunLoopMode,停止滑动时会切换回来
- 想让
NSTimer
滑动时也跑,默认是加到DefaultMode的,需要手动加到CommonModes里,使其滑动时也执行,[(NSRunLoop) addTimer:forMode:]
RunLoop 与 GCD 的关系
- 只在用到 main queue 时
- GCD 中 dispatch 到 main queue 的 block 被分发到 main RunLoop 中执行
RunLoop 的挂起与唤醒
- 指定用于唤醒的 mach_port 端口
- 调用 mach_msg 监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态
- 由另一个线程(或另一个进程中的某个线程)向内核发送该端口的msg后,trap状态被唤醒,RunLoop继续下一轮
作用
- 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行,对于子线程可以使用run来常驻线程。
- 保证NSTimer正常运转
- 线上监测App卡顿情况
- 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
- 节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情
过程
- 进入loop
- do while 保活线程:触发Timer回调、触发Source()回调、执行block
- 进入休眠
- 等待mach_port消息(如Timer时间到、Runloop超时、被调用者唤醒)
- 唤醒,处理消息
- 判断是否进入下一个loop
AFNetworking的RunLoop用法
123456789101112131415161718 | + (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }}+ (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread;} |
- 在子线程开启RunLoop,添加一个不会用到的port作为Source防止RunLoop停止
[runloop run]
使子线程常驻,从而接收NSURLConnection的回调- 在AF的3.0版本里替换成NSURLSession就不需要常驻线程了
利用RunLoop延时加载图片
- 当scrollview滑动时加载图片可能导致卡顿
- 原本做法可以通过delegate处理是否加载
- 利用RunLoop:把图片加载的方法放在
NSDefaultRunLoopMode
里,这样当滑动时就切换出了这个Mode,暂停加载
Crash时重启RunLoop
- 接到 Crash 的 Signal 后手动重启 RunLoop
- 不适用于 BAD_ACCESS
监测卡顿的方法
- 创建观察者
- 把观察者添加到主线程的Runloop的common模式下观察,然后创建一个常驻子线程专门用来监控主线程的Runloop状态
- 一旦发现runloop进入睡眠前的状态或者唤醒后的状态在设置的时间阈值内没有变化,即可判定为卡顿,用第三方库PLCrashReporter来获取堆栈信息,上报服务器。后续分析。
AutoreleasePool
配合runloop的,每次runloop开启时重建自动释放池,休息前释放掉池里的东西如Timer
ARC下自动创建的在子线程结束后释放,手动创建的在作用域大括号结束后释放
底层实现 AutoReleasePoolPage 是一个双向链表,有push release pop操作
iOS系统架构
四个层
- 第一层:用户体验层:SpringBoard
- 第二层:应用框架层:CocoaTouch
- 第三层:核心框架层:Metal、图形媒体核心框架
- 第四层:Darwin层:XNU、内核、驱动
iOS的可执行文件和动态库都是Mach-O格式,加载App实际就是加载Mach-O文件。
App启动过程与优化
main()执行前
- 加载可执行文件(App的.o文件的集合)
- 加载动态链接库,进行rebase指针调整和bind符号绑定
- Objc Runtime 的初始处理,包括 Objc 相关类的注册、Category注册、selector 唯一性检查
- +load()初始化
这个阶段的优化有 1. 减少动态库加载(合并动态库)2. 减少加载启动后不会去用的类和方法 3. 少用+load,或用 +initialize替换,因为runtime 的 Method Swizzling 操作每次4ms
main()执行后
- 指 main() 执行开始到 didFinishLaunchingWithOptions 方法里首屏渲染相关方法完成
- 配置初始化文件的IO操作
- 首屏列表大数据的读取
- 首屏渲染的大量计算
优化方法有把各种与首屏渲染不相干的初始化挪走或子线程处理
首屏渲染完成后
- 指 didFinishLaunchingWithOptions 作用域内执行首屏渲染之后的所有方法执行完成
- 非首屏其他业务服务模块的初始化
- 监听的注册
- 配置文件的读写
把可能卡住主线程的方法挪走或子线程处理
UIScrollView 的代理方法
- 滚动完 scrollViewDidScroll
- 缩放完 scrollViewDidZoom
- 将要开始拖动 scrollViewWillBeginDragging
- 将要结束拖动 scrollViewWillEndDragging
- 滑动将要减速、 滑动减速完成
- 滚动动画完成
- 将要开始缩放、已经结束缩放
响应链和事件传递
hitTest方法检测看是否返回
继承
UIResponder
的类才能响应,如UIApplication
、UIView
、UIViewController
。而CALayer
是继承自NSObject
的,不能响应事件首先传递给
UIApplication
,然后向下分发给UIWindow
,然后分发给最下层的UIView
,逐步调用hitTest
从屏内向外找,当某个UIView
返回YES时就递归对其SubView
执行hitTest
,直到找到最后一个
- 某
UIView
不接受事件的情况:- alpha < 0.01
- userInteractionEnabled = NO
- hidden = YES
UIView 和 CALayer 的区别和联系
- UIView能响应,CALayer不能
- View的frame(和bounds)是简单返回layer的frame(bound), 而layer的frame由几个参数决定
- UIView 是 CALayer 的代理
- 每个UIView内都有一个CALayer(即有个属性)
轮播图朴素实现的集中方法
- UICollectionView, 简单粗暴放100个Cell
- UIScrollView 首尾各放一个展示
- UIScrollView 三个ImageView实现
- UIImageView 自己实现layer转场动画
TableView 和 CollectionView 必选的代理方法
- delegate都是可选
- datasource各有两个必选
- table: cellForRowAtIndexPath、numberOfRowsInSection
- collection: cellForItemAtIndexPath、numberOfItemsInSection
UITableView 的优化思路
流程
- 获取数据;
- 把数据转化成model、存进数组;
- tableview调用reloadData刷新数据;
- 在代理方法cellForRowAtIndexPath里,创建自定义的cell,把model赋值给cell;
- cell在对应的model的set方法里,根据拿到的model,设置图片的image,设置label的text等(控件都以懒加载形式初始化);
- 在代理方法heightForRowAtIndexPath里,根据model,算出当前行应该显示多少的高度;
- 在cell的layoutSubviews方法里,布局子控件。
优化思路
- 避免主线程阻塞
1/2步里的获取数据、数据处理等耗时操作,应该放入后台线程异步处理,处理好后再通知主线程刷新界面。
- 避免频繁的对象创建
对象的创建会发送内存分配、属性调整等。
所以,首先,尽量用轻量的对象代替重量的对象。比如CALayer代替UIView。
接着,多利用缓存思想,对象创建后缓存起来,需要的时候再拿出来用。合理利用内存开销,减少CPU开销。
把 Cell setModel里的一些操作放在第二步数据转model里
- 减少对象的属性赋值操作
尤其是UIView的frame/bounds等属性的赋值操作,会产生比较大的CPU消耗。
尽量少让Cell里空间动态变化,有规律的话筛分成多个固定cell
- 异步绘制
文本渲染、图像绘制都是比较消耗性能的操作,而UILabel等控件都是在主线程进行的文本绘制。这会对性能产生比较大的影响。
UIKit和CoreAnimation相关操作必须在主线程中进行,其它的可以在后台线程异步执行
- 简化视图结构
GPU在绘制图像前,会把重叠的视图进行混合,视图结构越复杂,这个操作就越耗时,如果存在透明视图,混合过程会更加复杂。所以,我们可以:
- 尽量避免复杂的图层结构
- 少使用透明的视图
- 不透明的视图,设置opaque = YES?
- 减少离屏渲染
老生常谈之圆角问题,圆角是开发中经常使用到的美化方式,但一般的设置cornerRadius时会配合masksToBounds属性,这就会造成离屏渲染。关于这种问题的处理,大致有两个思路:
- 异步绘制一张圆角的图片来显示;
- 用一个圆角而中空的图来盖住。
tableview需要刷新数据时,使用
[tableview beginUpdates]、[tableview insertRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationNone]、
[tableview endUpdates];而非
[tableview reloadData]从而刷新更少的行减少CPU压力
- 对于固定行高,前一个设置属性比后一个实现代理方法效率高
123456 | cell.tableview.rowHeight?=?50.0;-?(CGFloat)tableView:(UITableView?*)tableView?heightForRowAtIndexPath:(NSIndexPath?*)indexPath{????return?50.0;} |
NSDateFormatter这个对象的相关操作很费时,需要避免频繁的创建和计算
利用RunLoop延时加载图片
利用RunLoop:把图片加载的方法放在NSDefaultRunLoopMode
里,这样当滑动时就切换出了这个Mode,暂停加载