2021年6月面试小记
6月离开公司,开启了找工作之旅,记录一下遇到的面试题,未完,持续更新
SDWebImageView
- 流程
sd_setImageWithUrl
sd_internalSetImageWithUrl
loadImageWithUrl
quryCacheForKey
- diskResult
- downloadImage
- storImage
- SDWebImageCache
内存泄露
- 产生原因
- 循环引用造成
- 检查方式
- Product -> Analyze
- Instruments -> Leaks
- 具体产生场景
消息转发机制
快速查找
- 是否支持tagged pointer对象
- 为空则直接返回Zero
- 获取
isa
- 获取isa, 然后获取class isa & iSA_Mask
- 开启缓存查找
- 通过isa平移16获取到cache
- 通过cache&掩码获取到buckets
- 通过平移获取到mask(arm64为中为右移48位)
- 在通过mask & sel获取到方法下标index
- 在通过平移在buckets中获取到查找的buket,取到imp
- 比较通过index获取到的bucket中的sel与我们查找的sel是否为同一个
- 缓存命中,返回imp
- 如果不相等,则判断此bucket是否为第buckets的第一个元素
- 如果是第一个元素,则将bucket设置为buckets的最后一个元素,进行第二次递归查找
- 如果不是第一个元素,则从最后一个元素递归向前查找
- 如果一直没有找到则退出递归,进入
__objc_msgSend_uncached
慢速查找流程
慢速查找 (lookUpImpOrForward)
- 排除一些干扰因素,是否是已知类呀,是否完成初始化
- 确认继承链,因为这里会存在类方法和实例方法的区别,所以需要确定其继承链
- 然后进去死循环查找流程
-
当前查找的类是否是不断优化的类
- 查找其缓存
-
找到了就直接返回imp
-
没找到循环继续
-
- 查找其缓存
-
通过二分查找,在方法列表中查找
- 找到了,写入cache,返回imp
- 查找其父类curClass = curClass -> superClass
- 现在父类缓存中找
- 找到了是否为forward_imp
- 结束循环,进入动态方法决议
- 存入查找类的cache,返回imp
- 找到了是否为forward_imp
- 一直没有找到,则imp为forward_imp,进入动态方法决议
- 现在父类缓存中找
-
- imp = forward_imp 动态方法决议
动态方法决议
- 判断是否是元类
- resolveInstanceMethod
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
//获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
//获取sayMaster的签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- resolveClassMethod
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(sayNB)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
消息转发
- 快速转发 快速转发,可以转发给其它类或对象,其它的类会对象如果实现了查找方法的类方法或者对象方法,则不报错,如果没有则进入慢速转发
+ (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
return [Teacher alloc];
}
- 慢速转发 这里在
methodSignatureForSelector
中返回方法签名,在forwardInvocation中可以处理,也可以不处理,处理方式 这里也需要要在对应的类里面有对应的实现
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
/// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s- %@", __func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s- %@", __func__, anInvocation);
[anInvocation invokeWithTarget:[Student alloc]];
}
[anInvocation invokeWithTarget:[Student alloc]];
多线程的使用
NSThread的使用
- 通过alloc来启用,需要手动开启
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(threadDo) object:NULL];
[thread start];
[thread2 start];
- 通过detachNewThread直接开启
[NSThread detachNewThreadSelector:@selector(threadDo) toTarget:self withObject:@"ThreadName1"];
[NSThread detachNewThreadWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
- 通过performSelector来开启
[self performSelectorInBackground:@selector(threadDo) withObject:@"ThreadName2"];
[self performSelectorOnMainThread:@selector(threadDo) withObject:@"ThreadName3" waitUntilDone:YES];
NSThread支持KVO,可以监听到threa的执行状态
isExecuting
是否正在执行isCancelled
是否被取消isFinished
是否完成isMainThread
是否是主线程threadPriority
优先级
GCD
- dispatch_after
- dispatch_once
- dispatch_apply
- dispatch_group_t
- dispatch_semaphore_t控制并发数
- dispatch_source_t可以实现timer不依赖于runloop,精度比timer高
NSOperation
Block
block类型
__NSGlobalBlock__
:全局block,存储在全局区
此时的
block
没有访问外界变量,无参也无返回值
void(^block)(void) = ^{
NSLog(@"hello world");
}
__NSMallocBlock__
:堆区block
int a = 10;
void(^block)(void) = ^{
NSLog(@"hello world - %d", a);
}
NSlog(@"%@", block);
此时的block会访问外界变量,即底层拷贝a,所以是堆区block
__NSStackBlock__
:栈区block
int a = 10;
NSlog(@"%@", ^{
NSLog(@"hello world - %d", a);
});
在完成a的底层拷贝前,此时的block还是栈区block,拷贝完成之后,从上面的堆区block可以看出,就变成堆区block了
int a = 10;
void(^__weak block)(void) = ^{
NSLog(@"hello world - %d", a);
}
NSlog(@"%@", block);
可以通过__weak不进行强持有,block就还是栈区block
总结
- block直接存储在全局区
- 如果block访问外界变量,并进行block相应拷贝
block的循环引用
- 造成循环引用的原因
- 互相持有,导致释放不掉
- 解决循环引用的方法
- weak-strong-dance
如果block未嵌套block,直接使用__weak修饰的self即可,否则,需要搭配__strong来使用
__weak typeof(self) weakSelf = self;
self.dkblock = ^{
NSLog(@"%@", weakSelf.name);
};
self.dkblock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf.name);
});
};
- __block修饰对象(需要注意在block内部使用完成之后置为nil,block必须调用)
__block ViewController *vc = self;
self.dkblock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
vc = nil; 记得手动释放
});
};
- 传递self作为block的参数,提供给block内部使用
self.myBlock = ^(ViewController *vc) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", vc.name);
});
}
block为什么要用copy修饰,使用strong修饰会有什么问题吗?
- block用strong和copy修饰都可以,对于block,编译器重写了strong底层逻辑,使其和copy是一样的原理,即把block从栈区复制到堆区
- 使用copy是因为block初始化时位于栈区,copy可以把栈区的对象复制到堆区,而栈上的block对象在作用域结束后释放
block的的底层结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
block的底层结构是一个结构体,也可以说block其实是一个对象、函数
block为什么需要调用
在底层block的类型是
__main_block_impl_0
结构体,通过其同名构造函数创建,第一个传入的block的内部实现代码块,即_main_block_func_0
,用fp来表示,然后赋值给impl的FuncPtr,然后再main中进行了调用,这也是block为什么需要调用的原因,如果不调用,内部实现的代码块将无法执行
block是如何捕获外界变量的
__weak原理
- block使用使用__weak修饰的变量时,会生成
__Block_byref_x_0
结构体 - 结构体用来保存原始变量的指针和值
- 将变量生成的结构体对象的指针地址传递给block,然后再block内部就可以对外界变量进行操作了
block的三重拷贝
- 通过
__block_copy
实现对象的自身拷贝,从栈区到堆区 - 通过
__block_byref_copy
方法,将对象拷贝为block_byref
结构体类型 - 调用
_block_object_assign
方法,对_block
修饰的当前变量进行拷贝
Extension类扩展
- 类扩展在编译器,会作为类的一部分,和类一起编译进来
- 类的扩展只是声明,依赖当前的主类,没有.m文件,可以理解为一个.h文件
- 声明属性和成员变量,也可以声明方法
- 在当前类.h中声明的属性和方法是共有的,在.m中声明的方法和属性是私有的
- 通过Extention新建的声明的属性与方法是私有的
Category分类
- 给类添加新的方法
- 不能给类添加成员属性,添加了成员属性,也无法取到
- 分类中使用@property定义的变量,只会生成Setter,Getter方法的声明,不会生成对应的实现
- 可以通过runtime给分类添加属性
关联对象
- Objc_setAssociatedObject
- 创建一个AssociationsManager管理类
- 获取全局静态Hasmap
- 判断插入的关联值是否存在
- 创建一个空的ObjctAssociationMap去取查询的简直对
- 如果发现没有这个key就插入一个空的BucketT进去
- 标记关联对象
- 用当前的policy和value组成一个ObjcAssociation替代原来的BucketT中的空
- 标记一下ObjctAssociationMap的第一次为false
Objec_getAssociatedObject
- 创建一个AssociationsManager管理类
- 获取全局静态Hasmap
- 根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器
- 如果迭代查询器不是最后能获取
- 找到ObjctAssociationMap的迭代查询器获取一个经过policy修饰的value
- 返回value
- AssociationsManager
KVO
kvo与NSNotificationCenter有什么区别
- 相同点
- 两者的实现都是观察者模式,都是用于监听
- 都能实现一对多的操作
- 不同点
KVO对可变集合的监听
- 通过[arr addObject:object]这种方式向数组添加元素,是不会触发KVO
- 对可变集合需要调用对应的KVC方法,监听才能生效
KVO的实现原理
- 添加KVO之后,实例对象的isa指向了一个新的派生类
NSKVONotifying_Class
- 重写了原本类的观察属性的setter方法
- 新增了_isKVO来判断当前是否是KVO类
- 在观察移除之前对象的isa指向一直是派生类
- 移除观察之后对象的呃isa指向原有类
- 派生类一旦产生就会一直存在内存中,不会被销毁
KVC
API
KVC设值原理
- 查找是否有这三种
setter
方法set<Key>
_set<Key>
setIs<Key>
如果能找到上面三个中的任意一个,则直接设值属性的value
- 查找
accessInstanceVariablesDirectly
是否返回YES- 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为
_<Key>
_is<Key>
<key>
is<Key>
- 如果返回NO,则进入3
- 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为
如果能找到任意一个实例变量i,ii,iii,iv,则直接赋值,否则进入3
KVC取值原理
- 首先查找getter方法,顺序为
get<Key>
<Key>
is<Key>
_<Key>
如果找到执行5
- 继续查找
countOf<Key>
、objectIn<Key>AtInde
、<Key>AtIndex
- 如果找到
countOf<Key>
和其他两个中的一个,则会创建一个响应所有NSArray
方法的集合代理对象,并返回该对象,即NSKeyValueArray
,是NSArray
的子类。代理对象随后将接收到的所有NSArray
消息转换为countOf<Key>
,objectIn<Key>AtIndex:
和<key>AtIndexes:
消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
- 如果找到
- 查找
countOf <Key>
,enumeratorOf<Key>
和memberOf<Key>
这三个方法- 如果这三个方法都找到,则会创建一个响应所有
NSSet
方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet
消息转换为countOf<Key>
,enumeratorOf<Key>
和memberOf<Key>:
消息的某种组合,用于创建它的对象
- 如果这三个方法都找到,则会创建一个响应所有
- 检查类方法
InstanceVariablesDirectly
是否YES
,依次搜索_<key>
_is<Key>
<key>
is<Key>
- 根据属性的类型值,返回不同的结果
- 对象指针,直接返回结果
- 如果是NSNumber支持标量的类型,则将其存储在NSNumber实例中并返回
- 如果是NSNumber不支持的标量类型,请转换为NSValue对象
- 如果一直没有找到,系统会执行该对象的
valueForUndefinedKey:
方法,默认抛出NSUndefinedException
异常
RunLoop
怎么保证子线程的数据回来更新UI操作不打断用户的滑动操作
将更新UI的事件,放到主线程的NSDefaultRunloopModel上执行,这样就会等用户不再滑动,主线程的RunLoop由UITrakingRunLoopModel切换到NSDefaultRunloopModel时再去更新UI
RunLoop有几种模式
- kCFRunloopDefultModel:默认模式,主线程也是在此model下执行
- UITrakingRunLoopModel:跟踪用户事件
- UIInitializetionRunLoopModel:在刚启动APP时第一个Model,启动完成后就不再使用
- GSEventReceiveRunloopModel:接受内部事件,一般用不到
- kCFRunloopCommonModels:伪model,是同步source/timer/observer到多个Model的一种解决方案
面向对象的三大特性
网络
http与https有什么区别
- Http协议 = Http协议 + SSL/TLS协议
SSL全程是
Secure Sockets Layer
,即安全套接层协议,是为网络通信提供安全及数据完整性的一种安全协议。TLS全称是Transport Layer Security
,即安全传输层协议,即HTTPS是安全的HTTP
TCP
- 三次握手
- 发出链接请求
- 客户端的TCP首先向服务端的TCP发送一条特殊的SYN报文
- 发送SYN报文后,客户端进入SYN_SENT状态,等待服务端确认,并将SYN比特置为1的报文段
- 授予链接
- 收到SYN报文后,服务端会为该TCP链接分配TCP缓存和变量,服务端的TCP进入SYN_RCVD状态,等待客户端TCP发送确认报文
- 向客户端发送允许链接的SYNACK报文段
- 确认,并建立链接
- 收到SYNACK报文段后,客户端也要为TCP分配缓存和变量,客户端的TCP进入ESTABLISHED状态
- 向服务端TCP发送一个报文段,这最后一个报文段对服务端的允许连接的报文表示了确认(将 server_isn + 1 放到报文段首部的确认字段中)。因为连接已经建立了,所以该 SYN 比特被置为 0。 这个阶段,可以在报文段负载中携带应用层数据
- 收到客户端该报文段后,服务端TCP也会进入ESTABLISHED状态,可以发送和接收包含有效载荷数 据的报文段。
- 发出链接请求
- 四次挥手
- 客户端发出终止FIN=1报文段
- 客户端向服务端发送FIN=1的报文段,并进入FIN_WAIT_1状态
- 服务端向客户端发送确认报文ACK=1
- 收到客户端发来的FIN=1的报文后,向客户端发送确认报文
- 服务端TCP进入CLOSE_WAIT状态
- 客户端送到确认报文后,进入FINAL_WAIT_2状态,等待服务端FIN=1的报文
- 服务端向客户端发送FIN=1报文段
- 服务端发送FIN=1的报文
- 服务端进入LAST_ACK状态
- 客户端向服务端发送ACK=1报文段
- 收到服务端的终止报文后,向服务端发送一个确认报文,并进入TIME_WAIT状态
- 如果ACK丢失,TIME_WAIT会使客户端TCP重传ACK报文。最后关闭,进入CLOSE状态,释放缓存和变量
- 服务端收到之后,TCP也会进入CLOSE状态,释放资源
- 客户端发出终止FIN=1报文段
- 为什么建立链接需要三次握手,而断开链接缺需要四次挥手
- 因为,在服务端发送ACK信号后,还有可能数据传输没有完成
- 数据传输完成才会发送FIN=1的信号
- 在四次握手中,客户端为什么在TIME_WAIT后必须等待2MSL时间呢
- 为了保证客户端发送的最后一个ACK报文段能够到达服务器
- 为什么要三次握手,而不是二次或者三次
PUT和POST,POST与GET的区别
PUT VS POST
push和post都有更改指定URL的语义,但PUT被定义为idempotent的方法,post则不是。idempotent多个请求产生的效果是一样的
GET VS POST
- GET参数通过URL传递,POST放在Request body中
- GET请求会被浏览器主动cache,而POST不会,除非手动设置
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
- Get 请求中有非 ASCII 字符,会在请求之前进行转码,POST不用,因为POST在Request body中,通过 MIME,也就可以传输非 ASCII 字符
- 一般我们在浏览器输入一个网址访问网站都是GET请求
- HTTP的底层是TCP/IP。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。但是请求的数据量太大对浏览器和服务器都是很大负担。所以业界有了不成文规定,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url
- GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)
- 在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
HTTP的请求方式有哪些
cookie和session的区别
- 存放位置不同
- cookie存放在客户的浏览器
- session存在在服务器上
- 安全程度不同
- cookie不是很安全,其他人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session
- 性能使用程度不同
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
- 数据存储大小不同
- 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制
- 会话机制不同
Swift
swift类与结构体的区别
- 关键字
class
struct
- class定义的属性必须初始化, struct不用初始化
// class定义
class Person {
final var name:String = ""
}
// strcut定义
struct Person{
var name:String
}
- 定义函数
class
可以使用关键字static
修饰,struct
只能使用static
修饰
- 扩展下标
class和struct都可以使用扩展下标
- 初始化
结构体有默认的初始化方法
什么时候使用结构体
swift怎么防止父类方法在子类被重写
class Person {
final var name:String = ""
final func personName() {
}
}
final
关键字
转载自:https://juejin.cn/post/6971808317282730015