iOS开发面试整理之专题篇

Runloop篇

  1. 什么是RunLoop?
    1. 从字面意思看:运行循环/跑圈
    2. 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如:source/Timer/Observer)
    3. 一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要自己手动启动(调用run方法)
    4. RunLoop只能选择一个Mode启动,如果当Mode中没有任何source(source0/source1),Timer,那么就直接退出RunLoop
    5. runloop加深理解:
      1. APP启动完成后,runloop进入休眠状态,等待用户的点击事件、定时器、系统事件,一旦有这些事件,立即处理,没有继续休眠
  2. 自动释放池什么时候释放?
    1. 通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(KCFRunLoopBeforeWaiting)
    2. 详细分析
      1. 分两种情况:手动干预释放和系统自动释放
        1. 手动干预释放就是指定autoreleasepool(手动创建的),当前作用域大括号结束就立即释放
        2. 系统自动去释放:不手动指定autoreleasepool,Autorelease对象会在当前的 runloop 迭代结束时释放
          1. kCFRunLoopEntry(1):第一次进入会自动创建一个autorelease
          2. kCFRunLoopBeforeWaiting(32):进入休眠状态前会自动销毁一个autorelease,然后重新创建一个新的autorelease
          3. kCFRunLoopExit(128):退出runloop时会自动销毁最后一个创建的autorelease
  3. 在开发中如何使用RunLoop?什么应用场景?
    1. 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
    2. 在子线程中开启一个定时器 在子线程中进行一些长期监控
    3. 可以控制定时器在特定模式下运行
    4. 可以让某些事件(行为/任务)在特定模式下执行
    5. 可以添加Observer监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
  4. NSTimer定时器
    1. 定时器默认情况下是添加在RunLoop的默认(Default)模式下的,因此当滚动界面时,定时器会停止,滚动停止定时器重新工作
    2. 解决办法:
      1. 使用将定时器添加到占位(CommonMode)模式中
      2. 使用GCD定时,GCD的定时器不受runLoop的Mode影响(滚动界面时,定时器不受影响)GCD定时器实现不一样,GCD比NSTimer准确
    3. 可以添加NSTimer到指定的Mode中,比如:滚动开始就有作用
    4. NSTimer会保留目标对象,如果定时器重复定时(反复执行任务),容易引入保留环,一次性的定时器执行完会自动失效调用invalidate

Runtime篇

  1. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
    1. 无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放
    2. 对象的内存销毁时间表,分四个步骤
      1. 调用 -release :引用计数变为零
        1. 对象正在被销毁,生命周期即将结束.
        2. 不能再有新的 __weak 弱引用,否则将指向 nil.
        3. 调用 [self dealloc]
      2. 父类调用 -dealloc
        1. 继承关系中最直接继承的父类再调用 -dealloc
        2. 如果是 MRC 代码 则会手动释放实例变量们(iVars)
        3. 继承关系中每一层的父类 都再调用 -dealloc
      3. NSObject 调 -dealloc
        1. 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
      4. 调用 object_dispose()
        1. 为 C++ 的实例变量们(iVars)调用 destructors
        2. 为 ARC 状态下的 实例变量们(iVars) 调用 -release
        3. 解除所有使用 runtime Associate方法关联的对象
        4. 解除所有 __weak 引用
        5. 调用 free()
  2. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
    1. 不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量;
    2. 分析如下:
      1. 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量
      2. 运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
  3. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
    1. 每一个类对象中都一个对象方法列表(对象方法缓存)
    2. 类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)
    3. 方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
    4. 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
    5. 当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找
    6. 具体实现
      1. 在寻找IMP的地址时,runtime提供了两种方法,而根据官方描述,第一种方法可能会更快一些
      2. 通过该传入的参数不同,找到不同的方法列表,方法列表中保存着下面方法的结构体,结构体中包含这方法的实现,selector本质就是方法的名称,通过该方法名称,即可在结构体中找到相应的实现。

         IMP class_getMethodImplementation(Class cls, SEL name);
         IMP method_getImplementation(Method m)
        
      3. 第一种
        1. 获取实例方法和类方法

           //类方法(假设有一个类A)
             class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));
           //实例方法
           class_getMethodImplementation([A class],@selector(methodName));
          
      4. 第二种

         //类方法
         Method class_getClassMethod(Class cls, SEL name)
         //实例方法
         Method class_getInstanceMethod(Class cls, SEL name)
         //获取IMP地址
         IMP method_getImplementation(Method m)
        

