一面 : 内存管理,运行时,类的加载顺序,多线程,小算法。
二面 :数据库 AF JSPatch HTTPS

面试题

一面题目

  1. 通知是同步的吗?子线程中发通知,执行的监听方法,是在哪个线程中执行的?
  2. 同步或者异步执行串行队列会生成子线程么,同步或者异步执行并行队列会生成子线程么?重点多了解一下 Dispatch,包括各种用法等。
  3. NSTimer 为什么会强引用?假如 target 是 weak 修饰的会强引用吗,在 dealloc 里注销 timer 可以么? dealloc 的调用时机。
  4. load 方法的加载时机,里面可以访问分类的方法吗,子类的 load 方法调用时,分类已经加载到运行时了吗?
  5. category 怎么添加属性,关联对象的内存管理,关联一个 int 类型怎么释放掉?
  6. class 方法 和 object_getClass 方法的区别。
  7. super 实际上是什么?
  8. 消息转发机制。
  9. 项目中有没有遇到什么问题,怎么解决的?
  10. 图片怎么加载到内存中的,假如一张图片 30k ,在手机内存中实际占据的空间大小是多少?
  11. JSPatch 或者 JS 的交互,是怎么实现的?
  12. 平时项目总用户是多少?日活是多少?bug 率是多少?假如线上出现了 bug ,怎么快速定位到,有没有好的技术方案快速定位到 bug 出错点?
  13. 合并有序数组。
  14. 抓包工具,Charles 的了解和使用。
  15. sourceTree 中遇到的比较棘手的问题。git 命令

二面题目

  1. 判断字符串是否是回滚字符串,例: abbiuibba ,延伸,假如是数字,并要求时间和空间复杂度最小。
  2. 深入了解 AF 包括网络的 cookie session 的原理,AF 最多接受多少域名?
  3. HTTPS
  4. 项目中用到的数据库,为什么要用数据库存储数据?数据库的表结构是怎样的?
  5. 针对于防止中间人攻击,数据加密等是怎么做的,或者怎么缓存的。
  6. 数据结构,二叉树的层次遍历。
  7. 组件化的实现,通信和怎么解耦的?
  8. 感觉自己闪光点在哪里?

答案

一面

  1. 通知是同步的,不管在哪个线程注册通知,发送通知的时候,只会跟发送时所在的线程相关,比如在子线程中发出通知,执行的方法是在子线程里面;在主线程里发通知,执行的方法是在主线程里。

  2. 看下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    dispatch_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 其实也会对内部使用的对象进行强引用,但是没有循环引用的关系就可以。

  3. 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 中,图片中讲解的更详细。
  4. load 方法,调用在运行时把类加载以后,main 函数之前,即类的方法列表已经加载好了,里面也有分类的方法,这个时候可以调用自己分类的方法,load 方法是系统自动调用的,而且是通过使用函数内存地址的方式,而不是使用 objc_msgSend 的方式,所以 load 方法不会被覆盖掉。其实这个时候,所有的类都加载到运行时里面了,也可以调用其他类的方法,但是假如其他类的方法依赖于自身的 load ,就可能会出错。
    initialize 方法,只有在 类或子类收到第一条送消息时会调用,包括实例方法和类方法,在调用自己的实现时,会先循环递归调用父类的实现,然后再调用 objc_msgSend 方法,所以假如子类没有实现,收到第一条消息的时候,会先递归调用父类的方法,接着 objc_msgSend ,发现自己没有这个实现,然后找到 父类的方法进行调用,这两次的调用区别在哪里?在于第一次,是父类执行这个方法,而第二次,是子类执行这个方法。而且,也会被分类实现的 initialize 方法所覆盖掉。

  5. 对一个对象添加 关联对象 实际上是在一张全局表中,将 关联对象跟对象做了绑定,这张表存储着某个对象所有的对应关系,内存缓存策略表明了这个关联对象什么时候释放,假如是 retain ,那么这个关联对象跟随着主对象释放而释放,假如是 assign ,分两种情况:

    • 关联对象是自定义的对象或者普通对象 alloc 以后超出作用域就会释放
    • NSString NSNumber 不会释放,即再次请求的时候不会野指针。

    第二种很奇怪,我暂时还没搞清楚。

  6. 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,返回的是自己本身,即类。

  7. 当一个对象接收到没有实现的消息时,会有下面几个阶段:

    1. 查找父类
    2. 动态解析
    3. 消息重定向到某个对象
    4. 回调传回一个包装好的消息,允许我们做进一步转发。
  8. super ,实际上是一个编译器指示符,遇到时,编译器生成一个结构体:

    1
    2
    3
    4
    struct objc_super {
    id receiver;
    Class superClass;
    };

    这个结构体保存有一个 receiver 指针指向子类,还有一个指向 superClass的Class 指针,当我们调用 [super message] 方法时,编译器转为:

    1
    id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

    内部可能是拿 receiver 和 根据 receiversuperClasssel,找到的方法,去做的方法调用,可能会使用 objc_msgSend(id theReceiver, SEL theSelector, ...) 方法,不过这时候,接受者是super->receiver

  9. 这个地方是个加分的地方,要好好准备解答。

  10. 图片加载到 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 都是使用这个方法在子线程进行了图片预先解压缩,当图片进行了解压缩后,系统就不会再次解压缩了。

  11. 暂定

  12. 项目日活大概 3 w,总用户大概 200w ,bug 率 大概 0.2% 。