2018年最新iOS面试题及答案

我结合了自己近期面试经历,总结了一份iOS面试题,基本会考到,也是比较重要比较重要比较关键的基础知识,供参考。
时间比较短,有些知识层面写的比较浅,覆盖的知识面也不够广,有时间的话我会补充。
面试的话基本会考基础,关于C、C++、数据结构、计算机网络、操作系统、计算机组成原理、数字逻辑等知识可能都会问到,本篇博文也是只针对iOS新手所写。我在总结的时候也是结合代码掌握的,死记硬背效果不太大。
对于即将跳槽的老司机来说,更多的会是项目方面及更深层次的知识点,本博可略过。
哪里出错了请大神指点,万分感谢。
希望大家面试成功~
侵删,联系@Apach3@Apach3。


@property参数:

内存管理特性(set方法内存管理):

  1. assign: setter方法直接赋值,不进行任何retain操作,不改变引用计数,常用于“纯量类型”(CGFloat、NSInteger等)和C数据类型(int、float、double、char等)的简单赋值操作,id类型也要用assign,所以iOS中的代理delegate属性都会用assign
  2. retain:生成符合内存管理的set方法(release旧值,retain新值),适用于OC对象的成员变量
  3. copy:生成符合内存管理的set方法(release旧值,copy新值),适用于NSString、NSArray等不可变对象,和strong类似,不过该属性会被复制一个新的副本,当以copy标示的对象B指向一个可变类型(NSMutableString、NSMutableArray等)的对象A时,改变A的值不会使B的值改变,例:
@property (nonatomic, strong) NSString *string1;
@property (nonatomic, copy) NSString *string2;

- (void)test {
    NSMutableString *string = [NSMutableString stringWithFormat:@"apach3"];
    self.string1 = string;
    self.string2 = string;
    sting = nil;
    NSLog(@"sting: %@", string);
    NSLog(@"sting1: %@", self.string1);
    NSLog(@"sting2: %@", self.string2);
}

打印结果为string和string1都为null,string2为apach3,证毕
4. strong:强引用,使用该特性实例变量在赋值时,会释放旧的值同时设置新值,引用计数+1,当引用计数为0的时候,该对象会被从内存中释放,适用于一般OC对象
5. weak:弱引用,不会使引用计数增加,相比于assign,在所指向的对象被释放后,weak指针会被置为nil,这样能有效的防止野指针,多用于处理循环引用(代理或block)的问题、storyboard或xib创建的控件(控件放在view上已经形成了如下引用关系:UIViewController->UIView->subView->UIButton,相当于xib/sb对这个button是强引用,你声明的属性对它是弱引用)
6. unsafe_unretained:同weak类似,或者说assign等同于ARC下的unsafe_unretained,在对象被释放后,该属性不会被设置为nil,后续调用容易造成野指针
7. __autoreleasing:内存管理是谁申请谁释放,__autoreleasing则可以使对象延迟释放,比如想传一个未初始化的对象引用到一个方法中,在此方法中实例化此对象,那么可以用__autoreleasing,例如:

- (void) generateErrorInVariable:(__autoreleasing NSError **)paramError {
    NSArray *objects = [[NSArray alloc] initWithObjects:@"A simple error", nil];
    NSArray *keys = [[NSArray alloc] initWithObjects:NSLocalizedDescriptionKey, nil];
    NSDictionary *errorDictionary = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
    *paramError = [[NSError alloc] initWithDomain:@"MyApp"code:1 userInfo:errorDictionary];
}
- (void)test {
    NSError *error = nil;
    [self generateErrorInVariable:&error];
    NSLog(@"Error = %@", error);
}
注:
  1. weak和strong通常用于ARC,非ARC的retain相当于ARC的strong,非ARC的assign相当于ARC的weak
  2. strong,weak, unsafe_unretained往往都是用来声明属性的,如果想声明临时变量就得用__strong,__weak,__unsafe_unretained,__autoreleasing,其用法与上面介绍的类似
  3. 相比而言对于delegate来说weak比assign更好一些,虽然delegate所指向的对象的生命周期是覆盖了delegate成员变量本身所在的生命周期,当本身的生命周期内,本身被销毁,其delegate也就没有存在的意义了,但是如果delegate又被其他地方引用,在被销毁的时候weak声明的delegate成员变量会被赋值为nil,相比于assign它是更安全的做法,而我们常用的UITableView的delegate属性是这样定义的:@property (nonatomic, assign) id delegate;,这里用assign的原因是为了在ARC下兼容iOS4及更低版本来实现弱引用机制,所以尽量使用weak

