zoukankan      html  css  js  c++  java
  • 一些iOS面试基础题总结

    一些iOS面试基础题总结

    目录

    1. 多线程
    2. AutoLayout
    3. objc_msgSend
    4. Runtime
    5. 消息转发
    6. Category
    7. NSObject 与 objc_class
    8. Runloop
    9. AutoreleasePool
    10. iOS系统架构
    11. App启动过程和优化
    12. UIScrollView 的代理方法
    13. 响应链和事件传递
    14. UIView 和 CALayer 的区别和联系
    15. 轮播图朴素实现的几种方法
    16. TableView 和 CollectionView 必选的代理方法
    17. UITableView 的优化思路

    多线程

    线程之间同步

    1. 原子操作 Atomic
    2. 加锁(互斥锁、递归锁、读写锁)NSLock,OSSpinLock

    多线程之间通信

    • performSelectorOnMainThread:withObject:waitUntilDone:

    如何保证线程安全

    1. OSSpinLock 自旋锁
    2. dispatch_semaphore
    3. NSLock 等各种锁
    4. @synchronized

    多线程的坑

    1. 常驻线程

    常驻线程多了影响CPU效率

    AFNetworking2.0因为用的NSURLConnection有缺陷,需要所在线程一直存活,所以保持了个常驻线程,3.0用了NSURLSession,可以指定回调的delegateQueue于是弃用常驻线程。

    [runloop run]是常驻线程,[runloop runUntilDate]指定保活时长

    1. 并发

    GCD本着最大化CPU效率的原则会多创建线程,但如果是IO类操作,需要等待数据的空档会继续创建新线程导致内存失控。类似数据库操作尽量用串行队列避免多线程并发导致问题。因为创建线程需要堆栈内存,切换线程也消耗CPU。

    1. 死锁

    串行队列(如主队列)同步操作

    AutoLayout

    更新屏幕时,Layout Engine从上到下调用layoutSubviews()通过 Cassowary 算法计算各个子视图的位置,算出来后将子视图的 frame 从 Layout Engine 里拷贝出来,接下来的过程就跟手写frame是一样的了。iOS12优化了性能,以前元素多了会导致性能下降,现正不会了

    objc_msgSend

    1. 检查这个selector是不是要忽略的
    2. 检查target是不是nil
      • 如果有相应处理nil的函数就跳转到该函数
      • 如果没有就自动清理现场并返回,这就是OC中给nil发消息不会崩溃的原因
    3. 确定不是给nil发消息后,在该class的缓存中查找方法对应的IMP实现
      • 如果找到就跳转进去
      • 如果没找到就在方法分发表里继续查找,直到找到NSObject为止
    4. 如果还没找到就开始消息转发,上述过程就是通过SEL快速查找IMP的过程

    Runtime

    C语言中,编译期函数的调用就决定调用哪个函数,而OC只有在真正运行时才根据函数名称找到对应函数来调用。需要一个运行时系统来动态地创建类和对象、消息传递和转发

    1. 讲一下 OC 的消息机制

      • OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
      • objc_msgSend底层有3大阶段:消息发送(当前类、父类中查找)、动态方法解析、消息转发
      • 当递归地找不到selector时,启动消息转发:resolveInstanceMethod、resolveClassMethod、forwardingTargetForSelector
    2. 什么是Runtime?平时项目中有用过么?

      • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
      • OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
      • 平时编写的OC代码,底层都是转换成了Runtime API进行调用
    3. Runtime 的应用(优点):

      • 实现多继承 Multiple Inheritance
      • Method Swizzling -> 无侵入埋点 -> 使用Category进行
      • Aspect Oriented Prigramming 面向切面编程AOP -> 如日志、身份验证、缓存等模块
      • isa Swizzling -> KVO的实现
      • Associated Object关联对象(给Category添加属性) objc_set(get)AssociatedObject
      • 动态地增加方法
      • NSCoding 的自动归档和自动解档
      • 字典和模型相互转换!
      • 异常保护(保护数组越界问题)
    4. Runtime 的缺点

      • Method Swizzling 不是原子操作,放在+load里没问题,放在+initialize里就有问题了
      • 重写方法而不调用super方法可能有问题
    5. Runtime 注意事项

      • Swizzling应该总在+load中执行
      • Swizzling应该总在dispatch_once中执行 -> 因为会改变全局状态所以应该只执行一次
      • +load中执行Swizzling时,不要调用[super load] -> 避免偶数次执行Swizzling
    • SEL其本身是一个Int类型的地址,地址中存放着方法的名字。

    Method Swizzling

    几个常见方法

    1. method_setImplementation

    为一个方法名设置IMP(实现)

    1. method_exchangeImplementations

    交换两个方法名的实现,即执行两次 method_setImplementation

    1. class_addMethod

    根据官方注释解释,这个方法用于给指定的类增加方法名和IMP(实现),如果该已经存在这个方法名,不做事,返回NO,如果该类不存在这个方法名(即使父类存在),添加这个方法,返回YES

    1. 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); // 交换 IMP 指针
    }
    }
    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的运行时会给出三次拯救程序崩溃的机会:

    1. Method resolution

    objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。

    1. Fast forwarding

    如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。

    1. 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内有任意多个SourceTimerObserver

    Timer

    • CFRunloopTimer
    • NSTimerperformSelector:afterDelay:CADisplayLink都是对RunloopTimer的封装

    大专栏  一些iOS面试基础题总结="Source">Source

    • CFRunlopSource,是Runloop的数据源抽象类
    • Source分Source0Source1
    • Source0处理App内部事件、App自己负责管理触发、如UIEventCFSocker
    • Source1有Runloop和内核管理,Mach Port驱动,如CFMachPortCFMessagePort

    Observer

    • 向外部报告Runloop当前状态的更改
    • 框架中很多机制都由RunLoopObserver触发,如CAAnimation
    • 与 AutoreleasePool 的关系:UIKit通过Observer在RunLoop两个Sleep之间对AutoreleasePool进行Pop和Push,将这次Loop中产生的Autorelease对象释放

    CFRunLoopMode

    • 每个Runloop内有多个Mode,但Runloop同一时间只能执行一个Mode,换Mode需要停止当前Loop切换然后重启Loop。
    1. 默认Mode:NSDefaultRunLoopMode, 空闲状态、普通事件等
    2. 界面追踪Mode:UITrackingRunLoopMode, 滑动时(ScrollView)
    3. 私有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继续下一轮

    作用

    1. 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行,对于子线程可以使用run来常驻线程。
    2. 保证NSTimer正常运转
    3. 线上监测App卡顿情况
    4. 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
    5. 节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情

    过程

    1. 进入loop
    2. do while 保活线程:触发Timer回调、触发Source()回调、执行block
    3. 进入休眠
    4. 等待mach_port消息(如Timer时间到、Runloop超时、被调用者唤醒)
    5. 唤醒,处理消息
    6. 判断是否进入下一个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

    监测卡顿的方法

    1. 创建观察者
    2. 把观察者添加到主线程的Runloop的common模式下观察,然后创建一个常驻子线程专门用来监控主线程的Runloop状态
    3. 一旦发现runloop进入睡眠前的状态或者唤醒后的状态在设置的时间阈值内没有变化,即可判定为卡顿,用第三方库PLCrashReporter来获取堆栈信息,上报服务器。后续分析。

    AutoreleasePool

    配合runloop的,每次runloop开启时重建自动释放池,休息前释放掉池里的东西如Timer
    ARC下自动创建的在子线程结束后释放,手动创建的在作用域大括号结束后释放
    底层实现 AutoReleasePoolPage 是一个双向链表,有push release pop操作

    iOS系统架构

    四个层

    • 第一层:用户体验层:SpringBoard
    • 第二层:应用框架层:CocoaTouch
    • 第三层:核心框架层:Metal、图形媒体核心框架
    • 第四层:Darwin层:XNU、内核、驱动

    iOS的可执行文件和动态库都是Mach-O格式,加载App实际就是加载Mach-O文件。

    App启动过程与优化

    1. main()执行前

      • 加载可执行文件(App的.o文件的集合)
      • 加载动态链接库,进行rebase指针调整和bind符号绑定
      • Objc Runtime 的初始处理,包括 Objc 相关类的注册、Category注册、selector 唯一性检查
      • +load()初始化

        这个阶段的优化有 1. 减少动态库加载(合并动态库)2. 减少加载启动后不会去用的类和方法 3. 少用+load,或用 +initialize替换,因为runtime 的 Method Swizzling 操作每次4ms

    2. main()执行后

      • 指 main() 执行开始到 didFinishLaunchingWithOptions 方法里首屏渲染相关方法完成
      • 配置初始化文件的IO操作
      • 首屏列表大数据的读取
      • 首屏渲染的大量计算

        优化方法有把各种与首屏渲染不相干的初始化挪走或子线程处理

    3. 首屏渲染完成后

      • 指 didFinishLaunchingWithOptions 作用域内执行首屏渲染之后的所有方法执行完成
      • 非首屏其他业务服务模块的初始化
      • 监听的注册
      • 配置文件的读写

        把可能卡住主线程的方法挪走或子线程处理

    UIScrollView 的代理方法

    1. 滚动完 scrollViewDidScroll
    2. 缩放完 scrollViewDidZoom
    3. 将要开始拖动 scrollViewWillBeginDragging
    4. 将要结束拖动 scrollViewWillEndDragging
    5. 滑动将要减速、 滑动减速完成
    6. 滚动动画完成
    7. 将要开始缩放、已经结束缩放

    响应链和事件传递

    1. hitTest方法检测看是否返回

    2. 继承UIResponder的类才能响应,如UIApplicationUIViewUIViewController。而CALayer是继承自NSObject的,不能响应

    3. 事件首先传递给UIApplication,然后向下分发给UIWindow,然后分发给最下层的UIView,逐步调用hitTest从屏内向外找,当某个UIView返回YES时就递归对其SubView执行hitTest,直到找到最后一个

    • UIView不接受事件的情况:
      1. alpha < 0.01
      2. userInteractionEnabled = NO
      3. hidden = YES

    UIView 和 CALayer 的区别和联系

    1. UIView能响应,CALayer不能
    2. View的frame(和bounds)是简单返回layer的frame(bound), 而layer的frame由几个参数决定
    3. UIView 是 CALayer 的代理
    4. 每个UIView内都有一个CALayer(即有个属性)

    轮播图朴素实现的集中方法

    1. UICollectionView, 简单粗暴放100个Cell
    2. UIScrollView 首尾各放一个展示
    3. UIScrollView 三个ImageView实现
    4. UIImageView 自己实现layer转场动画

    TableView 和 CollectionView 必选的代理方法

    • delegate都是可选
    • datasource各有两个必选
    • table: cellForRowAtIndexPath、numberOfRowsInSection
    • collection: cellForItemAtIndexPath、numberOfItemsInSection

    UITableView 的优化思路

    流程

    1. 获取数据;
    2. 把数据转化成model、存进数组;
    3. tableview调用reloadData刷新数据;
    4. 在代理方法cellForRowAtIndexPath里,创建自定义的cell,把model赋值给cell;
    5. cell在对应的model的set方法里,根据拿到的model,设置图片的image,设置label的text等(控件都以懒加载形式初始化);
    6. 在代理方法heightForRowAtIndexPath里,根据model,算出当前行应该显示多少的高度;
    7. 在cell的layoutSubviews方法里,布局子控件。

    优化思路

    1. 避免主线程阻塞

    1/2步里的获取数据、数据处理等耗时操作,应该放入后台线程异步处理,处理好后再通知主线程刷新界面。

    1. 避免频繁的对象创建

    对象的创建会发送内存分配、属性调整等。
    所以,首先,尽量用轻量的对象代替重量的对象。比如CALayer代替UIView。
    接着,多利用缓存思想,对象创建后缓存起来,需要的时候再拿出来用。合理利用内存开销,减少CPU开销。
    把 Cell setModel里的一些操作放在第二步数据转model里

    1. 减少对象的属性赋值操作

    尤其是UIView的frame/bounds等属性的赋值操作,会产生比较大的CPU消耗。
    尽量少让Cell里空间动态变化,有规律的话筛分成多个固定cell

    1. 异步绘制

    文本渲染、图像绘制都是比较消耗性能的操作,而UILabel等控件都是在主线程进行的文本绘制。这会对性能产生比较大的影响。
    UIKit和CoreAnimation相关操作必须在主线程中进行,其它的可以在后台线程异步执行

    1. 简化视图结构

    GPU在绘制图像前,会把重叠的视图进行混合,视图结构越复杂,这个操作就越耗时,如果存在透明视图,混合过程会更加复杂。所以,我们可以:

    • 尽量避免复杂的图层结构
    • 少使用透明的视图
    • 不透明的视图,设置opaque = YES

    1. 减少离屏渲染

    老生常谈之圆角问题,圆角是开发中经常使用到的美化方式,但一般的设置cornerRadius时会配合masksToBounds属性,这就会造成离屏渲染。关于这种问题的处理,大致有两个思路:

    • 异步绘制一张圆角的图片来显示;
    • 用一个圆角而中空的图来盖住。

    tableview需要刷新数据时,使用
    [tableview beginUpdates]、[tableview insertRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationNone]、
    [tableview endUpdates];而非
    [tableview reloadData]从而刷新更少的行减少CPU压力

    1. 对于固定行高,前一个设置属性比后一个实现代理方法效率高
    1
    2
    3
    4
    5
    6
    cell.tableview.rowHeight = 50.0;

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        return 50.0;
    }
    1. NSDateFormatter这个对象的相关操作很费时,需要避免频繁的创建和计算

    2. 利用RunLoop延时加载图片

    利用RunLoop:把图片加载的方法放在NSDefaultRunLoopMode里,这样当滑动时就切换出了这个Mode,暂停加载

  • 相关阅读:
    判断日期是否是法定节假日或者休息日
    linux版powershell安装教程(.net core版)
    两款【linux字符界面下】显示【菜单】,【选项】的powershell脚本模块介绍
    powershell中使用超大内存对象
    powershell脚本,命令行参数传值,并绑定变量的例子
    在docker容器中安装和使用,linux版的powershell
    powershell开源新闻及简介
    用powershell+excel行列转置三步走
    让powershell同时只能运行一个脚本(进程互斥例子)
    powershell玩转SQL SERVER所有版本
  • 原文地址:https://www.cnblogs.com/lijianming180/p/12041540.html
Copyright © 2011-2022 走看看