加密篇

  1. 加密分为对称加密和非对称加密(又叫公钥密码)
    1. 对称加密:加解密用一个密钥
      1. 常见的对称加密有:DES、3DES、AES
      2. DES加密
        1. 密钥长度为56bit,本来也是有64位,但是每隔7bit有个错误校验位
        2. 每次只能加密64bit数据,需要数据分组加密
        3. 3DES,DES3重加密
      3. AES加密
        1. 密钥长度有128、192、256bit三种
        2. 非对称加密最安全的一种方式
    2. 非对称加密:由于密钥配送泄露的问题,出现了非对称加密
      1. 公钥用来加密,私钥用来解密。
      2. 由消息接收者生成公私钥对,私钥自己保存,公钥公开
      3. 常用的非对称加密有:RSA、ECC
        1. RSA是由3个人做出来的算法。三个人的首字母取名。
    3. 混合密码系统
      1. 对称加密缺点:钥匙配送问题
      2. 非对称加密缺点:加密、解密速度较慢。
      3. 混合密码: 对称加密、非对称加密相结合
        1. 网络上的密码通信所用的SSL/TLS都运用了混合密码系统
        2. 原理
          1. 消息加密:加密通过对称加密,形成加密消息
          2. 钥匙配送:对称密钥通过公钥加密,形成密钥消息
          3. 最后发送消息为:对称加密的消息+公钥加密的密码
    4. 单向散列函数(不可逆加密)
      1. 单向散列函数:根据消息内容计算出散列值
      2. 单向散列函数的特点:
        1. 任意长度的消息,计算出固定长度的散列值
        2. 计算速度快,能快速计算出散列值
        3. 消息不同,散列值也不同
        4. 具备单向性,不可逆的
      3. 常见的单向散列函数
        1. MD4、MD5:产生128bit的散列值
        2. SHA-1: 产生160bit的散列值
        3. SHA-2: SHA-256、SHA-384、SHA-512,散列值长度分别是256bit、384bit、512bit
        4. SHA-3:全新标准
      4. 单向散列函数的作用:防止数据被篡改
        1. 通常的笨方法:要防止被篡改,每次必须要拷贝一下源文件放到安全的地方,下次查看时将源文件与拷贝副本进行比较。
        2. 如果源文件非常大,这样耗内存
        3. 解决办法:将源文件的单向散列值保存到安全地方,下次拿到源文件生成散列值与曾经保存的进行比对。
    5. 数字签名
      1. 问题:公钥加密不能解决消息被串改问题,因为公钥公开任何人都可以加密,私钥在消息接收者手里。
      2. 解决办法:消息发送者生成公钥、私钥,并且将公钥公开,消息发送者私钥加密,消息接收者公钥解密,这就叫数字签名
      3. 数字签名与公钥密码区别:
        1. 数字签名是为了防止消息不被串改,不管保密性。
        2. 公钥密码是为了防止消息被查看,不管是否被串改。
        3. 数字签名:私钥加签,公钥验签; 公钥密码:私钥解密,公钥加密。
      4. 存在问题:
        1. 我们知道直接用公钥加密消息整体是非常耗时的,公钥密码采用了混合密码系统解决这个问题的,那数字签名怎么办呢?
        2. 解决:
          1. 生成消息体的单向散列函数(时间快)
          2. 用私钥加签单向散列函数值(函数值小)
      5. 消息发送者最终发送的是:消息明文+消息函数散列值的签名
      6. 数字签名的作用:保证消息的完整性、防止消息被篡改
    6. 证书:
      1. 公钥密码与数字签名都有一个共同的bug,如果在发送公钥的过程中公钥被中间人拦截,替换成中间人的了呢? 那么消息发送者或者验签者用的公钥是中间人的,那么就出现了问题。
        1. 公钥加密:消息发送者加密的消息,只能由中间人解密
        2. 数字签名:中间人串改成自己的签名,消息验签者可以接受
      2. 证书就是为了解决这个问题的
      3. 证书机构CA用自己的私钥加签用户的公钥生成证书放到数据库,使用者到证书机构下载证书,通过证书机构的公钥验证证书的合法性(证书内部的公钥是否合法、被替换)。
        1. 证书的内容包括:申请者的公钥明文、申请者的信息、申请者公钥明文加签的签名

