以100道面试题构建自己的iOS开发体系
2021-02-28 本文已影响0人
强子ly
你属于那种学习者呢?
序
曾几何时,特别喜欢看、收集别人分享的面试真题,直到看到图中这个学习方法,若有所思。在百度三面被挂掉之后,沉下心来,整理构建自己的开发体系,方便以后查看。
- 有些还没有写完后面再添加
- 主体脉络来自:《李明杰底层原理》、《慕课网面试视频》、《iOS与OSX多线程和内存管理》
- 希望大家在2021年都能拿到自己满意的offer
目录
- 一、设计基本原则
- 二、内存管理
- 三、多线程
- 四、Block
- 五、Runtime
- 六、Runloop
- 七、KVO
- 八、KVC
- 九、Category
- 十、网络
- 十一、UI
- 十二、其他
- 十三、OC对象相关
一、设计基本原则
1、简述六大设计基本原则(有时候也称 SOLID 五大原则)
定义:一个类只负责一件事
优点:类的复杂度降低、可读性增强、易维护、变更引起的风险降低
应用:系统提供的UIView和CALayer的关系:UIView负责时间传递、事件响应;CALayer负责动画及展示
定义:对修改关闭、对扩展开放
- 设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类的代码
优点:灵活、稳定(不需修改内部代码,使得被破坏的程度大大下降)
关键:抽象化
使用:
- 我们可以把把行为添加到一个协议中,使用时遵守这个协议即可。
- 添加类目(Category)方式创建
定义:所有引用父类的地方必须能透明地使用其子类的对象。
- 通俗点说就是,父类可以被子类无缝替换,且原有功能不受任何影响
优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的所有属性和方法
- 提高代码的可重用性、扩张性,项目的开放性
缺点:程序的可移植性降低,增加了对象间的耦合性
定义:客户端不应该依赖它不需要的接口
- 使用多个专门的协议、而不是一个庞大臃肿的协议。
- 协议中的方法应当尽量少
例:UITableViewDataSource、UITableViewDelegate
优点:解耦、增强可读性、可扩展性、可维护性
定义:抽象不应该依赖于具体实现,具体实现可以依赖于抽象
核心思想:面向接口编程
优点:代码结构清晰,维护容易
实例:平时我们使用 protocol 匿名对象模式就是依赖倒置原则的最好体现
定义:一个对象应该对其他对象有尽可能少的了解。
- 也就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
迪米特法则应用:
- 外观模式(Facade)
- 中介者模式(Mediator)
- 匿名对象
优点:使对象之间的耦合降到最底,从而使得类具有很好的可读性和可维护性。
特点总结
- 单一职责原则主要说明:类的职责要单一
- 里氏替换原则强调:不要破坏继承体系
- 依赖倒置原则描述要:面向接口编程
- 接口隔离原则讲解:设计接口的时候要精简
- 迪米特法则告诉我们:要降低耦合
- 开闭原则讲述的是:对扩展开放,对修改关闭
二、内存管理
规则
- 在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)⏰⏰⏰⏰⏰
#[iOS 野指针定位:野指针嗅探器](https://www.jianshu.com/p/9fd4dc046046)
- 一个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相关
- iOS 多线程:『GCD』详尽总结 - [行走少年郎]
- GCD中队列与任务嵌套的组合测试
- iOS gcd线程死锁问题
- GCD信号量-dispatch_semaphore_t
- 关于PerformSelector和多线程的知识点✨✨✨
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
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(test1) withObject:nil afterDelay:0];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"---222");
});
NSLog(@"---333");
}
- (void)test1 {
NSLog(@"---111");
}
2021-06-29 11:03:46.464232+0800 preformselect[9443:149014] ---333
2021-06-29 11:03:46.542993+0800 preformselect[9443:149014] ---222
2021-06-29 11:03:46.543766+0800 preformselect[9443:149014] ---111
面试题二、如何打造线程安全的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线程是如何调度的
面试题八、如何实现多个任务执行完后,再统一处理?
- 同步阻塞
- 栅栏函数
- 线程组
⚠️基于runloop的线程保活、销毁与通信:https://www.jianshu.com/p/4d5b6fc33519
11、线程和线程之间如何通信?
线程通信的表现:
- 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;
}
12、谈谈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;
}
}
13、进程和线程的区别
区别:
- 一个线程只能属于一个进程.
- 一个进程可以有多个线程,但至少有一个线程。
- 线程是操作系统可识别的最小执行和调度单位。
资源分配:
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源
- 同一进程中的多个线程共享代码段、数据段、扩展段
- 但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量
14、Notification与线程相关
官方文档:
- 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。
- 换句话说就是在哪个线程发送通知,就在哪个线程接受通知。
如何实现在不同线程中post和转发一个Notification?
重定向的实现思路:
1、自定义一个通知队列(用数组类型),让它去维护那些我们需要重定向的Notification
2、我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程
3、如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification
4、指定的线程在收到信号后,将Notification从队列中移除,并进行处理
// 查看一下这个api
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
15、dispatch_once底层实现
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
16、线程锁相关
线程锁的作用:
- 我们在使用多线程的时候,多个线程可能会访问同一块资源,就很容易引发数据错乱和数据安全等问题
- 这时候就需要我们保证每次只有一个线程访问这一块资源
线程锁类型:
- 互斥锁
- 自旋锁
- 信号量
- 递归锁
- atomic
1、互斥锁
- 标记用来保证在任一时刻,只能有一个线程访问对象
- NSLock
- @synchronized (self)
2、自旋锁
- OSSpinLock(YYKit作者有一篇文章写它不安全,可以自己研究一下)
- os_unfair_lock
3、信号量(Semaphore - dispatch_semaphore_t)
- 多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用
- 在进入一个关键代码段之前,线程必须获取一个信号量;关键代码段完成后,该线程必须释放信号量
- 其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量
4、递归锁(NSRecursiveLock)
- 同一个线程可以多次加锁,不会造成死锁
5、atomic
- atomic 修饰的对象,系统会保证在其自动生成的 getter/setter 方法中的操作是完整的,不受其他线程的影响
线程不安全:
- 如果有另一个线程同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制
- 这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作
面试一、
- 多线程同步,一个线程加锁一个互拆量,解锁前,第二线程试图锁定此互斥量,简述此时互斥锁和自旋锁处理流程✨✨✨
四、Block
17、block相关
1、block本质是什么?
- block是将函数及其执行上下文封装起来的对象
2、关于block的截获特性,你是否有了解?block的截获变量特性是怎样的?
变量捕获机制分析:
- 对于“基本数据类型”的“局部变量”截获其值
- 对于“对象”类型的局部变量“连同所有权修饰符”一起截获
- 以“指针形式”截获局部静态变量(指针传递)
- 不截获全局变量、静态全局变量(直接访问)
改外部变量必要条件
- 将auto从栈copy到堆
原因:栈中内存管理是由系统管理,出了作用域就会被回收,堆中才是可以由程序员管理
3、对栈上的block进行copy之后,假如在mrc环境下,内存是否回泄漏?
- copy操作之后,堆上的block没有额外的成员变量指向它,正如我们alloc对象后,没有进行release,造成内存泄漏
4、面试题:请思考,这段代码有问题么?
{
__block MCBlock *blockSelf = self;
_blk = ^int(int num){
return num * blockSelf.var;
}
_blk(3);
}
- 在MRC下,不会产生循环引用
- 在ARC下,会产生循环引用,造成内存泄漏
5、为什么block会产生循环引用?
- 如果当前block对当前对象的某一成员变量进行截获,block会对当前对象有一个强引用
- 而当前block由于当前对象对其有一个强引用,产生了一个自循环引用的一个循环引用的问题
6、Block不允许修改外部变量的值
原因:
- block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域---改变了作用域。
- 在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。
- 比如想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
- 所以 Apple 在编译器层面做了限制,如果在 block 内部试图修改 auto 变量(无修饰符),那么直接编译报错。
- 可以把编译器的这种行为理解为:对 block 内部捕获到的 auto 变量设置为只读属性---不允许直接修改。
7、如何实现对外部变量的捕获?
- 将变量设置为全局变量。原理:block内外可直接访问全局变量
- 加 static (放在静态存储区/全局初始化区)。原理是block内部对外部auto变量进行指针捕获
- 最优解:使用__block 关键字
8、__block
- 将auto变量封装为结构体(对象),在结构体内部新建一个同名的auto变量
- block内截获该结构体的指针
- 在block中使用自动变量时,使用指针指向的结构体中的自动变量
__block int var = 10;
void(^blk)(void) = ^{
var = 20;
};
blk();
转换后的代码:
struct __Block_byref_var_0 {
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var; // 10 => 20 该结构体持有相当于原来自动变量的成员变量
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_var_0 *var; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
9、block在修改NSMutableArray需不需要添加__block
- 不需要
- 当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。
- 所以,在block里面对指针指向内容做的修改,在block外面也一样生效。
10、block是如何捕获局部变量的?
- block捕获外界变量时,在内部会自动生成同一个属性来保存
11、UIView动画中block回调里self要不要弱引用?
- 不需要,它不会造成循环引用,因为它是类方法。
- 之所以需要弱引用本身,是因为怕对象之间产生循环引用,当前控制器不可能强引用一个类,所以循环无法形成。
12、block里面会不会存在self为空的情况(weak strong的原理)?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
__strong typeof(weakself) strongself = weakself;
[strongself.dataSource reloadDataWithCompletion:nil];
}];
- 有时候weakSelf在block里在执行reloadDataWithCompletion还存在
- 但在执行reloadDataWithCompletion前,可能会被释放了
- 为了保证self在block执行过程里一直存在,对他强引用strongSelf
13、__block与__weak的区别
- _block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型
- __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)
- __block对象可以在block中被重新赋值,__weak不可以。
14、多层block嵌套如何使用weakSelf?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
__strong typeof(weakself) strongself = weakself;
__weak typeof(self) weakSelf2 = strongself;
[strongself.dataSource reloadDataWithCompletion:^(BOOL result) {
__strong typeof(self) strongSelf2 = weakSelf2;
}];
}];
15、Masonry对于block内部引用self会不会造成循环引用?
- 不会
- 这个block没有copy,是在栈上,使用完直接释放了,
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
18、 代理、Block利弊
- 与委托代理模式的代码相比,用block写出的代码更为整洁
代理优点:
- 代理语法清晰,可读性高,易于维护
- 它减少代码耦合性,使事件监听与事件处理分离
- 一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高
代理缺点:
- 实现代理的过程较繁琐
- 跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱
- 当多个对象同时传值时不易区分,导致代理易用性大大降低
block优点:
- 语法简洁,代码可读性和维护性较高
- 配合GCD优秀的解决多线程问题
block缺点:
- Block中得代码将自动进行一次retain操作,容易造成内存泄漏
- Block内默认引用为强引用,容易造成循环应用
运行成本:
delegate运行成本低,block的运行成本高
- block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是假引用技术,使用完block置nil才会消除
- delegate只是保存了一个对象的指针,直接回调,没有额外的消耗。就像c的函数指针,只多了一个查表动作
19、有哪些情况会出现内存泄漏。
- block循环引用
- delegate循环引用问题
- NSTimer循环引用
- 地图类处理
- 线程保活target:self
20、__weak来解决block中的循环引用,还有别的方法吗。
- __block
- 将对象传进入修改
五、Runtime
21、以下方法打印什么
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
这两个都打印出来的是:Son.
- [self class] 会先在当前类的方法列表中查找class这个方法
- [super class] 会先到父类中去查找class方法
- 两者在找不到的时候,都会继续向祖先类查询class方法,最终到NSObject类
- NSObject中class的实现
- (Class)class {
return object_getClass(self);
}
22、Runtime如何通过selector找到对应的IMP地址?IMP和SEL关系是?
- SEL:类方法的指针,相当于一种编号,区别与IMP!
- IMP:函数指针,保存了方法的地址!
关系:SEL是通过表取对应关系的IMP,进行方法的调用!
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
23、Runtime的相关术语
SEL、id、Class、Method、IMP、Cache、Property
- 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
- 为什么要设计metaclass
- class_copyIvarList & class_copyPropertyList区别
- class_rw_t 和 class_ro_t 的区别
24、交互两个方法的现实有什么风险?
- class_replaceMethod
- method_exchangeImplementations
- class_getInstanceMethod
个人经验总结:
- 当我们写的类没有继承的关系的时候,俩种方法都没什么问题
- 当有继承关系又不确定方法实现没实现,最好用class_replaceMethod方法
- 1、多次hook方法会存在什么问题?✨✨✨✨
- 2、如何拦截所有button点击事件?
@implementation UIButton (ActionExtend)
+ (void)load {
[super load];
Method sendActionMethod = class_getInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:));
Method hooksendActionMethod = class_getInstanceMethod([UIButton class], @selector(wj_sendAction:to:forEvent:));
method_exchangeImplementations(sendActionMethod, hooksendActionMethod);
}
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
[super sendAction:action to:target forEvent:event];
}
- (void)wj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
NSLog(@"拦截方法 : %s", __func__);
[self wj_sendAction:action to:target forEvent:event];
}
@end
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
25、对象关联底层数据结构
通过 runtime 的源码得知:
- 关联属性并没有添加到 category_t(分类)里边
- 运行时也不会合并到元类对象里边
- 而是存储在一个全局的AssociationsManager里边
#import <objc/runtime.h>
// 添加
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
// 获取
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
// 移除
objc_removeAssociatedObjects(<#id _Nonnull object#>)
- 关联对象的应用?系统如何实现关联对象的
- 关联对象的如何进行内存管理的?关联对象如何实现weak属性
- 关联的对象,需要在主对象dealloc的时候释放么?
被关联的对象的生命周期内要比对象本身释放晚很多, 它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。
层级关系
26、消息转发流程,向一个nil对象发送消息会怎样
转发过程(一定要回答出从缓存中查找)
- 消息发送
- 动态方法解析
- 消息转发
1、消息发送过程 objc_msgSend(receiver, selector)
- 向一个对象发送消息时,runtime会根据对象的isa指针找到所属类
- 在该类的方法列表及父类方法列表中寻找方法(缓存)
- 如果在最顶层父类中依然找不到对应方法,会报 unrecognized selector send to xxx
2、向一个nil对象发送消息会怎样?✨✨✨
- objc_msgSend会通过判断self来决定是否发送消息
- 如果self为nil,那么selector也会为空,直接返回,不会出现问题
- 但对于[NSNull null]对象发送消息时,是会crash的,因为NSNull类只有一个null方法
在崩溃前有三次拯救程序崩溃的机会,就是接下来的消息转发
3、消息转发流程
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
27、performSelector相关
概念
- performSelector是'运行时'系统负责去'找方法'的,在'编译时'不做'校验'
- Cocoa支持在运行时向某个类添加方法(即编译时方法不存在,但是运行时存在)。此时必然需要使用performSelector去调用
- 为了程序的健壮性,会使用检查方法- (BOOL)respondsToSelector:(SEL)aSelector
简单分析
1、
- 这三个方法均为同步执行,与线程无关,主线程和子线程中均可调用成功,等同于直接调用该方法
- 例如:[self performSelector:@selector(test2)]; 与 [self test2]; 执行效果上完全相同。
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
2、
原理:
- 内部创建了 NSTimer ,添加到当前的runloop上
- 时间结束后,系统会使用runtime,通过方法名称去方法列表中找到对应的方法实现并调用🌟🌟🌟🌟🌟
- 这两个方法为异步执行,即使delay传参为0,仍为异步执行
- 必须在主线程调用, 在子线程调用永远不会起作用(子线程默认不开启runloop,手动开启另说⚠️)
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSString *> *)modes;
3、
- 主线程和子线程中均可执行,均会调用主线程的aSelector方法;
- 如果 waitUntilDone = YES:等待当前线程执行完以后,主线程才会执行aSelector方法;🌟
- 如果 waitUntilDone = NO:不等待当前线程执行完,就在主线程上执行aSelector方法。🌟
- 如果当前线程就是主线程,那么aSelector方法会马上执行。🌟
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
4、
- 调用指定线程中的某个方法(参考3)
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
5、
- 开启子线程,在后台执行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg
六、Runloop
28、RunLoop相关
什么是RunLoop?
- RunLoop 实际上是一个对象
- 这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)
- 从而保持程序的持续运行
- 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能
// 简单的理解为如下代码
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running); // 判断是否需要退出
return 0;
}
- 讲讲runloop,项目中有用到么?
- runloop内部实现逻辑?
- timer与runloop的关系?
- 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
- runloop是怎么响应用户操作的,具体流程是什么样的?
- 说说runloop的几种状态?
- runloop的mode作用是什么
- # [利用RunLoop的原理去监控卡顿](https://www.cnblogs.com/qiyiyifan/p/11089735.html)✨✨✨✨
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
// 睡眠中等待消息
int message = sleep_and_wait;
// 处理消息
retVal = process_message(message);
} while (retVal == 0)
return 0;
}
}
- 说说runloop的几种状态?
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop*
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer*
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source*
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠*
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒*
kCFRunLoopExit = (1UL << 7), // 即将退出Loop*
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态改变
-
runloop内部实现逻辑
RunLoop运行逻辑图.png
- 应用范畴
- 定时器(Timer)、PerformSelect
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
- 基本应用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
- runloop和线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
/*
* 从字典中获取,如果没有则直接创建
*/
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
29、 NSTimer相关
- CADisplayLink 与 NSTimer 有什么不同?
- iOS三种定时器的用法NSTimer、CADisplayLink、GCD
- iOS常见三种定时器-NSTimer、CADisplayLink、GCD定时器
1、NSTimer准吗?如果不准的话原因是什么?如何解决?
原因:
- NSTimer的触发时间到的时候,会在RunLoop中被检测一次;
- 如果在这一次的RunLoop中做了耗时的操作,会处于阻塞状态
- 时间超过了定时器的间隔时间,触发时间就会推迟到下一个runloop周期
解决方法:
- 在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作
- 使用CADisplayLink(一般用来做UI展示更新,同样存在runloop卡顿问题)
- 使用GCD定时器
2、使用NSTimer是如何处理循环引用的?
- 使用类方法
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
3、谈谈常用的三种定时器优缺点(NSTimer、CADisplayLink、GCD定时器)✨✨✨✨✨
- NSTimer和CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
- 相比之下GCD的定时器会更加准时,因为GCD不是依赖RunLoop,而是由内核决定
- CADisplayLink和NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
4、在viewWillDisappear或者viewDidDisappear方法中将 timer = nil,是否还会造成循环引用?()
- 问题一:如果只是想在离开此页时要释放,进入下一页时不要释放,场景就不适用了
- runloop->timer;controller->timer
⌛️⌛️⌛️⌛️⌛️
拓展、如何利用runloop监控卡顿✨✨✨
七、KVO
30、KVO相关
KVO 的 全称Key-Value Observing,俗称“键值监听”,可以用于某个对象属性值的改变
1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)
- 利用runtimeAPI动态生成一个子类(NSKVONotifying_XXXX),并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会动用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey
- 父类原来的setter方法
- didChangeValueForKey
- 内部触发监听器(ObserveValueForKeyPath:ofObject:change:context)
2、如何手动触发KVO
- 手动调用willChangeValueForKey
- 修改成员变量值
- 手动调用didChangeValueForKey
3、直接修改成员变量会触发KVO么
- 不会触发KVO(原因看KVO的本质)
4、object_getClass(self.person) 和 [self.person class];分别打印什么?为什么?
- object_getClass(self.person); -> NSKVONotifying_MJPerson
- [self.person class]; -> MJPerson
- 原因:NSKVONotifying_MJPerson重写底层实现,目的:隐藏动态创建的类,不让用户感知
- (Class)class {
return [MJPerson class];
}
// 伪代码 Function框架
void _NSSetIntValueForKey(){
[self willChangeValueForKey:@"age"];
[self setAge:age];
[self didChangeValueForKey:@"age"];
}
// 通知监听器
- (void)didChangeValueForKey:(NSString *)key {
[obser observeValueForKeyPath:key ofObject:self change:nil content:nil];
}
其他:
根据地址打印方法:p (IMP)0X1065....
类对象: object_getClass(self.person);
原类对象:object_getClass(object_getClass(self.person));
KVO原理
- 使用kvo什么时候移除监听(dealloc不能移除的情况)?
八、KVC
31、KVC相关
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
1、通过KVC修改属性会出发KVO么?
- 能触发KVO()
- KVC在修改属性时,会调用willChangeValueForKey:和didChangeValueForKey:方法;
2、KVC的赋值和取值过程是怎样的?原理是什么?
- 见下图
3、使用场景
- 单层字典模型转化:[self.model setValuesForKeysWithDictionary:dict];
- 通过KVC修改未暴露的属性:
UILabel *placeholderLabel=[self.userTextField valueForKeyPath:@"placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];
- 使用valueForKeyPath可以获取数组中的最小值、最大值、平均值、求和
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
- 数组内部去重
[dataArray valueForKeyPath:@"@distinctUnionOfObjects.self"]
- 数组合并(去重合并:distinctUnionOfArrays.self、直接合并:unionOfArrays.self)
NSArray *temp1 = @[@3, @2, @2, @1];
NSArray *temp2 = @[@3, @4, @5];
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@distinctUnionOfArrays.self"]);
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@unionOfArrays.self"]);
输出两个数组:( 5, 1, 2, 3, 4 ), ( 3, 2, 2, 1, 3, 4, 5 )。
- 大小写转换(uppercaseString)及 打印字符串长度同样适用(length)
NSArray *array = @[@"name", @"w", @"aa", @"jimsa"];
NSLog(@"%@", [array valueForKeyPath:@"uppercaseString"]);
打印:
(NAME,W,AA,JIMSA)
KVC赋值原理 - setValue:forKey:
- 首先会按照setKey、_setKey的顺序查找方法,找到方法,直接调用方法并赋值;
- 未找到方法,则调用+ (BOOL)accessInstanceVariablesDirectly;
- 若accessInstanceVariablesDirectly方法返回YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常;
- 若accessInstanceVariablesDirectly方法返回NO,则直接抛出异常;
KVC取值原理 - valueForKey:
- 首先会按照getKey、key、isKey、_key的顺序查找方法,找到直接调用取值
- 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出异常;
- 若返回的YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到则取值;
- 找不到则抛出异常;
九、Category
32、Category相关
1、Category的使用场合是什么?
- 将一个类拆成很多模块(其实就是解耦,将相关的功能放到一起)
2、说说Category的实现原理
- 通过runtime动态将分类的方法合并到类对象、元类对象中
- Category编译之后的底层结构是 struct_category_t , 里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将 Category 的数据,合并到类信息中(类对象、元类对象)
3、category和extension区别
- Extension在编译时,就把信息合并到类信息中
- Category是在运行时,才会将分类信息合并到类信息中
- 分类声明的属性,只会生成getter/setter方法声明,不会自动生成成员变量和getter/setter方法实现,而扩展会
- 分类不可用为类添加实例变量,而扩展可以
分类的局限性:
- 无法为类添加实例变量,但可通过关联对象进行实现
- 分类的方法如果和类重名,会覆盖原来方法的实现
- 多个分类的方法重名,会调用最后编译的那个分类的实现
4、为什么category不能添加属性?使用Runtime就可以了?
- 分类没有自己的isa指针
- 类最开始生成了很多基本属性,比如IvarList,MethodList
- 分类只会将自己的method attach到主类,并不会影响到主类的IvarList
- 实例变量没有setter和getter方法。也没有自己的isa指针
- 关联对象都由AssociationsManager管理
- AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。
- 相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址
- 而这个map的value又是另外一个AssAssociationsHashMap,里面保存了关联对象的kv对
5、Category中有load方法么?load方法什么时候调用的?load方法能继承么?
- 有
- +load方法会在runtime加载类、分类时调用;
- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序
- 先调用类的+load,(按照编译先后顺序,先编译,先调用),调用子类的+load之前会调用父类的+load
- 再调用分类的+load按照编译先后顺序调用(先编译,先调用)
6、test方法和load方法的本质区别?(+load方法为什么不会被覆盖)
- test方法是通过消息机制调用 objc_msgSend([MJPerson class], @selector(test))
- +load方法调用,直接找到内存中的地址,进行方法调用
7、load调用顺序
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次
调用顺序
- 先调用类的+load方法,之后按照编译先后顺序调用(先编译,先调用,调用子类的+load之前会先调用父类的+load)
- 再调用分类的+load,之后按照编译先后顺序调用(先编译,先调用)
8、不同Category中存在同一个方法,会执行哪个方法?如果是两个都执行,执行顺序是什么样的?
- 根据Build Phases->Compile Sources中添加的文件顺序,后面的会覆盖前面的
9、load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时他们之间的调用过程?🌟🌟🌟🌟🌟
区别:
调用方式不同
- load是根据函数地址直接调用
- initialize是通过objc_msgSend调用
调用时刻
- load是runtime加载 类/分类 的时候调用(只会调用1次)
- initialize是类第一次接收消息时调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
调用顺序
- load:先调用类的load。先编译的类,优先调用load(调用子类的load之前,会先调用父类的load)
- 再调用分类的load(先编译的分类,优先调用load)
- initialize:先初始化父类, 再初始化子类(可能最终调用的是父类的initialize方法)
10、⚠️:
- category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
- category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面
- 这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。
11、为什么不能动态添加成员变量?
- 方法和属性并不“属于”类实例,而成员变量“属于”类实例
- “类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。
- 假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用
十、网络
33、TCP、UDP各自的优缺点及区别
TCP优点:( 可靠,稳定)
- 在传递数据之前,会有三次握手来建立连接,
- 在数据传递时,有确认、窗口、重传、拥塞控制机制,
- 在数据传完后,还会断开连接用来节约系统资源
TCP缺点:(慢,效率低,占用系统资源高)
- TCP在传递数据之前,要先建连接,这会消耗时间
- 在数据传递时(确认机制、重传机制、拥塞控制机制)等都会消耗大量的时间
- 因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击
UDP的优点:(快)
- UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制
- UDP是一个无状态的传输协议,所以它在传递数据时非常快
UDP的缺点:(不可靠,不稳定)
- 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包
小结TCP与UDP的区别:
- TCP面向连接(如打电话要先拨号建立连接); UDP是无连接的,即发送数据
- TCP提供可靠的服务。通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流; UDP是面向报文的
- 每一条TCP连接只能是点到点的; UDP支持一对一,一对多,多对一和多对多的交互通信
34、Scoket连接和HTTP连接的区别
- HTTP协议是基于TCP连接的,是应用层协议,主要解决如何包装数据。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。
- HTTP连接:短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源。服务器不能主动给客户端响应,iPhone主要使用类NSURLConnection
- Socket连接:长连接,客户端跟服务器端直接使用Socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据
35、HTTP协议的特点,关于HTTP请求GET和POST的区别
特点:
- HTTP超文本传输协议,是短连接,是客户端主动发送请求,服务器做出响应,服务器响应之后,链接断开
- HTTP是一个属于应用层面向对象的协议,HTTP有两类报文:请求报文和响应报文
- HTTP请求报文:一个HTTP请求报文由请求行、请求头部、空行和请求数据4部分组成
- HTTP响应报文:由三部分组成:状态行、消息报头、响应正文
GET请求
- 参数在地址后拼接,不安全(因为所有参数都拼接在地址后面)
- 不适合传输大量数据(长度有限制,为1024个字节)
POST请求
- 参数在请求数据区放着,相对GET请求更安全
- 数据大小理论上没有限制
- 提交的数据放置在HTTP包的包体中
36、断点续传怎么实现的?
- 断点续传主要依赖于 HTTP 头部定义的 Range 来完成
- 有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源
- 当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小
37、网络层相关面试
- 网络七层协议
- Charles原理
- HTTPS的连接建立流程
- 解释一下三次握手和四次挥手
- TCP分片 和 IP分片
- Cookie和Session
[网络相关之Cookie和Session](https://www.jianshu.com/p/5f250c621e81?utm_campaign=hugo)
38、DNS是什么?DNS解析过程
域名系统(Domain Name System,DNS)
因特网上的主机,可以使用多种方式标识:
- 一种标识方法就是用它的主机名,比如·www.baidu.com、www.google.com、gaia.cs.umass.edu等
- 另外一种方式,就是直接使用定长的、有着清晰层次结构的IP地址
1、区别:
- 主机名:方便人们记忆和接受,但长度不一、没有规律的字符串,路由器并不方便处理
- IP地址:路由器方便处理,不便于人们记忆
为了折衷这两种方式,需要一种能进行主机名到IP地址转换的目录服务,就是 域名系统(Domain Name System,DNS)
2、作用:
- 将用户提供的主机名解析为IP地址
3、DNS解析过程(以www.163.com为例:)
- 打开浏览器,输入一个域名(www.163.com)。客户端会发出一个DNS请求到本地DNS服务器(本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动)
- 本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,直接返回结果。如果没有,向DNS根服务器进行查询。
- 根DNS服务器没有记录具体的域名和IP地址的对应关系,而是给出域服务器的地址,告诉他可以到域服务器上去继续查询
- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。
- .com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
- 最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,
- 本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
过程:本地服务器->根服务器->域服务器->域名解析服务器
- 整合成流程图
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
39、TCP建立连接的三次握手中,第二次握手发送的包会包含的标记,最正确的描述是?
40、HTTP和HTTPS的区别?Https为什么更加安全?
1、传输信息安全性不同
- http协议:是超文本传输协议,信息是明文传输。(如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息)
- https协议:是具有安全性的ssl加密传输协议,为浏览器和服务器之间的通信加密,确保数据传输的安全
2、连接方式不同
- http协议:http的连接很简单,是无状态的。
- https协议:是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议。
3、端口不同
- http协议:使用的端口是80。
- https协议:使用的端口是443.
4、证书申请方式不同
- http协议:免费申请。
- https协议:需要到ca申请证书,一般免费证书很少,需要交费。
https验证、传输过程
41、如何设计一个网络库
42、AFNetworking如何是https的,如何适配ipv6的?
十一、UI
43、Storyboard/Xib和纯代码UI相比,有哪些优缺点?
storyboard/xib优点
- 简单直接。直接通过拖拽和点选即可完成配置。
- 跳转关系清楚
缺点:
- 协作冲突(多人提交代码)
- 很难做到页面继承和重用
- 不便于进行模块化管理
- 影响性能(多图层渲染)
44、自动布局AutoLayout原理,性能如何
戴明-iOS开发高手课 - 03
45、说明比较方法:layoutifNeeded、layoutSubviews、setNeedsLayout
46、如果页面 A 跳转到 页面 B,A 的 viewDidDisappear 方法和 B 的 viewDidAppear 方法哪个先调用?
- A -->viewWillDisappear
- B-->viewWillAppear
- A-->viewDidDisappear
- B-->viewDidAppear
47、离屏渲染,隐式动画和显式动画相关
⚠️经常看到,圆角会触发离屏渲染。但其实这个说法是不准确的,因为圆角触发离屏渲染也是有条件的!
1、离屏渲染触发条件:
- 背景色、边框、背景色+边框,再加上圆角+裁剪,因为 contents = nil 没有需要裁剪处理的内容,所以不会造成离屏渲染。
- 一旦为contents设置了内容,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。
2、在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题?
3、切圆角一定会触发离屏渲染吗?
4、iOS 9及之后的系统版本,苹果进行了一些优化
- 只设置contents或者UIImageView的image,并加上圆角+裁剪,是不会产生离屏渲染的。
- 但如果加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。
- 使用类似于UIButton的视图的时候需要注意
48、frame和bouns的区别。什么时候frame和bouns的高宽不相等
旋转后怎么样
49、事件响应过程(响应链)
- iOS开发---图解事件的产生和响应 ✨✨✨✨✨
- iOS触摸事件全家桶
- iOS开发 事件响应链应用(原题:举例说明事件响应链)
- iOS触摸事件传递响应之被忽视的手势识别器工作原理
- iOS点击事件和手势冲突✨✨✨✨✨
1、事件的传递 (寻找最合适的view的过程)
- 当一个事件发生后,事件会从父控件传给子控件 (UIApplication->UIWindow->UIView->initial view)
2、事件的响应
- 首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView)
- 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件
- 如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递
- 一直到window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃
3、⚠️注意
- 事件的传递是从上到下(父控件到子控件)
- 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)
4、重要方法:
4.1、hitTest:withEvent:
- 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
- 寻找并返回最合适的view(能够响应事件的那个最合适的view)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判断自己能否接收触摸事件
if (self.userInteractionEnabled == NO
|| self.hidden == YES
|| self.alpha <= 0.01) {
return nil;
}
// 2.判断触摸点在不在自己范围内
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
for(NSInteger i = self.subviews.count; i >= 0; i --) {
UIView *childView = self.subviews[I];
CGPoint childPoint = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
// 没有找到比自己更合适的view
return self;
}
4.2、pointInside:withEvent:
- 判断点在不在当前view上(方法调用者的坐标系上)
- 如果返回YES,代表点在方法调用者的坐标系上;
- 返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
5、穿透
- 假设有一个黄色控件和白色控件,白色空间覆盖在黄色控件上
- 点击白色view想要黄色view来响应该事件,就是所谓的穿透
方法一、
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint yellowPoint = [self convertPoint:point toView:_yellowView];
if ([_yellowView pointInside:yellowPoint withEvent:event]) {
return _yellowView;
}
return [super hitTest:point withEvent:event];
}
方法二、
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint yellowPoint =[_yellowView convertPoint:point fromView:self];
if ([_yellowView pointInside:yellowPoint withEvent:event]){
return NO;
} else {
return [super pointInside:point withEvent:event];
}
}
50、异步绘制相关(setNeedsLayout、drawRect)🌟🌟
51、手势识别的过程
这里主要说的是关于runloop的概念点
- 当_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel,将当前的 touchesBegin/Move/End 系列回调打断
- 随后系统将对应的 UIGestureRecognizer 标记为待处理
- 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件
- 这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调。
- 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理
52、IBOutlet连出来的视图属性为什么可以被设置成weak?
IBOutletUITableView相关优化
- 每个UITableViewCell都有一个UIImageView需要加载,如果没有缓存,有五个Cell请求一个url,同时请求肯定不实际,如何处理?( UITableViewCell 异步加载图片缓存机制优化)
- 微信UITableView滑动的时候,动图是不动的,为什么?(Runloop的mode)
两个半透明控件重贴部分颜色加深问题
十二、其他
53、数组和链表的区别
- 数组在内存上给出了连续的空间
- 链表,内存地址上可以是不连续的,每个链表的节点包括原来的内存和下一个节点的信息(单向的一个,双向链表的话,会有两个)
数组:
- 优点: 使用方便,查询效率比链表高,内存为一连续的区域
- 缺点: 大小固定,不适合动态存储,不方便动态添加
链表:
- 优点: 可动态添加删除,大小可变
- 缺点: 只能通过顺次指针访问,查询效率低
54、谈谈你对编译、链接的理解
55、leak工具使用
56、应用程序启动过程,启动优化
- 应用启动时是用怎样加载所有依赖的Mach-O文件的?
- 请列举你所知道main()函数之前耗时的因素都有哪些(简述App main函数执行前启动流程)✨✨
App启动分为两种:
- 冷启动(Cold Launch):从零开始启动app
- 热启动(Warm Launch):app已在内存中,在后台存活,再次点击图标启动app
启动时间的优化,主要是针对冷启动进行优化
1、通过添加环境变量可以打印app的启动时间分析(详情请见下图)
- DYLD_PRINT_STATISTICS
- DYLD_PRINT_STATISTICS_DETAILS(比上一个详细)
- 一般400毫秒以内正常
打印结果:
Total pre-main time: 238.05 milliseconds (100.0%) // main函数调用之前(pre-main)总耗时
dylib loading time: 249.65 milliseconds (104.8%) // 动态库耗时
rebase/binding time: 126687488.8 seconds (18128259.6%)
ObjC setup time: 10.67 milliseconds (4.4%) // OC结构体准备耗时
initializer time: 52.83 milliseconds (22.1%) // 初始化耗时
slowest intializers : // 比较慢的加载
libSystem.B.dylib : 6.63 milliseconds (2.7%)
libBacktraceRecording.dylib : 6.61 milliseconds (2.7%)
libMainThreadChecker.dylib : 31.82 milliseconds (13.3%)
2、冷启动可以概括为3大阶段
- dyld
- runtime
- main
3、dyld(dynamic link editor),Apple的动态连接器,可以装载Mach-O(可执行文件、动态库等)
- 装载app的可执行文件,同时递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完成后,会通知runtime进行下一步处理
4、runtime所做的事情
- 调用map_images函数中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册objc类、初始化类对象等等)
- 调用C++静态初始化器和__attribure__((constructor))修饰的函数(JSONKit中存在具体应用)
- 到此为止,可执行文件和动态库中所有的符号(Class, Protocol, Selector, IMP...)都已按格式成功加载到内存中,被runtime所管理
5、总结
- app的启动由dylb主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
- 并由runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
- 接下来就是ApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
6、按照不同的阶段优化
dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少objc类、分类的数量、减少selector数量(定期清理不必要的类、分类)
- 减少C++虚构函数
- Swift尽量使用struct
runtime
- 使用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、Objc的+load方法
main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 按需加载
DYLD_PRINT_STATISTICS设置为1
57、包体积优化
安装包瘦身(ipa):资源文件、可执行文件
资源文件(图片、音频、视频等)
- 采取无损压缩(使用工具)
- 去除没有用到的资源(https://github.com/tinymind/LSUnusedResources)
可执行文件瘦身:
- 编译器优化(Xcode相关配置)
- 利用AppCode(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code
- 生成LinkMap,可以查看可执行文件的具体组成
- 可借助第三方工具解析LinkMap文件:http://github.com/huanxsd/LinkMap
57、项目的优化、性能优化
启动速度:
- 启动过程中做的事情越少越好(尽可能将多个接口合并)
- 不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新节目)
- 在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)
- 尽量减小包的大小
- 辅助工具(友盟,听云,Flurry)
页面浏览速度
- json的处理(iOS 自带的NSJSONSerialization,Jsonkit,SBJson)
- 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)
- 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)
- 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)
- 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载
- 算法的优化(核心算法的优化,例如有些app 有个 联系人姓名用汉语拼音的首字母排序)
操作流畅度优化
- Tableview 优化(tableview cell的加载优化)
- ViewController加载优化(不同view之间的跳转,可以提前准备好数据)
58、说说你自己吧
- 你在项目中技术亮点、难点
- 你的发展方向(职业规划)
- 你的优点、你的缺点
59、说说组件化,你是如何组件化解耦的
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
60、静态库、动态库相关
1、什么是库?
- 共享代码,实现代码的复用,一般分为静态库和动态库。
2、静态库和动态库的区别
静态库(.a和.framework 样式):
- 链接时完整的拷贝到可执行文件,多次使用多次拷贝,造成冗余,使包变的更大
- 但是代码装载速度快,执行速度略比动态库快
动态库:(.dylib和.framework)
- 链接时不复制,程序运行时由系统加在到内存中,供系统调用,系统加在一次,多次使用,共用节省内存。
3、为什么framework既是静态又是动态?
- 系统的framework是动态的,自己创建的是静态的。
4、.a 和 .framework 的区别是什么?
- .a 是单纯的二进制文件,需要 .h文件配合,不能直接使用
- .framework是二进制文件+资源文件,可以直接使用。 .framework = .a + .h + sorrceFile(资源文件)
十三、OC对象相关
61、对 OC 中 Class 的源码理解?其中 cache 的理解?说说NSCache缓存策略
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
62、protocol中能否添加属性
- OC语言的协议里面是支持声明属性的
- 但在协议中声明属性其实和在其中定义方法一样,只是声明了getter和setter方法,并没有具体实现
63、OC内联函数(inline)
作用:
- 替代宏
inline函数与宏有区别
- 解决函数调用效率的问题
- 函数之间调用,是内存地址之间的调用,当函数调用完毕之后还会返回原来函数执行的地址。
- 函数调用有时间开销,内联函数就是为了解决这一问题
inline相比于宏的优点
- 避免了宏的缺点:需要预编译.因为inline内联函数也是函数,不需要预编译.
- 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。消除了它的隐患和局限性。
- 可以使用所在类的保护成员及私有成员
inline相比于函数的优点✨✨
- inline函数避免了普通函数的,在汇编时必须调用call的缺点:取消了函数的参数压栈,减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快.
- 集成了宏的优点,使用时直接用代码替换(像宏一样)
64、id和NSObject ,instancetype的区别?
- id和instancetype都可以做方法的返回值。
- id类型的返回值在编译期不能判断对象的真实类型,即非关联返回类型
- instancetype类型的返回值在编译期可以判断对象的真实类型,即关联返回类型。
- id可以用来定义变量, 可以作为返回值, 可以作为形参
- instancetype只能用于作为返回值。
非关联返回类型、关联返回类型
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
65、方法签名有什么作用?✨✨✨✨✨
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
66、nil、Nil、NULL、NSNull的区别?
- nil:指向一个对象的空指针
- Nil:指向一个类的空指针,
- NULL:指向其他类型(如:基本类型、C类型)的空指针, 用于对非对象指针赋空值.
- NSNull:在集合对象中,表示空值的对象.
NSNull在Objective-C中是一个类 .NSNull有 + (NSNull *)null; 单例方法.多用于集合(NSArray,NSDictionary)中值为空的对象.
NSArray *array = [NSArray arrayWithObjects: [[NSObject alloc] init], [NSNull null], @"aaa", nil, [[NSObject alloc] init], [[NSObject alloc] init], nil];
NSLog(@"%ld", array.count);// 输出 3,NSArray以nil结尾
67、NSDictionary底层实现原理✨✨✨✨
- 在OC中NSDictionary是使用hash表来实现key和value的映射和存储的。
hash表存储过程简单介绍:
- 根据key值计算出它的hash值h;
- 假设箱子的个数是n,那么键值对应该放在第(h%n)个箱子中。
- 如果该箱子中已经有了键值对,就是用开放寻址法或者拉链法解决冲突。使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。
68、父类的property是如何查找的?
- 子类中的propert_list、method_list、ivar_list并不包含父类
- 子类对象的_IMPL包含父类的
从以上几点回答
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
69、+load与 +initialize
共同点:
- 方法只会执行一次
- 在类使用之前,就自动调用了这两个方法
区别:
- 执行时机不同()
- load方法:如果类自身没有定义,并不会调用其父类的load方法;
- initialize方法:如果类自身没有定义,就会调用其父类的initialize方法;
执行的前提条件:
- load 只要类所在文件被引用,就会执行;
- 如果类没有引用进项目,就不会有load的执行;
- initialize 需要类或者其子类的第一个方法被调用,才会执行,而且是在第一个方法执行之前,先执行;
- 即使类文件被引用进项目,但是没有使用,那么initialize就不会调用执行;
70、iOS如何实现多继承,代码书写一下
- 使用协议组合
- NSProxy
71、类与结构体的区别
- 结构体只能封装数据,而类还可以封装行为
- 赋值:结构体是拷贝,对象之间是地址
- 结构体变量分配在栈空间(如果是一个局部变量的情况下),而对象分配在堆空间
72、crash崩溃怎么解,崩溃到底层代码
NSSetUncaughtExceptionHandler可以统计闪退
TODO(待填充);⌛️⌛️⌛️⌛️⌛️
73、属性、成员变量、set、get方法相关
- 属性可以与set方法和get方法 三者同时存在吗,如果不行,请说明原因?
换句话说就是:iOS中同时重写属性的set与get方法时,为什么访问不了下划线属性?
原因:
- 属性的setter方法和getter方法是不能同时进行重写,
- 因为,一旦你同时重写了这两个方法,那么系统就不会帮你生成这个成员变量了
解决方式:
@synthesize authType = _authType;
- 意思是,将属性的setter,getter方法,作用于这个变量。
74、isa和superclass相关
1、对象的isa指针指向哪里?superclass指针呢?(⚠️图-总结图)
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class
- class的superclass指向父类的class(如果没有父类,superclass指针为nil)
- meta-class的superclass指向父类的meta-class
- ⚠️基类的meta-class的superclass指向基类的class
2、方法调用查找(⚠️⚠️⚠️图-instance调用对象的轨迹;图-类方法调用轨迹)
- 对象方法的调用:通过instance的isa找到class,最后找到对象方法的实现进行调用
- 类方法的调用:当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
3、class对象的superclass指针
Student : Person : NSObject
当Student的instance对象要调用Personal的对象方法时:
- 先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用
4、meta-class对象的superclass指针
当Student的class要调用Person的类方法时
- 先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用
isa指针
superclass指针
meta-class
总结.jpg
instance调用对象的轨迹
类方法调用轨迹
75、OC的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值,存放在instance对象中
76、class、meta-class的结构
struct objc_class : objc_object {
Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用于获取具体的类信息
}
& FAST_DATA_MASK
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; // 类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
77、简述__kindof关键字
- 表示当前类或者他的子类(iOS9推出的,一般用于消除警告)
- 声明数组存贮指定UIView类型的元素
- 如果元素被赋值为UIWebView或UIButton这样的子类型,编译器就会报警告
- 为了解决这个问题,__kindof就应运而生。
@property (nonatomic, strong) NSMutableArray<UIView *> *viewList;
👇
@property (nonatomic, strong) NSMutableArray <__kindof UIView *> * viewList;
⚠️:新版Xcode这个问题已经优化,例子属于老实例,理解这个意思就行了
78、关于NSProxy
- 消除NSTimer循环引用
- 多继承
@interface LYBird ()
@property (nonatomic, copy) NSString *bridName;
@end
@implementation LYBird
- (void)onFly {
NSLog(@"%@正在飞翔", self.bridName);
}
@end
@interface LYFish ()
@property (nonatomic, copy) NSString *fishName;
@end
@implementation LYFish
- (void)onSwimming {
NSLog(@"%@正在游泳", self.fishName);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
LYBird *bird = [[LYBird alloc] init];
LYFish *fish = [[LYFish alloc] init];
LYProxy *proxy = [LYProxy alloc];
[proxy transformToObject:bird];
[proxy performSelector:@selector(setBridName:) withObject:@"鹰隼"];
[proxy performSelector:@selector(onFly)];
[proxy transformToObject:fish];
[proxy performSelector:@selector(setFishName:) withObject:@"🦈"];
[proxy performSelector:@selector(onSwimming)];
}
79、共用体和结构体的区别
部分参考文章: