iOS 开发工程师面试心得

最近找工作已经面试了6家公司,既有初创公司也有上市公司,现利用周末时间总结一下面试被问的频率高的技术问题,本文最近几天会持续更新。

技术点 频率
Runtime机制及其应用 ★★★★★
RunLoop原理及其应用 ★★★★★
多线程技术 ★★★★★
网络技术 ★★★★★
内存管理理解及其应用 ★★★★★
性能优化 ★★★★★
系统架构设计 ★★★★☆
Block ★★★★☆
CoreGraphics、UIBezierPath、CoreAnimation ★★★★☆
数据结构和算法 ★★★★☆
第三方库 ★★★☆☆

其中智能硬件行业的公司会问蓝牙BLE的技术点:数据分包、传输、校验、加密。
另外,AXKit(个人开源库)的简介中的功能的实现思路也是必问的。

Runtime常见的应用场景有哪些?

主要有以下几种:关联对象,给分类增加属性、动态添加方法、Method Swizzling 交换方法、拦截系统方法的调用、自动归档解档、字典转模型、万能控制器跳转、KVO的底层实现、JSPatch热更新。

关联对象,给分类增加属性

1
2
3
4
5
6
// 关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
// 移除关联的对象
void objc_removeAssociatedObjects(id object)

例如给一个名为Person的类的分类添加一个books属性:

1
2
3
4
5
6
7
8
9
10
static const void *KEY_BOOKS = &KEY_BOOKS;

@implementation Person (Books)
- (void)setBooks:(NSArray *)books{
objc_setAssociatedObject(self, KEY_BOOKS, books, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)books{
return objc_getAssociatedObject(self, KEY_BOOKS);
}
@end

动态添加方法(performSelector)

1
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);

通过performSelector调用某个方法,如果在运行时找不到Selector对应的实现,会执行消息转发,在resolveInstanceMethodresolveClassMethod中将函数与对象的Selector关联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void eat(id self, SEL sel) {
NSLog(@"%@ %@",self, NSStringFromSelector(sel));
}

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
class_addMethod(self, @selector(eat), eat, "[email protected]:");
}
return [super resolveInstanceMethod:sel];
}

@end

Method Swizzling 交换方法、拦截系统方法的调用

每个类都维护一个方法列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系。

1
2
3
4
5
6
7
8
9
10
11
+ (void)exchangeClassMethodImplementations:(Class)cls selector1:(SEL)selector1 selector2:(SEL)selector2{
Method m1 = class_getClassMethod(cls, selector1);
Method m2 = class_getClassMethod(cls, selector2);
method_exchangeImplementations(m1, m2);
}

+ (void)exchangeInstanceMethodImplementations:(Class)cls selector1:(SEL)selector1 selector2:(SEL)selector2{
Method m1 = class_getInstanceMethod(cls, selector1);
Method m2 = class_getInstanceMethod(cls, selector2);
method_exchangeImplementations(m1, m2);
}

自动归档解档

遍历Model自身所有属性,并对属性进行encode和decode操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}

访问私有变量

通过getIvar可以获取到任意已知name的成员变量的值。

1
2
Ivar ivar = class_getInstanceVariable([Model class], "_str1");
NSString * str1 = object_getIvar(model, ivar);

字典转模型的KVC实现

遍历Model自身所有属性,从json中找到与属性对应的值,通过KVC将其赋值。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Ivar:成员变量 以下划线开头
// Property:属性
+ (instancetype)modelWithDict:(NSDictionary *)dict {
id objc = [[self alloc] init];
unsigned int count = 0;
// 获取成员变量数组
Ivar *ivarList = class_copyIvarList(self, &count);

// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 获取成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 获取key
NSString *key = [ivarName substringFromIndex:1];

// 去字典中查找对应value
// key:user value:NSDictionary
id value = dict[key];

// 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
// 并且是自定义对象才需要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型 userDict => User模型
Class modelClass = NSClassFromString(ivarType);
value = [modelClass modelWithDict:value];
}