读写特性(是否要生成set方法):

readwrite:这是默认参数,同时生成set和get方法的声明和实现,可读、写
readonly:只生成set方法的声明与实现,只读

多线程特性(用于多线程管理):

atomic:这是默认参数,原子性,性能低,会被加锁(一个操作执行过程不能被中断,要么执行完要么不执行,不可以在中途被CPU暂停调度,在多线程环境下不会出现变量被修改的问题,保证数据同步),做金融等要求高安全的时候使用
nonatomic:非原子性,性能高,不加锁,操作是直接从内存取数值,无法保证数据同步

方法名特性(用于set、get方法重命名):

setter:给成员变量的set方法重命名,set方法默认命名:
- (void)set成员变量名(成员变量名称首字母大写):(成员变量数据类型)成员变量名;
getter:给成员变量的set方法重命名,get方法默认命名:- (成员变量数据类型)成员变量名;
synthesize:合成访问器方法,property声明了成员变量的访问方法,synthesize定义了由property声明的方法

注:
  1. 对应关系:property声明方法->.h文件声明getter和setter方法、synthesize定义方法->.m文件实现getter和setter方法(需要@synthesize name = _name)
  2. Xcode4.5及以后版本可以省略@synthesize,编译器会自动帮你加上get和set方法,而且默认访问_name这个成员变量,如果找不到,会自动生成一个_name私有成员变量

TCP&UDP/HTTP&HTTPS/GET&POST:

移步我写的这篇博客:
TCP&UDP/HTTP&HTTPS/GET&POST之间的不同

设计模式:

设计模式是一种编码经验,就是用一种比较成熟的逻辑去处理某一种类型的事情
  1. MVC模式: model、view、controller,把模型、视图、控制器进行解耦和编写,是一切设计的基础,所有新的模式都是基于MVC的改进
  2. MVVM模式: model、view、viewmodel,把模型、视图、业务逻辑层进行解耦和编写,是对胖模型的拆分,本质是给控制器减负,将弱的业务逻辑放到VM中去处理
  3. 单例模式:通过static关键词,声明全局变量,在整个进程运行期间只会被赋值一次
  4. 观察者模式: KVO是典型的通知模式,观察某个属性的状态,状态发生变化时通知观察者
  5. 委托模式:代理+协议的组合,实现1对1的反向传值操作
  6. 工厂模式:通过一个类方法,批量的根据已有模版生产对象

#import/#include的区别,@class,#import”“和#import<>的区别:

  1. #import是OC导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件只会导入一次,不会重复导入
  2. @class告诉编译器某个类的声明,当执行时,才会查看类的实现文件,可以解决头文件的相互包容
  3. <>用来包含系统的头文件,”“用来包含用户头文件

frame和bounds的区别

frame:该view在父view坐标系统中的位置和大小
bounds:该view在本身坐标系统中的位置和大小


category:

  1. category只能给某个已有的类扩充方法,不能扩充成员变量
  2. category可以添加属性,只不过@property只会生成setter和getter声明,不会生成对应的实现方法及成员变量
  3. 如果category和原有类方法重名,会优先调用category中的方法,也就是category中的方法会覆盖掉类中的原有方法,所以不要重名

附(通过Runtime为category添加getter和setter方法):

#import 

@interface NSArray (MyCategory)

//不会生成添加属性的getter和setter方法,必须我们手动生成
@property (nonatomic, copy) NSString *blog;

@end
#import "NSArray+MyCategory.h"
#import 

@implementation NSArray (MyCategory)

// 定义关联的key
static const char *key = "blog";

/**
 blog的getter方法
 */
- (NSString *)blog {
    // 根据关联的key,获取关联的值。
    return objc_getAssociatedObject(self, key);
}

/**
 blog的setter方法
 */
