面向对象的基础题

  • 面向对象的几个设计原则?

    • SOLID 原则,总共五个,额外增加一个迪米特法则
      • Single Responsibility Principle(单一原则)
      • Open Close Principle(开闭原则)
      • Liskov Substitution Principle(里氏替换原则)
      • Interface Segregation Principle(接口分离原则)
      • Dependency Inversion Principle(依赖倒置原则)
      • Law of Demeter(Leaset Knowledge Principle) 迪米特法则(最少知道原则)
  • Hash 表的实现?

    • 通过把关键码值(key)映射到表中的一个位置来访问记录,Hash 实现的关键是散列函数和冲突解决(链地址法和开放定址法)。
  • 什么是进程和线程?有什么区别?

    • 进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
    • 线程:操作系统能够进行运算调度的最小单位
    • 区别:线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 内存的几大区域?各自的职能?

    • 栈区:由编译器自动分配和释放,一般存放函数的参数值,局部变量等。
    • 堆区:由程序员分配和释放,若不释放,则程序结束由操作系统回收。
    • 全局区 (static):由编译器管理(分配和释放),程序结束由系统释放。全局变量和静态变量(相邻两块区,初始化和未初始化)。
    • 文字常量区:由编译器管理(分配释放),程序结束由系统释放;存放常量字符。
    • 程序代码区:存放函数的二进制代码。
  • 架构、框架和设计模式的区别?

    • 架构:一种顶层概括性的设计概念,像是蓝图,将不同的需求抽象为具体组件并使组件互相通信。
    • 框架:软件框架是提取特定领域软件的共性部分形成的体系结构,不同领域的软件项目有不同的框架类型。
    • 设计模式:一套被反复使用、多数人知晓、经过分类编目的总结,代码设计的一个成果,可以用在不同软件框架中一种实际问题的解决方案。(在软件开发中总结出的一种适合解决某种问题的方法)
  • MVC、MVVM 和 MVP 架构的不同?

    • MVC
      • 【优点】简单易上手、没有多层调用
      • 【缺点】耦合性强;不利于单元测试,Controller 担负了太多的事件处理,处理用户操作,管理 View 的声明周期,API 接口调用,处理错误,处理回调,监听通知,处理视图方向等,容易造成臃肿,维护不方便
    • MVP
      • 【优点】利于单元测试、明确责任分配
      • 【缺点】要写一些额外的代码(如绑定),若业务逻辑复杂,也会造成 Presenter 的臃肿
    • MVVM
      • 【优点】责任划分明确;逻辑清晰;可测性高;双向绑定,配合第三方开源库使用方便
      • 【缺点】(1)数据绑定使得 Bug 很难被定位;(2)对于过大的项目,数据绑定需要花费更多的内存;依赖于开源库实现双向绑定,学习起来比较复杂;

iOS 基础面试题

UI

  • UIView 和 CALayer 的区别?
    • UIView 和 CALayer 都是 UI 操作的对象。两者都是 NSObject 的子类,发生在 UIView 上的操作本质上也发生在对应的 CALayer 上。
    • UIView 是 CALayer 用于交互的抽象。UIView 是 UIResponder 的子类( UIResponder 是 NSObject 的子类),提供了很多 CALayer 所没有的交互上的接口,主要负责处理用户触发的种种操作。
    • CALayer 在图像和动画渲染上性能更好。这是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分。CALayer 在无需处理交互时进行渲染可以节省大量时间。
    • CALayer 的动画要通过逻辑树、动画树和显示树来实现
  • loadView 是干嘛用的?
    • loadView 用来自定义 view,只要实现了这个方法,其他通过 xib 或 storyboard 创建的 view 都不会被加载 。
  • layoutIfNeeded、layoutSubviews 和 setNeedsLayout 的区别?
    • layoutIfNeeded:方法调用后,在主线程对当前视图及其所有子视图立即强制更新布局。
    • layoutSubviews:方法只能重写,我们不能主动调用,在屏幕旋转、滑动或触摸界面、子视图修改时被系统自动调用,用来调整自定义视图的布局。
    • setNeedsLayout:方法与 layoutIfNeeded 相似,不同的是方法被调用后不会立即强制更新布局,而是在下一个布局周期进行更新。
  • iOS 的响应链?什么情况会影响响应链?
    • 事件 UIResponder:继承该类的对象才能响应
    • 事件处理:touchesBegain、touchesMoved、touchesEnded、touchesCancelled;
    • 事件响应过程:
      1. UIApplication(及 delegate) 接收事件传递给 keyWindow
      2. keyWindow 遍历 subViews 的 hitTest:withEvent: 方法找到点击区域内的视图处理事件
      3. UIView 的子视图也会遍历其 hitTest:withEvent: 方法,以此类推,直到找到点击区域内最上层的视图,将视图逐步返回给 UIApplication
      4. 最后找到的视图成为第一响应者,若无法响应,调用其 nextResponder 方法,一直找到响应链中能处理该事件的对象。
      5. 最后到 Application 依然没有能处理该事件的对象的话,就废弃该事件;
      • 关键是 hitTest:withEvent: 方法和 pointInside 方法
      • ⚠️ 以下几种情况会忽略,hidden 为 YES,alpha 小于等于 0.01,userInteractionEnabled 为 NO,
    • UIControl 的子类和 UIGestureRecognizer 优先级较高,会打断响应链;
  • 说几种给 UIImageView 添加圆角的方式?
    • cornerRadius(iOS9 之后不会导致离屏渲染)
    • CoreGraphic 绘制
    • UIBezierPath(本质同上)
  • iOS 有哪些实现动画的方式?
    • UIView Animation:系统提供的基于 CALayer Animation 的封装,可以实现平移、缩放、旋转等功能。
    • CALayer Animation:底层 CALayer 配合 CAAnimation 的子类,可以实现更复杂的动画
    • UIViewPropertyAnimator:iOS10 后引入的用于处理交互的动画,可以实现 UIView Animation 的所有效果。
  • 使用 drawRect 有什么影响?
    • 处理 touch 事件时会调用 setNeedsDisplay 进行强制重绘,带来额外的 CPU 和内存开销。