// 给模型中属性赋值
if (value) {
[objc setValue:value forKey:key];
}
}

return objc;
}

参考资料:http://www.cnblogs.com/jys509/p/5207159.html#autoid-3-4-0

实现万能控制器跳转

利用runtime动态生成对象、属性、方法这特性,我们可以先跟服务端商量好,定义跳转规则,比如要跳转到A控制器,需要传属性id、type,那么服务端返回字典给我,里面有控制器名,两个属性名跟属性值,客户端就可以根据控制器名生成对象,再用kvc给对象赋值。

参考资料:http://www.cocoachina.com/ios/20150824/13104.html

JSPatch热更新

能做到通过JS调用和改写OC方法最根本的原因是 Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,还可以新注册一个类,为类添加方法。所以 JSPatch 的原理就是:JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。

参考资料:https://segmentfault.com/a/1190000003648832

KVO的底层实现

KVO是基于runtime机制实现的,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法。

OC方法调用流程是怎样的?

OC是运行时语言,方法调用的本质,就是让对象发送消息:

1
id objc_msgSend ( id self, SEL op, ... );

具体的流程如下:

  1. 通过isa指针找到所属类
  2. 查找类的cache列表, 如果没有则下一步
  3. 查找类的”方法列表”
  4. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
  5. 找不到, 就沿着继承体系继续向上查找
  6. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
  7. 找不到, 执行”消息转发”。

消息转发机制的理解和应用?

如果收到无法解读的消息,会尝试做消息转发,即以下三种补救措施:

1. 动态方法解析

1
2
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

2. 备用接收者

1
- (id)forwardingTargetForSelector:(SEL)aSelector;

3. 完整的消息转发

1
- (void)forwardInvocation:(NSInvocation *)anInvocation;

如何让多个方法指向同一个实现?

isa指针是什么?

在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针,指向对象的类。

  • 每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向类。
  • 每一个类本质上都是一个对象,类其实是元类(metaclass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。
  • 所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环。

在分类中重写了原类中的方法,实际调用情况是怎样的?

只调用分类中的实现而不调用原类中的实现。

因为category的底层实现是在加载的时候,把category中的方法添加到原类的方法列表中,当调用方法时会遍历方法列表找到对应的响应子就返回,不再向下遍历。因为category的优先级高于类的优先级,使得原类中的选择子遍历不到。

成员变量的本质是什么?

成员变量的定义

Ivar: 实例变量类型,是一个指向objc_ivar结构体的指针

1
typedef struct objc_ivar *Ivar;

操作函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取所有成员变量
class_copyIvarList

// 获取成员变量名
ivar_getName

// 获取成员变量类型编码
ivar_getTypeEncoding

// 获取指定名称的成员变量
class_getInstanceVariable

// 获取某个对象成员变量的值
object_getIvar

// 设置某个对象成员变量的值
object_setIvar

获取成员变量的方法:

1
2
3
4
5
6
7
8
9
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Model class], &count);
for (unsigned int i = 0; i < count; i ++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
const char * type = ivar_getTypeEncoding(ivar);
NSLog(@"类型为 %s 的 %s ",type, name);
}
free(ivars);

属性的定义

objc_property_t:声明的属性的类型,是一个指向objc_property结构体的指针

1
typedef struct objc_property *objc_property_t;

操作函数:

1
2
3
4
5
6
7
8
9
10
11
// 获取所有属性,不会获取无@property声明的成员变量
class_copyPropertyList

// 获取属性名
property_getName

// 获取属性特性描述字符串
property_getAttributes

// 获取所有属性特性
property_copyAttributeList

参考资料:https://www.jianshu.com/p/d361f169423b

RunLoop的作用是什么?

RunLoop的作用有:

  1. 等待事件发生:使程序一直运行接受用户输入
  2. 计划事件:决定程序在何时应该处理哪些Event
  3. 调用解耦:事件产生方不需要等待事件处理结束再产生下一个事件
  4. 节省CPU时间:等待事件发生时不需要耗费CPU