线程篇

  1. 线程死锁
    1. 结论: 一个线程里面,同步(sync)调用同一个串行队列会死锁
    2. 代码举例:
      1. 例1:主线程中同步执行主队列(主队列是串行队列)

         - (void)viewDidLoad {
             [super viewDidLoad];
             NSLog(@"1");
             //卡死
             dispatch_sync(dispatch_get_main_queue(), ^{
                 NSLog(@"2");
             });
             NSLog(@"3");
         }
        
      2. 例2:主线程中异步执行主队列(主队列是串行队列)

         - (void)viewDidLoad {
             [super viewDidLoad];
             NSLog(@"1");
             //不会卡死
             dispatch_async(dispatch_get_main_queue(), ^{
                 NSLog(@"2");
                 //卡死
                 dispatch_sync(dispatch_get_main_queue(), ^{
                     NSLog(@"3");
                 });
                 NSLog(@"4");
             });
             NSLog(@"5");
         }
        
      3. 例3: 子线程、串行队列,同步执行

         - (void)viewDidLoad {
             [super viewDidLoad];
             //串行队列
             dispatch_queue_t q = dispatch_queue_create("hhhhh", DISPATCH_QUEUE_SERIAL);
             //子线程
             dispatch_async(q, ^{
                 NSLog(@" %@----111", [NSThread currentThread]);
                 //同一个串行队列,同步执行,即同一个线程执行,结果,卡死
                 dispatch_sync(q, ^{
                     NSLog(@" %@----2222", [NSThread currentThread]);
                 });
             });
         }
        
      4. 例3: 子线程、并发队列,同步执行

         - (void)viewDidLoad {
             [super viewDidLoad];
             //并发队列
             dispatch_queue_t q = dispatch_queue_create("hhhhh", DISPATCH_QUEUE_CONCURRENT);
             //子线程
             dispatch_async(q, ^{
                 NSLog(@" %@----111", [NSThread currentThread]);
                 //同一个并行队列,同步执行,即同一个线程执行,结果,正常
                 dispatch_sync(q, ^{
                     NSLog(@" %@----2222", [NSThread currentThread]);
                 });
             });
         }
        
  2. 线程通信
    1. 从子线程切回到主线程

       dispatch_sync(dispatch_get_main_queue(), ^{ });
      
    2. 多个线程间切换执行代码

      1. 在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
      2. 线程间通信的体现
        1. 1个线程传递数据给另1个线程
        2. 在1个线程中执行完特定任务后,转到另1个线程继续执行任务
      3. 线程间通信常用方法

         - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg;
         - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
         - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
        