OC 基础

  • NS_ENUM 和 NS_OPTIONS 的区别?
    • 有符号和无符号的区别,ENUM 是 NSInteger,OPTIONS 是 NSUInteger。
  • iOS 的内存管理机制?
    • 引用计数,从 MRC(Manuel Reference Count)到 ARC(Automatic Reference Count)。(最好能说明对象的生命周期)
  • @property 后的相关修饰词有哪些?
    • 原子性:
      1. nonatomic:非原子性,编译器不会对其进行加锁(同步锁的开销较大)
      2. atomic:原子性(默认),编译器合成的方法会通过锁定机制确保原子性(非线程安全)
    • 读写权限:
      1. readwrite:可读写(默认),若不用 @dynamic 修饰编译器会自动为其生成 setter 和 getter 方法,否则编译器不生成由用户自己实现
      2. readonly:只读,若不用 dynamic 修饰仅生成 getter 方法,否则编译器不生成 getter 方法 有用户来实现
    • 内存管理语义:
      1. strong:修饰引用类型(表示持有当前实例),保留新值释放旧值,若修饰 IMMutable 不可变类型建议用 copy
      2. copy:指修饰不可变集合类型、AttributeString、Block(ARC 下 strong 和 copy 一样,把栈中的 Block 拷贝到堆中,copy 表示不需要 copy 操作了)(设置方法不保留新值而是将其 copy,不可变类型的可变类型子类)
      3. weak:修饰引用类型(非持有关系),不保留新值,不释放旧值(属性所指对象被摧毁时,属性值也会清空置 nil)
      4. assign:修饰基本数据类型(非引用类型),执行简单赋值操作
      5. unsafe_unretained:修饰引用类型(非拥有关系),所指对象被摧毁时,属性值不会被清空(unsafe)
    • 方法名:
      1. getter= 指定方法名
      2. setter(不常用)
  • dynamic 和 synthesis 的区别?
    • dynamic:告诉编译器不要帮我自动合成 setter 和 getter 方法,自己来实现,若没有实现,当方法被调用时会导致消息转发。
    • synthesis:指定实例变量的名字,子类重载父类属性也需要 synthesis( 重新 set 和 get、使用 dynamic,在 Protocol 定义属性、在 category 定义属性,默认不会自动合成)。
  • array 为何用 strong 修饰?mutableArray 为何用 copy 修饰?
    • array:若用 strong 修饰,在其被赋值可变的子类后,内容可能会在不知不觉中修改,用 copy 防止被修改。
    • mutableArray 若用 copy 修饰会返回一个 NSArray 类型,若调用可变类型的添加、删除、修改方法时会因为找不到对应的方法而 crash。
  • 深拷贝和浅拷贝(注意 NSString 类型)?
    • NSString:strong 和 copy 修饰的属性是同等的,指向同一个内存地址,mutableCopy 才是内存拷贝;
    • NSMutableString:strong 和 copy 不同,strong 指向同一个内存地址,copy 则会进行内存拷贝,mutableCopy 也会进行内存拷贝;
    • NSArray:对于字符类型和自定义对象结果是不同的,strong 和 copy 都指向相同内存地址,mutableCopy 也仅仅是指针拷贝;但是 mutableCopy 可以把 Array 变成 MutableArray
    • PS:copy 产生一个不可变类型,mutableCopy 产生一个可变类型;对于字符类型 mutableCopy 会拷贝字符,copy 对于 NSArray 是不拷贝的,对于 NSMutableArray 是拷贝的;对于对象类型,仅仅是指针拷贝;(建议手动试试)
  • Block 的几种类型?
    • _NSConcreteGlobalBlock:全局 Block 也是默认的 block 类型,一种优化操作,私有和公开,保持私有可防止外部循环引用,存储在数据区,当捕获外部变量时会被 copy 到堆上
    • _NSConcreteStackBlock:栈 Block,存储在栈区,待其作用域结束,由系统自动回收
    • _NSConcreteMallocBlock:堆 Block,计数器为 0 的时候销毁
  • isEqual 和“==”的区别?
    • ==:比较两个指针本身,而不是其所指向的对象。
    • isEqual:当且仅当指针值也就是内存地址相等;若重写该方法则要保证 isEqual 相等则 hash 相等,hash 相等 isEqual 不一定相等;若指针值相等则相等,值不相等就判断对象所属的类,不属于同一个类则不相等,同属一个类时再判断每个属性是否相等。
  • id 和 NSObject 的区别?
    • id:指向对象的指针,编译时不做类型检查,运行时向其发送消息才会对其检查。
    • NSObject:NSObject 类及其子类,编译时做类型检查,向其发送的消息无法处理时就会执行消息转发。
    • id<NSObject>:编译器不对其做类型检查,对象所属的类默认实现名为 NSObject 的 Protocol,既能响应方法又不对其做类型检查.
  • 通知、代理、KVO 和 Block 的不同(结合应用场景回答)?
    • 通知: 适用于毫无关联的页面之间或者系统消息的传递,属于一对多的信息传递关系。例如接收系统音量、系统状态、键盘等,应用模式的设置和改变,都比较适合用通知去传递信息。
    • 代理: 一对一的信息传递方式,适用于相互关联的页面之间的信息传递,例如 push 和 present 出来的页面和原页面之间的信息传递。
    • block: 一对一的信息传递方式,效率会比代理要高 (毕竟是直接取 IMP 指针的操作方式)。适用的场景和代理差不多,都是相互关联页面之间的页面传值。
    • KVO:属性监听,监听对象的某一属性值的变化状况,当需要监听对象属性改变的时候使用。例如在 UIScrollView 中,监听 contentOffset,既可以用 KVO,也可以用代理。但是其他一些情况,比如说 UIWebView 的加载进度,AVPlayer 的播放进度,就只能用 KVO 来监听了,否则获取不到对应的属性值。
  • 什么是循环引用?__weak、__strong 和 __block 的区别?
    • 循环引用:两个对象互相持有对方,一个释放需要另外一个先释放,delegate 的 weak 声明就是为了防止循环引用。(一般指双向强引用,单向强引用不需要考虑,如:UIView 动画 Block)
    • __weak:Block 会捕获在 Block 中访问 Block 作用域外的实例,这样会有内存泄漏的风险,用 __weak 修饰表示在 Block 不会导致该实例引用计数器加 1,也可以在 Block 执行结束后强制将 Block 置 nil,这样 Block 捕获的实例也会跟着释放,如果捕获的仅是基本数据类型,Block 只会对其值进行拷贝一份,此时值再怎么变化也不会影响 Block 内部的操作。
    • __strong:在 Block 中使用 __weak 修饰的实例很容易被释放,所以需要加锁判断是否释放,未释放则对其进行强引用持有,保证向该实例发送消息的时候不会导致崩溃
    • __block:Block 默认不允许修改外部变量的值,以 int 类型为例,若在 Block 中访问变量就把该变量进行 Copy 一份保存到 Block 函数内,然后变量在 Block 外部无论怎么改变都不会影响 Block 中使用的变量的值,若在 Block 改变外部变量的值,变量必须要用 __block 修饰,Block 是把该变量的栈内存地址拷贝到堆中,所以可以直接把改变的新值写入内存。(把栈中变量的指针地址拷贝到堆中)
  • 内存泄漏、野指针和僵尸对象的区别?
    • 内存泄露:在堆中申请的不再使用的内存没有释放,程序结束释放(Analyzer,Leaks)
    • 野指针:指向的内存已经被释放,或被系统标记为可回收(用 Malloc Scribble 调试)。
    • 僵尸对象:已经被释放的对象,指针指向的内存块认为你无权访问或它无法执行该消息。(EXC_Bad_Access,开启 NSZombieEnabled 检测)
  • nil、Nil、NULL、NSNull 的区别?
    • nil:空实例对象(给对象赋空值)
    • Nil:空类对象(Class class = Nil)
    • NULL:指向 C 类型的空指针
    • NSNull:类,用于空对象的占位符(用于替代集合中的空对象,还有判断对象是否为空对象)
  • static 和 const 的区别?
    • const:声明全局只读变量,(若前面没有 static 修饰,在另外一个文件中声明同名的常量会报错)
    • static:修饰变量的作用域 (本文件内),被修饰的变量只会分配一份内存,在上一次修改的基础上进行修改。
    • 一般两者配合使用,如:static const NSTimeInterval kAnimationDuration = 1.0;不会创建外部符合,编译时预处理指令会把变量替换成常值。
  • iOS 中有哪些设计模式?
    • 【单例】保证应用程序的生命周期内仅有一个该类的实力对象,易于外界访问。如:UIApplication、NSBundle、NSNotificationCenter、NSFileManager、NSUserDefault、NSURLCache 等;
    • 【观察者】定义了一种一对多的依赖关系,可以让多个观察者同时监听一个主题对象,当主题对象状态或值发生改变,会通知所有的观察者;KVO 当对象属性变化时,通知观察此属性的对象。案例代表:通知和 KVO
    • 【类簇】(隐藏抽象基类背后的实现细节)如:UIButton、NSNumber、NSData、NSArray、NSDictionary、NSSting。用 isMemberOfClass 和 isKindOfClass 来判断。
    • 【命令模式】(运行时可以调用任意类的方法),代表:NSInvocation,封装一个请求或行为作为对象,包含选择器、方法名等。
    • 【委托模式】“我想知道列表中被选中的内容在第几行”?可以,接受我的委托就可以知道;只是接受我的委托就要帮我完成这几件事情,有必须要完成的,有不必要完成的,至于你怎么完成我就不关心了。
    • 【装饰器模式】:装饰器模式在不修改原来代码的情况下动态的给对象增加新的行为和职责,它通过一个对象包装被装饰对象的方法来修改类的行为,这种方法可以做为子类化的一种替代方法。 案例代表:Category 和 Delegation
  • 静态库和动态库的区别?
    • 静态库:.a(.h 配合) 和.framework(资源文件.bundle) 文件,编译好的二进制代码,使用时 link,编译时会拷贝一份到 target 程序中,容易增加 target 体积。
    • 动态库:.tbd 和.dylib,程序运行时动态加载到内存,可共享使用,动态载入有性能损失且有依赖性(系统直接提供给的 framework 都是动态库!)
  • iOS 中内省的几个方法?
    • 内省是指对象在运行时将其自身细节泄露为对象的能力。 这些细节包括对象在继承树中的位置,它是否符合特定的协议,以及它是否响应某个特定的消息。以及它是否响应某一消息。
    1. class 和 superclass 方法
    2. isKindOfClass: 和 isMemberOfClass:
    3. respondsToSelector:
    4. conformsToProtocol:
    5. isEqual:

OC 进阶

  • Foundation 和 CoreFoundation 的转换?

    • __bridge:负责传递指针,在 OC 和 CF 之间转换,不转移管理权,若把 OC 桥接给 CF,则 OC 释放后 CF 也无法使用。
    • __bridge_retained:将 OC 换成 CF 对象,并转移对象所有权,同时剥夺 ARC 的管理权,之后需要使用 CFRelease 释放。
    • __bridge_transfer:将 CF 转换成 OC,并转移对象所有权,由 ARC 接管,所以不需要使用 CFRelease 释放。
  • array 和 set 的区别?查找速度和遍历速度谁更快?

    • array:分配的是一片连续的存储单元,是有序的,查找时需要遍历整个数组查找,查找速度不如 Hash。
    • set:不是连续的存储单元,且数据是无序的,通过 Hash 映射存储的位置,直接对数据 hash 即可判断对应的位置是否存在,查找速度较快。
    • 遍历速度:array 的数据结构是一片连续的内存单元,读取速度较快,set 是不连续的非线性的,读取速度较慢;
  • 什么是内联函数?为什么需要它?

    • 用 inline 修饰的函数,不一定是内联函数,而内联函数一定是 inline 修饰的。
    • 普通函数:编译会生成 call 指令 (入栈、出栈),调用时将 call 指令地址入栈,并将子程序的起始地址送入,执行完毕后再返回原来函数执行的地址,所以会有一定的时间开销。
    • #define 宏定义和 inline 修饰的函数代码被放入符号表中,使用时直接替换,没有调用的开销;#define 宏定义的函数:使用时从符号表替换,有格式要求,不会对参数作有效性检查且返回值不能强转;inline 修饰的内联函数:不需要预编译,使用时直接替换且作类型检查,是一段直接在函数中展开的代码(宏是直接文本替换)
    • PS:inline 定义的内联函数代码被放入符号表中,在使用时直接替换 (把代码嵌入调用代码),不像普通函数需要在函数表中查找,省去压栈和出栈,没有了调用的开销,提高效率;
    • PS:inline 修饰的内联函数只是向编译器申请,最后不一定按照内联函数的方式执行,可能申请会失败,因为函数内不允许使用循环或开关语句,太大会被编译器还原成普通函数;inline 关键词一定要与函数定义一起使用,声明的话不算内联函数;
  • 图片显示的过程?

    1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
    2. 然后将生成的 UIImage 赋值给 UIImageView ;
    3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
    4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
      • 分配内存缓冲区用于管理文件 IO 和解压缩操作;
      • 将文件数据从磁盘读到内存中;
      • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
      • 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。(必须要了解位图相关的知识点)
  • dispatch_once 如何只保证只执行一次?

    • 多线程中,若有一个线程在访问其初始化操作,另外一个线程进来后会延迟空待,有内存屏障来保证程序指令的顺序执行
    void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
        volatile long *vval = val;
        if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {
            func(ctxt); // block真正执行
            dispatch_atomic_barrier();
            *val = ~0l;
        } 
        else 
        {
            do
            {
                 _dispatch_hardware_pause();
            } while (*vval != ~0l);
            dispatch_atomic_barrier();
        }
    }
    复制代码
  • NSThread、NSRunLoop 和 NSAutoreleasePool 三者之间的关系?

    • 根据官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。
    • 根据官方文档中对 NSAutoreleasePool 的描述,我们可知,在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。
    • 需要手动添加 autoreleasepool 的情况:
      1. 如果你编写的循环中创建了大量的临时对象;
      2. 如果你创建了一个辅助线程
  • 分类可扩展的区别?(可从内存布局、加载顺序、分类方法和原类方法的执行顺序来回答)

    • extension:必须在.m 中添加,编译时作为类的一部分参与内存布局,生命周期与类一样,添加私有实现,隐藏类的私有信息。
    • category:独立的.h 和.m 文件,独立编译动态加载 dyld,为已有类扩展功能。
    • 分类局限:无法添加实例变量,可以添加属性但不会为其生成实例变量(在运行时,对象的内存布局已确定,若添加实例变量就会破坏类的内存布局,这对编译型语言来说是灾难性的)
    • 分类的加载:把实例方法、属性和协议添加到类上,若有协议和类方法则添加到元类, 运行时分类的方法列表被动态添加到类的方法列表中,且在类的原有方法前面;
    • 方法顺序:【load 方法】先调用父类的 load 方法再调用分类的,【重写的方法】由于分类方法在原类方法的前面,所以优先调用分类中的方法。
  • OC 对象释放的流程?runloop的循环周期会检查引用计数,释放流程:release->dealloc->dispose

    • release:引用计数器减一,直到为 0 时开始释放
    • dealloc:对象销毁的入口
    • dispose:销毁对象和释放内存
      • objc_destructInstance:调用 C++ 的清理方法和移除关联引用
        • clearDeallocating:把 weak 置 nil,销毁当前对象的表结构,通过以下两个方法执行(二选一)
          • sidetable_clearDeallocating:清理有指针 isa 的对象
          • clearDeallocating_slow:清理非指针 isa 的对象
      • free:释放内存
  • CDDisplayLink 和 NSTimer 的区别?

    • CADisplayLink:以和屏幕刷新率相同的频率将内容画到屏幕上的定时器,需手动添加 runloop
    • NSTimer:可自动添加到当前线程的 runloop,默认 defualt 模式,也可以选择添加的 loopMode;
  • 用 runtime 实现方法交换有什么风险?

    • 风险 1:若不在 load 中交换是非原子性的,在 initial 方法中不安全
    • 风险 2:重写父类方法时,大部分都是需要调用 super 方法的,swizzling 交换方法,若不调用,可能会出现一些问题,如:命名冲突、改变参数、调用顺序、难以预测、难以调试。