RunLoop相关参考资料:RunLoop知识树

RunLoop的几种mode?

  • NSDefaultRunLoopMode/kCFRunLoopDefaultMode

    default模式,可以用于大多数操作。在大多数时间,应该使用这种模式来启动和设置输入源。

  • NSEventTrackingRunLoopMode

    Cocoa使用这种模式来约束鼠标拖拽或其它用户界面追踪循环的事件。

  • NSRunLoopCommonModes/kCFRunLoopCommonModes

    这是一个通用的模式组,使用这种模式关联输入源,同样会关联这个模式组里面的每一种模式。对于Cocoa应用来说,这个集合包含了 default、modal以及event tracking模式。

  • NSConnectionReplyMode

  • NSModalPanelRunLoopMode

RunLoop和线程的关系?

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要让线程随时处理事件但并不退出,就需要给线程开启RunLoop。

程序不自动退出是因为主线程自动开启了RunLoop。

开启RunLoop的方法:[NSRunLoop currentRunLoop]获取当前线程的runloop,若不存在系统会创建一个。

RunLoop有哪些应用?

scrollView滑动(mode切换保证滑动不卡顿)

当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

1
2
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

RunLoop监听:AFNetworking中监听网络请求

使用NSOperation+NSURLConnection并发模型都会面临NSURLConnection下载完成前线程退出导致NSOperation对象接收不到回调的问题。NSURLConnection的delegate方法需要在connection发起的线程runloop中调用,于是AFNetWorking单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也是它接收,不占用主线程,也不耗CPU资源。

NSTimer(timer触发)

1
2
3
4
NSTimer *timer = [NSTimer timerWithTimeInterval:60 block:^(NSTimer * _Nonnull timer) {

} repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • UIEvent事件响应和手势识别(source0事件处理 & source1底层硬件触发)
  • AutoRelease(observer,runloop一次循环结束或autorelease pool满了的时候释放)
  • NSObject(NSDelayPerforming(timer),NSThreadPerformAddition(source0))
  • dispatch_get_main_queue()(serve for dispatch main)
  • UI界面刷新、CATransition、CAAnimation(observer) CADisplayLink(source1)
  • NSURLConnection(source0处理回调 & source1接收Socket消息) AFNetworking(内置全局线程和runloop管理网络请求)
  • AsyncDisplayKit

对象什么时候会被释放?

当对象没有被任何变量引用(也可以说是没有指针指向该对象)的时候,就会被释放。

那怎么知道对象已经没有被引用了呢?OC采用引用计数来进行管理:

  • 每个对象都有一个关联的整数,称为引用计数器
  • 当代码需要使用该对象时,则将对象的引用计数加1
  • 当代码结束使用该对象时,则将对象的引用计数减1
  • 当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放。

当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用,需要使用autoreleasepool。对象接收到autorelease消息时,它会被添加到了当前的自动释放池中,当自动释放池被销毁时,会給池里所有的对象发送release消息。autoreleasepool一般是在事件处理之前创建,在事件处理之后释放。

经典内存泄漏及解决方案?

僵尸对象和野指针

僵尸对象是指内存已经被回收的对象,指向僵尸对象的指针就是野指针,向野指针发送消息会导致崩溃:

1
EXC_BAD_ACCESS

解决方案:当对象释放后,应该将其置为nil。

循环引用

ARC时代最常出现的内存管理问题是循环引用,即对象A强引用对象B,对象B强引用对象A,或者多个对象强引用形成一个闭环。block会对内部使用的对象进行强引用,因此在使用的时候应该确定不会引起循环引用。

解决方案:使用弱引用或主动断开循环。

循环产生的内存峰值

循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏。

解决方案:在循环中创建自己的autoreleasepool,及时释放占用内存大的临时变量,减少内存占用峰值。

1
2
3
4
5
6
for (int i=0; i<5000; i++) {
@autoreleasepool {
NSNumber *num = [NSNumber numberWithInt:i];
[num performOperationOnNumber];
}
}

重写dealloc方法

由于释放对象是会调用dealloc方法,因此重写dealloc方法来查看对象释放的情况,如果没有调用则会造成内存泄露。

1
2
3
- (void)dealloc {
NSLog(@"deallocated");
}

说说你所知道的排序算法及其时间复杂度?

类别 排序方法 平均时间复杂度 最优时间复杂度 最差时间复杂度 空间复杂度 稳定性 复杂性
插入排序 直接插入 O(n2) O(n) O(n2) O(1) 稳定 简单
插入排序 希尔排序 O(n1.3) O(n) O(n2) O(1) 不稳定 复杂
选择排序 直接选择 O(n2) O(n2) O(n2) O(1) 不稳定 简单
选择排序 堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1) 不稳定 复杂
交换排序 冒泡排序 O(n2) O(n) O(n2) O(1) 稳定 简单
交换排序 快速排序 O(nlog2n) O(nlog2n) O(n2) O(nlog2n) ~ O(n) 不稳定 复杂
归并排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n) 稳定 复杂
基数排序 O(d(r+n)) O(d(r+n)) O(d(r+n)) O(rd+n) 稳定 复杂

