目前主要参考了摸鱼周报中的这篇文章以及自己的一些补充,# iOS面试总结(2020年6月),后续的一些面试题也会补充进来。
Swift
- Swift钟struct跟class有什么区别
- struct是值引用,轻量,存放于栈区,无法被继承。
- class是类型引用,存放于堆区,可以继承
- Swift中方法调用有哪些
直接派发,函数表派发,消息机制派发。
- Swift与OC的区别
Swift | Object-C | |
---|---|---|
语言特性 | 静态语言,更加安全 | 动态语言,不太安全 |
语法 | 更精简 | 冗长 |
命名空间 | 有 | 吴 |
方法调用 | 直接调用,函数派发,消息转发 | 消息转发 |
语言效率 | 性能更高,速度更快 | 略低 |
编程特性 | 函数式编程,响应式编程,面向协议编程 | 面向对象 |
- 面向协议编程
面向协议通过协议的方式组织各个类的关系。Swift的底层几乎都是构建在协议之上。
解决问题:
- 面向对象的菱形问题
- 横切关注点
- 动态派发的安全性
- Enum数据结构的内存分布
具体可以参考: Swift中Enum数据结构的内存分布问题
- Swift中的await/async
简单理解就是非抢占式的任务调度。设置潜在暂停点,在系统底部分配合适的线程进行执行,不会阻塞当前线程。
OC语法
- Block是如何实现的?Block对应的数据结构是什么样子的? block的作用是什么?它对应的数据结构又是什么样子的?
Block本质也是一个OC对象,底层通过strcut+函数的
void blockMain() {
void(^MyBlock)(void) = ^{
printf("block test");
};
MyBlock();
}
clang后,会变成如下代码:
struct __blockMain_block_impl_0 {
struct __block_impl impl;
struct __blockMain_block_desc_0* Desc;
__blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa; // block指向block所属的类型(_NSConcreteGlobalBlock, _NSConcreteStackBlock, _NSConcreteMallocBlock)
int Flags;
int Reserved;
void *FuncPtr; // 回调的函数指针
};
static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};
而block的调用就会转变成,简化后就是:
((__block_impl *)MyBlock)->FuncPtr
__block关键字的作用就是让block捕获该变量,那么编译后会生成一个关于这个变量的结构体:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding; // 通过这个指针,对值进行修改。
int __flags;
int __size;
int a; // __block int a = 2;
};
- GCD中的Block在堆上还是栈上
堆上。可以通过isa指针确认。
- NSCoding协议的作用
一种编码协议,归档跟解档时使用。一般被NSKeyedArchiver做自定义对象持久化时使用。
- KVO实现原理
利用Runtime生成一个中间对象,让原对象的isa指针指向它,然后重写setter方法,插入willChangeValueForKey和didChangeValueForKey方法。当属性变化时会调用,会调用这两个方法通知到外界属性变化。
- NSOperation有哪些特性,与GCD相比有哪些优点?
NSOperation需要和NSOperationQueue配合使用来实现多线程的方案。
- 特点: 添加任务依赖,控制并发数,以及任务运行状态的监控
- 任务执行状态的控制,isReady,isExecuting,isFinished,isCancelled
- 状态监控:
-
如果只写了main方法,底层控制变更任务执行完成状态,以及任务退出。
-
如果重写了start方法,那么需要自己控制任务执行状态。
- 系统是怎样移除一个isFinished=YES的NSOperation的?
通过KVO通知NSOperationQueue来达到对NSOperation做移除的操作。
比如可以参考SDWebImage的下载图片相关的源代码实现。
- NSNotificaiton是同步还是异步的,如果发通知时在子线程,接收在哪个线程?
同步。在子线程。详见nsnotification-and-multithreading。
NSNotificationCenter是线程安全的,那个线程POST就会在哪个线程去接收,因此如果在子线程POST一个通知去更改UI的话,会引起Error。
UI
- 事件响应链是如何传递的。
- 事件的传递
从UIApplication开始,到Window,再逐级向下,直到找到最深层的子视图。用hitTest:withEvent & pointInside:withEvent
- 事件的响应
从识别到的视图开始判断是否能验证,不能的话,逐级向上。如果都不能,则忽略这个事件。
- 什么是异步渲染
异步渲染可以理解为在子线程中进行绘制,然后在主线程中显示。
比较典型的就是图片的展示优化,即我们可以在子线程中先将图片解码完毕后,再通过主线程进行图片的赋值。
- layoutSubviews在什么时机调用
- init初始化的时候不会被调用。
- addSubview时。
- 设置frame且变化的时候。
- 滚动UIScrollView且引起View的重新布局的时候。
- 一张图片的展示经历了哪些步骤
- 从磁盘中加载一张图片
- 将生成的UIImage赋值给UIImageView,注意,这个时候的图片还不需要解码。
- 接着一个隐式的CATransaction会捕捉到UIImageView的图形树的变化
- 图片进行解码,默认在主线程进行(可以通过在子线程中解码来对App进行性能上的优化)
- 将解码后的图片给到UIImageView,然后展示到屏幕上。
- 什么是离屏渲染?什么情况下会导致离屏渲染?
屏幕上显示的内容,其实是从frame buffer中获取的,如果因为一些特殊的原因,无法直接将所有的渲染结果放到frame buffer中,而是要借助别的buffer,则导致了离屏渲染。(视图的渲染遵循画家算法)
导致离屏渲染的情况:
- cornerRadius + clipsToBounds
- group opacity组透明度
- mask遮罩
- UIBlurEffect毛玻璃效果
- CoreAnimation这个框架的作用,以及它跟UIKit的关系
CorAnimation是一个图像渲染的框架,包括一些动画的展示。他的架构是在UIKit的下面的。
引用计数
- ARC的原理?在什么时候隐式添加release操作?
通过编译器自动管理相应的引用计数的状态。
在编译阶段就会添加对应的retain和release代码。
- 循环引用的场景,以及如何避免。
即两个或者两个以上的出现引用环,导致对象无法释放。一般在block,delegate,NSTimer中会比较容易出现这个问题。
解决方案就是给其中一个引用指定为弱引用。
- 为什么当我们在使用block时外面是weak 声明一个weakSelf,还要在block内部使用strong再持有一下?
用weak修饰是因为希望block对象捕获的是一个对对象的弱持有,我们都知道,block会将对象的修饰符也会一并捕获的。
block里面进行strong是为了防止这个对象被提前释放。
- Autoreleasepool是实现机制是什么?它是什么时候释放内部的对象的?它内部的数据结构是什么样的?当我提到哨兵对象时,会继续问哨兵对象的作用是什么,为什么要设计它?
AutoReleasePool是通过双向链表,加上固定大小的AutoReleasePage实现的,它会对加入其中的对象做延迟释放。
当调用drain方法时会进行释放,以及runloop结束的时候。
哨兵对象类似一个指针,指向自动释放池的栈顶位置,它的作用就是用于标记当前自动释放池需要释放内部对象时,释放到那个地方结束,每次入栈时它用于确定添加的位置,然后再次移动到栈顶。
- weak的实现原理是什么?当引用对象销毁是它是如何管理内部的Hash表的?
放到一张全局的hash表中,用weak对象所指向的那个对象的内存作为key,应该是storeWeak。
释放的时候,是在NSObject调用dealloc的时候,会进行一个释放的操作。对象的isa指针会有一个是否有weak指针的标识。
Runtime
- 消息发送的流程是怎样的?
OC的方法调用本质上就是执行了objc_msgSend
具体流程如下:
- 先确定调用方法的类是否加载完毕,如果没有加载完毕的话进行加载
- 判断对象是否为空,如果为空,返回。如果isa是NSTaggedPoint类型的话,直接返回结果。
- 从cache中查找
- cache中没有的话,就从方法列表中查找,查找到则缓存
- 如果该类中没有找到,那么不断从父类中查找,直到查找到NSObject。
- 关联对象时什么情况下会导致内存泄漏
关联对象可以理解为是持有了一个对象,如果产生了引用环,那么就会产生循环引用。
- 消息转发流程
消息转发就是当前receiver没有找到对应的方法,会执行以下步骤:
- 执行resolveInstanceMethod:/ resolveClassMethod:需要我们确定是否要进行转发
- 如果第一步返回YES,会执行forwardingTargetForSelector。
- methodSignatureForSelector用于指定方法签名,forwardInvocation用于处理Invocation,进行完整转发
- 如果消息转发也不能处理,那么就会调用doesNotRecognizeSelector,引发奔溃。
- category能否添加实例变量
分类是运行时被编译的,类的内存在编译期就已经分配好了,所以无法添加实例变量。我们可以通过在属性的get&set方法中,添加关联属性的方案去解决这个问题。
- 元类的作用是什么
元类的作用就是存储类方法,元类本身也是一个对象,满足OC万物皆对象的设计理念。
- 类方法跟类属性存储到什么地方?
存储到元类中。
- 讲几个runtime的应用场景
- hook系统方法进行交换
- 关联对象
- KVO
Runloop
- 讲一下对Runloop的理解
Runloop就是一个事件运行循环,它保证了在没有任务的时候线程不退出,有任务的时候及时响应,与线程一一对应。
Runloop跟线程,事件响应,手势识别,页面更新,定时器等都有紧密联系。
- Runloop能实现什么功能?
- 检测卡顿
- 线程保活
- 性能优化,将一些耗时的操作放到runloop wait的情况下处理
性能优化
- 对UITableView进行性能优化有哪些方式
- 缓存高度
- 异步渲染
- 减少离屏渲染
- 提前组装好渲染数据
- Xcode的Instruments有哪些调试工具?
- Activity Monitor(活动监视器):监控进程的CPU、内存、磁盘、网络使用情况。是程序在手机
运行真正占用内存大小
- Allocations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史
- Core Animation(图形性能):显示程序显卡性能以及CPU使用情况
- Core Data:跟踪Core Data文件系统活动
- Energy Log:耗电量监控
- File Activity:检测文件创建、移动、变化、删除等
- Leaks(泄漏):一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录
- Network:用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接
- System Usage:记录关于文件读写,sockets,I/O系统活动,输入输出
- Time Profiler(时间探查):方法执行耗时分析
- Zombies:测量一般的内存使用,专注于检测过度释放的野指针对象。也提供对象分配统计以及主动分配的内存地址历史
- 讲一下做过哪些性能优化的工作
- App启动优化
- 包体积的优化
- UITableView的优化
- 卡顿优化
- 如何检测卡顿
- FPS,通过CADisplayLink计算刷新次数
- 通过Runloop检测, 两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
- 缩小包体积有哪些方案
- 图片压缩,无用图片删除
- 一些大图可以动态下发
- 删除无用类,无用方法(LInkMap & Mach-O)
- 减少三方库的依赖,动态库的合并