- (void)setBlog:(NSString *)blog {
    // 第一个参数:给哪个对象添加关联
    // 第二个参数:关联的key,通过这个key获取
    // 第三个参数:关联的value
    // 第四个参数:关联的策略
    objc_setAssociatedObject(self, key, blog, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

测试代码:

NSArray *myArray = [[NSArray alloc]init];
myArray.blog = @"https://apach3q.github.io";
NSLog(@"谁说Category不能添加属性?我用Category为NSArray添加了一个blog属性,blog=%@",myArray.blog);

打印结果:

谁说Category不能添加属性?我用Category为NSArray添加了一个blog属性,blog=https://apach3q.github.io

extension:

  1. extension被称为扩展、延展、匿名分类,同category不同的是,extension不但可以声明方法,还可以声明属性、成员变量,一般用于声明私有方法、私有属性、私有成员变量
  2. extension通常存在于一个.h文件中,或者寄生于一个类的.m文件中
  3. extension在编译期决议,它就是类的一部分,而category是在运行期决议
  4. extension在编译期和头文件里的@interface以及实现文件里的@implementation一起形成一个完整的类,它、extension伴随类的产生而产生,亦随之一起消亡

拷贝:

浅拷贝(shallow copy):在浅拷贝操作时,对于被拷贝对象的每一层都是指针拷贝
深拷贝(one-level-deep copy):在深拷贝操作时,对于被拷贝对象,至少有一层是深拷贝
完全拷贝(real-deep copy):在完全拷贝操作时,对于被拷贝对象的每一层都是对象拷贝


内存管理:

内存中分为存放变量且由系统自动回收的栈区和存放对象的堆区,所以对内存的管理属于对对象的管理,或者说内存管理实际上就是对引用计数器的管理
  1. 手动引用计数器(MRC:Manual Reference Counting)
  2. 自动引用计数器(ARC:Automatic Reference Counting)
  3. 自动释放池(Autorelease Pool)

instancetype和id区别:

  1. id在编译的时候不能判断对象的真实类型,instancetype可以在编译的时候判断对象的真实类型
  2. id可以用来定义变量,可以作为返回值,可以作为形参,instancetype只能作为返回值
注:自定义构造方法返回值尽量使用instancetype,不要使用id

protocol:

  1. protocol(协议)就是用来声明的,不做实现,并且不能声明变量
  2. 如果协议只用在某个类中,应该把协议定义在该类中
  3. 如果这个协议用在很多类,应单独定义在一个文件中

delegate:

1. A类定义协议:@protocol AClassDelegate @end,在里面定义方法:- (void)change:(NSInteger)number;
2. A类中声明属性:@property (nonatomic, weak) id aDelegate;
3. A类中声明方法,方法实现通知B类:

if ([self.delegate respondsToSelector:@selector(change:)]) {
    [self.delegate change:@100];
}

4. B类遵守A类的代理,并使用self.delegate = self;将这个设置为A类的代理,然后实现代理方法:

- (void)change:(NSInteger)number {
    NSLog(@"%ld", number);
}

block:

  1. A类内给block一个别名:typedef void (^myBlock)(int);
  2. A类中声明属性:@property (nonatomic, copy) myBlock block;
  3. A类中声明方法,方法回调:self.block(100);
  4. B类中接受回调:
__weak typeof(self) weakSelf = self;
self.classA.block = ^(int a) {
    weakSelf.label.text = [NSString stringWithFormat:@"%d", a];
};
注:block内不要写self(想用self就按我上面例子用__weak typeof(self) weakSelf = self;来写),block的类型是copy

M、V、C:

M(model):程序中用于处理应用程序逻辑的部分,通常负责存取数据。
V(view): 用于构建视图的类,通常根据model创建视图
C(controller):控制model和view如何展示在屏幕上

C-M:单向通信,controller需要讲model呈现给用户,需要知道模型的一切,还需要有同model完全通信的能力
C-V: controller通过view来布局用户界面
M-V: model独立与UI,并不需要和view直接通信,view通过controller获取model数据
V-C: view不能对controller知道的太多,因此要通过间接通信


NSTimer准么:

NSTimer不准,原因是因为NSTimer使用的时候会被加在当前RunLoop中,模式是默认的NSDefaultRunLoopMode,如果当前线程是UI线程,某些UI事件会将RunLoop切换成NSEventTrackingRunLoopMode模式,那么默认的NSDefaultRunLoopMode模式中注册的事件是不被执行的,也就是NSTimer就不会被执行
解决方法:
  1. 在子线程中进行NSTimer操作,主线程进行UI操作
  2. 主线程中进行NSTimer操作,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到RunLoop中,这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopModeNSEventTrackingRunLoopMode的结合
  3. 使用GCD

NSDictionary实现原理:

方法:- (void)setObject:(id)anObject forKey:(id)aKey;
NSDictionary底层原理是一个哈希表,根据关键码值而直接进行访问的数据结构,哈希表本质是一个数组,数组的每个元素存放的是一个键值对
存储过程:
  1. 根据key计算哈希值h
  2. 假设数组中有n个元素,那么这个键值对应该放在第(h%n)个位置
  3. 该箱子如果有键值对,则使用开放寻址法或拉链法解决冲突

内存的几大区域:

  1. 栈区:由编译器自动分配并释放,存放函数的参数值,局部变量等,优点是快速高效,缺点是有限制,数据不灵活
  2. 堆区:由程序员分配和释放,优点是灵活方便,但是效率有一定降低
  3. 全局区(静态区):全局变量和静态变量的存储,程序结束由系统释放
  4. 文字常量区:存放常量字符串,程序结束后由系统释放
  5. 代码区:存放函数的二进制代码,用来存储程序的代码/指令
例子:
int a = 10;//全局初始化区
char *p;//全局未初始化区
main {
    int b;//栈区
    char s[];//栈区
    char *p1;//栈区
    char *p2 = "1234";//p2在栈区,1234在常量区
    static int c = 0;//全局区
    w1 = (char *)malloc(10);
    w2 = (char *)malloc(20);//分配得来的10和20字节的区域在堆区
}

KVC:

1. KVC可以自动将数值或结构体型的数据打包成NSNumber或NSValue对象,但我们不能直接将数值通过KVC赋值,需要把数据转换为NSNumber和NSValue类型传入,可以通过KVC修改、获取属性的值:

Person *person = [[Person alloc] init];
[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
NSLog(@"age=%@",[person valueForKey:@"age"]);

2. KVC中可以使用KeyPath,假设people对象有属性address,address有属性country,这样就可以通过- (nullable id)valueForKeyPath:(NSString *)keyPath;- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;来获取、修改对应的对象:

NSString *country = [people valueForKeyPath:@"address.country"];
[people setValue:@"USA" forKeyPath:@"address.country"];

3. KVC提供验证key对应value是否可用的方法- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

注:

1. setValue:forKey:方法赋值的原理
例如对于:[item setValue:@"value" forKey:@"property"],具体实现为:
首先去模型中查找有没有setProperty,找到,直接调用赋值[self setProperty:@"value"]
去模型中查找有没有property属性,有则直接访问属性赋值property = value
去模型中查找有没有_property属性,有则直接访问属性赋值_property = value
找不到,就会直接报错setValue:forUndefinedKey:报找不到的错误
2. 使用KVC要有以下三个条件:
必须保证模型中定义的属性要大于或等于字典中key的数量
模型中的基本数据类型无法进行转换
属性的名字必须和键相同,否则找不到相关属性会报错


KVO:

KVO(Key Value Observing)是基于观察者设计模式来实现的,可以方便地对指定对象的某个属性进行观察,当属性发生变化时进行通知

1. 添加监听:

self.abook = [[Book alloc]init];
self.abook.price = @"0";//先设一个初始值
[_abook addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

2. 按钮方法触发监听:[self.abook setValue:newPrice forKey:@"price"];
3. 实现监听:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqual:@"price"]) {
        NSLog(@"old price: %@",[change objectForKey:@"old"]);
        NSLog(@"new price: %@",[change objectForKey:@"new"]);
    }
}

4. 移除监听:

- (void)dealloc {
    [_abook removeObserver:self forKeyPath:@"price"];
}
注:KVO是同步的,并且发生与所观察的值发生变化的同样的线程上,不要把KVO和多线程混起来

NSNotification:

NSNotification(通知)可以用来传递参数、通信等作用,与delegate的一对一不同的是,通知是多对多的,而且通知是同步操作,只有当响应的通知代码执行完毕后,发出通知的对象的代码才会继续往下执行

1. A类发送一个通知:

NSNotificationCenter *notification = [NSNotificationCenter defaultCenter];
[notification postNotificationName:@"Apach3NewNotification" object:self];

2. B类注册成为Observer:

NSNotificationCenter *notification = [NSNotificationCenter defaultCenter];
[notification addObserver:self selector:@selector(doNext:) name:@"Apach3NewNotification" object:nil];

3. B类处理通知:- (void)doNext:(NSNotification *)notification;
4. 程序不使用的时候,在dealloc方法中移除观察者:

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}

