iOS常见面试题
类(class)和结构体(struct)有什么区别
在swift中,class是引用类型,struct是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个指向。
class的如下功能是struct没有的:
- 可以继承,子类可以使用父类的特性和方法
- 类型转换可以在运行时检查和解释一个实例的类型,也就是说因为没有继承所以
struct
无法做到类型转换 - 可以用
deinit
来释放资源 - 一个类可以被多次引用
struct的优点
- 结构小,适用于复制操作,相较于
class
的实例被多次引用,struct
更加安全 - 无须担心内存泄漏或者多线程的问题
swift是面向对象还是函数式编程语言
swift即是面向对象的编程语言,也是函数式编程语言。
swift支持reduce、map、filter
等函数式方法,所以是函数式编程语言
在swift中什么是可选性(Optional)
Objective-C
中nil
: 表示缺少一个合法的对象,它指向不存在对象的指针
swift
中的nil
:表示任意类型的值缺失,它是一个确定的值,这个值就是表示缺失
所以在swift
中当我们在声明类型的时候需要考虑该类型会不会有值缺失的情况,此为可选类型。
?
声明可选类型:
let status: Int? = 1 //声明可选Int类型常量,值为1
var address: String? = "上海" //声明可选String类型变量,初始值为"上海"
var p: Person? // 声明可选Person类变量,初始值为nil
需要注意的是Int
与Int?
是不同的,Int?
表示可选的Int
类型,可以赋值为nil
,而Int
不可以赋值为nil
隐式可选类型
隐式可选类型!
也是可选类型的一种。都知道可选类型在使用中需要确定它是否是空值,为了告诉编译器可选类型是有值的
var dog: String? = "wangcai"
let cat: String = dog // Error: Value of optional type 'String?' not unwrapped; did you mean to use '!' or '?'?
如上,常量cat
在接受赋值时无法确定变量dog
是否有值,所以编译器会报错。可以用隐式解析来表示该可选类型有值
var dog: String? = "wangcai"
let cat: String = dog!
如果一个变量之后可能变成
nil
的话请不要使用隐式解析可选类型。如果你需要在变量的生命周期中判断是否是nil
的话,请使用普通可选类型。
在swift中,什么是泛型
泛型的功能主要是为了增加代码的灵活性而生的。
如我们需要实现一个方法来交换两个Int
值:
func swap (_ a: inout Int, _b: inout Int) {
(a, b) = (b, a)
}
此时如果我们还需要实现交换Float
类型的值交换,就需要另外再写一个同样逻辑的方法,这样并不高效。泛型就是为了解决这种问题而来的:
func swap<T> (_ a: inout T, _b: inout T) {
(a, b) = (b, a)
}
说明并比较关键词:Open,Public,Internal,File-private和Private
Swift有五个级别的访问控制权限,从高到低分别为Open, Public, Internal, File-private 和 Private
它们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量。比如一个Private的class中不能含有Public的string值。反之,Public的class中可以含有Private的Int值
- Open: 具备最高的访问权。其修饰的类和方法能被任意Module访问和重写
- Public: 权限仅次于Open。与Open唯一的区别是,它修饰的对象可以在任意Module中被访问,但不能重写和继承
- Internal(默认): 表示当前定义的Moudle中访问和重写,可以被一个Moudle中的多个文件访问但不能被其他Moudle访问
- File-Private : 表示其修饰的对象只能在当前文件中使用
说明比较关键词:Strong, Weak和Unowned
内存管理
一个对象在没有任何强引用指向它时,所占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。
- Strong代表强引用,是默认属性。当一个对象被声明为Strong时,表示父层级对该对象有一个强引用的指向。此时,该对象的引用计数会增加1
- Weak代表弱引用。当一个对象被声明为Weak时,表示父层级对该引用对象没有指向,该对象必须是可选类型变量,所以该对象的引用计数不会增加1。在该独享被释放后,弱引用也随即消失。继续访问该对象,程序会得到nil,不会崩溃。
- Unowned与弱引用的本质一样。唯一不同的是,对象被释放后,依然有一个无效的引用指向对象,它不是Optional,也不指向nil。如果继续访问该对象,则程序就会崩溃。
在Swift中,如何理解copy-on-write
内存管理
当值类型(比如struct)在复制时,复制的对象和原对象实际上在内存中指向同一对象。当且仅当修改复制后的对象时,才会在内存中重新创建一个新的对象。下面举一个例子:
![](…/images/屏幕快照 2021-09-24 下午7.21.27.png)
复制的数组arrayB
和原数组arrayA
一开始共享同一个地址,直到其中之一发生改变。这样设计使得值类型可以被多次复制而不会耗费多余的内存,只有变化的时候才会增加开销。
什么是属性观察
属性观察是指在当前类型内对特定属性进行监视并作出响应行为。具体有两种:willSet
和didSet
var title: String {
willSet {
print("title will set from \(title) to \(newValue)")
}
didSet {
print("title did set from \(oldValue) to \(title)")
}
}
但是在初始化方法对属性的设定以及在willSet
和didSet
中对属性的再次设定都不会触发属性观察。
在结构体中如何修改成员变量的方法
一般在结构体中不允许修改成员变量,但是可以通过在函数func changeName()
的前面加上关键词mutating
表示该方法会修改结构体中自己的成员变量。
protocol Pet {
var name: String { get set }
}
struct MyDog: Pet {
var name: String
mutating func changeName(name: String) -> Void {
self.name = name
}
}
如何用swift实现或(||)操作
虽然解法有很多如:
func ||(left: Bool, right: Bool)-> Bool {
if left {
return true
}else {
return right
}
}
但其实这种做法是违背了或(||)操作的本质的,也就是当表达式左边的值为真的时候无须计算表达式右边的值。
正确做法:
func ||(left: Bool, right: @autoclosure () -> Bool) -> Bool {
if left {
return true
}else {
return right()
}
}
autoclosure
可以将表达式右边的值的计算推迟到判定left为false时,这样就可以避免因为第一种方法带来的不必要的开销了。
实现一个函数:输入任意一个整数,输出为输入的整数+2
柯里化currying
这种情况直接返回num+2
明显不是一个最优解,如果再遇到num+3,num + 4
用直接返回会造成大量重复代码。
可以通过柯里化一个方法模板来避免这种情况:
func addTo(adder: Int) -> (Int) -> Int {
// 通过柯里化将增量adder放入函数中并作为addTo函数的返回值
return {
num in return num + adder
}
}
// 将设定好的增量函数赋值给addTwo
let addTwo = addTo(adder: 2)
// 最后传递给增量函数addTwo一个被相加的数
let result = addTwo(6)
实现一个函数:求0~100(包括0和100)中为偶数并且恰好是其他数字平方的数字
函数式编程
func evenSquareNums(from: Int, to: Int) -> [Int] {
var res = [Int]()
for num in from...to where num % 2 == 0 {
if (from...to).contains(num * num) {
res.append(num * num)
}
}
return res
}
evenSquareNums(from: 0, to: 100)
这个函数可以用函数式编程进行优化:
var res = (0...10).map{ $0 * $0 }.filter{ $0 % 2 == 0 }
Objective-C 面试理论题
什么是ARC
内存管理
它是Objective-C的内存管理机制,简单的说是代码中自动加入了retain/release,原先需要手动添加用来处理内存管理的引用计数的代码可以由编译器自动处理。
以前的手动释放称为MRC
Garbage Collection在运行时管理内存,可以解决retain cycle,而ARC在编译时管理内存。
说明比较关键词:strong,weak,assign和copy
内存管理
- strong 表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0,就不会被销毁。
- weak 表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无须手动设置,该对象会自行在内存中被销毁。
- assign主要用于修改基本数据类型,如NSInteger和CGFloat,这些主要存在于栈中。
- weak一般用于修饰对象,assign一般用来修饰基本数据类型。原因是assign修饰的对象被释放后,指针的地址依然存在,造成“野指针”,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成“野指针”。
- copy与strong类似。不同之处是,strong的复制时多个指针指向同一个地址,而copy的复制时每次会在内存中复制一份对象,指针指向不同地址。copy一般用在修饰有对应可变类型的不可变对象上,如NSString,NSArray和NSDictionary。
在oc中,基本数据类型默认关键字是atomic,readwrite和assign;普通属性的默认关键字是atomic,readwrite和strong。
runloop和线程有什么关系
线程
runloop是每一个线程一直运行的一个对象,它主要用来负责响应需要处理的事件和消息。每一个线程都有且仅有一个runloop与其对应,没有线程就没有runloop。
在所有线程中,只有主线程的runloop是默认启动的,main函数会设置一个NSURLLoop对象。而其他线程的runloop默认是没有启动的,可以通过[NSRunLoop currentRunLoop]
来启动。
Runloop的作用就是为了保持线程不会被退出,可以等待事件而不会因为事件结束了就退出了,等待期间会处于休眠状态。
说明并比较关键词:____weak和____block
变量修改
- __ weak 与 weak基本相同。前者用于修饰变量,后者用于修饰属性。__weak主要用于防止block中的循环引用。
- __ block 也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。__block用于修饰某些block内部将要修改的外部变量。
- __ weak 和 __ block 的使用场景几乎与block息息相关。所谓的block,就是oc对于闭包的实现。闭包就是没有名字的函数,或者可以理解为指向函数的指针。
什么是block?它和代理的区别是什么
回调
在iOS中,block和代理都是回调的方式,block是一段封装好的代码。
而代理的声明和实现一般分开,比如UITableViewDelegate就是代理的声明在UITableView中,实现在某个UIViewController中。
Block和代理的区别首先在于,block集中代码块,而代理分散代码块,所以block更适用于轻便简单的回调,如网络传输。而代理适合于公共接口较多的情况,这样做也更易于解耦代码架构。
Block运行成本比代理高。block出栈时,需要将使用的栈内存复制到堆内存,如果是对象就加引用计数。
delegate则只是保存了一个对象指针,直接回调并没有额外的消耗。
Objective-C面试实战题
架构解耦代码考查
属性声明
请问下面代码有什么问题?
typedef enmu {
Normal;
VIP;
}CustomerType;
@Interface Customer: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) UIImage *profileImage;
@property (nonatomic, assign) CustomerType customerType;
@end
- enum定义的写法不够好。官方推荐使用NS_ENUM来定义枚举。同时在枚举的诶个类型前应加上enum的名称。
- UIImage不应该出现在Customer中。Customer明显是一个Model类,UIImage应该归属于View部分。无论是MVC还是MVVM,抑或是VIPER,Model都应该和View划清界限,避免整个架构耦合。
typedef NS_ENUM(NSinteger, CustomerType) {
CustomerTypeNormal;
CustomerTypeVIP;
}
@interface Customer: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSData *profileImageData;
@property (nonatomic, assign) CustomerType customerType;
@end
内存管理语法考查
请问下面的代码打印结果是什么?
NSString *firstStr = @"helloworld";
NSString *secondStr = @"helloworld";
if (firstStr == secondStr){
NSLog(@"Equal")
}else {
NSLog(@"Not Equal")
}
- ==这个符号判断的不是两个值是否相等,而是这两个指针是否指向同一个对象。如果要判断两个NSString的值是否相同,那么应该用isEqualToString这个方法。
- 上面的代码中,两个指针指向不同的对象,尽管它们的值相同。但是iOS的编译器优化了内存分配,当两个指针指向两个值一样的NSString时,两者指向同一个内存地址。
多线程语法考查
多线程
请问下面的代码有什么问题?
-(void)viewDidLoad {
UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)];
alertLabel.text = @"Wait 4 seconds...";
[self.view addSubView:alertLabel];
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
[backgroundQueue addOperationWithBlock:^{
[NSThread sleepUnitlDate:[NSDate dateWithTimeIntervalSinceNow:4]]
alertLabel.text = @"Ready to go!";
}];
}
所有与UI相关的操作应该在主线程进行。
-(void)viewDidLoad {
UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)];
alertLabel.text = @"Wait 4 seconds...";
[self.view addSubView:alertLabel];
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
[backgroundQueue addOperationWithBlock:^{
[NSThread sleepUnitlDate:[NSDate dateWithTimeIntervalSinceNow:4]]
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
alertLabel.text = @"ready to go!"
}]
}];
}
以scheduledTimerWithTimeInterval的方式触发的timer,在滑动页面上的列表时,timer会暂停,为什么?该如何解决
线程 runloop
造成此问题的原因在于滑动页面上的列表时,当前线程的runloop切换了mode的模式,导致timer暂停。
runloop中的mode主要用来指定事件在runloop中的优先级,具体有以下几种。
- Default(NSDefaultRunLoopMode):默认设置,一般情况下使用。
- Connection(NSConnectionReplyMode): 用于处理NSConnection相关事件,开发者一般用不到。
- Modal(NSModalPanelRunLoopMode): 用于处理modal panels事件。
- Event Tracking (NSEventTrackingRunLoopMode): 用于处理拖曳和用户交互的模式
- Common (NSRunLoopCommonModes):模式合集,默认包括Default、Modal和Event Tracking三大模式,可以处理几乎所有事件。
在滑动列表时,runloop的mode由原来的Default模式切换到了Event Tracking模式,timer原来运行在Default模式中,被关闭后自然就停止工作了。
解决方法: 方法一时将timer加入NSRunloopCommonModes中。方法二是将timer放到另一个线程中,然后开启另一个线程的runloop,这样可以保证与主线程互不干扰,而现在主线程正在处理页面滑动。
// 方法一
[[NSRunLoop currentRunLoop] addTimer: timer forMode:NSRunLoopCommonModes]
// 方法二
dispatch_async(dispatch_get_global_queue(0, 0) ^{
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] run];
})
Swift VS Objective-C
Swift为什么将String,Array和Dictionary设计成值类型
- 值类型相比引用类型,最大的优势在于可以高效地使用内存。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵扯合并、移位、重新连接等。也就是说,Swift这样设计大幅减少了堆上的内存分配和回收的次数。值类型的一个特点是在传递和赋值时进行复制,每次复制肯定会产生额外开销,但是在 Swift 中这个消耗被控制在了最小范围内,在没有必要复制的时候,值类型的复制都是不会发生的。也就是说Swift的值类型只有在发生更改的时候才会进行真正的复制,否则相同的值用的都是同一块内存。而引用类型则做不到这一点。
- Swift将String,Array和Dictionary设计成值类型也是为了线程安全。通过Swift的let设置,使得这些数据达到了真正意义上的不变,也从根本上解决了多线程中内存访问和操作顺序的问题。
如何用Swift将协议中的部分方法设计成可选
@optional和@required是Objective-C中特有的关键字。
在Swift中,默认所有方法在协议中都是必须实现的。而且在协议中,方法不可以被直接定义为optional。下面先给出两种解决方案:
-
在协议和方法前都加上@objc关键字,然后再在方法前加上optional关键字。该方法实际上是把协议转化为Objetcive-C的方式,然后进行可选定义。示例如下:
@objc protocol SomeProtocol { func requiredFunc() @objc optional func optionalFunc() }
-
用扩展来规定可选方法。在Swift中,协议扩展可以定义部分方法的默认实现,这样,这些方法在实际调用中就是可选实现的了。示例如下:
protocol SomeProtocol { func requiredFunc() func optionalFunc() } extension SomeProtocol { func optionalFunc(){ print("Dumb Implementation") } } class SomeClass: SomeProtocol { func requiredFunc(){ print("Only need to implement the required") } }