runtime 源码相关

  • 知道 AutoreleasePoolPage 吗?它是怎么工作的?

    • AutoreleasePool 由 AuthReleasePoolPage 实现,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL,并且返回插入的 POOL_SENTINEL 的内存地址。这个地址也就是我们前面提到的 pool token,在执行 pop 操作的时候作为函数的入参。
    • 通过调用 autoreleaseFast 函数来执行具体的插入操作;autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
      1. 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
      2. 当前 page 存在且已满时,创建一个新的 page,并将对象添加到新创建的 page 中,然后关联 child page。
      3. 当前 page 不存在时,即还没有 page 时,创建第一个 page,并将对象添加到新创建的 page 中。
  • KVO 的底层实现?(看过 RAC 源码的应该知道,RAC 监听方法也是基于此原理,只是稍微有些不同)

    1. 当一个 object 有观察者时,动态创建这个 object 的类的子类
    2. 对每个被观察的 property,重写其 setter 方法
    3. 在重写的 setter 方法中调用 willChangeValueForKey: 和 didChangeValueForKey 通知观察者
    4. 当一个 property 没有观察者时,重写方法不会被删除,直到移除所有的观察者才会删除且删除动态创建的子类。
    // 1)监听前的准备
    [human addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { /* 监听后的处理 */ }
    // 2)关闭系统观察者的自动通知
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }
    // 3)调用这两个方法
    [self willChangeValueForKey:@"name"];
    /* 在此对值进行修改 */
    [self didChangeValueForKey:@"name”];
    复制代码
  • 被 weak 修饰的对象是如何被置 nil 的?知道 SideTable 吗?

    • Runtime 对注册的类会进行内存布局,有个 SideTable 结构体是负责管理类的引用计数表和 weak 表,weak 修饰的对象地址作为 key 存放到全局的 weak 引用表中,value 是所有指向这个 weak 指针的地址集合,调用 release 会导致引用计数器减一,当引用计数器为 0 时调用 dealloc,在执行 dealloc 时将所有指向该对象的 weak 指针的值设为 nil,避免悬空指针。
  • 什么是关联对象?可以用来干嘛?系统如何管理管理对象?支持 KVO 吗?

    • 关联对象:在运行时动态为指定对象关联一个有生命周期的变量(通过 objc_setAssociatedObject 和 objc_getAssociatedObject),用于实现 category 中属性保存数据的能力和传值。(支持 KVO,让关联的对象作为观察者)
    • 管理关联对象:系统通过管理一个全局哈希表,通过对象指针地址和传递的固定参数地址来获取关联对象。根据 setter 传入的参数策略,来管理对象的生命周期。通过一个全局的 hash 表管理对象的关联,通过对象指针地址获取对象关联表,再根据自定义 key 查找对应的值(外表:key(对象指针)-value(hash 表),内表:key(自定义 name)-value(管理的值))
  • isa、对象、类对象、元类和父类之间的关系?

    • 类:对象是类的一个实例,类也是另一个类的实例,这个类就是元类 (metaclass)。元类保存了类的类方法。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,一直找到继承链的根部,找不到就转发。
    • 元类:元类 (metaclass) 也是一个实例,那么元类的 isa 指针又指向哪里呢?为了设计上的完整,所有的元类的 isa 指针都会指向一个根元类 (root metaclass)。根元类 (root metaclass) 本身的 isa 指针指向自己,这样就行成了一个闭环。上面提到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的 isa 指针在实际上很少用到。不过这么设计保证了面向对象的干净,即所有事物都是对象,都有 isa 指针。
    • 继承:我们再来看看继承关系,由于类方法的定义是保存在元类 (metaclass) 中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以,为了保证父类的类方法可以在子类中可以被调用,所以子类的元类会继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。

  • 知道创建类的方法 objc_allocateClassPair?方法里面具体做了什么事情?

    // objc-class-old.mm
    Class objc_allocateClassPair(Class supercls, const char *name, 
                             size_t extraBytes)
    {
        Class cls, meta;
        if (objc_getClass(name)) return nil;
        // fixme reserve class name against simultaneous allocation
        if (supercls  &&  (supercls->info & CLS_CONSTRUCTING)) {
            // Can't make subclass of an in-construction class 
            return nil;
        }
        // Allocate new classes. 
        if (supercls) { //  若父类存在,父类的内存空间 + 额外的空间 = 新类的内存大小
            cls = _calloc_class(supercls->ISA()->alignedInstanceSize()+ extraBytes);
            meta = _calloc_class(supercls->ISA()->ISA()->alignedInstanceSize() + extraBytes);} else {        //  若父类不存在,基类的内存空间 + 额外的空间 = 新类的内存大小(objc_class 是 objc_object 的子类)
            cls = _calloc_class(sizeof(objc_class) + extraBytes);
            meta = _calloc_class(sizeof(objc_class) + extraBytes);}
        // 初始化
        objc_initializeClassPair(supercls, name, cls, meta);
        return cls;
    }
// objc-runtime-new.mm
Class objc_allocateClassPair(Class superclass, const char *name, 
                         size_t extraBytes)
{
    Class cls, meta;
    rwlock_writer_t lock(runtimeLock);
    // Fail if the class name is in use.
    // Fail if the superclass isn'</span>t kosher.
    <span class="hljs-keyword">if</span> (getClass(name)  ||  !verifySuperclass(superclass, <span class="hljs-literal">true</span>/*rootOK*/)) {
        <span class="hljs-built_in">return</span> nil;
    }
    // Allocate new classes.
    cls  = alloc_class_for_subclass(superclass, extraBytes);
    meta = alloc_class_for_subclass(superclass, extraBytes);
    // fixme mangle the name <span class="hljs-keyword">if</span> it looks swift-y?
    objc_initializeClassPair_internal(superclass, name, cls, meta);
    <span class="hljs-built_in">return</span> cls;
}
复制代码
  • class_ro_t 和 class_rw_t 的区别?
    • class_ro_t 是 readonly,class_rw_t 是 readwrite;
    • 在编译之后,class_ro_t 的 baseMethodList 就已经确定。当镜像加载的时候,methodizeClass 方法会将 baseMethodList 添加到 class_rw_t 的 methods 列表中,之后会遍历 category_list,并将 category 的方法也添加到 methods 列表中。这里的 category 指的是分类,基于此,category 能扩充一个类的方法。这是开发时经常需要使用到。
    • class_ro_t 在内存中是不可变的。在运行期间,动态给类添加方法,实质上是更新 class_rw_t 的 methods 列表。
  • 除了 objc_msgSend,还知不知道别的消息发送函数?
    • objc_msgSend_stret:当 CPU 的寄存器能够容纳下消息返回类型时,该函数才能处理此消息,若超过 CPU 寄存器则由另一个函数执行派发,原函数会通过分配在栈上的变量来处理消息所返回的结构体。
    • objc_msgSendSuper_stret:向父类实例发送一个返回结构体的消息
    • objc_msgSendSuper:向超类发送消息([super msg])
    • objc_msgSend_fpret:消息返回浮点数时,在某些结构的 CPU 中调用函数时需要对“浮点数寄存器”做特殊处理,不同的架构下浮点数表示范围不一样(寄存器是 CPU 的一部分,用于存储指令、数据和地址)
  • 什么是方法交换?怎么用的?
    • SEL 方法地址和 IMP 函数指针是通过 DispatchTable 表来映射,可以通过 runtime 动态修改 SEL,所以可以实现方法的交换(使用经验根据项目来谈即可)。

数据持久化

  • plist:XML 文件,读写都是整个覆盖,需读取整个文件,适用于较少的数据存储,一般用于存储 App 设置相关信息。
  • NSUserDefault:通过 UserDefault 对 plist 文件进行读写操作,作用和应用场景同上。
  • NSKeyedArchiver:被序列化的对象必须支持 NSCoding 协议,可以指定任意数据存储位置和文件名。整个文件复写,读写大数据性能低。
  • CoreData:是官方推出的大规模数据持久化的方案,它的基本逻辑类似于 SQL 数据库,每个表为 Entity,然后我们可以添加、读取、修改、删除对象实例。它可以像 SQL 一样提供模糊搜索、过滤搜索、表关联等各种复杂操作。尽管功能强大,它的缺点是学习曲线高,操作复杂。
  • SQLite(FMDB、Realm)

多线程

  • 串行队列和并发队列的区别?同步和异步的区别?

    • 串行队列(Serial Queue):指队列中同一时间只能执行一个任务,当前任务执行完后才能执行下一个任务,在串行队列中只有一个线程。
    • 并发队列(Concurrent Queue):允许多个任务在同一个时间同时进行,在并发队列中有多个线程。串行队列的任务一定是按开始的顺序结束,而并发队列的任务并不一定会按照开始的顺序而结束。
    • 同步(Sync):会把当前的任务加入到队列中,除非等到任务执行完成,线程才会返回继续运行,也就是说同步会阻塞线程。
    • 异步(Async):也会把当前的任务加入到队列中,但它会立刻返回,无需等任务执行完成,也就是说异步不会阻塞线程。
    • PS:无论是串行还是并发队列都可以执行执行同步或异步操作。注意在串行队列上执行同步操作容易造成死锁,在并发队列上则不用担心。异步操作无论是在串行队列还是并发队列上都可能出现线程安全的问题。
  • GCD 和 NSOperation 的区别?

    • NSOperation VS GCD
    1. 语法:面向对象(重量级) - 面向 C(轻量级)
    2. 相比 GCD 的优点:
      1. 取消某个操作(未启动的任务)、
      2. 指定操作间的依赖关系(根据需求指定依赖关系)
      3. 通过键值观察机制监控 NSOperation 对象的属性 (KVO)
      4. 指定操作的优先级
      5. 重用 NSOperation 对象
      6. 提供可选的完成 block

线程安全

  • 如何保证线程安全?

    • 原子操作(Atomic Operation):是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何的上下文切换(context switch)。
    • 内存屏障(Memory Barrier):确保内存操作以正确的顺序发生,不阻塞线程(锁的底层都会使用内存屏障,会减少编译器执行优化的次数,谨慎使用)。
      • 互斥锁(pthread_mutex):原理与信号量类似,但其并非使用忙等,而是阻塞线程和休眠,需切换上下文。
      • 递归锁(NSRecursiveLock):本质是封装了互斥锁的 PTHREAD_MUTEX_RECURSIVE 类型的锁,允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。
      • 自旋锁(OSSpinLock):通过全局变量,来判断当前锁是否可用,不可用就忙等。
      • @synchronized(self):系统提供的面向 OC 的 API,通过把对象 hash 当做锁来用。
      • NSLock:本质是封装了互斥锁的 PTHREAD_MUTEX_ERRORCHECK 类型的锁,它会损失一定性能换来错误提示,因为面向对象的设计,其性能稍慢。
      • 条件变量(NSConditionLock):底层通过 (condition variable)pthread_cond_t 来实现的,类似信号量具有阻塞线程与信号机制,当某个等待的数据就绪后唤醒线程,比如常见的生产者 - 消费者模式。
      • 信号量(dispatch_semaphore)
        // 传入值需>=0,若传0则阻塞线程并等待timeout
        dispatch_semaphore_create(1):
        // lock 资源已被锁,会使得signal值-1
        dispatch_semaphore_wait(signal, overTime):
        // unlock,会使得signal值+1
        dispatch_semaphore_signal(signal):
        复制代码
  • 什么是死锁?如何避免死锁?

    • 死锁:两个或两个以上的线程,互相等待彼此停止以获得某种资源,但是没有一方会提前退出的情况。
    • 避免在串行队列中执行同步任务;避免 Operation 相互依赖;
  • 什么是优先倒置?

    • 低优先级任务会先于高优先级任务执行,假设:任务优先级 A>B>C,A 等待访问被 C 正在使用的临界资源,同时 B 也要访问该资源,B 的优先级高于 C,同时,C 优先级任务被 B 次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,B 次高优先级任务获得执行权,而高优先级任务 A 只能被阻塞。 可以设置相同的任务优先级避免优先倒置。

项目经验相关题

  • 什么时候重构?怎么重构的?(包括但不限于以下几点,仅供参考)
    • 当前架构支撑不了业务、当前设计模式弊端太多、项目急着上线没有来得及优化等等。
    1. 优化业务逻辑;删除没用的代码(包括第三方库、变量、方法等,注释除外);
    2. 保持类和方法的单一性原则,把不属于本类功能的部门移植到一个新类中
    3. 根据实际使用的需求、优化基类中的属性(像 BaseObject 等)
    4. 修改一点就测试一点,保证每一步的重构不影响原有功能
    5. 规范(命名、语法等)
    6. 编译优化
    • PS: 重构是一个长期的过程,每一次紧迫迭代的新功能都可能需要优化,特别是在多人开发的时候,如果来不及在项目发布前 Code Review,在重构时 Code Review 就显得很有必要。
  • AppDelegate 如何瘦身?
    • 主要是在保证功能和效率以及性能的前提下,把第三方的初始化代码拆解到别的文件中处理
    1. category
      • 优点:简单、直接,不需要任何第三方来协助;
      • 缺点:添加属性不太方便,需要借助关联对象来实现;
    2. FRDModuleManager
      • 优点:简单、易维护,耦合性低;
      • 缺点:模块增多需分配不少内存,对象都是长期持有的(若有依赖关系,优先级不明确)
  • 如何解决卡顿?
    • 卡顿主要是因为主线程执行了一些耗时操作导致
    • 耗时计算:尽可能放到子线程中执行
    • 图片解压缩:在子线程强制解压缩
    • 离屏渲染:避免导致离屏渲染(注意绘制方法)
    • 优化业务流程:减少中间层,业务逻辑
    • 合理分配线程:UI 操作和数据源放到主线程,保证主线程尽可能处理少的非 UI 操作,同时控制 App 子线程数量。
    • 预加载和延时加载:平衡 CPU 和 GPU 使用率;优先加载可视内容,提升界面绘制速度。
  • 如何排查 Crash?
    • 断点:一般的 Crash
    • zombie object:僵尸对象
    • dSYM:iOS 编译后保存 16 进制函数地址映射信息的文件 (根据 Crash 的内存地址,定位对应的 Crash 代码的范围)
  • 如何检测内存泄漏?有没有遇到内存警告?怎么解决的?
    • dealloc 方法:手动检测的笨方法
    • 第三方库 PLeakSniffer 和 MLeaksFinder:自动检测
  • 有何优化 App 启动速度?(main 前和 main 后)
    • 设置环境变量:DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 为 1(更详细),Xcode-> Edit Scheme-> Run-> Arguments,添加以上二选一 环境变量
        main之前:
            Total pre-main time:  94.33 milliseconds (100.0%) // main函数前总共需要94.33ms
            dylib loading time:  61.87 milliseconds (65.5%)    // 动态库加载
            rebase/binding time:   3.09 milliseconds (3.2%)    // 指针重定位
            ObjC setup time:  10.78 milliseconds (11.4%)    // Objc类初始化
            initializer time:  18.50 milliseconds (19.6%)    // 各种初始化
            slowest intializers :    // 在各种初始化中,最耗时的如下:
                libSystem.B.dylib :   3.59 milliseconds (3.8%)
                libBacktraceRecording.dylib :   3.65 milliseconds (3.8%)
                GTFreeWifi :   7.09 milliseconds (7.5%)
    复制代码
    • main() 函数之前耗时的影响因素

      • 动态库加载越多,启动越慢。
      • ObjC 类越多,启动越慢
      • C 的 constructor 函数越多,启动越慢
      • C++ 静态对象越多,启动越慢
      • ObjC 的 +load 越多,启动越慢
    • main() 函数之后耗时的影响因素

      • 执行 applicationWillFinishLaunching 的耗时
      • rootViewController 及其 childViewController 的加载、view 及其 subviews 的加载
    • 具体优化内容:

      1. 移除不需要用到的动态库
      2. 移除不需要用到的类
      3. 合并功能类似的类和扩展(Category)
      4. 压缩资源图片
      5. 优化 applicationWillFinishLaunching
      6. 优化 rootViewController 加载
      7. 挖掘最后一点性能优化
    • PS:应该在 400ms 内完成 main() 函数之前的加载,整体过程耗时不能超过 20 秒,否则系统会 kill 掉进程,App 启动失败

开源库

这部分主要跟简历中提到的相关库有关,建议对简历中提到的开源库,一定要有所准备。

SDWebImage

SDWebImage 几乎是每个 iOS 开发者都用过的开源库,也是在简历中曝光度比较高的开源库之一,同时也几乎是面试都会问到的,所以要准备充分再去。

  • 从调用到显示的过程?
    1. 根据请求的文件 URL 路径判断当前是否存在还没结束的操作,若有的话就取消并移除与 URL 映射的操作。
    2. 设置 placeholder 占位图,不管 URL 是否为空都会设置,且在当前队列同步执行。
    3. 判断 URL 是否为空,为空则执行 completedBlock 回调,并结束。
    4. URL 非空,若是首次则开始相关类的初始化,如:加载进度及其相关的 Block、SDWebImageManager(SDWebImageCache 管理缓存、SDWebImageDownloader 管理),若非首次还是使用最初初始化的实例,因为 SDWebImageManager 是以单例的形式存在。
    5. 开始进入 SDWebImageManager 的范围,URL 容错处理、创建负责加载图片和取消加载图片的类,判断当前的 URL 是否在黑名单里,若存在则执行回调,返回当前的操作类,不再处理下载失败过的 URL,并结束。
    6. 若 URL 不在黑名单里,则开始在内存缓存中查找,找到后执行 doneBlock 回调,在该回调方法中执行判断当前操作是否已取消,未取消则回调并异步设置图片,在回调的 Block 中,每次都会检查当前操作是否已取消,若取消则不处理,并结束。
    7. 内存缓存没找到,则在磁盘缓存中查找,一般是在串行队列异步执行,根据 URL 路径找到存储的数据并取出,然后缩放解压缩返回,执行回调设置图片,并结束。
    8. 磁盘没找到则下载图片,下载完成后缓存,设置图片;SDWebImage 对图片作了哪些优化:子线程强制解压缩,从硬盘获取的图片和下载的图片都进行解压缩操作,提高渲染效率,节省主线程的工作量。

ReactiveCocoa

该库比较复杂,可问的问题也非常多,以下仅供参考,建议自己找答案

  • 冷热信号的区别?
  • RAC 如何监听方法?
  • bind 方法做了什么?
  • RAC 中的 RACObserver 和 KVO 有什么区别?
  • RAC 的 map 和 flattenMap 的区别?

工具

难免会遇到一些有关常用工具的问题,提供几个仅供参考

  • Git、SVN?
    • 问题可深可浅,浅:基本用法或相关命令,深:Git 的工作原理
  • CocoaPods
    • pod update 和 pod install 的区别
  • CI(持续集成、持续部署)
    • 后期更新

  • IOS

    iOS 是由蘋果公司為 iPhone 開發的操作系統。它主要是給 iPhone、iPod touch、iPad 以及 Apple TV 使用。就像其基於的 Mac OS X 操作系統一樣,它也是以 Darwin 為基礎的…

    189 引用 • 1 回帖
感谢    赞同    分享    收藏    关注    反对    举报    ...