iOS常用数据存储方式:

数据存储有四种方案:NSUserDefault、KeyChain、file、DB

其中File有三种方式:plist、Archive(归档)
DB包括:SQLite、FMDB、CoreData


iOS沙盒目录:

  1. Application:存放程序源文件,上架前经过数字签名,上架后不可修改
  2. Documents:常用目录,iCloud备份目录,存放数据(这里不能存缓存文件,否则上架不被通过)
  3. Library:
    Caches:存放体积大又不需要备份的数据(常用的缓存路径)
    Preference:设置目录,iCloud会备份设置信息
  4. tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能

多线程:

移步我写的其他两篇博客:
iOS之多线程的基本操作
iOS之多线程总结

SDWebImage:

  1. 入口setImageWithURL:placeholderImage:options:
    先把placeholderImage显示,然后SDWebImageManager根据url开始处理图片
  2. 进入SDWebImageManagerdownloadWithURL:delegate:options:userInfo:
    先进入SDImageCache从缓存中查找图片是否下载
  3. 内存图片中查找缓存是否有图片,如果内存中有图片缓存,SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:到SDWebImageManager
  4. SDWebImageManagerDelegate回调webImageManager:didFinishWithImage:
    到UIImageView+WebCache等前端展示图片
  5. 如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存
  6. 根据URLKey在硬盘缓存目录下尝试读取图片文件
  7. 如果在硬盘读取到文件,将图片添加到内存缓存,SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:
  8. 如果在硬盘读取不到文件,则需要下载图片,回调imageCache:didNotFindImageForKey:userInfo:
  9. 共享或重新生成一个下载器SDWebImageDownloader开始下载图片
  10. 图片下载由NSURLConnection来做,判断下载中、下载失败、下载完成
  11. 按图片下载进度加载效果使用connection:didReceiveData:中的ImageIO
  12. connectionDidFinishLoading:数据下载完成交给SDWebImageDecoder做图片解码处理
  13. 图片解码处理在一个NSOperationQueue完成,不会拖慢UI线程
  14. 主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo回调给SDWebImageDownloader
  15. imageDownloader:didFinishWithImage:回调给SDWebImageManager告知图片下载完成。
  16. 通知所有downloadDelegates下载完成,回调给需要展示的地方展示
  17. 将图片保存在SDImageCache中,内存缓存和硬盘缓存同时保存

