18.11.14 面试复盘
一面 : 内存管理,运行时,类的加载顺序,多线程,小算法。
二面 :数据库 AF JSPatch HTTPS
面试题
一面题目
- 通知是同步的吗?子线程中发通知,执行的监听方法,是在哪个线程中执行的?
- 同步或者异步执行串行队列会生成子线程么,同步或者异步执行并行队列会生成子线程么?重点多了解一下 Dispatch,包括各种用法等。
- NSTimer 为什么会强引用?假如 target 是 weak 修饰的会强引用吗,在 dealloc 里注销 timer 可以么? dealloc 的调用时机。
- load 方法的加载时机,里面可以访问分类的方法吗,子类的 load 方法调用时,分类已经加载到运行时了吗?
- category 怎么添加属性,关联对象的内存管理,关联一个 int 类型怎么释放掉?
- class 方法 和 object_getClass 方法的区别。
- super 实际上是什么?
- 消息转发机制。
- 项目中有没有遇到什么问题,怎么解决的?
- 图片怎么加载到内存中的,假如一张图片 30k ,在手机内存中实际占据的空间大小是多少?
- JSPatch 或者 JS 的交互,是怎么实现的?
- 平时项目总用户是多少?日活是多少?bug 率是多少?假如线上出现了 bug ,怎么快速定位到,有没有好的技术方案快速定位到 bug 出错点?
- 合并有序数组。
- 抓包工具,Charles 的了解和使用。
- sourceTree 中遇到的比较棘手的问题。git 命令
二面题目
- 判断字符串是否是回滚字符串,例: abbiuibba ,延伸,假如是数字,并要求时间和空间复杂度最小。
- 深入了解 AF 包括网络的 cookie session 的原理,AF 最多接受多少域名?
- HTTPS
- 项目中用到的数据库,为什么要用数据库存储数据?数据库的表结构是怎样的?
- 针对于防止中间人攻击,数据加密等是怎么做的,或者怎么缓存的。
- 数据结构,二叉树的层次遍历。
- 组件化的实现,通信和怎么解耦的?
- 感觉自己闪光点在哪里?
答案
一面
通知是同步的,不管在哪个线程注册通知,发送通知的时候,只会跟发送时所在的线程相关,比如在子线程中发出通知,执行的方法是在子线程里面;在主线程里发通知,执行的方法是在主线程里。
看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"并行队列异步执行:%@",[NSThread currentThread]); /// 产生新线程
});
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"并行队列同步执行:%@",[NSThread currentThread]); /// 打印的是主线程
});
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"串行队列异步执行:%@",[NSThread currentThread]); /// 主线程
});
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", NULL);
dispatch_async(serialQueue, ^{
NSLog(@"串行队列异步执行:%@",[NSThread currentThread]); /// 线程一
});
dispatch_async(serialQueue, ^{
NSLog(@"串行队列异步执行:%@",[NSThread currentThread]); /// 线程一 两次执行都是在子线程中执行,说明,每个 serialQueue 都最多产生一个子线程,
});
dispatch_sync(serialQueue, ^{
NSLog(@"串行队列同步执行:%@",[NSThread currentThread]); /// 打印的是主线程
});
dispatch_sync(serialQueue, ^{
NSLog(@"串行队列同步执行:%@",[NSThread currentThread]); /// 打印的是主线程
});可以统计出:
1
2
3串行队列 并行队列
async异步 每个queue最多一个线程 产生新线程
sync同步 当前线程 当前线程对 sync 同步函数来说,把任务同步的放到并行队列还是串行队列,实际上都是在当前线程,也就是 sync 操作处在的线程中,并没有产生新线程。
对 async 异步函数来说,把任务异步放到并行队列里,肯定会产生新线程,放到一个串行队列里,不会产生新线程,每次生成一个串行队列,都对应了一个线程(非主线程),async 不会再多生成一个线程。
还有个问题,这四种情况下,任务到底是同步还是异步执行呢?- async + 并行队列,肯定是并行执行。
- async + 串行队列,串行执行。
- sync 更简单了,因为都是在同一个线程里处理,肯定都是串行执行。
要注意,一个线程一次肯定只能执行一个任务,肯定是按照顺序执行的。
dispatch 其实也会对内部使用的对象进行强引用,但是没有循环引用的关系就可以。NSTimer 会对 target 进行一个强引用,即使将 target 置为一个 weak 指针,也会对齐指向的对象进行一个强引用,而且即使 target 不对 timer 进行引用,也会发生内存泄漏,因为 timer 会被 runloop 所持有,从而导致 timer 不会释放,继而导致 target 不会释放。delloc 只有当对象销毁时才会调用,不会销毁,当然不会调用。有好多解决 timer 的方案:
- 开发的时候规范化,初始化一个 timer ,就记得把它 invalidate 和 置为 nil 注销掉。
- iOS 10.0 之后,提供了 block 的初始化方法,这个里面,只要防止 block 对 target 进行引用就好了,但是针对 timer 还是要 invalidate 和 置为 nil。
- 当 A 使用 timer 的时候,找一个中间者去生成它,这个中间者 B 随着A的释放而释放,在 dealloc 中对 timer 进行销毁,timer 的 target 被 B 弱引用,timer 执行方法的时候,target 里实现消息转发,转发到真正的使用者 A 中,图片中讲解的更详细。
load 方法,调用在运行时把类加载以后,main 函数之前,即类的方法列表已经加载好了,里面也有分类的方法,这个时候可以调用自己分类的方法,load 方法是系统自动调用的,而且是通过使用函数内存地址的方式,而不是使用 objc_msgSend 的方式,所以 load 方法不会被覆盖掉。其实这个时候,所有的类都加载到运行时里面了,也可以调用其他类的方法,但是假如其他类的方法依赖于自身的 load ,就可能会出错。
initialize 方法,只有在 类或子类收到第一条送消息时会调用,包括实例方法和类方法,在调用自己的实现时,会先循环递归调用父类的实现,然后再调用 objc_msgSend 方法,所以假如子类没有实现,收到第一条消息的时候,会先递归调用父类的方法,接着 objc_msgSend ,发现自己没有这个实现,然后找到 父类的方法进行调用,这两次的调用区别在哪里?在于第一次,是父类执行这个方法,而第二次,是子类执行这个方法。而且,也会被分类实现的 initialize 方法所覆盖掉。对一个对象添加 关联对象 实际上是在一张全局表中,将 关联对象跟对象做了绑定,这张表存储着某个对象所有的对应关系,内存缓存策略表明了这个关联对象什么时候释放,假如是 retain ,那么这个关联对象跟随着主对象释放而释放,假如是 assign ,分两种情况:
- 关联对象是自定义的对象或者普通对象 alloc 以后超出作用域就会释放
- NSString NSNumber 不会释放,即再次请求的时候不会野指针。
第二种很奇怪,我暂时还没搞清楚。
object_class 运行时底层,是返回了这个对象里的 isa 指针,对于一个对象来说,isa 指针是自己的类,对于类来说,isa 指针是自己的元类。
而在 NSObject 的实现里,实例方法 class ,和类方法 class 的实现如下:1
2
3
4
5
6
7+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}可以看到,实例方法的
-class
,其实就是调用了object_getClass()
,返回的是自己的 isa 指针,即类,对类再进行调用,返回的就是元类。而类方法+class
,返回的是自己本身,即类。当一个对象接收到没有实现的消息时,会有下面几个阶段:
- 查找父类
- 动态解析
- 消息重定向到某个对象
- 回调传回一个包装好的消息,允许我们做进一步转发。
super ,实际上是一个编译器指示符,遇到时,编译器生成一个结构体:
1
2
3
4struct objc_super {
id receiver;
Class superClass;
};这个结构体保存有一个
receiver
指针指向子类,还有一个指向superClass
的Class 指针,当我们调用[super message]
方法时,编译器转为:1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
内部可能是拿
receiver
和 根据receiver
和superClass
、sel
,找到的方法,去做的方法调用,可能会使用objc_msgSend(id theReceiver, SEL theSelector, ...)
方法,不过这时候,接受者是super->receiver
。这个地方是个加分的地方,要好好准备解答。
图片加载到 UIImageView 上,需要以下几步:
- 从硬盘中加载到内存中
- 将压缩的图片解码成未压缩的位图 CPU 中
- 将未被压缩的位图渲染到 UIImageView 上
位图就是一个像素数组,存储着图片中的每一个点,假如一张图片尺寸为 30 * 30,质量为 30k,那么加载到手机中,解压缩后,原始数据大小其实为:30 * 30 * 4 字节(每个像素占得字节数)。
而一张图片的二进制数据,是经过压缩后的位图图形格式,大小就是我们一般说的图片有 30k 大。
一般将图片渲染到屏幕上时,必须将他解压缩出来得到原始的位图数据,才能执行后续的渲染工作,解压缩是很耗时的操作,Core Graphics 提供了一个解压缩方法
CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo);
一般来说一些第三方比如 YYKit、SD 都是使用这个方法在子线程进行了图片预先解压缩,当图片进行了解压缩后,系统就不会再次解压缩了。暂定
项目日活大概 3 w,总用户大概 200w ,bug 率 大概 0.2% 。