iOS面试技术问题总结

记录一下自己最近面试过程中遇到的一些面试题。

1. OC中@property的作用是什么?可以有哪些关键字修饰?

@Property是声明属性的语法,作为OC的一项特性,主要作用就在于封装对象中的数据。可以快速方便的为实例变量创建存取器,并允许通过点语法使用存取器。
@property本质就是ivar(实例变量) 和 getter / setter(存取方法)。

存取器(accessor):用于获取和设置实例变量的方法。用于获取实例变量值的存取器是getter,用于设置实例变量值的存取器是setter。

关键字修饰:

  • 线程安全的(关于是否原子访问): atomic, nonatomic
  • 访问权限的(关于访问控制操作): readonly, readwrite
  • 内存管理(MRC)(关于set方法中属性引用计数相关): assign, retain, copy
  • 内存管理(ARC)(增加了weak、strong属性): assign, strong, weak, copy
  • 指定方法名称: getter= / setter=

2. ARC下,不显示指定任何属性关键字时,默认的关键字都有哪些?

  • 基本数据类型默认关键字是atomic, readwrite, assign
  • 普通的OC对象默认关键字是atomic, readwrite, strong

3. 谈谈对@protocol的理解?@property能否在@protocol中使用?

协议声明了任何类都能够选择实现的程序接口。协议能够使两个不同继承树上的类相互交流并完成特定的目的,因此它提供了除继承外的另一种选择。如果协议遵守者实现了协议中的方法,那么声明协议的类就能够通过遵守者调用协议中的方法。
总结:

  • @Protocol是用来声明一系列方法定义公共接口(不能声明成员变量),不能写实现
  • 只要某个类遵守了这个协议,就拥有了这个协议中的所有方法声明
  • 只要父类遵守了某个协议,那么它的子类也遵守
  • OC不能多继承但是能够遵守多个协议;继承: 遵守协议<>
  • 基协议:<NSObject>基协议,是最基本的协议其中声明了很多最基本的方法,例如要辨别id <协议名>这个指针所指的对象属于哪个类,就要用到-isMemberOf:这个方法,而这个方法是<NSObject>这个协议中的方法之一,所以我们自定义的协议都需要继承<NSObject>
  • 协议可以遵守协议,一个协议遵守了另一个协议,就可以拥有另一个协议中的方法声明(称为协议继承)

协议中能够声明方法,以及属性。但是不能定义实例变量。@property包含了实例变量、setter方法和getter方法。在类中定义的属性,当然三者都有,然而由于@protocol特性的限制,@property在@protocol中并不会合成实例变量,只会合成存取方法。这就要求该协议的遵守者必须自己写出setter和getter方法的实现。但是有一种情况是不需要的,那就是遵守者本来就有这个属性,此时系统会为这个属性自动生成存取方法,既然已经实现了,那么遵守者就没必要去实现协议中的这个属性了。尽管可以实现‘伪属性’,但是,我们还是应该尽量把属性定义在主接口中,而不应该定义在协议中。

4. 通过Category给现有类添加的属性,可以直接使用吗?

比如建立个UIView的Loading分类并增加beginLoading属性,如下:

@interface UIView (Loading)
@property (nonatomic, strong) UIView *beginLoading;
@end

你会发现编译器并不会报任何错误,build一下也不会有问题,但是运行后会发生crash。这是因为在运行时找不到getter and setter methods,所以发生崩溃。如何解决?可以利用runtimeAssociated Objects动态关联属性来解决问题。
参考:iOS之Category属性的理解

5. @synthesize和@dynamic分别有什么作用?

@synthesize@dynamic是@property的两个对应词。如果@synthesize@dynamic都没写,那么默认的就是@syntheszie var = _var;
@synthesize告诉编译器:如果你没有手动实现setter和getter方法,编译器会自动帮你生成。
@dynamic 告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成。(当然对于readonly的属性只需提供getter即可)。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var = someVar,由于缺setter方法会导致程序崩溃;或者当运行到someVar = var时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

