我结合了自己近期面试经历,总结了一份iOS面试题,基本会考到,也是比较重要比较重要比较关键的基础知识,供参考。
时间比较短,有些知识层面写的比较浅,覆盖的知识面也不够广,有时间的话我会补充。
面试的话基本会考基础,关于C、C++、数据结构、计算机网络、操作系统、计算机组成原理、数字逻辑等知识可能都会问到,本篇博文也是只针对iOS新手所写。我在总结的时候也是结合代码掌握的,死记硬背效果不太大。
对于即将跳槽的老司机来说,更多的会是项目方面及更深层次的知识点,本博可略过。
哪里出错了请大神指点,万分感谢。
希望大家面试成功~
侵删,联系@Apach3@Apach3。
@property参数:
内存管理特性(set方法内存管理):
- assign: setter方法直接赋值,不进行任何retain操作,不改变引用计数,常用于“纯量类型”(CGFloat、NSInteger等)和C数据类型(int、float、double、char等)的简单赋值操作,id类型也要用assign,所以iOS中的代理delegate属性都会用assign
- retain:生成符合内存管理的set方法(release旧值,retain新值),适用于OC对象的成员变量
- 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);
}
注:
- weak和strong通常用于ARC,非ARC的retain相当于ARC的strong,非ARC的assign相当于ARC的weak
- strong,weak, unsafe_unretained往往都是用来声明属性的,如果想声明临时变量就得用__strong,__weak,__unsafe_unretained,__autoreleasing,其用法与上面介绍的类似
- 相比而言对于delegate来说weak比assign更好一些,虽然delegate所指向的对象的生命周期是覆盖了delegate成员变量本身所在的生命周期,当本身的生命周期内,本身被销毁,其delegate也就没有存在的意义了,但是如果delegate又被其他地方引用,在被销毁的时候weak声明的delegate成员变量会被赋值为nil,相比于assign它是更安全的做法,而我们常用的UITableView的delegate属性是这样定义的:
@property (nonatomic, assign) id<UITableViewDelegate> 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声明的方法
注:
- 对应关系:property声明方法->.h文件声明getter和setter方法、synthesize定义方法->.m文件实现getter和setter方法(需要
@synthesize name = _name
) - Xcode4.5及以后版本可以省略@synthesize,编译器会自动帮你加上get和set方法,而且默认访问_name这个成员变量,如果找不到,会自动生成一个_name私有成员变量
TCP&UDP/HTTP&HTTPS/GET&POST:
移步我写的这篇博客:
TCP&UDP/HTTP&HTTPS/GET&POST之间的不同
设计模式:
设计模式是一种编码经验,就是用一种比较成熟的逻辑去处理某一种类型的事情
- MVC模式: model、view、controller,把模型、视图、控制器进行解耦和编写,是一切设计的基础,所有新的模式都是基于MVC的改进
- MVVM模式: model、view、viewmodel,把模型、视图、业务逻辑层进行解耦和编写,是对胖模型的拆分,本质是给控制器减负,将弱的业务逻辑放到VM中去处理
- 单例模式:通过static关键词,声明全局变量,在整个进程运行期间只会被赋值一次
- 观察者模式: KVO是典型的通知模式,观察某个属性的状态,状态发生变化时通知观察者
- 委托模式:代理+协议的组合,实现1对1的反向传值操作
- 工厂模式:通过一个类方法,批量的根据已有模版生产对象
#import/#include的区别,@class,#import”“和#import<>的区别:
- #import是OC导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件只会导入一次,不会重复导入
- @class告诉编译器某个类的声明,当执行时,才会查看类的实现文件,可以解决头文件的相互包容
- <>用来包含系统的头文件,”“用来包含用户头文件
frame和bounds的区别
frame:该view在父view坐标系统中的位置和大小
bounds:该view在本身坐标系统中的位置和大小
category:
- category只能给某个已有的类扩充方法,不能扩充成员变量
- category可以添加属性,只不过@property只会生成setter和getter声明,不会生成对应的实现方法及成员变量
- 如果category和原有类方法重名,会优先调用category中的方法,也就是category中的方法会覆盖掉类中的原有方法,所以不要重名
附(通过Runtime为category添加getter和setter方法):
#import <Foundation/Foundation.h>
@interface NSArray (MyCategory)
//不会生成添加属性的getter和setter方法,必须我们手动生成
@property (nonatomic, copy) NSString *blog;
@end
#import "NSArray+MyCategory.h"
#import <objc/runtime.h>
@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:
- extension被称为扩展、延展、匿名分类,同category不同的是,extension不但可以声明方法,还可以声明属性、成员变量,一般用于声明私有方法、私有属性、私有成员变量
- extension通常存在于一个.h文件中,或者寄生于一个类的.m文件中
- extension在编译期决议,它就是类的一部分,而category是在运行期决议
- extension在编译期和头文件里的@interface以及实现文件里的@implementation一起形成一个完整的类,它、extension伴随类的产生而产生,亦随之一起消亡
拷贝:
浅拷贝(shallow copy):在浅拷贝操作时,对于被拷贝对象的每一层都是指针拷贝
深拷贝(one-level-deep copy):在深拷贝操作时,对于被拷贝对象,至少有一层是深拷贝
完全拷贝(real-deep copy):在完全拷贝操作时,对于被拷贝对象的每一层都是对象拷贝
内存管理:
内存中分为存放变量且由系统自动回收的栈区和存放对象的堆区,所以对内存的管理属于对对象的管理,或者说内存管理实际上就是对引用计数器的管理
- 手动引用计数器(MRC:Manual Reference Counting)
- 自动引用计数器(ARC:Automatic Reference Counting)
- 自动释放池(Autorelease Pool)
instancetype和id区别:
- id在编译的时候不能判断对象的真实类型,instancetype可以在编译的时候判断对象的真实类型
- id可以用来定义变量,可以作为返回值,可以作为形参,instancetype只能作为返回值
注:自定义构造方法返回值尽量使用instancetype,不要使用id
protocol:
- protocol(协议)就是用来声明的,不做实现,并且不能声明变量
- 如果协议只用在某个类中,应该把协议定义在该类中
- 如果这个协议用在很多类,应单独定义在一个文件中
delegate:
1. A类定义协议:@protocol AClassDelegate <NSObject> @end
,在里面定义方法:- (void)change:(NSInteger)number;
2. A类中声明属性:@property (nonatomic, weak) id<AClassDelegate> aDelegate;
3. A类中声明方法,方法实现通知B类:
if ([self.delegate respondsToSelector:@selector(change:)]) {
[self.delegate change:@100];
}
4. B类遵守A类的代理<AClassDelegate>
,并使用self.delegate = self;
将这个设置为A类的代理,然后实现代理方法:
- (void)change:(NSInteger)number {
NSLog(@"%ld", number);
}
block:
- A类内给block一个别名:
typedef void (^myBlock)(int);
- A类中声明属性:
@property (nonatomic, copy) myBlock block;
- A类中声明方法,方法回调:
self.block(100);
- 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就不会被执行
解决方法:
- 在子线程中进行NSTimer操作,主线程进行UI操作
- 主线程中进行NSTimer操作,然后使用NSRunLoop的
addTimer:forMode:
方法来把Timer按照指定模式加入到RunLoop中,这里使用的模式是:NSRunLoopCommonModes
,这个模式等效于NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
的结合 - 使用GCD
NSDictionary实现原理:
方法:- (void)setObject:(id)anObject forKey:(id)aKey;
NSDictionary底层原理是一个哈希表,根据关键码值而直接进行访问的数据结构,哈希表本质是一个数组,数组的每个元素存放的是一个键值对
存储过程:
- 根据key计算哈希值h
- 假设数组中有n个元素,那么这个键值对应该放在第(h%n)个位置
- 该箱子如果有键值对,则使用开放寻址法或拉链法解决冲突
内存的几大区域:
- 栈区:由编译器自动分配并释放,存放函数的参数值,局部变量等,优点是快速高效,缺点是有限制,数据不灵活
- 堆区:由程序员分配和释放,优点是灵活方便,但是效率有一定降低
- 全局区(静态区):全局变量和静态变量的存储,程序结束由系统释放
- 文字常量区:存放常量字符串,程序结束后由系统释放
- 代码区:存放函数的二进制代码,用来存储程序的代码/指令
例子:
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沙盒目录:
- Application:存放程序源文件,上架前经过数字签名,上架后不可修改
- Documents:常用目录,iCloud备份目录,存放数据(这里不能存缓存文件,否则上架不被通过)
- Library:
Caches:存放体积大又不需要备份的数据(常用的缓存路径)
Preference:设置目录,iCloud会备份设置信息 - tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能
多线程:
移步我写的其他两篇博客:
iOS之多线程的基本操作
iOS之多线程总结
SDWebImage:
- 入口
setImageWithURL:placeholderImage:options:
先把placeholderImage显示,然后SDWebImageManager根据url开始处理图片 - 进入
SDWebImageManagerdownloadWithURL:delegate:options:userInfo:
先进入SDImageCache从缓存中查找图片是否下载 - 内存图片中查找缓存是否有图片,如果内存中有图片缓存,SDImageCacheDelegate回调
imageCache:didFindImage:forKey:userInfo:
到SDWebImageManager - SDWebImageManagerDelegate回调
webImageManager:didFinishWithImage:
到UIImageView+WebCache等前端展示图片 - 如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存
- 根据URLKey在硬盘缓存目录下尝试读取图片文件
- 如果在硬盘读取到文件,将图片添加到内存缓存,SDImageCacheDelegate回调
imageCache:didFindImage:forKey:userInfo:
- 如果在硬盘读取不到文件,则需要下载图片,回调
imageCache:didNotFindImageForKey:userInfo:
- 共享或重新生成一个下载器SDWebImageDownloader开始下载图片
- 图片下载由NSURLConnection来做,判断下载中、下载失败、下载完成
- 按图片下载进度加载效果使用
connection:didReceiveData:中的ImageIO
connectionDidFinishLoading:
数据下载完成交给SDWebImageDecoder做图片解码处理- 图片解码处理在一个NSOperationQueue完成,不会拖慢UI线程
- 主线程
notifyDelegateOnMainThreadWithInfo:
宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo
回调给SDWebImageDownloader imageDownloader:didFinishWithImage:
回调给SDWebImageManager告知图片下载完成。- 通知所有downloadDelegates下载完成,回调给需要展示的地方展示
- 将图片保存在SDImageCache中,内存缓存和硬盘缓存同时保存
tableview卡顿解决:
- cell重用,需要注册重用标识符,每次需要显示cell的时候,先从缓冲池内去寻找有没有可以用的cell,没有的话再重新创建
- cell的重新布局,cell的布局比较浪费时间,一般创建时就布局好
- 减少cell内控件的数量
- 不要使用clearColor,无背景色,透明度也不要设置为0,渲染耗时比较长
- 更新某组的话,使用reloadSection进行局部更新
- 加载网络数据使用异步加载
- 少使用addView给cell动态添加view
- 按需加载cell
- 不要实现无用的代理方法,tableview只遵守两个协议
- 预渲染图像
- 使用正确的数据结构
如何优化:
- 首页启动速度
- 启动过程中做的事情越少越好(尽可能将多个接口合并)
- 不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新节目)
- 在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)
- 尽量减小包的大小
- 量化启动时间
- 启动速度模块化
v辅助工具(友盟,听云,Flurry)
- 页面浏览速度
- json的处理(iOS自带的NSJSONSerialization,Jsonkit,SBJson)
- 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)
- 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)
- 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)
- 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载)
- 算法的优化(核心算法的优化,例如有些app有联系人姓名用汉语拼音的首字母排序)
- 操作流畅度优化
- tableview优化(cell的加载优化)
- viewController加载优化(不同view之间的跳转,可以提前准备好数据)
- 数据库的优化
- 数据库设计上面的重构
- 查询语句的优化
- 分库分表(数据太多的时候,可以分不同的表或者库)
- 服务器端和客户端的交互优化
- 客户端尽量减少请求
- 服务端尽量做多的逻辑处理
- 服务器端和客户端采取推拉结合的方式(可以利用一些同步机制)
- 通信协议的优化(减少报文的大小)
- 电量使用优化(尽量不要使用后台运行)
- 非技术性能优化
- 产品设计的逻辑性(产品的设计一定要符合逻辑,或者逻辑尽量简单)
- 界面交互的规范(每个模块的界面的交互尽量统一,符合操作习惯)
- 代码规范(这个可以隐形带来app性能的提高,比如用if else还是switch,或者是用!还是==)
- code review(坚持code Review持续重构代码,减少代码的逻辑复杂度)
7种常用排序算法:
移步我的这篇排序算法博客:
7种常用排序算法
本文链接地址:2018年最新iOS面试题及答案
加油!