性能篇

  1. UI卡顿掉帧原因
    1. iOS设备的硬件时钟会发出Vsync(垂直同步信号)
    2. 然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。
    3. 随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。
    4. 也就是说,一帧的显示是由CPU和GPU共同决定的。
    5. 一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。
      1. 1s = 1000 ms ,每帧需要 1000/60= 16.7ms
      2. 因此,每隔16.7ms就要产生一帧画面
  2. 离屏渲染
    1. On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
    2. Off-Screen Rendering:离屏渲染,分为CPU离屏渲染和GPU离屏渲染两种形式。GPU离屏渲染指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作,应当尽量避免的则是GPU离屏渲染
    3. GPU离屏渲染何时会触发呢?
      1. 圆角(当和maskToBounds一起使用时)、图层蒙版(透明)、阴影,设置 layer.shouldRasterize = YES
    4. 为什么要避免GPU离屏渲染?
      1. GPU需要做额外的渲染操作。
      2. 通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响。
      3. 另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。所以可以的话应尽量减少offscreen-render的图层
  3. 谈一谈APP的性能优化
    1. APP安装包ipa优化
      1. 安装包组成:可执行文件、资源
      2. 资源优化
        1. 删除无用的图片资源,用第三方工具LSUnusedResources检查
        2. 无用的nib资源
        3. 无损压缩图片(ImageOptim、PPDuck)
      3. 可执行文件优化
        1. Xcode编译器优化,关闭C++/OC/C的异常支持
        2. AppCode开发IDE检查冗余代码(没有使用的类)
        3. 第三方工具(OCLint)检查无用冗余代码,删除
        4. LinkMap查阅项目文件大小,然后针对优化
    2. APP启动速度优化(冷启动)
      1. 设置环境变量(DYLD_PRINT_STATISTICS)监控APP启动时间
      2. dyld阶段:
        1. 减少动态库、合并一些动态库(定期清理不必要的动态库)
        2. 减少类、分类、selector数量,定期清理不用的类
        3. 减少C++虚函数
        4. Swift尽量使用struct
      3. runtime:
        1. 尽量使用initialize替换+load方法
    3. APP运行内存优化
      1. 少用单例使用
      2. 不用的对象即时释放、多用懒加载
      3. 检查内存泄漏
        1. instruct通过leaks
        2. 基层基类实现dealloc
    4. APP卡顿优化
      1. CPU少计算
        1. 尽量使用CALayer
        2. 不要频繁使用UIview的frame、bounds,平凡改动布局
        3. 原图的size最好刚好跟UIImageView保持一致
      2. 线程
        1. 控制线程最大并发量
        2. 把耗时操作放到子线程
        3. 图片绘制解码放在子线程,解码完成后切到主线程刷新UI
      3. 防止离屏渲染
        1. 少使用光栅化layer.shouldRasterize = YES
        2. 透明度
        3. 少设置圆角,尽量让UI出图或者自己绘制CoreGraphics
        4. 设置阴影效果
    5. APP耗电优化
      1. 网络:
        1. 减少网络请求,多次请求结果相同,尽量使用缓存
        2. 先判断网络是否可用,不要尝试网络请求
      2. 定位优化,少使用导航,降低定位精度

MVC与MVVM

  1. 谈谈对MVC的理解
    1. M:model模型、V:view视图、C:viewController控制器
    2. 传统的MVC:控制器获取数据,然后将数据转化为model模型,控制器将模型数据赋值给视图属性
      1. 优点:视图View直接暴露控件属性,复用性高
      2. 缺点:控制器代码量比较大,重
    3. 优化后的MVC:控制器获取数据,然后将数据转化为model模型,直接将model传递给视图view,视图View如何通过显示数据全部封装在视图View控件中
      1. 优点: 视图View只暴露模型属性,减轻控制器代码量、体现对象的封装性
      2. 缺点:视图控件复用性不是太好,不过可以设置多种model实现View复用
    4. 如何抽取控制器(C)代码
      1. 创建对应控制器工具类(Tool、service),抽取当前控制器的网络请求、数据处理等逻辑代码
    5. MVC常用误区:
      1. 子视图里面封装网络请求、跳转页面等逻辑
      2. 不易维护代码、项目架构混乱
  2. 谈谈对MVVM的理解
    1. M:model模型、V:view+viewController、VM:viewModel
    2. 特点:
      1. VM直接展示数据
      2. VM绑定视图View,数据驱动视图
      3. VM将控制器C数据处理逻辑抽取出来,减轻控制器重力
Table of Contents