序
在百度三面被挂掉之后,沉下心来,整理构建自己的开发体系,方便以后查看。 金三银四已经降临,为此提供了找了不少学习方向给大家,也是一些进价方向,希望能帮大家快速提升自己的短板!
本章节:
- 百度三面被挂掉之后,沉下心来总结,构建自己的iOS开发体系(上)
- 百度三面被挂掉之后,沉下心来总结,构建自己的iOS开发体系(下)待更新请关注
标:不要浪费美好的年华,做自己觉得对的事情!
目录
- 一、设计原则、设计模式
- 二、内存管理
- 三、多线程
- 四、Block
- 五、Runtime
- 六、Runloop
- 七、KVO
- 八、KVC
- 九、Category
- 十、网络
- 十一、UI
- 十二、其他
- 十三、OC对象相关
2021年【最新iOS开发面试题】感谢观看赠资料:
下载地址:
一、设计原则、设计模式
1、六大设计基本原则
定义:一个类只负责一件事
优点:类的复杂度降低、可读性增强、易维护、变更引起的风险降低
应用:系统提供的UIView和CALayer的关系:UIView负责时间传递、事件响应;CALayer负责动画及展示
定义:对修改关闭、对扩展开放
- 设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类的代码
优点:灵活、稳定(不需修改内部代码,使得被破坏的程度大大下降)
关键:抽象化
使用:
- 我们可以把把行为添加到一个协议中,使用时遵守这个协议即可。
- 添加类目(Category)方式创建
定义:所有引用父类的地方必须能透明地使用其子类的对象。
- 通俗点说就是,父类可以被子类无缝替换,且原有功能不受任何影响
优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的所有属性和方法
- 提高代码的可重用性、扩张性,项目的开放性
缺点:程序的可移植性降低,增加了对象间的耦合性
定义:抽象不应该依赖于具体实现,具体实现可以依赖于抽象
核心思想:面向接口编程
优点:代码结构清晰,维护容易
实例:平时我们使用 protocol 匿名对象模式就是依赖倒置原则的最好体现
定义:客户端不应该依赖它不需要的接口
- 使用多个专门的协议、而不是一个庞大臃肿的协议。
- 协议中的方法应当尽量少
例:UITableViewDataSource、UITableViewDelegate
优点:解耦、增强可读性、可扩展性、可维护性
定义:一个对象应该对其他对象有尽可能少的了解。
- 也就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
迪米特法则应用:
- 外观模式(Facade)
- 中介者模式(Mediator)
- 匿名对象
优点:使对象之间的耦合降到最底,从而使得类具有很好的可读性和可维护性。
特点总结
- 单一职责原则主要说明:类的职责要单一
- 里氏替换原则强调:不要破坏继承体系
- 依赖倒置原则描述要:面向接口编程
- 接口隔离原则讲解:设计接口的时候要精简
- 迪米特法则告诉我们:要降低耦合
- 开闭原则讲述的是:对扩展开放,对修改关闭
- 设计模式
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
二、内存管理
规则
- 在iOS中,使用 “引用计数” 来管理OC对象的内存
- 新创建的OC对象,引用计数是1;
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
- 当引用计数减为0,OC对象就会销毁,释放占用的内存空间
- 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用release或者aoturelease释放
2、引用计数怎么存储?
- 可以直接存储在isa指针中
- 如果不够存储的话,会存储在SideTable结构体的refcnts散列表中
struct SideTable {
spinlock_t stock;
RefcountMap refcnts; // 存放着对象引用计数的散列表
weak_table_t weak_table;
}
3、ARC具体为引用计数做了哪些工作?
- 编译阶段自动添加代码
ARC是LLVM编译器和Runtime系统相互协作的一个结果
- 编译器帮我们实现内存管理相关的代码
- Runtime在程序运行过程中处理弱引用
4、深拷贝与浅拷贝
概念:
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象,原对象的引用计数+1
- 完全拷贝:深拷贝的一种,能拷贝多层内容(使用归解档技术)
执行结果:
- copy:不可变拷贝,产生不可变副本
- mutableCopy:可变拷贝,产生可变副本
准则:不可变对象的copy方法是浅拷贝,其余都是深拷贝🚩🚩🚩🚩🚩
原因:
- 它是不可变对象,没有必要拷贝一份出来,指向同一块地址还节省内存
- 不可变对象调用copy返回他本身,不可变对象copy就相当于是retain
1、对象的拷贝
- 遵守协议(<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;
}
2、集合对象的拷贝
- 对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,只能算是单层深复制
- 即虽然新开辟了内存地址,但是存放在内存上的值(也就是数组里的元素仍然之乡员数组元素值,并没有另外复制一份),这就叫做单层深复制
- 对于集合类的对象如何实现每一层都深拷贝呢?(1、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
归档和解档的概念补充:
有时存在这样的需求,即将程序中使用的多个对象及其属性值,以及它们的相互关系保存到文件中,或者发送给另外的进程。为了实现此功能,foundation框架中,可以把相互关联的多个对象归档为二进制文件,而且还能将对象的关系从二进制文件中还原出来。
5、weak指针实现原理,SideTable的结构是什么样?
1、常用知识点:
- 所引用对象的计数器不会+1,并在引用对象被释放的时候自动被设置为nil
- 通常用于解决循环引用问题
2、weak指针实现原理
- Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。
- weak表其实就是一个哈希表,key:对象的内存地址;value:指向该对象的所有弱引用的指针
- 当对象销毁的时候,通过对象的地址值,取出对象的弱引用表,将表里面的弱引用清除
3、为什么弱引用不会导致循环引用?
- 没有增加引用计数
4、SideTable的结构是什么样的?
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
};
5、weak属性如何自动置nil的? 具体到如何查找的?
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
6、自动释放池相关
1、以下代码输出什么?会有什么问题?
for (int i = 0; i < 1000000; i ++) {
NSString *string = @"Abc";
string = [string lowercaseString];
// string = [string stringByAppendingString:@"xyz"];
string = [string stringByAppendingFormat:@"xyz"];
NSLog(@"%d-%@", i, string);
}
问题解析:
- 每执行一次循环,就会有一个string加到当前NSRunloop中的自动释放池中
- 只有当自动释放池被release的时候,自动释放池中的标示了autorelease的这些数据所占用的内存空间才能被释放掉
- 当someLargeNumber大到一定程度时,内存空间将被耗尽而没有被释放掉,所以就出现了内存溢出的现象。
解决方案:在循环里面加个自动释放池
for (int i = 0; i < 1000000; i ++) {
@autoreleasepool {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingFormat:@"xyz"];
NSLog(@"%d-%@", i, string);
}
}
2、自动释放池底层结构:
- AutoreleasPool是通过以AutoreleasePoolPage为结点的 “双向链表” 来实现的
3、AutoreleasPool运行的三个过程:
- objc_autoreleasePoolPush()
- [objc autorelease]
- objc_autoreleasePoolPop(void *)
objc_autoreleasePoolPush()
- 调用的AutoreleasePoolPage的push函数
- 一个push操作其实就是创建一个新的Autoreleasepool
- 对应AutoreleasePoolPage的具体实现就是往AutoreleasePoolPage中的next位置插入一个 POOL_SENTINEL
- 并且返回插入的 POOL_SENTINEL 的内存地址。这个地址也就是我们前面提到的 pool token
- 在执行 pop 操作的时候作为函数的入参
push 函数通过调用 autoreleaseFast 函数来执行具体的插入操作
autoreleaseFast 函数在执行具体的插入操作时三种情况不同的处理
- 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
- 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
- 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中
objc_autoreleasePoolPop(void *) 函数本质
- 就是是调用的 AutoreleasePoolPage 的 pop 函数
- pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,即 pool token 。
- 当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。
- 直到 pool token 所在 page 的 next 指向 pool token 为止。
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
4、autoreleasepool和线程的关系?
7、Copy、Strong、Weak、Assign的区别?
assign
- 用于对基本数据类型进行赋值操作,不更改引用计数
- 也可以用来修饰对象,但是被assign修饰的对象在释放后,指针的地址还是存在的,指针并没有被置为nil,成为野指针
- 之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。
weak:
- 修饰Object类型,修饰的对象在释放后,指针地址会被置为nil,是一种弱引用
- 在ARC环境下,为避免循环引用,往往会把delegate属性用weak修饰
- weak和strong不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。
strong:
- ARC下的strong等同于MRC下的retain都会把对象引用计数加1
copy:
- 会在内存里拷贝一份对象,两个指针指向不同的内存地址。
- 一般用来修饰NSString等有对应可变类型的对象,因为他们有可能和对应的可变类型(NSMutableString)之间进行赋值操作,为确保可变对象变化时,对象中的字符串不被修改 ,应该在设置属性时拷贝一份。
- 而若用strong修饰,如果可变对象变化,对象中的字符串属性也会跟着变化。
1、block属性为什么需要用copy来修饰?
- 因为在MRC下,block在创建的时候,它的内存是分配在栈(stack)上的,而不是在堆(heap)上,可能被随时回收。
- 他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。
- 通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。
- 在ARC下写不写都行,编译器会自动对block进行copy操作。
2、代理为什么使用weak修饰?
- weak指明该对象并不负责保持delegate这个对象,delegate的销毁由外部控制
- 如果用strong修饰,强引用后外界不能销毁delegate对象,会导致循环引用
3、为什么NSMutableArray一般不用copy修饰?
- (void)setData:(NSMutableArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
拷贝完成后:可变数组->不可变数组,在外操作时(添加、删除等)会存在问题
4、说到野指针了,什么是“僵尸对象”?
#[iOS-野指针与僵尸对象](https://www.cnblogs.com/junhuawang/p/9213093.html)⏰⏰⏰⏰⏰
- 一个OC对象引用计数为0被释放后就变成僵尸对象,僵尸对象的内存已经被系统回收
- 虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用
- 它的内存是随时可能被别的对象申请而占用的
8、- (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一共做了两件事
*
* 1、将对象弱引用表清空,即将弱引用该对象的指针置为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());
}
三、多线程
9、多线程 - GCD相关
GCD核心概念:「任务」、「队列」
1、任务:
- 概念:指操作,线程中执行的那段代码,GCD主要放在block中;
- 执行任务的方式:「同步执行」、「异步执行」;
- 区别:是否等待队列的任务执行结束,是否具备开启新县城的能力;
同步执行(sync)
- 同步添加任务到指定队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行
- 只能在当前线程中执行任务,不具备开启新线程的能力
异步执行(async)
- 异步添加任务到指定队列中,不会做任何等待,可以继续执行任务
- 可以在新的线程中执行任务,具备开启新县城的能力
- ⚠️异步执行虽然具有开启新线程的能力,但不一定开启新线程。(与任务指定的队列类型有关)
2、队列(Dispatch Queue)
- 概念:执行任务的等待队列,即用来存放任务的队列
- 结构:特殊的线性表,采用FIFO(先进先出)原则。即每读取一个任务,则从队列中释放一个任务
串行队列:(Serial Dispatch Queue)
- 每次只有一个任务被执行,任务依次执行(只开启一个线程,一个任务执行完成后,再执行下一个任务)
并发队列:(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
- ⚠️并发队列的「并发」功能只有在异步(dispatch_async)方法下才有效
3、GCD使用步骤
- 创建一个队列(串行队列/并发队列)
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行/异步执行)
4、死锁条件:
- 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列。
面试题一、打印顺序
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
2021-03-01 16:47:46.122744+0800 ZF_Beta[17625:344152] 执行任务1
2021-03-01 16:47:46.122977+0800 ZF_Beta[17625:344152] 执行任务5
2021-03-01 16:47:46.122984+0800 ZF_Beta[17625:344229] 执行任务2
2021-03-01 16:47:46.123171+0800 ZF_Beta[17625:344229] 执行任务3
2021-03-01 16:47:46.123300+0800 ZF_Beta[17625:344229] 执行任务4
dispatch_queue_t ser = dispatch_queue_create("ser", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(ser, ^{
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(ser, ^{
NSLog(@"4");
});
NSLog(@"5");
2021-02-26 11:25:15.703849+0800 ZF_Beta[6156:123418] 1
2021-02-26 11:25:15.704053+0800 ZF_Beta[6156:123418] 3
2021-02-26 11:25:15.704062+0800 ZF_Beta[6156:123698] 2
2021-02-26 11:25:15.704231+0800 ZF_Beta[6156:123418] 4
2021-02-26 11:25:15.704311+0800 ZF_Beta[6156:123418] 5
- (void)viewDidLoad {
[self performSelector:@selector(test3) withObject:nil afterDelay:0];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"---111");
});
NSLog(@"---333");
}
- (void)test3 {
NSLog(@"---222");
}
---333
---111
---222
- (void)viewDidLoad {
[self performSelector:@selector(test1) withObject:nil afterDelay:0];
[self performSelector:@selector(test2) withObject:nil];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"===3");
});
[UIView animateWithDuration:10 animations:^{
NSLog(@"===4");
}];
}
- (void)test1 {
NSLog(@"===1");
}
- (void)test2 {
NSLog(@"===2");
}
2021-03-04 17:41:03.759310+0800 property[25604:424718] ===2
2021-03-04 17:41:03.759642+0800 property[25604:424718] ===4
2021-03-04 17:41:03.788454+0800 property[25604:424718] ===3
2021-03-04 17:41:03.789335+0800 property[25604:424718] ===1
面试题二、如何打造线程安全的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(), ^{
// 合并图片
});
面试题四、什么是线程安全?
- 多线程操作过程中往往都是多个线程并发执行的,因此同一个资源可能被多个线程同时访问,造成资源抢夺。
- 线程安全就是多条线程同时访问一段代码,不会造成数据混乱的情况
面试题五、如何设置常驻线程?
面试题六、在异步线程发送通知,在主线程接收通知。会不会有什么问题?
面试题七、GCD线程是如何调度的
面试题八、如何实现多个任务执行完后,再统一处理?
- 同步阻塞
- 栅栏函数
- 线程组
⚠️基于runloop的线程保活、销毁与通信:https://www.jianshu.com/p/4d5b6fc33519
下一章:百度三面被挂掉之后,沉下心来总结,构建自己的iOS开发体系(下)
小编文章请观看合集
- 直击2020——iOS 面试题大全(补充完整版)
- “新”携程,阿里,腾讯iOS面试常见问题合集(附答案)
- 新iOS面试题全集合(目前不断更新)
- 新iOS开发京东零售的面试题
- iOS开发,跳槽面试应该注意的Swift面试题
- iOS某些大厂以及小公司的面试题!
文章来源作者:强子ly