tableview卡顿解决:

  1. cell重用,需要注册重用标识符,每次需要显示cell的时候,先从缓冲池内去寻找有没有可以用的cell,没有的话再重新创建
  2. cell的重新布局,cell的布局比较浪费时间,一般创建时就布局好
  3. 减少cell内控件的数量
  4. 不要使用clearColor,无背景色,透明度也不要设置为0,渲染耗时比较长
  5. 更新某组的话,使用reloadSection进行局部更新
  6. 加载网络数据使用异步加载
  7. 少使用addView给cell动态添加view
  8. 按需加载cell
  9. 不要实现无用的代理方法,tableview只遵守两个协议
  10. 预渲染图像
  11. 使用正确的数据结构

如何优化:

  1. 首页启动速度
    • 启动过程中做的事情越少越好(尽可能将多个接口合并)
    • 不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新节目)
    • 在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)
    • 尽量减小包的大小
    • 量化启动时间
    • 启动速度模块化
      v辅助工具(友盟,听云,Flurry)
  2. 页面浏览速度
    • json的处理(iOS自带的NSJSONSerialization,Jsonkit,SBJson)
    • 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)
    • 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)
    • 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)
    • 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载)
    • 算法的优化(核心算法的优化,例如有些app有联系人姓名用汉语拼音的首字母排序)
  3. 操作流畅度优化
    • tableview优化(cell的加载优化)
    • viewController加载优化(不同view之间的跳转,可以提前准备好数据)
  4. 数据库的优化
    • 数据库设计上面的重构
    • 查询语句的优化
    • 分库分表(数据太多的时候,可以分不同的表或者库)
  5. 服务器端和客户端的交互优化
    • 客户端尽量减少请求
    • 服务端尽量做多的逻辑处理
    • 服务器端和客户端采取推拉结合的方式(可以利用一些同步机制)
    • 通信协议的优化(减少报文的大小)
    • 电量使用优化(尽量不要使用后台运行)
  6. 非技术性能优化
    • 产品设计的逻辑性(产品的设计一定要符合逻辑,或者逻辑尽量简单)
    • 界面交互的规范(每个模块的界面的交互尽量统一,符合操作习惯)
    • 代码规范(这个可以隐形带来app性能的提高,比如用if else还是switch,或者是用!还是==)
    • code review(坚持code Review持续重构代码,减少代码的逻辑复杂度)

7种常用排序算法:

移步我的这篇排序算法博客:
7种常用排序算法

本文链接地址:2018年最新iOS面试题及答案

加油!

你可能感兴趣的:(ios开发)