标签:selector server 多个 分析 产生 大神 解释 休眠 简化
一些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 模板 1 2 3 4 5 6 7 8 9 10 + (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); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 + (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
的封装
即 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用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + (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 作用域内执行首屏渲染之后的所有方法执行完成
非首屏其他业务服务模块的初始化
监听的注册
配置文件的读写
把可能卡住主线程的方法挪走或子线程处理
滚动完 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压力
对于固定行高,前一个设置属性比后一个实现代理方法效率高
1 2 3 4 5 6 cell.tableview.rowHeight?=?50.0 ; -?(CGFloat )tableView:(UITableView ?*)tableView?heightForRowAtIndexPath:(NSIndexPath ?*)indexPath { ????return ?50.0 ; }
NSDateFormatter这个对象的相关操作很费时,需要避免频繁的创建和计算
利用RunLoop延时加载图片
利用RunLoop:把图片加载的方法放在NSDefaultRunLoopMode
里,这样当滑动时就切换出了这个Mode,暂停加载
一些iOS面试基础题总结
标签:selector server 多个 分析 产生 大神 解释 休眠 简化
原文地址:https://www.cnblogs.com/lijianming180/p/12041540.html