fort0 2019-12-15
一些iOS面试基础题总结
常驻线程多了影响CPU效率
AFNetworking2.0因为用的NSURLConnection有缺陷,需要所在线程一直存活,所以保持了个常驻线程,3.0用了NSURLSession,可以指定回调的delegateQueue于是弃用常驻线程。
[runloop run]是常驻线程,[runloop runUntilDate]指定保活时长
GCD本着最大化CPU效率的原则会多创建线程,但如果是IO类操作,需要等待数据的空档会继续创建新线程导致内存失控。类似数据库操作尽量用串行队列避免多线程并发导致问题。因为创建线程需要堆栈内存,切换线程也消耗CPU。
串行队列(如主队列)同步操作
更新屏幕时,Layout Engine从上到下调用layoutSubviews()通过 Cassowary 算法计算各个子视图的位置,算出来后将子视图的 frame 从 Layout Engine 里拷贝出来,接下来的过程就跟手写frame是一样的了。iOS12优化了性能,以前元素多了会导致性能下降,现正不会了
C语言中,编译期函数的调用就决定调用哪个函数,而OC只有在真正运行时才根据函数名称找到对应函数来调用。需要一个运行时系统来动态地创建类和对象、消息传递和转发
讲一下 OC 的消息机制
什么是Runtime?平时项目中有用过么?
Runtime 的应用(优点):
Runtime 的缺点
Runtime 注意事项
为一个方法名设置IMP(实现)
交换两个方法名的实现,即执行两次 method_setImplementation
根据官方注释解释,这个方法用于给指定的类增加方法名和IMP(实现),如果该已经存在这个方法名,不做事,返回NO,如果该类不存在这个方法名(即使父类存在),添加这个方法,返回YES
根据官方注释解释,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用 class_addMethod 来为该类增加一个新方法。若已存在,则等同于 method_setImplementation 为该方法名替换IMP(实现)
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的运行时会给出三次拯救程序崩溃的机会:
objc运行时会调用+resolveInstanceMethod:
或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。
如果目标对象实现了-forwardingTargetForSelector:
,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
通过Runtime给类添加方法,可以把类的实现分开在几个不同的文件,减少单个文件体积、不同功能组织到不同的Category里、可以由多人开发一个类、可以按需加载想要的category、可以把framework的私有方法公开
与extension不同的是,extension需要有类的源码才能添加,所以无法为系统类添加extension。
而category是运行时添加的。所以,extension可以添加实例变量,而category只能添加实例方法、类方法、协议、属性,但是不能添加实例变量。
category并不是完全替换掉原来类的方法,而是附加到方法列表的前面,而runtime寻找方法是顺着找的,找到category覆盖的方法后就执行了
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指向自己
元类中保存了创建类对象以及类方法所需的所有信息
可以先粗看这篇YYKit大神ibireme的文章,大概过一遍,不用纠结源码和看不懂的地方。
然后看这个孙源的线下分享会视频,最后再细看一遍那篇文章
运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。简单来说就是让软件一直活着。
Runloop
,主线程的Runloop默认开启,子线程如果不手动开启就没有。Source
、Timer
、Observer
CFRunloopTimer
NSTimer
、performSelector:afterDelay:
、CADisplayLink
都是对RunloopTimer
的封装CFRunlopSource
,是Runloop的数据源抽象类Source0
和Source1
Source0
处理App内部事件、App自己负责管理触发、如UIEvent
、CFSocker
Source1
有Runloop和内核管理,Mach Port
驱动,如CFMachPort
、CFMessagePort
NSDefaultRunLoopMode
, 空闲状态、普通事件等UITrackingRunLoopMode
, 滑动时(ScrollView)NSRunLoopCommonModes
是一个集合(打标签),默认包含上面的1和2两个Mode,可以自己添加Mode进去NSTimer
滑动时也跑,默认是加到DefaultMode的,需要手动加到CommonModes里,使其滑动时也执行,[(NSRunLoop) addTimer:forMode:]
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 run]
使子线程常驻,从而接收NSURLConnection的回调NSDefaultRunLoopMode
里,这样当滑动时就切换出了这个Mode,暂停加载配合runloop的,每次runloop开启时重建自动释放池,休息前释放掉池里的东西如Timer
ARC下自动创建的在子线程结束后释放,手动创建的在作用域大括号结束后释放
底层实现 AutoReleasePoolPage 是一个双向链表,有push release pop操作
四个层
iOS的可执行文件和动态库都是Mach-O格式,加载App实际就是加载Mach-O文件。
main()执行前
这个阶段的优化有 1. 减少动态库加载(合并动态库)2. 减少加载启动后不会去用的类和方法 3. 少用+load,或用 +initialize替换,因为runtime 的 Method Swizzling 操作每次4ms
main()执行后
优化方法有把各种与首屏渲染不相干的初始化挪走或子线程处理
首屏渲染完成后
把可能卡住主线程的方法挪走或子线程处理
hitTest方法检测看是否返回
继承UIResponder
的类才能响应,如UIApplication
、UIView
、UIViewController
。而CALayer
是继承自NSObject
的,不能响应
事件首先传递给UIApplication
,然后向下分发给UIWindow
,然后分发给最下层的UIView
,逐步调用hitTest
从屏内向外找,当某个UIView
返回YES时就递归对其SubView
执行hitTest
,直到找到最后一个
UIView
不接受事件的情况:1/2步里的获取数据、数据处理等耗时操作,应该放入后台线程异步处理,处理好后再通知主线程刷新界面。
对象的创建会发送内存分配、属性调整等。
所以,首先,尽量用轻量的对象代替重量的对象。比如CALayer代替UIView。
接着,多利用缓存思想,对象创建后缓存起来,需要的时候再拿出来用。合理利用内存开销,减少CPU开销。
把 Cell setModel里的一些操作放在第二步数据转model里
尤其是UIView的frame/bounds等属性的赋值操作,会产生比较大的CPU消耗。
尽量少让Cell里空间动态变化,有规律的话筛分成多个固定cell
文本渲染、图像绘制都是比较消耗性能的操作,而UILabel等控件都是在主线程进行的文本绘制。这会对性能产生比较大的影响。
UIKit和CoreAnimation相关操作必须在主线程中进行,其它的可以在后台线程异步执行
GPU在绘制图像前,会把重叠的视图进行混合,视图结构越复杂,这个操作就越耗时,如果存在透明视图,混合过程会更加复杂。所以,我们可以:
老生常谈之圆角问题,圆角是开发中经常使用到的美化方式,但一般的设置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,暂停加载