iOS开发面试整理之专题篇
Runloop篇
- 什么是RunLoop?
- 从字面意思看:运行循环/跑圈
- 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如:source/Timer/Observer)
- 一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要自己手动启动(调用run方法)
- RunLoop只能选择一个Mode启动,如果当Mode中没有任何source(source0/source1),Timer,那么就直接退出RunLoop
- runloop加深理解:
- APP启动完成后,runloop进入休眠状态,等待用户的点击事件、定时器、系统事件,一旦有这些事件,立即处理,没有继续休眠
- 自动释放池什么时候释放?
- 通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(
KCFRunLoopBeforeWaiting
) - 详细分析
- 分两种情况:手动干预释放和系统自动释放
- 手动干预释放就是指定autoreleasepool(手动创建的),当前作用域大括号结束就立即释放
- 系统自动去释放:不手动指定autoreleasepool,Autorelease对象会在当前的 runloop 迭代结束时释放
- kCFRunLoopEntry(1):第一次进入会自动创建一个autorelease
- kCFRunLoopBeforeWaiting(32):进入休眠状态前会自动销毁一个autorelease,然后重新创建一个新的autorelease
- kCFRunLoopExit(128):退出runloop时会自动销毁最后一个创建的autorelease
- 分两种情况:手动干预释放和系统自动释放
- 通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(
- 在开发中如何使用RunLoop?什么应用场景?
- 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
- 在子线程中开启一个定时器 在子线程中进行一些长期监控
- 可以控制定时器在特定模式下运行
- 可以让某些事件(行为/任务)在特定模式下执行
- 可以添加Observer监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
- NSTimer定时器
- 定时器默认情况下是添加在RunLoop的默认(Default)模式下的,因此当滚动界面时,定时器会停止,滚动停止定时器重新工作
- 解决办法:
- 使用将定时器添加到占位(CommonMode)模式中
- 使用GCD定时,GCD的定时器不受runLoop的Mode影响(滚动界面时,定时器不受影响)GCD定时器实现不一样,GCD比NSTimer准确
- 可以添加NSTimer到指定的Mode中,比如:滚动开始就有作用
- NSTimer会保留目标对象,如果定时器重复定时(反复执行任务),容易引入保留环,一次性的定时器执行完会自动失效调用invalidate
Runtime篇
- 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
- 无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放
- 对象的内存销毁时间表,分四个步骤
- 调用 -release :引用计数变为零
- 对象正在被销毁,生命周期即将结束.
- 不能再有新的 __weak 弱引用,否则将指向 nil.
- 调用 [self dealloc]
- 父类调用 -dealloc
- 继承关系中最直接继承的父类再调用 -dealloc
- 如果是 MRC 代码 则会手动释放实例变量们(iVars)
- 继承关系中每一层的父类 都再调用 -dealloc
- NSObject 调 -dealloc
- 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
- 调用 object_dispose()
- 为 C++ 的实例变量们(iVars)调用 destructors
- 为 ARC 状态下的 实例变量们(iVars) 调用 -release
- 解除所有使用 runtime Associate方法关联的对象
- 解除所有 __weak 引用
- 调用 free()
- 调用 -release :引用计数变为零
- 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量;
- 分析如下:
- 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量
- 运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
- runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
- 每一个类对象中都一个对象方法列表(对象方法缓存)
- 类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)
- 方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
- 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
- 当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找
- 具体实现
- 在寻找IMP的地址时,runtime提供了两种方法,而根据官方描述,第一种方法可能会更快一些
-
通过该传入的参数不同,找到不同的方法列表,方法列表中保存着下面方法的结构体,结构体中包含这方法的实现,selector本质就是方法的名称,通过该方法名称,即可在结构体中找到相应的实现。
IMP class_getMethodImplementation(Class cls, SEL name); IMP method_getImplementation(Method m)
- 第一种
-
获取实例方法和类方法
//类方法(假设有一个类A) class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName)); //实例方法 class_getMethodImplementation([A class],@selector(methodName));
-
-
第二种
//类方法 Method class_getClassMethod(Class cls, SEL name) //实例方法 Method class_getInstanceMethod(Class cls, SEL name) //获取IMP地址 IMP method_getImplementation(Method m)
加密篇
- 加密分为对称加密和非对称加密(又叫公钥密码)
- 对称加密:加解密用一个密钥
- 常见的对称加密有:DES、3DES、AES
- DES加密
- 密钥长度为56bit,本来也是有64位,但是每隔7bit有个错误校验位
- 每次只能加密64bit数据,需要数据分组加密
- 3DES,DES3重加密
- AES加密
- 密钥长度有128、192、256bit三种
- 非对称加密最安全的一种方式
- 非对称加密:由于密钥配送泄露的问题,出现了非对称加密
- 公钥用来加密,私钥用来解密。
- 由消息接收者生成公私钥对,私钥自己保存,公钥公开
- 常用的非对称加密有:RSA、ECC
- RSA是由3个人做出来的算法。三个人的首字母取名。
- 混合密码系统
- 对称加密缺点:钥匙配送问题
- 非对称加密缺点:加密、解密速度较慢。
- 混合密码: 对称加密、非对称加密相结合
- 网络上的密码通信所用的SSL/TLS都运用了混合密码系统
- 原理
- 消息加密:加密通过对称加密,形成加密消息
- 钥匙配送:对称密钥通过公钥加密,形成密钥消息
- 最后发送消息为:对称加密的消息+公钥加密的密码
- 单向散列函数(不可逆加密)
- 单向散列函数:根据消息内容计算出散列值
- 单向散列函数的特点:
- 任意长度的消息,计算出固定长度的散列值
- 计算速度快,能快速计算出散列值
- 消息不同,散列值也不同
- 具备单向性,不可逆的
- 常见的单向散列函数
- MD4、MD5:产生128bit的散列值
- SHA-1: 产生160bit的散列值
- SHA-2: SHA-256、SHA-384、SHA-512,散列值长度分别是256bit、384bit、512bit
- SHA-3:全新标准
- 单向散列函数的作用:防止数据被篡改
- 通常的笨方法:要防止被篡改,每次必须要拷贝一下源文件放到安全的地方,下次查看时将源文件与拷贝副本进行比较。
- 如果源文件非常大,这样耗内存
- 解决办法:将源文件的单向散列值保存到安全地方,下次拿到源文件生成散列值与曾经保存的进行比对。
- 数字签名
- 问题:公钥加密不能解决消息被串改问题,因为公钥公开任何人都可以加密,私钥在消息接收者手里。
- 解决办法:消息发送者生成公钥、私钥,并且将公钥公开,消息发送者用私钥加密,消息接收者用公钥解密,这就叫数字签名
- 数字签名与公钥密码区别:
- 数字签名是为了防止消息不被串改,不管保密性。
- 公钥密码是为了防止消息被查看,不管是否被串改。
- 数字签名:私钥加签,公钥验签; 公钥密码:私钥解密,公钥加密。
- 存在问题:
- 我们知道直接用公钥加密消息整体是非常耗时的,公钥密码采用了混合密码系统解决这个问题的,那数字签名怎么办呢?
- 解决:
- 生成消息体的单向散列函数(时间快)
- 用私钥加签单向散列函数值(函数值小)
- 消息发送者最终发送的是:消息明文+消息函数散列值的签名
- 数字签名的作用:保证消息的完整性、防止消息被篡改
- 证书:
- 公钥密码与数字签名都有一个共同的bug,如果在发送公钥的过程中公钥被中间人拦截,替换成中间人的了呢? 那么消息发送者或者验签者用的公钥是中间人的,那么就出现了问题。
- 公钥加密:消息发送者加密的消息,只能由中间人解密
- 数字签名:中间人串改成自己的签名,消息验签者可以接受
- 证书就是为了解决这个问题的
- 证书机构CA用自己的私钥加签用户的公钥生成证书放到数据库,使用者到证书机构下载证书,通过证书机构的公钥验证证书的合法性(证书内部的公钥是否合法、被替换)。
- 证书的内容包括:申请者的公钥明文、申请者的信息、申请者公钥明文加签的签名
- 公钥密码与数字签名都有一个共同的bug,如果在发送公钥的过程中公钥被中间人拦截,替换成中间人的了呢? 那么消息发送者或者验签者用的公钥是中间人的,那么就出现了问题。
- 对称加密:加解密用一个密钥
线程篇
- 线程死锁
- 结论: 一个线程里面,同步(sync)调用同一个串行队列会死锁
- 代码举例:
-
例1:主线程中同步执行主队列(主队列是串行队列)
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"1"); //卡死 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); }); NSLog(@"3"); }
-
例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: 子线程、串行队列,同步执行
- (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]); }); }); }
-
例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]); }); }); }
-
- 线程通信
-
从子线程切回到主线程
dispatch_sync(dispatch_get_main_queue(), ^{ });
-
多个线程间切换执行代码
- 在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
- 线程间通信的体现
- 1个线程传递数据给另1个线程
- 在1个线程中执行完特定任务后,转到另1个线程继续执行任务
-
线程间通信常用方法
- (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;
-
性能篇
- UI卡顿掉帧原因
- iOS设备的硬件时钟会发出Vsync(垂直同步信号)
- 然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。
- 随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。
- 也就是说,一帧的显示是由CPU和GPU共同决定的。
- 一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。
- 1s = 1000 ms ,每帧需要 1000/60= 16.7ms
- 因此,每隔16.7ms就要产生一帧画面
- 离屏渲染
- On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
- Off-Screen Rendering:离屏渲染,分为CPU离屏渲染和GPU离屏渲染两种形式。GPU离屏渲染指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作,应当尽量避免的则是GPU离屏渲染
- GPU离屏渲染何时会触发呢?
- 圆角(当和maskToBounds一起使用时)、图层蒙版(透明)、阴影,设置
layer.shouldRasterize = YES
- 圆角(当和maskToBounds一起使用时)、图层蒙版(透明)、阴影,设置
- 为什么要避免GPU离屏渲染?
- GPU需要做额外的渲染操作。
- 通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响。
- 另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。所以可以的话应尽量减少offscreen-render的图层
- 谈一谈APP的性能优化
- APP安装包ipa优化
- 安装包组成:可执行文件、资源
- 资源优化
- 删除无用的图片资源,用第三方工具LSUnusedResources检查
- 无用的nib资源
- 无损压缩图片(ImageOptim、PPDuck)
- 可执行文件优化
- Xcode编译器优化,关闭C++/OC/C的异常支持
- AppCode开发IDE检查冗余代码(没有使用的类)
- 第三方工具(OCLint)检查无用冗余代码,删除
- LinkMap查阅项目文件大小,然后针对优化
- APP启动速度优化(冷启动)
- 设置环境变量(DYLD_PRINT_STATISTICS)监控APP启动时间
- dyld阶段:
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少类、分类、selector数量,定期清理不用的类
- 减少C++虚函数
- Swift尽量使用struct
- runtime:
- 尽量使用initialize替换+load方法
- APP运行内存优化
- 少用单例使用
- 不用的对象即时释放、多用懒加载
- 检查内存泄漏
- instruct通过leaks
- 基层基类实现dealloc
- APP卡顿优化
- CPU少计算
- 尽量使用CALayer
- 不要频繁使用UIview的frame、bounds,平凡改动布局
- 原图的size最好刚好跟UIImageView保持一致
- 线程
- 控制线程最大并发量
- 把耗时操作放到子线程
- 图片绘制解码放在子线程,解码完成后切到主线程刷新UI
- 防止离屏渲染
- 少使用光栅化
layer.shouldRasterize = YES
- 透明度
- 少设置圆角,尽量让UI出图或者自己绘制CoreGraphics
- 设置阴影效果
- 少使用光栅化
- CPU少计算
- APP耗电优化
- 网络:
- 减少网络请求,多次请求结果相同,尽量使用缓存
- 先判断网络是否可用,不要尝试网络请求
- 定位优化,少使用导航,降低定位精度
- 网络:
- APP安装包ipa优化
MVC与MVVM
- 谈谈对MVC的理解
- M:model模型、V:view视图、C:viewController控制器
- 传统的MVC:控制器获取数据,然后将数据转化为model模型,控制器将模型数据赋值给视图属性
- 优点:视图View直接暴露控件属性,复用性高
- 缺点:控制器代码量比较大,重
- 优化后的MVC:控制器获取数据,然后将数据转化为model模型,直接将model传递给视图view,视图View如何通过显示数据全部封装在视图View控件中
- 优点: 视图View只暴露模型属性,减轻控制器代码量、体现对象的封装性
- 缺点:视图控件复用性不是太好,不过可以设置多种model实现View复用
- 如何抽取控制器(C)代码
- 创建对应控制器工具类(Tool、service),抽取当前控制器的网络请求、数据处理等逻辑代码
- MVC常用误区:
- 子视图里面封装网络请求、跳转页面等逻辑
- 不易维护代码、项目架构混乱
- 谈谈对MVVM的理解
- M:model模型、V:view+viewController、VM:viewModel
- 特点:
- VM直接展示数据
- VM绑定视图View,数据驱动视图
- VM将控制器C数据处理逻辑抽取出来,减轻控制器重力