6. 说一说ARC下的assign与weak区别?

  • weak :
    1)ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如最常见的delegate代理属性。
    2)如果自身已经对它进行一次强引用,没有必要再次强引用时也会使用weak。比如自定义IBOutlet控件属性一般使用weak,当然也可以使用strong。
  • assign :
    assign是指针赋值,不对引用计数操作,适用于基本数据类型如NSInteger, int, float, struct等值类型,不适用于引用类型。
  • 不同点:
    weak,表明该属性定义了一种“非拥有关系” (nonowning relationship)。为属性设置新值时,设置方法既不保留新值,也不释放旧值。
    assign也可以修饰对象,但是用assign修饰的对象在释放后,指针的地址还是存在的,也就是说指针并没有被置为nil,会造成众所周知的野指针异常。然而,assign修饰的基础数据类型(例如NSInteger等)和C数据类型(int, float, double, char)等一般分配在栈空间上,栈空间的内存会由系统自动处理,当分配的栈空间的内存没有被指针指向时就会被销毁,所以不会造成野指针异常。
    weak比assign多了一个功能就是当属性所指向的对象消失的时候(也就是内存引用计数为0)会自动赋值为 nil,这样再向 weak修饰的属性发送消息就不会导致野指针操作crash。
  • 总结:
    assign 适用于基本数据类型如int,float,struct等值类型,不适用于引用类型。因为值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要自己手动管理内存或通过ARC管理。
    weak适用于delegate等引用类型,不会导致野指针问题,也不会循环引用,非常安全。

7. iOS中weak的实现原理?

参考文章:iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

8. NotificationCenter为什么要removeObserver? 如何实现自动remove?

首先NotificationCenter是一个单例类,通过[NSNotificationCenter defaultCenter]来获取单例对象。
它有三个核心函数和一个观察者数组:

  • 订阅消息: addObserver()。订阅感兴趣的消息。
  • 发布消息: postNotification()。发布消息。
  • 退订消息: removeObserver()。不感兴趣了,就退订。
  • 观察者数组: _observers

Notification是一个单例类,通常在释放场景或者某个对象之前,都要取消场景或对象订阅的消息,否则,注册通知的类被销毁以后再当消息产生时,会因为对象不存在,即向野指针发送了消息,而产生一些意外的BUG。
实现自动remove:通过自释放机制,通过动态属性将remove转移给第三者解除耦合,达到自动实现remove

9. NSNotification、KVO、Delegate和Block的区别?

KVO就是cocoa框架实现的观察者模式,通过KVO可以监测一个值得变化,比如View的高度变化。是一对多的关系,一个值得变化会通知所有的观察者。一般使用场景是数据,需求是数据变化,比如股票价格变化,一般使用KVO(观察者模式)。
NSNotification是通知,也是一对多的使用场景。NSNotification的特点就是需要被观察者先主动发出通知,然后观察者注册监听后再来进行相应,比KVO多了发送通知的异步,但是其优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用也更灵活。
Notification一般是进行全局通知,是弱关联,消息发出后,不需要知道是谁发的也可以做出相应的反应,同理发消息的人也不需要知道接收的人也可以正常发出的消息。
Delegate是强关联,就是委托和代理双方互相知道,是一对一关系。
Block是delegate的另一种形式,是函数式编程的一种形式。使用场景跟delegate一样,相比delegate更灵活,而且比代理的实现更直观。

10. UIViewController的生命周期及调用顺序?

UIViewController中与其生命周期有关的几个函数如下:

//类的初始化方法
+ (void)initialize;
//对象初始化方法
- (instancetype)init;
//从归档初始化
- (instancetype)initWithCoder:(NSCoder *)coder;
//加载视图
-(void)loadView;
//将要加载视图
- (void)viewDidLoad;
//将要布局子视图
-(void)viewWillLayoutSubviews;
//已经布局子视图
-(void)viewDidLayoutSubviews;
//内存警告
- (void)didReceiveMemoryWarning;
//已经展示
-(void)viewDidAppear:(BOOL)animated;
//将要展示
-(void)viewWillAppear:(BOOL)animated;
//将要消失
-(void)viewWillDisappear:(BOOL)animated;
//已经消失
-(void)viewDidDisappear:(BOOL)animated;
//被释放
-(void)dealloc;

除了initialize,initinitWithCoder不是存在所有对象的声明周期中,其他函数都会在UIViewController的声明周期中有序的被调用。那么具体的调用顺序是怎样的呢,最好的办法是实践一下,通过编号打印,结果如下:

