iOS面试题
[toc]
设计基本原则
简述六大设计基本原则(也称 SOLID 五大原则)
单一职责原则 (SRP, Single Responsibility Principle)
- 定义: 一个类只负责一件事。
- 优点: 类的复杂度降低、可读性增强、易维护、变更引起的风险降低。
- 应用: 系统提供的UIView和CALayer的关系:UIView负责时间传递、事件响应;CALayer负责动画及展示
开闭原则(OCP, Open-Close Principle)
-
定义: 对修改关闭、对扩展开放.
- 设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类的代码.
优点: 灵活、稳定(不需修改内部代码,使得被破坏的程度大大下降)
关键: 抽象化
-
使用:
- 我们可以把把行为添加到一个协议中,使用时遵守这个协议即可。
- 添加类目(Category)方式创建
里氏替换原则 (LSP,Liskov Substitution Principle)
-
定义: 所有引用父类的地方必须能透明地使用其子类的对象。
- 通俗点说就是,父类可以被子类无缝替换,且原有功能不受任何影响
-
优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的所有属性和方法
- 提高代码的可重用性、扩张性,项目的开放性
缺点: 程序的可移植性降低,增加了对象间的耦合性
接口隔离原则(ISP, Interface Segregation Principle)
-
定义: 客户端不应该依赖它不需要的接口
- 使用多个专门的协议、而不是一个庞大臃肿的协议。
- 协议中的方法应当尽量少
例: UITableViewDataSource、UITableViewDelegate
优点: 解耦、增强可读性、可扩展性、可维护性
依赖倒置原则(DIP, Dependence Inversion Principle)
定义: 抽象不应该依赖于具体实现,具体实现可以依赖于抽象
核心思想: 面向接口编程
优点: 代码结构清晰,维护容易
实例: 平时我们使用 protocol 匿名对象模式就是依赖倒置原则的最好体现
迪米特法则(LOD, Law Of Demeter) / 最小知道原则 (LKP,Least Knowledge Principle)
-
定义: 一个对象应该对其他对象有尽可能少的了解。
- 也就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
-
迪米特法则应用:
- 外观模式(Facade)
- 中介者模式(Mediator)
- 匿名对象
优点: 使对象之间的耦合降到最底,从而使得类具有很好的可读性和可维护性。
设计基本原则特点总结
- 单一职责原则主要说明:类的职责要单一
- 开闭原则讲述的是:对扩展开放,对修改关闭
- 里氏替换原则强调:不要破坏继承体系
- 接口隔离原则讲解:设计接口的时候要精简
- 依赖倒置原则描述要:面向接口编程
- 迪米特法则告诉我们:要降低耦合
面向对象的三大特征
- 面向对象编程思想主要有三大特征,分别是:封装,继承 和 多态。
- 封装 是指把类中的细节进行包装,对外提供定义好的接口。封装对实现细节进行隐藏,使用者需要通过规定的访问来访问数据,这样避免了使用者进行不合理的赋值操作。
- 继承 是使用已存在的类定义作为基础建立新类的技术,新类的定义可增加新的数据或新的功能,也可以用父类的功能,但不能选择性的继承父类。在继承中,子类拥有父类非 private 的属性和方法;子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;子类可以用自己的方式实现父类的方法。继承使得系统在变化中有了延续性,同时继承也是封装过程中可变的因素,通过继承还可以缩小代码量。
- 多态 是指允许不同的子类类型对同一消息做出不同的行为。多态可以大量减少代码量的同时,提高代码的维护性和扩展性。
什么是 MVC 设计模式?
- 模型-视图-控制器(Model-View-Controller,MVC)是一种广泛应用于用户交互应用程序中的软件设计模式。iOS 中的 MVC 将软件系统分为 Model、View、Controller 三部分,其中
- Model 对象 封装了应用程序的数据,并定义操控和处理该数据的逻辑和运算;
- View 对象 是应用程序中用户可以看见的对象,其主要目的是显示来自应用程序 Model 对象的数据,并使该数据可被编辑;
- Controller 对象 在应用程序的一个或多个 View 对象和一个或多个 Model 对象之间充当媒介,Controller 可以直接访问 Model,Model 通过 Notification 和 KVO 机制与 Controller 间接通信;Controller 也可以直接控制View,View 通过 action 向 Controller 报告事件的发生,但 Model 和 View 不能互相通信。
什么是 MVVM?主要目的是什么?有哪些优点?
- MVVM,为 Model-View-ViewModel 的简写,本质是 MVC 设计模式的改进版,将视图处理逻辑从 C 中剥离出来给 V,剩下的业务逻辑部分被称做 View-Model。MVVM 模式和 MVC 模式一样,主要目的是分离视图和模型,其优点如下:
- 低耦合:View 可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候,Model 可以不变,当 Model 变化的时候 View 也可以不变;
- 可重用性:如果把一些视图逻辑放在一个 ViewModel 里面,则很多 view 可重用这段视图逻辑;
- 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计;
- 可测试:界面素来是较难于测试的,而现在测试可以针对 ViewModel 来写。
哪些类不适合使用单例模式?即使他们在周期中只会出现一次。
- 同一类型的对象需要在不同的用例场景发生变化
内存管理
-
规则
- 在iOS中,使用 “引用计数” 来管理OC对象的内存
- 新创建的OC对象,引用计数是1;
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
- 当引用计数减为0,OC对象就会销毁,释放占用的内存空间
- 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用
- release或者aoturelease释放
引用计数怎么存储?
- 可以直接存储在isa指针中
- 如果不够存储的话,会存储在SideTable结构体的refcnts散列表中
struct SideTable {
spinlock_t stock;
RefcountMap refcnts; // 存放着对象引用计数的散列表
weak_table_t weak_table;
}
ARC具体为引用计数做了哪些工作?
- 编译阶段自动添加代码
- ARC是LLVM编译器和Runtime系统相互协作的一个结果
- 编译器帮我们实现内存管理相关的代码
- Runtime在程序运行过程中处理弱引用
深拷贝与浅拷贝
-
概念:
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象,原对象的引用计数+1
- 完全拷贝:深拷贝的一种,能拷贝多层内容(使用归解档技术)
-
执行结果:
- copy:不可变拷贝,产生不可变副本
- mutableCopy:可变拷贝,产生可变副本
-
准则:不可变对象的copy方法是浅拷贝,其余都是深拷贝🚩🚩🚩🚩🚩
-
原因:
- 它是不可变对象,没有必要拷贝一份出来,指向同一块地址还节省内存
- 不可变对象调用copy返回他本身,不可变对象copy就相当于是retain
-
原因:
对象的拷贝
- 遵守协议
(<NSCopying, NSMutableCopying>)
- 实现协议方法
- (id)copyWithZone:(NSZone *)zone {
Person *person = [Person allocWithZone:zone];
person.name = self.name;
return person;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
Person *person = [Person allocWithZone:zone];
person.name = self.name;
return person;
}
集合对象的拷贝
- 对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,只能算是单层深复制
- 即虽然新开辟了内存地址,但是存放在内存上的值(也就是数组里的元素仍然之乡员数组元素值,并没有另外复制一份),这就叫做单层深复制
- 对于集合类的对象如何实现每一层都深拷贝呢?
initWithArray:copyItems
2、归档解档技术
#import <Foundation/Foundation.h>
@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@end
#import "Person.h"
@implementation Person
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self.name = [aDecoder decodeObjectForKey:@"name"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
}
@end
自动释放池
- 底层结构:AutoreleasPool是通过以AutoreleasePoolPage为结点的 “双向链表” 来实现的
-
AutoreleasPool运行的三个过程:
- objc_autoreleasePoolPush()
- [objc autorelease]
- objc_autoreleasePoolPop(void *)
属性修饰词:Copy、Strong、Weak、Assign
-
copy:
- 会在内存里拷贝一份对象,两个指针指向不同的内存地址。
- 一般用来修饰NSString等有对应可变类型的对象,因为他们有可能和对应的可变类型(NSMutableString)之间进行赋值操作,为确保可变对象变化时,对象中的字符串不被修改 ,应该在设置属性时拷贝一份。
- 而若用strong修饰,如果可变对象变化,对象中的字符串属性也会跟着变化。
-
strong:
- ARC下的strong等同于MRC下的retain都会把对象引用计数加1
-
weak:
- 修饰Object类型,修饰的对象在释放后,指针地址会被置为nil,是一种弱引用
- 在ARC环境下,为避免循环引用,往往会把delegate属性用weak修饰
- weak和strong不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。
-
assign:
- 用于对基本数据类型进行赋值操作,不更改引用计数
- 也可以用来修饰对象,但是被assign修饰的对象在释放后,指针的地址还是存在的,指针并没有被置为nil,成为野指针✨
- 之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。
block属性为什么需要用copy来修饰?
- 因为在MRC下,block在创建的时候,它的内存是分配在栈(stack)上的,而不是在堆(heap)上,可能被随时回收。
- 他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。
- 通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。
- 在ARC下写不写都行,编译器会自动对block进行copy操作。
代理为什么使用weak修饰?
- weak指明该对象并不负责保持delegate这个对象,delegate的销毁由外部控制;
- 如果用strong修饰,强引用后外界不能销毁delegate对象,会导致循环引用;
为什么NSMutableArray一般不用copy修饰?
- (void)setData:(NSMutableArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
拷贝完成后:可变数组->不可变数组,在外操作时(添加、删除等)会存在问题
什么是“僵尸对象”?
- 一个OC对象引用计数为0被释放后就变成僵尸对象,僵尸对象的内存已经被系统回收
- 虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用
- 它的内存是随时可能被别的对象申请而占用的
有哪些情况会出现内存泄漏
- block循环引用
- delegate循环引用问题
- NSTimer循环引用
- 地图类处理
- 线程保活target:self
- (void)dealloc底层执行了什么?
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj) {
ASSERT(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc() {
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer && // 无优化过isa指针
!isa.weakly_referenced && // 无弱引用
!isa.has_assoc && // 无关联对象
!isa.has_cxx_dtor && // 无cxx析构函数
!isa.has_sidetable_rc)) { // 不存在引用计数器是否过大无法存储在isa中(使用 sidetable 来存储引用计数)
// 直接释放
assert(!sidetable_present());
free(this);
} else {
// 下一步
object_dispose((id)this);
}
}
// 如果不能快速释放,则调用 object_dispose()方法,做下一步的处理
static id _object_dispose(id anObject) {
if (anObject==nil) return nil;
objc_destructInstance(anObject);
anObject->initIsa(_objc_getFreedObjectClass ());
free(anObject);
return nil;
}
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor(); // 是否存在析构函数
bool assoc = obj->hasAssociatedObjects(); // 是否有关联对象
// This order is important.
if (cxx) object_cxxDestruct(obj); // 销毁成员变量
if (assoc) _object_remove_assocations(obj); // 释放动态绑定的对象
obj->clearDeallocating();
}
return obj;
}
/*
* clearDeallocating一共做了两件事
*
* 将对象弱引用表清空,即将弱引用该对象的指针置为nil
* 2、清空引用计数表
* - 当一个对象的引用计数值过大(超过255)时,引用计数会存储在一个叫 SideTable 的属性中
* - 此时isa的 has_sidetable_rc 值为1,这就是为什么弱引用不会导致循环引用的原因
*/
inline void objc_object::clearDeallocating() {
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
多线程
GCD
GCD核心概念:「任务」、「队列」
-
任务:
概念:指操作,线程中执行的那段代码,GCD主要放在block中;
执行任务的方式:「同步执行」、「异步执行」;
区别:是否等待队列的任务执行结束,是否具备开启新县城的能力;-
同步执行(sync)
- 同步添加任务到指定队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行
- 只能在当前线程中执行任务,不具备开启新线程的能力
-
异步执行(async)
- 异步添加任务到指定队列中,不会做任何等待,可以继续执行任务
- 可以在新的线程中执行任务,具备开启新县城的能力
- 异步执行虽然具有开启新线程的能力,但不一定开启新线程。(与任务指定的队列类型有关)
-
-
队列(Dispatch Queue)
概念:执行任务的等待队列,即用来存放任务的队列
结构:特殊的线性表,采用FIFO(先进先出)原则。即每读取一个任务,则从队列中释放一个任务-
串行队列:(Serial Dispatch Queue)
- 每次只有一个任务被执行,任务依次执行(只开启一个线程,一个任务执行完成后,再执行下一个任务)
-
并发队列:(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
- 并发队列的「并发」功能只有在异步(dispatch_async)方法下才有效
-
-
GCD使用步骤
- 创建一个队列(串行队列/并发队列)
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行/异步执行)
-
死锁条件:
- 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列。
发送10个网络请求,然后再接收到所有回应之后执行后续操作,如何实现?
- GCD信号量,队列组
- 做法:通过dispatch_group_t来实现,将每个请求放入到Group中,将合并成大图的操作放在dispatch_group_notify中实现。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 / });
dispatch_group_async(group, queue, ^{ /加载图片2 / });
dispatch_group_async(group, queue, ^{ /加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
dispatch_group_enter()和dispatch_group_leave()
如何打造线程安全的NSMutableArray?
- 线程锁:使用线程锁在对数组读写时候加锁
- 派发队列:
《Effective Objective 2.0》中41条提出的观点,串行同步:将读取和写入都安排在同一个队列里,可保证数据同步。
如何异步下载多张小图最后合成一张大图?
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
什么是线程安全?
- 多线程操作过程中往往都是多个线程并发执行的,因此同一个资源可能被多个线程同时访问,造成资源抢夺。
- 线程安全就是多条线程同时访问一段代码,不会造成数据混乱的情况
如何设置常驻线程?🌟🌟
- 为当前线程开启一个 RunLoop (第一次调用 [NSRunLoop currentRunLoop] 方法时
实际是会先去创建一个 RunLoop ) - 向当前 RunLoop 中添加一个 Port/Source 等维持 RunLoop 的事件循环(如果
RunLoop 的 mode 中一个 item 都没有, RunLoop 会退出) - 启动该 RunLoop
在异步线程发送通知,在主线程接收通知。会不会有什么问题?
GCD线程是如何调度的
如何实现多个任务执行完后,再统一处理?
- 同步阻塞
- 栅栏函数
- 线程组
线程和线程之间如何通信?
-
线程通信的表现:
- 1个线程传递数据给另1个线程
- 在1个线程中执行完特定任务后,转到另1个线程继续执行任务
线程间通信常用方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
示例:
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 在子线程中调用download方法下载图片
[self performSelectorInBackground:@selector(download) withObject:nil];
}
- (void)download {
// 1.根据URL网络中下载图片
NSURL *urlstr=[NSURL URLWithString:@"fdsf"];
// 2、把图片转换为二进制的数据, 这一行操作会比较耗时
NSData *data=[NSData dataWithContentsOfURL:urlstr];
// 3、把数据转换成图片
UIImage *image=[UIImage imageWithData:data];
// 4、回到主线程中设置图片
[self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:NO];
}
//设置显示图片
- (void)settingImage:(UIImage *)image {
self.iconView.image=image;
}
谈谈atomic的实现机制,为什么不能保证绝对线程安全?
-
实现机制:
- 编译器自动生成
getter/setter
方法中添加锁保证线程安全
- 编译器自动生成
-
为什么不能保证绝对安全?
- 在
getter/setter
中加锁,仅保证存取时线程安全,不会让你拿到一个崩溃的值 - 无法保证对容器的修改是线程安全的,例:假设属性是可变容器
(@property (atomic) NSMutableArray *array)
时 - 重写
getter/setter
方法时,只能依靠自己在getter/setter
中保证线程安全
- 在
- (void)setCurrentImage:(UIImage *)currentImage {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
}
}
- (UIImage *)currentImage {
return _currentImage;
}
- (void)setCurrentImage:(UIImage *)currentImage {
@synchronized(self) {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
}
}
}
- (UIImage *)currentImage {
@synchronized(self) {
return _currentImage;
}
}
进程和线程的区别
-
区别:
- 一个线程只能属于一个进程.
- 一个进程可以有多个线程,但至少有一个线程。
- 线程是操作系统可识别的最小执行和调度单位。
-
资源分配:
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 同一进程中的多个线程共享代码段、数据段、扩展段。
- 但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
Notification与线程相关
- 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。
- 换句话说就是在哪个线程发送通知,就在哪个线程接受通知。
如何实现在不同线程中post和转发一个Notification?
重定向的实现思路:
- 自定义一个通知队列(用数组类型),让它去维护那些我们需要重定向的Notification
- 我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程
- 如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification
- 指定的线程在收到信号后,将Notification从队列中移除,并进行处理
Notification的使用场景是什么?同步还是异步?
- 一对多,同步
dispatch_once底层实现
线程锁
-
线程锁的作用:
- 我们在使用多线程的时候,多个线程可能会访问同一块资源,就很容易引发数据错乱和数据安全等问题
- 这时候就需要我们保证每次只有一个线程访问这一块资源
-
线程锁类型:
- 互斥锁
- 自旋锁
- 信号量
- 递归锁
- atomic
互斥锁
- 标记用来保证在任一时刻,只能有一个线程访问对象
NSLock
@synchronized (self)
自旋锁
- OSSpinLock(YYKit作者有一篇文章写它不安全,可以自己研究一下)
- os_unfair_lock
信号量(Semaphore - dispatch_semaphore_t)
- 多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用
- 在进入一个关键代码段之前,线程必须获取一个信号量;关键代码段完成后,该线程必须释放信号量
- 其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量
递归锁(NSRecursiveLock)
- 同一个线程可以多次加锁,不会造成死锁
atomic
- atomic 修饰的对象,系统会保证在其自动生成的
getter/setter
方法中的操作是完整的,不受其他线程的影响
线程不安全
- 如果有另一个线程同时在调
[name release]
,那可能就会crash,因为release
不受getter/setter
操作的限制 - 这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作