基数排序中,r代表关键字的基数,d代表长度,n代表关键字个数。

参考资料:《各种常用排序算法的时间复杂度和空间复杂度》

如何不用中间变量交换A和B?

加减法

该方法可以交换整型和浮点型数值的变量,但在处理浮点型的时候有可能出现精度的损失。

1
2
3
a = a + b;
b = a - b;
a = a - b;

异或法

可以完成对整型变量的交换,对于浮点型变量它无法完成交换。

1
2
3
a = a^b;
b = a^b;
a = a^b;

乘除法

可以处理整型和浮点型变量,但在处理浮点型变量时也存在精度损失问题。而且乘除法比加减法要多一条约束:b必不为0。

1
2
3
a = a * b;
b = a / b;
a = a / b;





暂时更新到这里,以下这些未整理完毕的问题我每天晚上抽时间逐个整理:

多线程

Q:谈谈你对多线程的理解、GCD几种常用函数的使用

Q:如何实现A/B/C三个任务同步执行,然后D/E/F并发执行

Q:进程和线程的关系

Q:如果tableview的cell中的图片还未加载完就滑出屏幕外了,这种情况如何处理

Q:数据库如何解决资源竞争问题

Q:你所知道的几种同步锁

Q:如何控制最大并发数

Q:如何处理线程安全问题

网络

Q:说说你对Socket的理解

Q:如何进行Socket连接

Q:TCP和UDP的区别

Q:HTTP协议

Q:说说你对AFNetworking的理解

Q:如何自己实现GET和POST网络请求

Q:NSURLConnection和NSURLSession的理解和使用

架构设计

Q:MVC设计模式

蓝牙

Q:说下你的数据分包策略

Q:数据传输校验的策略

Q:蓝牙数据加密策略

Block

Q:Block的几种类型

UI

Q:UIView和CALayer的关系和区别?

Q:贝塞尔曲线的绘制(折线图)

Q:心率助手这个动画是怎么实现的?

Q:什么时候会触发离屏渲染

第三方库

Q:常用的第三方库

Q:SDWebImage实现原理

Q:是否阅读过YYKit的源码

Q:AsyncDisplayKit是否会用

Q:为什么用AFNetworking

Q:是否了解ReactiveCocoa(RAC)框架?

性能优化

Q:性能优化的几种方法

关于AXKit

Q:NSString和NSDate的链式语法封装如何实现的?

Q:NSDateFormatter开销大,如何解决?

Q:代码冷却机制是如何实现的?

Q:万能控制器跳转是如何实现的?

iOS