initialize // initialize函数并不会每次创建对象都调用,只有在这个类第一次创建对象时才会调用,做一些类的准备工作,再次创建这个类的对象,initalize方法将不会被调用,对于这个类的子类,如果实现了initialize方法,在这个子类第一次创建对象时会调用自己的initalize方法,之后不会调用,如果没有实现,那么它的父类将替它再次调用一下自己的initialize方法,以后创建也都不会再调用。因此,如果我们有一些和这个相关的全局变量,可以在这里进行初始化。
init // init方法和initCoder方法相似,只是被调用的环境不一样,如果用代码进行初始化,会调用init,从nib文件或者归档进行初始化,会调用initCoder。
loadView // 开始加载视图的起始方法,除非手动调用,否则在ViewController的生命周期中没特殊情况只会被调用一次。
viewDidLoad // 这是是我们最常用的方法的,类中成员对象和变量的初始化我们都会放在这个方法中,在类创建后,无论视图的展现或消失,这个方法也是只会在将要布局时调用一次。
viewWillAppear // 视图将要展现时会调用
viewWillLayoutSubviews // 在viewWillAppear后调用,将要对子视图进行布局
viewDidLayoutSubviews // 已经布局完成子视图
viewDidAppare // 视图完成显示时调用
viewWillDisappear // 视图将要消失时调用
viewDidDisappear // 视图已经消失时调用
dealloc // controller被释放时调用。

参考文章:iOS对UIViewController生命周期和属性方法的解析

说下遇到的一道选择题:如果页面A跳转到页面B,以下选项正确的是()?
A、A页面的 viewDidDisappear 方法先调用;
B、B页面的 viewDidAppear 方法先调用;
C、不一定,都有可能;

答案 我是选的B
不过准确的说应该分情况而定,经过实际测试发现如果是push跳转的话,执行顺序是
页面A - [ViewControllerA viewDidDisappear:]
页面B - [ViewControllerB viewDidAppear:]
如果是present跳转的话,执行顺序是
页面B - [ViewControllerB viewDidAppear:]
页面A - [ViewControllerA viewDidDisappear:]
如有不对,感谢纠正。

11. 如何对下面数组中的元素去重(代码,伪代码,思路都可以)?

NSArray *array = @[@"12-11", @"12-11", @"12-11", @"12-12", @"12-13", @"12-12"];

1)利用containsObject判断

NSMutableArray *mutArray = [NSMutableArray array];
for (NSString *value in array) {
if (![mutArray containsObject:value]) {
[mutArray addObject:value];
}
}

2)利用NSSet自动去重特性

NSMutableSet *mutSet = [NSMutableSet set];
for (NSString *value in array) {
[mutSet addObject:value];
}

以上是自己写的两种方法,若有更好的答案感谢告知。

12. 请问下面的代码会输出什么?为什么?

@interface ClassB : ClassA
@end
@implementation ClassB
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

两句输出语句均输出:ClassB
简单来说,self和super都是指向当前实例的,不同的是[self class]会在当前类的方法列表中去找class这个方法,[super class]会直接开始在当前类的父类中去找calss这个方法,两者在找不到的时候,都会继续向祖类查询class方法,最终到NSObject类。那么问题来了,由于我们在ClassA和ClassB中都没有去重写class这个方法,最终自然都会去执行NSObject中的class方法,结果也自然应该是一样的。至于为什么是ClassB,可以看看NSObject中class的实现:

- (Class)class {
return object_getClass(self);
}

返回的都是self的类型,self此处正好就是ClassB,因此结果就会输出ClassB。
参考链接:有关super和self的问题

13. 说说你常使用的lldb调试命令?

LLDB是Xcode默认的调试器,它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。平时用Xcode运行程序,实际走的都是LLDB。熟练使用LLDB,可以让debug事半功倍。
常用LLDB命令:

  • expression
    expression命令的作用是执行一个表达式,并将表达式返回的结果输出。可以实现2个功能:
    1) 执行某个表达式。 我们在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。 假如我们在运行过程中,突然想把self.view颜色改成红色,看看效果。我们不必写下代码,重新run,只需暂停程序,用expression改变颜色,再刷新一下界面,就能看到效果
    image

    2) 将返回值输出。 也就是说我们可以通过expression来打印东西。 假如我们想打印self.view
    image

  • p & print & call
    print: 打印某个东西,可以是变量和表达式
    p: 可以看做是print的简写
    call: 调用某个方法
  • po
    OC里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果我们想打印对象。我们需要使用命令选项:-O。为了更方便的使用,LLDB为expression -O 定义了一个别名:po

    还有其他很多命令选项,不过一般用得比较少,所以就不具体的一一介绍了,如果想了解,在LLDB控制台上输入:help expression即可查到expression所有的信息

该题图片内容来自:熟练使用 LLDB,让你调试事半功倍

14. 如下ViewB是ViewA的子视图,其中1/4的区域在父视图ViewA上,如何让ViewB的3/4区域也响应事件?

image

参考这里:Cocoa Touch中的响应者链

15. 谈谈你对MVVM与MVC的理解?

16. 什么是Runloop?

热评文章