iOS最新面试题Objective - C部分(含答案)

743 阅读1小时+

一、Objective - C基础

Objective-C 底层原理

  • OC对象

    • NSObject本质

      • NSObject底层是结构体,有一个 Class isa 指针
      • 创建一个NSObject对象系统分配16字节(至少16),只使用了8字节(用于存放isa),objc源码
    • 复杂对象本质

      • 包含父类成员结构+子类成员属性

        • 内存对齐,结构体的大小必须使最大成员变量的倍数
    • 属性和方法

      • 实例对象存放成员变量,不存放方法(存一份在类对象就够了)
    • 内存分配注意点

      • 内存分配方式内存对齐、桶、堆内存、16的倍数。目的是内存优化(gnu 内存对齐)<sizeof(a) 是个运算符,编译就确定了,不是函数>
    • 对象分类三类

      • instance 实例对象

        • isa指针、成员变量值
      • class 类对象

        • isa指针、superclass指针、类的属性信息(@property)、类的对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar)
      • meta-class 元类对象

        • object_getclass([NSObject class]);
        • 与class类对象结构体一样
        • isa指针、superclass指针、类的类方法信息(class method)
    • isa指向哪里?

      • instance的isa指向class、class的isa指向meta-class、meta-class的isa指向基类的meta-class
      • 调用对象方法轨迹:isa找到class,方法不存在,就通过superclass找父类的方法,再不存在就找基类..
    • superclass指向哪里?

      • class的superclass指向父类的class(如果没有父类,superclass指针为null)、meta-class的superclass指向父类的meta-class(基类的meta-class的superclass指向基类的class)
      • 调用类方法轨迹:isa找meta-class,方法不存在,就通过superclass找父类
    • OC的类信息存放在哪里?

      • 对象方法、属性、成员变量、协议信息,存放在class对象中
      • 类方法,存放在meta-class对象中
      • 成员变量的具体值存放在instance对象中
  • KVO

    • 本质是什么?

      • 利用Runtime动态生成一个子类,让instance对象的isa指向这个子类NSKVONotifying_XXX
      • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
      • _NSSetXXXValueAndNotify函数内部触发监听器的监听方法
    • 如何手动触发KVO?

      • 手动调用willChangeValueForKey: 和 didChangeValueForKey:
  • KVC

    • 1、KVC赋值会触发KVO吗?

      • 会!
    • 赋值原理

      • 赋值顺序:①-(void)setKey; ②-(void)_setKey; 查看accessInstanceVariablesDirectly> ③_key; ④_isKey; ⑤key; ⑥isKey
    • 取值原理

      • 取值顺序:① getKey、key、 isKey、_key方法 ②查看accessInstanceVariablesDirectly ③按照
        _key、_isKey、key、isKey顺序查找成员变量
  • Cateogry

    • 通过runtime动态将分类合并到类/元类对象中

    • 实现原理

      • 1、通过Runtime加载某个类所有的Category数据
      • 2、把所有Category的方法、属性、协议数据,合并到一个大数组中(后面参与编译的Category数据,会在数组的前面)
      • 3、将合并后的分类数据(方法、属性、协议),插入到类原来的数据前面
    • Category与Class Extension的区别?

      • Class Extension在编译时数据就包含在类信息中
      • Category是在运行时才会将数据合并到类信息中
    • +load方法

      • 在runtime加载类、分类时调用,在程序运行过程中只调用一次

      • 调用顺序

        • 1、先调用类的+load。(按照编译先后顺序调用,先编译先调用。调用子类的+load之前会先调用父类的+load)
        • 2、在调用分类的+load。(按照编译先后顺序调用,先编译先调用)
    • +initialize方法

      • +initialize方法会在类第一次接收到消息时调用

      • 调用顺序

        • 先调用父类的+initialize,再调用子类的+initialize(先初始化父类,再初始化子类,每个类只会初始化一次)
  • 关联对象

    • 原理

      • 1、关联对象并不是存储在关联对象本身内存中
      • 2、关联对象存储在全局统一的一个AssociationManage中
      • 3、设置关联对象为null,就是移除关联对象
  • block

    • 原理/本质

      • 本质是OC对象,内部也有个isa指针
      • 封装了函数调用以及函数调用环境的OC对象
    • block的类型(三种,内存存储区域不同)

      • NSGlobalBlock

        • 数据区域Data区(与全局变量一起)
      • NSStackBlock

        • 栈(需要手动销毁)
      • NSMallocBlock

        • 堆(自动销毁)
    • block的变量捕获(capture)

      • 为了保证block内部能够正常访问外部变量,有个变量捕获机制

        • 局部变量(捕获!)

          • auto类型(自动变量,离开作用域就销毁):值传递
          • static类型(还能访问内存):指针传递
        • 全局变量(不捕获!)

          • 直接访问
    • __block修饰符

      • 用于解决修改block内部无法修改auto变量值的问题(不能修饰全局变量、静态变量 static)

        • 编译器会把__block变量包装成一个对象
    • __block内存管理

      • 1、当block在栈上时,并不会对__block变量产生强引用

      • 2、当block被copy到堆时

        • 会调用block内部的copy函数
        • copy函数内部会调用_Block_object_assign函数
        • _Block_object_assign函数会对__block变量形成强引用(retain)
    • block循环引用问题?

  • Runtime

    • objc_msgSend执行流程三大阶段

      • 1、消息发送

        • 先找自身缓存,自身缓存找不到找父类方法缓存找,找不到在父类方法列表查找,以此类推,找到就缓存到自身缓存中。找不到进入阶段2 ↓
      • 2、动态方法解析

        • ①调用+resolvenInstanceMethod: 或者+resolvenClassMethod: 方法来动态解析 ,进行动态添加方法
        • ②标记为已经动态解析 YES
        • ③回到阶段1、消息发送,因为已经动态加了方法且已标记为YES(如果没有添加,进入阶段3)
      • 3、消息转发

        • ①调用(id)forwordingTargetForSelector:(SEL)aSelector 返回转发对象,(返回nil就走下一步)

        • ②- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回方法签名(返回nil就报错,不为空就→)

          • 不为空调用 -(void)forwardInvocation:(NSInvocation *)anInvocation
  • RunLoop

    • 三种模式

      • NSDefalultRunLoopMode

      • UITrackingRunLoopMode

      • NSRunLoopCommonMode

        • 并不是一个真的模式
    • 运行逻辑

      • 1、通知Observers:进入Loop;通知Observers:即将处理Timers;通知Observers: 即将处理Sources
      • 2、处理Block;处理Source0(可能再次处理Block)
      • 3、如果存在Source1,跳转到5
      • 4、通知Observers,开始休眠(等待消息唤醒)
      • 5、通知Observers,结束休眠(被某个消息唤醒)①处理Timer ②处理GCD ③处理Source1
      • 6、根据前面的执行结果决定如何操作
    • NSTimer失效问题

      • NSTimer在默认模式下,切换到NSRunLoopCommonMode
    • 线程保活

      • 1、添加Source; addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode]
      • 2、加while(weakSelf&&!weakSelf.isStopped) 循环, 执行currentRunLoop runMode:beforeDate:
    • 利用RunLoop监控卡顿

      • 怎么才算卡顿?

        • 1、进入睡眠前方法执行时间过长而导致无法进入睡眠
        • 2、线程唤醒后接收消息时间过长尔无法进入下一步
      • 如何监控卡顿?

        • 关注两个阶段(进入睡眠之前和唤醒之后的两个loop状态定义的值)

          • 1、kCFRunLoopBeforeSources

            • 触发Source0回调
          • 2、kCFRunLoopAfterWaiting

            • 接收mach_port消息
      • 如何监听

        • 1、创建一个CFRunLoopObserverContext观察者
        • 2、将观察者添加到主线程RunLoop的Common模式下观察。
        • 3、再创建一个持续的子线程专门用来监控主线程的RunLoop状态
        • 4、一旦发现进入睡眠前的kCFRunLoopBeforeSource状态或唤醒后的kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判断为卡顿,再dump出堆栈信息
  • 多线程

    • pthread

    • NSThread

    • GCD

      • 同步 sync

        • 没有开启线程,串行执行任务
      • 异步 async

        • 并且是并发队列,才会开启新线程并发执行任务
      • Semaphore信号量

        • 控制最大并发数量,也可用来线程同步(把信号量设 1 )
    • 死锁

      • ①使用sync函数
      • ②往当前串行队列中添加任务
    • 队列组

    • NSOperation

      • 封装GCD
    • iOS线程同步方案(加锁、GCD串行队列)

      • OSSpinLock 自旋锁

        • (不安全了不建议使用,优先反转问题)
      • os_unfair_lock

        • OSSpinLock的替代方案
      • pthread_mutex

        • pthread_mutex_signal 激活一个等待该条件的线程
      • dispatch_semphore

        • 信号量
      • dispatch_queue(DISPATCH_QUEUE_SERIAL)

        • GCD串行队列
      • pthread_mutex 的OC封装

        • NSLock

        • NSRecursiveLock

          • 递归锁,保证能够递归调用
        • NSCondition

        • NSConditionLock

          • 带条件的lock(生产者消费者模式)
      • @synchronized

        • 性能最差
    • 读写安全方案

      • 1、多读单写(异步读,同步写),用于文件数据读写操作

        • pthread_rwlock 读写锁

          • 互斥锁,等待锁的过程会进入休眠
        • dispatch_barrier_async 异步栅栏调用

          • 传入的并发队列必须是dispatch_queue_create创建的(不是就没效果)
          • 如果传入的是一个串行或全局并发队列,那这个函数等同于dispatch_async效果
  • 内存管理

    • 定时器

      • GCD是最准确的,与RunLoop无关
    • 内存布局

      • 保留内存

      • 代码段(__TEXT)

        • 编译后的代码
      • 数据段(__DATA)

        • 字符串常量
        • 已初始化数据
        • 未初始化数据
      • 堆(heap)

        • 通过alloc、malloc、calloc等动态分配的空间
      • 栈(stack)

        • 函数调用开销,局部变量的开销
      • 内核区

    • Tagged Pointer

    • MRC

    • copy

    • weak 原理

      • 将弱引用存入到哈希表内,当对象销毁时就从表中取出弱引用并清除 (运行时操作)
    • ARC 原理

      • 利用LLVM+Runtime,LLVM自动生成插入retain,release代码
    • autoRelease

UI层基本原理

  • 事件传递机制

    • 当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程
    • 事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件
    • 拦截事件 hitTest:withEvent:
  • UI绘制原理

    • 在layer内部会创建一个backing store,我们可以理解为CGContextRef上下文。

    • 判断layer是否有delegate:

      • 如果有delegate,则会执行[layer.delegate drawLayer:inContext](这个方法的执行是在系统内部执行的),然后在这个方法中会调用view的drawRect:方法,也就是我们重写view的drawRect:方法才会被调用到。
      • 如果没有delegate,会调用layer的drawInContext方法,也就是我们可以重写的layer的该方法,此刻会被调用到。
    • 最后都由CALayer把绘制完的backing store(可以理解为位图)提交给GPU。

  • 异步绘制原理

    • [UIView setNeedsDisplay]方法的时候,不会立马发送对应视图的绘制工作

      • 调用[UIView setNeedsDisplay]后
      • 然后会调用系统的同名方法[view.layer setNeedsDisplay]方法并在当前view上面打上一个脏标记
      • 当前Runloop将要结束的时候才会调用[CALyer display]方法,然后进入到视图真正的绘制工作当中
    • 是否知道异步绘制?如何进行异步绘制?

      • 基于系统开的口子[layer.delegate dispayLayer:]方法
      • 并且实现/遵从了dispayLayer这个方法,我们就可以进行异步绘制
  • 离屏渲染

    • 触发离屏渲染的场景

      • 采用了光栅化的 layer (layer.shouldRasterize)
      • 使用了 mask 的 layer (layer.mask)
      • 需要进行裁剪的 layer
      • 设置了组透明度为 YES,并且透明度不为 1 的layer
      • 高斯模糊
      • 添加了投影的 layer (layer.shadow*)
      • 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等
    • 使用Instruments的不同工具来测试性能

Swift

性能优化

CPU与GPU

  • CPU

    • 对象的创建销毁、对象属性的调整、布局计算、文本计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
  • GPU

    • 纹理的渲染、

启动优化

  • 启动速度监控

    • 1、定时抓取主线程上方法调用堆栈,计算一段时间里各个方法的耗时(Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式)

    • 2、对objc_msgSend方法进行hook

      • 原理解释
      • 如何使用
  • 方案与实践

    • iOS冷启动阶段思路

      • System Interface

        加载主二进制、启动dyld、加载动态库以及libSystem初始化

        • 避免链接不使用的框架
        • 减少动态库的加载
        • 减少OC类,分类等
      • runtime Init

        执行 +load和staic initializer初始化函数等

        • 避免在+load里操作
        • 延迟加载+load
        • 减少staic initializer(C++ 静态全局变量)
      • UIKit init

      • Application init

        实例化UI Application 和 UI Application Delegate、处理生命周期回调、首帧渲染直到首页渲染完成

        • 减少或延迟各种SDK的初始化
      • initial Frame Render

        • 减少视图层级和视图数量
        • 懒加载View
        • 变AutoLayout为手动frame布局等
      • Extended

        • 去掉viewDidLoad和viewWillAppear中不必要的逻辑,少做事不做事
    • 方案

      • 低成本高收益方案

        • 生命周期延迟
        • +load治理
        • 动态库下线
        • 二进制重排
        • 首页预加载
      • 深入优化方案

        • 动态库懒加载
        • staic initlializer治理
        • 编译期写入I/O
        • 任务编排
  • 流程规范与监控

    • 规范

      • 1、新增或修改任务要有足够的理由,必须经过严格的code review
      • 2、首页渲染完成前不允许监听生命周期
      • 3、不允许新增+load耗时方法
      • 4、不允许新增C++ initialize
      • 5、新增动态库必须经过评估
      • 6、任务项相对上个版本有5ms以上的增长时,必须进行修改
    • 监控

卡顿排查与解决

  • UI界面卡顿优化(滑动掉帧等)

    • 尽量减少CPU GPU资源消耗
  • 崩溃类型的卡顿排查(线程卡顿)

耗电排查与解决

  • 1、如何获取电量

    • (1)引入 IOPowerSources.h、IOPSKeys.h 和 IOKit
    • (2)把 batteryMonitoringEnabled 置为 true
  • 2、如何诊断电量问题

    • (1)通过 task_threads 函数,获取所有的线程信息数组 threads以及线程总数 threadCount
    • (2)thread_basic_info 里有一个记录 CPU 使用百分比的字段 cpu_usage
    • (3)遍历所有线程,去查看是哪个线程的 CPU 使用百分比过高。(某个线程的 CPU 使用率长时间都比较高,可能有问题)
  • 3、如何优化电量

    • (1)避免让 CPU 做多余的事情。对于大量数据的复杂计算,应该把数据传到服务器去处理

      必须要在 App 内处理复杂数据计算,可以通过 GCD 的 dispatch_block_create_with_qos_class 方法指定队列的 Qos 为 QOS_CLASS_UTILITY,将计算工作放到这个队列的 block 里。在 QOS_CLASS_UTILITY 这种 Qos 模式下,系统针对大量数据的计算,以及复杂数据处理专门做了电量优化。

    • (2)I/O 操作也是耗电大户,优化I/O操作

      业内的普遍做法是,将碎片化的数据磁盘存储操作延后,先在内存中聚合,然后再进行磁盘存储。碎片化的数据进行聚合,在内存中进行存储的机制,可以使用系统自带的 NSCache 来完成。

      NSCache 是线程安全的,NSCache 会在到达预设缓存空间值时清理缓存,这时会触发 cache:willEvictObject: 方法的回调,在这个回调里就可以对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 操作的次数减少了,对电量的消耗也就减少了

    • (3)苹果维护了一个电量优化指南“Energy Efficiency Guide for iOS Apps”

包瘦身

  • 官方 App Thinning

  • 图片资源优化

    • 无用图片移除,图片压缩

      • LSUnusedResources
    • TinyPng或者ImageOptim、转webp

  • 代码瘦身

    • 删除无用功能代码(A/B测试结果删除)

    • 源代码瘦身

      • LinkMap 结合 Mach-O 找无用代码

        • AppCode(人工二次确认)
      • 运行时检查类是否真正被使用过

        • 通过isInitialized,判断一个类是否初始化过
    • 重复代码删除(Clang插件)

  • 可执行文件瘦身

    • 编译器优化

架构设计

组件化

  • 协议式

    • 协议式架构设计主要采用的是协议式编程的思路

      • 在编译层面使用协议定义规范,实现可在不同地方,从而达到分布管理和维护组件的目的
    • 缺陷

      • 协议式编程缺少统一调度层,导致难于集中管理
      • 协议式编程接口定义模式过于规范,从而使得架构的灵活性不够高。当需要引入一个新的设计模式来开发时,我们就会发现很难融入到当前架构中,缺乏架构的统一性。
  • 中间者

    • 优势

      • 拆分的组件都会依赖于中间者,但是组间之间就不存在相互依赖的关系
      • 其他组件都会依赖于这个中间者,相互间的通信都会通过中间者统一调度
      • 在中间者上也能够轻松添加新的设计模式,从而使得架构更容易扩展
      • 中间者架构的易管控带来的架构更稳固,易扩展带来的灵活性
    • 实现方案

      • CTMediator

        • CTMediator 本质就是一个方法,用来接收 target、action、params,对于调用者来说十分不友好

          • 通过响应者给 CTMediator 做的 category 或者 extension 发起调用
          • category 或 extension 以函数声明的方式,解决了参数的问题
          • 不会直接依赖 CTMediator 去发起调用,而是直接依赖 category Pod 去发起调用
        • 解耦的精髓在于业务逻辑能够独立出来,并不是形式上的解除编译上的耦合(编译上解除耦合只能算是解耦的一种手段而已)。更多的还是需要在功能逻辑和组件划分上做到同层级解耦,上下层依赖清晰

      • URLRoutor

        • 缺陷

          • 本地间调用无法传递非常规参数,复杂参数的传递方式非常丑陋

          • 必须要在app启动时注册URL响应者

          • 新增组件化的调用路径时,蘑菇街的操作相对复杂

Swift

性能优化

CPU与GPU

  • CPU

    • 对象的创建销毁、对象属性的调整、布局计算、文本计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
  • GPU

    • 纹理的渲染、

启动优化

  • 启动速度监控

    • 1、定时抓取主线程上方法调用堆栈,计算一段时间里各个方法的耗时(Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式)

    • 2、对objc_msgSend方法进行hook

      • 原理解释
      • 如何使用
  • 方案与实践

    • iOS冷启动阶段思路

      • System Interface

        加载主二进制、启动dyld、加载动态库以及libSystem初始化

        • 避免链接不使用的框架
        • 减少动态库的加载
        • 减少OC类,分类等
      • runtime Init

        执行 +load和staic initializer初始化函数等

        • 避免在+load里操作
        • 延迟加载+load
        • 减少staic initializer(C++ 静态全局变量)
      • UIKit init

      • Application init

        实例化UI Application 和 UI Application Delegate、处理生命周期回调、首帧渲染直到首页渲染完成

        • 减少或延迟各种SDK的初始化
      • initial Frame Render

        • 减少视图层级和视图数量
        • 懒加载View
        • 变AutoLayout为手动frame布局等
      • Extended

        • 去掉viewDidLoad和viewWillAppear中不必要的逻辑,少做事不做事
    • 方案

      • 低成本高收益方案

        • 生命周期延迟
        • +load治理
        • 动态库下线
        • 二进制重排
        • 首页预加载
      • 深入优化方案

        • 动态库懒加载
        • staic initlializer治理
        • 编译期写入I/O
        • 任务编排
  • 流程规范与监控

    • 规范

      • 1、新增或修改任务要有足够的理由,必须经过严格的code review
      • 2、首页渲染完成前不允许监听生命周期
      • 3、不允许新增+load耗时方法
      • 4、不允许新增C++ initialize
      • 5、新增动态库必须经过评估
      • 6、任务项相对上个版本有5ms以上的增长时,必须进行修改
    • 监控

卡顿排查与解决

  • UI界面卡顿优化(滑动掉帧等)

    • 尽量减少CPU GPU资源消耗
  • 崩溃类型的卡顿排查(线程卡顿)

耗电排查与解决

  • 1、如何获取电量

    • (1)引入 IOPowerSources.h、IOPSKeys.h 和 IOKit
    • (2)把 batteryMonitoringEnabled 置为 true
  • 2、如何诊断电量问题

    • (1)通过 task_threads 函数,获取所有的线程信息数组 threads以及线程总数 threadCount
    • (2)thread_basic_info 里有一个记录 CPU 使用百分比的字段 cpu_usage
    • (3)遍历所有线程,去查看是哪个线程的 CPU 使用百分比过高。(某个线程的 CPU 使用率长时间都比较高,可能有问题)
  • 3、如何优化电量

    • (1)避免让 CPU 做多余的事情。对于大量数据的复杂计算,应该把数据传到服务器去处理

      必须要在 App 内处理复杂数据计算,可以通过 GCD 的 dispatch_block_create_with_qos_class 方法指定队列的 Qos 为 QOS_CLASS_UTILITY,将计算工作放到这个队列的 block 里。在 QOS_CLASS_UTILITY 这种 Qos 模式下,系统针对大量数据的计算,以及复杂数据处理专门做了电量优化。

    • (2)I/O 操作也是耗电大户,优化I/O操作

      业内的普遍做法是,将碎片化的数据磁盘存储操作延后,先在内存中聚合,然后再进行磁盘存储。碎片化的数据进行聚合,在内存中进行存储的机制,可以使用系统自带的 NSCache 来完成。

      NSCache 是线程安全的,NSCache 会在到达预设缓存空间值时清理缓存,这时会触发 cache:willEvictObject: 方法的回调,在这个回调里就可以对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 操作的次数减少了,对电量的消耗也就减少了

    • (3)苹果维护了一个电量优化指南“Energy Efficiency Guide for iOS Apps”

包瘦身

  • 官方 App Thinning

  • 图片资源优化

    • 无用图片移除,图片压缩

      • LSUnusedResources
    • TinyPng或者ImageOptim、转webp

  • 代码瘦身

    • 删除无用功能代码(A/B测试结果删除)

    • 源代码瘦身

      • LinkMap 结合 Mach-O 找无用代码

        • AppCode(人工二次确认)
      • 运行时检查类是否真正被使用过

        • 通过isInitialized,判断一个类是否初始化过
    • 重复代码删除(Clang插件)

  • 可执行文件瘦身

    • 编译器优化

架构设计

组件化

  • 协议式

    • 协议式架构设计主要采用的是协议式编程的思路

      • 在编译层面使用协议定义规范,实现可在不同地方,从而达到分布管理和维护组件的目的
    • 缺陷

      • 协议式编程缺少统一调度层,导致难于集中管理
      • 协议式编程接口定义模式过于规范,从而使得架构的灵活性不够高。当需要引入一个新的设计模式来开发时,我们就会发现很难融入到当前架构中,缺乏架构的统一性。
  • 中间者

    • 优势

      • 拆分的组件都会依赖于中间者,但是组间之间就不存在相互依赖的关系
      • 其他组件都会依赖于这个中间者,相互间的通信都会通过中间者统一调度
      • 在中间者上也能够轻松添加新的设计模式,从而使得架构更容易扩展
      • 中间者架构的易管控带来的架构更稳固,易扩展带来的灵活性
    • 实现方案

      • CTMediator

        • CTMediator 本质就是一个方法,用来接收 target、action、params,对于调用者来说十分不友好

          • 通过响应者给 CTMediator 做的 category 或者 extension 发起调用
          • category 或 extension 以函数声明的方式,解决了参数的问题
          • 不会直接依赖 CTMediator 去发起调用,而是直接依赖 category Pod 去发起调用
        • 解耦的精髓在于业务逻辑能够独立出来,并不是形式上的解除编译上的耦合(编译上解除耦合只能算是解耦的一种手段而已)。更多的还是需要在功能逻辑和组件划分上做到同层级解耦,上下层依赖清晰

      • URLRoutor

        • 缺陷

          • 本地间调用无法传递非常规参数,复杂参数的传递方式非常丑陋
          • 必须要在app启动时注册URL响应者
          • 新增组件化的调用路径时,蘑菇街的操作相对复杂

MVC

MVVM

  • 双向绑定(RAC,RSSwift)

设计模式

系统化思维

五大设计原则

  • 单一功能原则:对象功能要单一,不要在一个对象里添加很多功能
  • 开闭原则:扩展是开放的,修改是封闭的
  • 里氏替换原则:子类对象是可以替代基类对象的
  • 接口隔离原则:接口的用途要单一,不要在一个接口上根据不同入参实现多个功能
  • 依赖反转原则:方法应该依赖抽象,不要依赖实例。iOS 开发就是高层业务方法依赖于协议

23种设计模式实现原理

  • 子主题 1

网络协议相关

IP层

TCP/UDP

  • TCP

    • TCP是一个传输层协议,提供端到端(Host-To-Host) 数据的可靠传输
    • 支持全双工,是一个连接导向的协议(面向连接的)
  • UDP

    • 目标是在传输层提供直接发送报文(Datagram)的能力

HTTP/HTTPS

  • 为什么可以相信一个 HTTPS 网站?

    • 当用户用浏览器打开一个 HTTPS 网站时,会到目标网站下载目标网站的证书
    • 浏览器会去验证证书上的签名,一直验证到根证书,如果根证书被预装,那么就会信任这个网站

DNS

Socket

  • Socket 是一种编程的模型

    • 客户端将数据发送给在客户端侧的Socket 对象,然后客户端侧的 Socket 对象将数据发送给服务端侧的 Socket 对象
    • Socket 对象负责提供通信能力,并处理底层的 TCP 连接/UDP 连接
    • 对服务端而言,每一个客户端接入,就会形成一个和客户端对应的 Socket 对象,如果服务器要读取客户端发送的信息,或者向客户端发送信息,就需要通过这个客户端 Socket 对象
  • Socket 还是一种双向管道文件

    • 操作系统将客户端传来的数据写入这个管道,也将线程写入管道的数据发送到客户端

算法

数组&链表

堆栈&队列

  • 面试题:【判断字符串括号是否合法】

  • 单调栈

    • 递增栈

      • 小数消除大数
    • 递减栈

      • 大数消除小数
  • 优先队列

    • 正常进,安装优先级出
    • 实现机制:1、Heap(堆)(Binary、Binomial、Fiboncci)

哈希表

  • Map/Set

    • 【有效的字母异位词】
    • 【两数之和】
    • 【三数之和】

  • 二叉树

    • 反转二叉树

      • 遍历二叉树
  • 二叉搜索树

  • 字典树

递归&分治

动态规划

  • 贪心算法
  • 买卖股票
  • 背包问题

LRU Cache

Bloom Filter(布隆过滤器)

斐波那契数列

Flutter

底层基本实现

Bloc与响应式

容器化&配置化

组件化

性能优化与实践

安全与密码学

单向散列函数(哈希函数)

  • SHA-1、MD5(已不安全)
  • SHA-256、SHA-384、SHA-512(目前流行)

加密算法

  • 对称加密

    • 序列算法(优先使用)

      • ChaCha20、AES-256、AES-128
    • 分组算法

  • 非对称加密

亮点与疑难解决

动态化

容器化

配置化

持续集成

fastlane

RunTime无埋点方案

产品主要想知道:页面进入次数、页面停留时间、点击事件的埋点(用来计算曝光率、转化率)

运行时方法替换方式进行埋点(AOP)

  • 写一个运行时方法替换的类 SMHook
  • 利用运行时接口将方法的实现进行了交换,原方法调用时就会被 hook 住,从而去执行指定的方法
  • 每个 UIViewController 生命周期到了 ViewWillAppear 时都会去执行 insertToViewWillAppear 方法

事件唯一标识区分不同埋点

  • NSStringFromClass([self class]) 方法来取类名,区别不同的 UIViewController
  • action 选择器名 NSStringFromSelector(action)” +“视图类名 NSStringFromClass([target class])”组合成一个唯一的标识
  • 通过视图的 superview 和 subviews 的属性,我们就能够还原出每个页面的视图树
  • 复用机制 UITableViewCell 用 indexPath

RunLoop运行步骤

1、通知 observers:RunLoop 要开始进入 loop 了。紧接着就进入 loop

2、开启一个 do while 来保活线程。通知 Observers:RunLoop 会触发 Timer 回调、Source0 回调,接着执行加入的 block

通知 Observers:RunLoop 的线程将进入休眠(sleep)状态

4、进入休眠后,会等待 mach_port 的消息,以再次唤醒

5、唤醒时通知 Observer:RunLoop 的线程刚刚被唤醒了

6、RunLoop 被唤醒后就要开始处理消息了

启动优化方案

main() 函数执行前

  • 加载可执行文件(App 的.o 文件的集合)
  • 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定
  • Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等
  • 初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量

main() 函数执行后(appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成)

  • 首屏初始化所需配置文件的读写操作
  • 首屏列表大数据的读取
  • 首屏渲染的大量计算等

首屏渲染完成后(加载时长)

  • 减少视图层级和视图数量
  • 懒加载View
  • 变AutoLayout为手动frame布局等
  • 预加载(缓存首页,骨架屏等)
  • 去掉viewDidLoad和viewWillAppear中不必要的逻辑,少做事不做事

监控崩溃与采集

崩溃类型

  • 信号可捕获到

    • KVO、数组越界、返回类型不匹配NULL

    • 多线程问题

      • 在子线程中进行 UI 更新可能会发生崩溃。多个线程进行数据的读取操作,因为处理时机不一致,比如有一个线程在置空数据的同时另一个线程在读取这个数据,可能会出现崩溃情况
    • 野指针

      • 指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃
  • 信号不可捕获

    • 后台任务超时

      • iOS 后台保活

        • Background Task (3 分钟)

          • 系统提供了 beginBackgroundTaskWithExpirationHandler 方法来延长后台执行时间,可以解决你退后台后还需要一些时间去处理一些任务的诉求
    • 主线程卡顿超阈值

      • 主线程无响应

        • 如果主线程超过系统规定的时间无响应,就会被 Watchdog 杀掉
    • 内存打爆

      • JetSam 机制

        • 操作系统为了控制内存资源过度使用而采用的一种资源管控机制
      • 通过内存警告获取内存限制值

        • didReceiveMemoryWarning

          • 强杀掉 App 之前还有 6 秒钟的时间
      • 定位内存问题信息收集

        • 谁分配的内存?定位到函数

          • 用 fishhook 去 Hook “malloc_logger”函数,分析统计

监控方案

  • 第三方的

    • Fabric或Bugly
  • 第三方开源自建服务器

    • PLCrashReporter

      • 第三方开源库捕获崩溃日志,然后上传到自己服务器上进行整体监控的

A/B测试方案(SkyLab)

三个部分

  • 策略服务,为策略制定者提供策略

    • 决策流程、策略维度
  • A/B 测试 SDK,集成在客户端内

    • 客户端SDK:SkyLab

      • 使用的是 MMKV 保存策略

      • SkyLab 对外的调用接口使用的是 Block ,来接收版本 A 和 B 的区别处理。

      • 如何做人群测试桶划分

        • 随机分配方式,将分配结果通过 MMKV 进行持续化存储,确保测试桶的一致性
  • 日志系统,负责反馈策略结果供分析人员分析不同策略执行的结果

服务端返回A/B实验

性能监控

线下性能

  • Energy Log 就是用来监控耗电量的
  • Leaks 就是专门用来监控内存泄露问题的
  • Network 就是用来专门检查网络情况
  • Time Profiler 就是通过时间采样来分析页面卡顿问题

线上监控(不要侵入到业务代码、采用性能消耗最小的监控方案)

  • CPU 使用率的线上监控(App 作为进程运行起来后会有多个线程,每个线程对 CPU 的使用率不同。各个线程对 CPU 使用率的总和,就是当前 App 对 CPU 的使用率)

    • thread_info.h 根据当前 task 获取所有线程
    • 遍历所有线程来获取单个线程的基本信息
    • thread_basic_info 结构体获取CPU 使用率的字段:cpu_usage
    • 累加这个字段就能够获取到当前的整体 CPU 使用率
  • FPS 线上监控

    • 通过注册 CADisplayLink 得到屏幕的同步刷新率
    • 记录每次刷新时间,然后就可以得到 FPS
  • 内存使用量的线上监控

    • 内存信息存在 task_vm_info
    • 类似于对 CPU 使用率的监控,我们只要从这个结构体里取出 phys_footprint 字段

启动优化监控方案

定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时(Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式)

对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时

使用RunLoop监控卡顿

原因

  • 复杂 UI 、图文混排的绘制量过大

  • 在主线程上做网络同步请求

  • 在主线程做大量的 IO 操作

  • 运算量过大,CPU 持续高占用

  • 死锁和主子线程抢锁

1.copy和strong修饰符的区别?为何NSString常用copy? 答案:strong增加引用计数,copy创建不可变副本。NSString用copy防止外部可变字符串(如NSMutableString)修改导致值变化,保障数据不可变性。

2.GCD与NSOperation的适用场景?如何实现任务依赖和优先级控制? 答案:GCD适合简单任务(如异步加载),NSOperation适合复杂任务(支持取消、依赖、优先级)。通过addDependency设置依赖,queuePriority控制优先级。

3.Runtime的消息转发机制分哪几步?如何动态添加方法? 答案:消息转发分为: 动态方法解析(resolveInstanceMethod:); 备用接收者(forwardingTargetForSelector:); 完整转发(methodSignatureForSelector:和forwardInvocation:)。 动态添加方法使用class_addMethod。

4.如何优化UITableView滚动性能? 答案: 复用Cell(dequeueReusableCellWithIdentifier:); 异步解码图片、离屏渲染(如使用CATiledLayer); 减少AutoLayout计算,预计算Cell高度; 使用willDisplayCell延迟加载非关键资源。

5.Block的循环引用如何产生?如何用__weak和__block解决? 答案:Block捕获外部对象强引用导致循环。解决: 使用__weak弱引用(如__weak typeof(self) weakSelf = self); __block修饰变量并在Block内手动置nil(需调用Block)。

  1. Swift与Objective-C的Runtime消息转发机制对比 问题:Objective-C的消息转发分为resolveInstanceMethod、forwardingTargetForSelector、methodSignatureForSelector三阶段,而Swift中@dynamic修饰的方法如何实现类似机制?请结合Swift的Method Dispatch机制分析。 深度解析:

1.Objective-C的消息转发流程:

动态方法解析:resolveInstanceMethod允许运行时添加方法实现。

快速转发:forwardingTargetForSelector将消息转发给其他对象处理。

完整转发:methodSignatureForSelector和forwardInvocation构造NSInvocation对象。

2.Swift的动态性限制:

Swift默认使用静态派发(值类型)或虚表派发(类),除非:

使用@dynamic修饰符(如CoreData的NSManagedObject子类)。

继承自NSObject并标记@objc dynamic。

3.Swift中的消息转发实现:

class MyClass: NSObject {  
  @objc dynamic func handleMessage() { /* 默认实现 */ }  
}  
 
let obj = MyClass()  
// 替换方法实现  
let originalMethod = class_getInstanceMethod(MyClass.self, #selector(handleMessage))  
let newMethod = class_getInstanceMethod(MyClass.self, #selector(newHandleMessage))  
method_exchangeImplementations(originalMethod, newMethod)  

面试延伸:

Swift如何实现KVO?答:通过@objc dynamic强制使用Objective-C Runtime。

Swift的协议扩展(Protocol Extension)是否支持动态派发?答:协议扩展方法静态派发,除非在协议中声明为@objc。

7.Swift Concurrency(async/await)与GCD的底层线程管理对比 问题:Swift的async/await语法在底层如何管理线程?与GCD的Work Item和队列优先级有何本质区别? 深度解析:

1.GCD的线程管理:

基于线程池和队列(Serial/Concurrent),通过优先级(QoS)调度任务。

问题:优先级反转(Priority Inversion)、线程爆炸(Thread Explosion)。

2.Swift Concurrency模型:

协作式线程池:由系统管理的线程池(默认线程数=CPU核心数),任务通过挂起(Suspend)而非阻塞(Block)让出线程。

结构化并发:通过TaskGroup和async let绑定子任务生命周期。

优先级继承:子任务自动继承父任务优先级,避免优先级反转。

3.性能对比:

// GCD  
DispatchQueue.global(qos: .userInitiated).async {  
  let data = fetchData()  
  DispatchQueue.main.async { updateUI(data) }  
} 

// Swift Concurrency  
Task(priority: .userInitiated) {  
  let data = await fetchData()  
  await MainActor.run { updateUI(data) }  
} 

优势:更少线程切换、自动取消传播、无回调地狱。

面试延伸:

MainActor与DispatchQueue.main的区别?答:MainActor是全局串行执行器,整合了Swift Concurrency的调度机制。

如何避免Task的循环引用?答:使用[weak self]捕获列表或Task.detached。

  1. Swift Actor模型与GCD线程调度的性能对比及死锁预防 问题:Swift的Actor如何通过隔离状态保证线程安全?对比GCD的串行队列,分析两者在10k任务并发下的性能差异。 深度解析:

Actor机制:每个Actor独立串行队列,通过await挂起非阻塞任务。

性能对比:

  • 指标 Swift Actor GCD串行队列
  • 上下文切换开销 低(协作式) 高(线程切换)
  • 死锁风险 低(编译器静态检查) 高(需手动避免)
  • 内存占用 更优(少量Continuation) 线程栈积累 死锁案例:

actor A { func foo() async { await bar() } func bar() async { ... } }
// 无死锁,编译器强制异步调用
9.Objective-C与Swift混编下的内存管理陷阱 问题:在Swift中调用Objective-C的retain/release方法会导致什么问题?如何通过Unmanaged正确转换Core Foundation对象? 深度解析:

陷阱场景:Swift的ARC自动插入retain/release,手动调用会导致重复释放。

安全转换:

let cfStr = CFStringCreateWithCString(nil, "test", kCFStringEncodingUTF8)  
let swiftStr = Unmanaged<CFString>.fromOpaque(cfStr).takeRetainedValue() 

调试工具:Xcode的Memory Graph可检测野指针和循环引用. 10.Metal API在实时滤镜渲染中的优化策略 问题:如何通过Metal的MTLHeap和MTLFence实现纹理内存的高效复用?对比Core Image的性能瓶颈。 深度解析:

MTLHeap优化:预分配GPU内存池,避免频繁申请释放。

MTLFence应用:同步GPU指令队列,防止资源竞争。

对比Core Image:

Metal:延迟<5ms,支持自定义Shader。

Core Image:延迟15-30ms,但API更简洁。

实战建议:

    混合使用Core Image预处理和Metal后处理。

11.LLVM编译优化对Swift协议扩展方法派发的影响 问题:分析Swift协议扩展方法在O2优化下的静态派发机制,如何通过@objc dynamic强制改为动态派发? 深度解析:

静态派发条件:方法未在协议声明中定义,直接通过扩展实现。

动态派发实现:

@objc protocol MyProtocol {}  
extension MyProtocol {  
  @objc dynamic func foo() { ... }  
} 

性能代价:动态派发增加方法查找开销约20%。

12.基于Combine框架的响应式状态管理设计模式 问题:在SwiftUI中如何通过Combine的@Published和CurrentValueSubject实现跨View共享状态?对比RxSwift的内存泄漏风险。 深度解析:

状态共享方案:

内存安全:

class Store: ObservableObject {  
  @Published var state = AppState()  
  private let subject = CurrentValueSubject<AppState, Never>(AppState())  
}  

Combine自动管理订阅生命周期。

RxSwift需手动dispose(),易遗漏导致泄漏。

  1. 请简述Objective - C中类和对象的区别

    • 类是对象的抽象模板,定义了对象的属性和行为规范。例如定义一个Person类,它规定了人可能具有的属性(如姓名、年龄)和行为(如说话、走路)。对象是类的具体实例,通过类创建出来的实际个体。比如根据Person类创建一个名为“张三”,年龄为 20 岁的对象。
  2. Objective - C中方法和函数有什么不同

    • 方法是属于类或对象的,调用时需要通过类名或对象名来调用,并且方法可以访问类或对象的属性。例如在Person类中有一个-(void)sayHello方法,需要通过Person对象来调用。函数是独立的代码块,不依赖于类或对象,调用时直接使用函数名。如int add(int a, int b)这样的函数可以直接调用。
  3. 解释@property和@synthesize的作用

    • @property用于在类的头文件中声明属性,它会自动生成属性的存取方法(getter 和 setter)的声明。例如@property(nonatomic, strong) NSString *name;@synthesize用于实现这些存取方法。在现代的 Xcode 中,如果没有手动实现存取方法,编译器会自动为@property声明的属性生成synthesize代码。
  4. 简述Objective - C中的协议(Protocol)

    • 协议是一种规范,定义了一组方法列表,但不实现这些方法。类可以遵循协议,实现协议中定义的方法。例如定义一个DelegateProtocol协议,其中有-(void)didFinishTask方法,某个类遵循这个协议后就需要实现该方法。协议常用于实现委托 - 代理模式。
  5. 什么是分类(Category),它有什么作用

    • 分类是为现有的类添加新的方法,而无需创建子类。例如可以为NSString类添加一个计算字符串长度是否大于 10 的分类方法。作用包括扩展类的功能、将类的代码按功能模块分开便于维护、在不修改原有类代码的情况下添加方法。
  6. 如何在Objective - C中实现单例模式

    • 单例模式确保一个类只有一个实例,并提供一个全局访问点。常见的实现方式如下:
@interface Singleton : NSObject
+ (instancetype)sharedInstance;
@end

@implementation Singleton
+ (instancetype)sharedInstance {
    static Singleton *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}
@end
  1. 解释Objective - C中的消息传递机制

    • 在Objective - C中,方法调用实际上是消息传递。当向一个对象发送消息时,运行时系统会根据对象的类找到对应的方法实现并执行。例如[object doSomething]就是向object对象发送doSomething消息。如果对象无法响应该消息,会触发消息转发机制。
  2. 简述Objective - C中的消息转发机制

    • 当对象接收到无法响应的消息时,会依次经历三个步骤:动态方法解析(询问类是否可以动态添加方法来处理该消息)、备用接收者(询问是否有其他对象可以处理该消息)、完整的消息转发(将消息封装成NSInvocation对象,询问如何处理该消息)。
  3. Objective - C中如何进行异常处理

    • 使用@try@catch@finally语句块。@try块中放置可能抛出异常的代码,@catch块用于捕获并处理异常,@finally块中的代码无论是否发生异常都会执行。例如:
@try {
    // 可能抛出异常的代码
} @catch (NSException *exception) {
    // 处理异常
    NSLog(@"Exception: %@", exception.reason);
} @finally {
    // 最终要执行的代码
}
  1. 简述Objective - C中的块(Block)

    • 块是一种匿名的代码块,可以捕获其所在上下文中的变量。块可以作为参数传递,也可以作为返回值返回。例如:
void (^myBlock)(void) = ^{
    NSLog(@"This is a block.");
};
myBlock();

二、iOS内存管理

  1. 解释ARC(自动引用计数)的工作原理

    • ARC 是编译器特性,它会在编译时自动插入内存管理代码,主要是根据对象的引用计数来管理内存。当一个对象被创建时,引用计数为 1,当有新的强引用指向它时,引用计数加 1,当强引用被释放时,引用计数减 1,当引用计数为 0 时,对象被释放。
  2. 什么是弱引用(weak)和强引用(strong)

    • 强引用会增加对象的引用计数,只要有强引用指向对象,对象就不会被释放。例如@property(strong) NSString *name; 。弱引用不会增加对象的引用计数,当对象的强引用计数为 0 时,弱引用会自动置为nil,避免野指针问题。例如@property(weak) id delegate;
  3. 如何避免循环引用问题

    • 可以使用弱引用(weak)来打破循环引用。例如在委托 - 代理模式中,代理属性通常使用weak修饰,避免代理对象和被代理对象之间形成循环引用。另外,在块中也可能出现循环引用,需要使用__weak修饰捕获的对象,例如:
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    [weakSelf doSomething];
};
  1. 解释autoreleasepool的作用

    • autoreleasepool用于延迟对象的释放。当对象调用autorelease方法时,它会被加入到当前的自动释放池中,当自动释放池销毁时,池中的对象会被释放。在主线程中,系统会自动创建和销毁自动释放池。例如在循环中创建大量临时对象时,可以使用@autoreleasepool来及时释放这些对象,减少内存占用。
for (int i = 0; i < 1000; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"%d", i];
        // 使用 str
    }
}
  1. 简述内存泄漏的原因及如何检测

    • 内存泄漏的原因主要有循环引用、对象没有正确释放等。检测内存泄漏可以使用 Xcode 的 Instruments 工具中的 Leaks 模板,它可以帮助开发者找出程序中存在的内存泄漏问题。

三、iOS视图和视图控制器

  1. 简述UIViewController的生命周期

    • init:初始化视图控制器。
    • loadView:加载视图,通常不需要手动调用。
    • viewDidLoad:视图加载完成,可进行视图的初始化设置。
    • viewWillAppear::视图即将显示。
    • viewDidAppear::视图已经显示。
    • viewWillDisappear::视图即将消失。
    • viewDidDisappear::视图已经消失。
    • dealloc:视图控制器释放。
  2. 如何实现视图控制器之间的跳转

    • 可以使用导航控制器(UINavigationController)的pushViewController:animated:方法进行压栈操作实现跳转,使用popViewControllerAnimated:方法进行出栈操作返回。也可以使用模态视图控制器(presentViewController:animated:completion:)进行模态跳转,使用dismissViewControllerAnimated:completion:方法关闭模态视图。
  3. 简述UITableView的工作原理

    • UITableView是一个用于显示列表数据的视图,它采用数据源(UITableViewDataSource)和代理(UITableViewDelegate)模式。数据源负责提供表格的行数、单元格数据等信息,代理负责处理表格的一些交互事件,如单元格的选中、行高设置等。UITableView会根据数据源提供的信息创建和管理单元格,当单元格滚动出屏幕时会被复用,以提高性能。
  4. 如何优化UITableView的性能

    • 复用单元格:使用dequeueReusableCellWithIdentifier:方法复用单元格,避免频繁创建新的单元格。
    • 异步加载图片:如果单元格中包含图片,使用异步加载方式,避免阻塞主线程。
    • 缓存计算结果:对于一些需要计算的内容(如行高),进行缓存,避免重复计算。
    • 减少子视图数量:尽量减少单元格中的子视图数量,降低绘制复杂度。
  5. 简述UICollectionView和UITableView的区别

    • UITableView主要用于显示单列或多列的列表数据,布局相对简单,通常是垂直或水平滚动的列表。UICollectionView则更加灵活,可以实现各种复杂的布局,如网格布局、瀑布流布局等,它通过布局对象(UICollectionViewLayout)来控制单元格的布局。

四、iOS多线程编程

  1. 简述iOS中的几种多线程编程方式

    • NSThread:是最基础的多线程编程方式,需要手动管理线程的生命周期。例如:
[NSThread detachNewThreadSelector:@selector(doSomethingInBackground) toTarget:self withObject:nil];
  • NSOperationQueue:基于NSOperation的多线程编程方式,通过将操作(NSOperation)添加到队列(NSOperationQueue)中来执行。可以设置操作的依赖关系和优先级。
  • GCD(Grand Central Dispatch) :是苹果提供的一种高效的多线程编程方式,通过将任务(block)添加到队列(dispatch_queue_t)中来执行。队列分为串行队列和并行队列。例如:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 后台任务
    dispatch_async(dispatch_get_main_queue(), ^{
        // 回到主线程更新 UI
    });
});
  1. 解释GCD中的串行队列和并行队列

    • 串行队列中的任务会按照添加的顺序依次执行,同一时间只有一个任务在执行。可以使用dispatch_queue_create创建自定义串行队列。并行队列中的任务会并发执行,只要有可用的线程,多个任务可以同时执行。系统提供了全局并行队列,如dispatch_get_global_queue
  2. 如何使用NSOperationQueue实现线程间通信

    • 可以在一个操作完成后,在其completionBlock中切换到主线程或其他队列执行后续操作。例如:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    // 后台任务
}];
operation.completionBlock = ^{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // 回到主线程更新 UI
    }];
};
[queue addOperation:operation];
  1. 简述线程安全问题及如何解决

    • 线程安全问题是指多个线程同时访问共享资源时,可能会导致数据不一致或程序崩溃等问题。解决线程安全问题的方法有使用锁机制(如NSLock@synchronized)、使用原子属性(atomic)、使用串行队列等。例如使用NSLock
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// 访问共享资源
[lock unlock];
  1. 解释信号量(dispatch_semaphore)的作用

    • 信号量用于控制并发访问的线程数量。可以通过dispatch_semaphore_create创建信号量,dispatch_semaphore_wait减少信号量的值,如果信号量的值为 0 则线程会阻塞,dispatch_semaphore_signal增加信号量的值。例如:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 执行任务
    dispatch_semaphore_signal(semaphore);
});

五、iOS网络编程

  1. 简述HTTP和HTTPS的区别

    • HTTP 是超文本传输协议,是明文传输的,数据在传输过程中容易被窃取和篡改。HTTPS 是在 HTTP 的基础上加入了 SSL/TLS 协议进行加密,保证了数据传输的安全性。HTTPS 的端口号是 443,而 HTTP 的端口号是 80。
  2. 如何在iOS中进行网络请求

    • 可以使用NSURLSession进行网络请求。例如发送一个 GET 请求:
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error: %@", error.localizedDescription);
    } else {
        // 处理返回的数据
    }
}];
[task resume];
  1. 解释RESTful API的概念

    • RESTful API 是一种符合 REST 架构风格的网络 API 设计规范。它使用 HTTP 协议的方法(GET、POST、PUT、DELETE)来表示对资源的操作,使用 URL 来表示资源。例如GET /users表示获取所有用户信息,POST /users表示创建一个新用户。
  2. 如何处理网络请求的超时问题

    • NSURLRequest中可以设置timeoutInterval属性来指定请求的超时时间。例如:
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.timeoutInterval = 10; // 设置超时时间为 10 秒
  1. 简述JSON和XML的区别

    • JSON 是一种轻量级的数据交换格式,语法简洁,易于解析和生成,常用于前后端数据交互。XML 是一种标记语言,结构较为复杂,适合表示复杂的数据结构和文档。JSON 的解析速度通常比 XML 快。

六、iOS数据存储

  1. 简述iOS中的几种数据存储方式

    • NSUserDefaults:用于存储简单的用户偏好设置,如用户名、开关状态等。数据以键值对的形式存储在 plist 文件中。例如:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"John" forKey:@"username"];
[defaults synchronize];
  • 文件存储:可以将数据以文件的形式存储在应用的沙盒目录中,如文本文件、图片文件等。例如:
NSString *text = @"Hello, World!";
NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/test.txt"];
[text writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
  • SQLite:是一种轻量级的嵌入式数据库,适合存储大量结构化数据。可以使用FMDB等第三方库来简化操作。
  • Core Data:是苹果提供的一个对象图管理和持久化框架,它将数据模型抽象成对象,方便开发者进行数据的增删改查操作。
  1. 如何使用Core Data进行数据存储

    • 首先创建数据模型文件(.xcdatamodeld),定义实体和属性。然后创建NSManagedObjectModelNSPersistentStoreCoordinatorNSManagedObjectContext。通过NSManagedObjectContext来进行数据的操作,例如创建新对象、保存数据等。示例代码如下:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface CoreDataManager : NSObject

@property (nonatomic, strong) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

+ (instancetype)sharedManager;
- (NSString *)applicationDocumentsDirectory;

@end

@implementation CoreDataManager

+ (instancetype)sharedManager {
    static CoreDataManager *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        // 获取数据模型
        self.managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
        
        // 创建持久化存储协调器
        self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
        
        // 添加持久化存储
        NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"MyData.sqlite"];
        NSError *error;
        if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:storePath] options:nil error:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
        
        // 创建管理对象上下文
        self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [self.managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
    }
    return self;
}

- (NSString *)applicationDocumentsDirectory {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}

@end


// 使用示例
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CoreDataManager *manager = [CoreDataManager sharedManager];
        
        // 创建新对象
        NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:@"MyEntity" inManagedObjectContext:manager.managedObjectContext];
        [newObject setValue:@"Value" forKey:@"attribute"];
        
        // 保存数据
        NSError *saveError;
        if (![manager.managedObjectContext save:&saveError]) {
            NSLog(@"Failed to save data: %@, %@", saveError, [saveError userInfo]);
        } else {
            NSLog(@"Data saved successfully.");
        }
        
        // 查询数据
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"MyEntity" inManagedObjectContext:manager.managedObjectContext];
        [fetchRequest setEntity:entity];
        
        NSError *fetchError;
        NSArray *results = [manager.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
        if (fetchError) {
            NSLog(@"Failed to fetch data: %@, %@", fetchError, [fetchError userInfo]);
        } else {
            for (NSManagedObject *object in results) {
                NSString *value = [object valueForKey:@"attribute"];
                NSLog(@"Attribute value: %@", value);
            }
        }
        
        // 更新数据
        if (results.count > 0) {
            NSManagedObject *firstObject = results[0];
            [firstObject setValue:@"UpdatedValue" forKey:@"attribute"];
            NSError *updateError;
            if (![manager.managedObjectContext save:&updateError]) {
                NSLog(@"Failed to update data: %@, %@", updateError, [updateError userInfo]);
            } else {
                NSLog(@"Data updated successfully.");
            }
        }
        
        // 删除数据
        if (results.count > 0) {
            NSManagedObject *firstObject = results[0];
            [manager.managedObjectContext deleteObject:firstObject];
            NSError *deleteError;
            if (![manager.managedObjectContext save:&deleteError]) {
                NSLog(@"Failed to delete data: %@, %@", deleteError, [deleteError userInfo]);
            } else {
                NSLog(@"Data deleted successfully.");
            }
        }
    }
    return 0;
}

七、iOS动画与图形处理

  1. 简述隐式动画和显式动画的区别

    • 隐式动画:是指在对视图的某些可动画属性(如 framealpha 等)进行修改时,系统自动产生的动画效果。这些动画效果是由 CALayer 提供的,不需要开发者手动创建动画对象。例如,直接修改视图的 alpha 值,系统会自动以渐变的方式改变视图的透明度。
    • 显式动画:需要开发者手动创建动画对象(如 CABasicAnimationCAKeyframeAnimation 等)并添加到图层上。显式动画可以更精确地控制动画的各个参数,如动画时长、重复次数、动画曲线等。
  2. 如何创建一个简单的平移动画

    • 可以使用 CABasicAnimation 来创建平移动画,示例代码如下:
// 创建一个 CABasicAnimation 对象
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
// 设置动画的起始值
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
// 设置动画的结束值
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 200)];
// 设置动画时长
animation.duration = 1.0;
// 将动画添加到图层上
[view.layer addAnimation:animation forKey:@"positionAnimation"];
  1. 解释关键帧动画(CAKeyframeAnimation)的原理

    • CAKeyframeAnimation 允许在动画过程中定义多个关键帧,每个关键帧指定一个特定的值,动画会按照这些关键帧的值在指定的时间内进行过渡。关键帧可以是位置、大小、颜色等属性的值。通过设置关键帧和对应的时间点,开发者可以创建出复杂的动画效果。例如,创建一个沿着特定路径移动的动画,就可以使用 CAKeyframeAnimation 并指定路径上的多个点作为关键帧。
  2. 简述 Core Graphics 框架的作用

    • Core Graphics 是一个基于 C 的绘图框架,提供了强大的 2D 图形绘制功能。它可以用于绘制线条、形状、文本、图像等,还可以进行图形的裁剪、渐变填充等操作。在 iOS 开发中,Core Graphics 常用于自定义视图的绘制,例如绘制图表、自定义按钮等。
  3. 如何在自定义视图中使用 Core Graphics 进行绘图

    • 首先需要创建一个自定义视图类,继承自 UIView,然后重写 drawRect: 方法,在该方法中使用 Core Graphics 的函数进行绘图。示例代码如下:
- (void)drawRect:(CGRect)rect {
    // 获取当前的图形上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 设置线条颜色
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    // 设置线条宽度
    CGContextSetLineWidth(context, 2.0);
    // 绘制一个矩形
    CGContextAddRect(context, CGRectMake(50, 50, 100, 100));
    // 绘制路径
    CGContextStrokePath(context);
}

八、iOS 系统框架与 API

  1. 简述 AVFoundation 框架的主要功能

    • AVFoundation 是一个用于处理音频和视频的框架,提供了录制、播放、编辑等功能。它可以用于创建视频播放器、音频播放器、摄像头应用等。例如,可以使用 AVPlayer 来播放视频和音频文件,使用 AVCaptureSession 来进行视频录制。
  2. 如何使用 AVPlayer 播放视频

    • 示例代码如下:
// 创建视频 URL
NSURL *videoURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"video" ofType:@"mp4"]];
// 创建 AVPlayerItem 对象
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:videoURL];
// 创建 AVPlayer 对象
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
// 创建 AVPlayerLayer 对象
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
playerLayer.frame = self.view.bounds;
// 将 AVPlayerLayer 添加到视图的图层上
[self.view.layer addSublayer:playerLayer];
// 播放视频
[player play];
  1. 解释 MapKit 框架的用途

    • MapKit 框架用于在 iOS 应用中集成地图功能。它提供了 MKMapView 控件,可以显示地图、标注位置、绘制路线等。开发者可以使用 MapKit 来创建地图导航应用、位置搜索应用等。
  2. 如何在应用中添加地图并标注位置

    • 示例代码如下:
// 创建 MKMapView 对象
MKMapView *mapView = [[MKMapView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:mapView];
// 创建一个标注点
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(37.33182, -122.03118);
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
annotation.coordinate = coordinate;
annotation.title = @"Apple Park";
// 将标注点添加到地图上
[mapView addAnnotation:annotation];
  1. 简述 HealthKit 框架的功能

    • HealthKit 框架用于在 iOS 应用中访问和存储用户的健康和健身数据。它可以获取用户的步数、心率、睡眠数据等,也可以将应用中产生的健康数据存储到健康应用中。使用 HealthKit 框架需要用户授权。

九、iOS 性能优化

  1. 如何优化应用的启动时间

    • 减少 main 函数之前的耗时操作:例如减少动态库的加载、减少全局变量的初始化等。
    • 优化 AppDelegate 中的代码:将不必要的初始化操作延迟到合适的时机执行,避免在 application:didFinishLaunchingWithOptions: 方法中进行大量的耗时操作。
    • 使用懒加载:对于一些不急需的资源和对象,采用懒加载的方式,在需要使用时再进行加载和初始化。
  2. 简述图片优化的方法

    • 压缩图片:使用图片压缩工具对图片进行压缩,减小图片的文件大小,同时保持合理的画质。
    • 选择合适的图片格式:对于具有透明通道的图片,使用 PNG 格式;对于色彩丰富的图片,使用 JPEG 格式。
    • 图片懒加载:在滚动视图(如 UITableViewUICollectionView)中,当图片滚动到可见区域时再进行加载,避免一次性加载大量图片导致内存飙升。
  3. 如何检测应用的性能瓶颈

    • 使用 Xcode 的 Instruments 工具,它提供了多种模板来检测应用的性能问题,如 Time Profiler 可以分析代码的执行时间,找出耗时的函数;Leaks 可以检测内存泄漏问题;Allocations 可以分析内存分配情况等。
    • 还可以使用第三方性能监测工具,如 Fabric、Bugly 等,它们可以帮助开发者收集应用的性能数据和崩溃信息。
  4. 简述代码优化的原则

    • 减少重复代码:将重复的代码提取成函数或类,提高代码的复用性。
    • 优化算法复杂度:选择合适的算法和数据结构,降低代码的时间复杂度和空间复杂度。
    • 提高代码的可读性:使用有意义的变量名和函数名,添加必要的注释,使代码易于理解和维护。

十、iOS 安全与隐私

  1. 简述数据加密的重要性及常用的加密算法

    • 重要性:在 iOS 应用中,数据加密可以保护用户的隐私和敏感信息,防止数据在传输和存储过程中被窃取和篡改。例如,用户的登录密码、银行卡信息等都需要进行加密处理。

    • 常用加密算法

      • 对称加密算法:如 AES(高级加密标准),加密和解密使用相同的密钥,加密速度快,适用于大量数据的加密。
      • 非对称加密算法:如 RSA,使用公钥加密,私钥解密,安全性高,常用于数字签名和密钥交换。
      • 哈希算法:如 MD5、SHA - 1、SHA - 256 等,用于生成数据的哈希值,常用于验证数据的完整性。
  2. 如何保护应用的 API 密钥

    • 使用环境变量:将 API 密钥存储在环境变量中,避免将密钥硬编码在代码中。在不同的环境(如开发、测试、生产)中使用不同的密钥。
    • 服务器端验证:在服务器端对 API 请求进行验证,确保请求来自合法的客户端,防止 API 密钥被滥用。
    • 代码混淆:对代码进行混淆处理,增加反编译的难度,保护 API 密钥不被轻易获取。
  3. 简述 iOS 应用的隐私保护机制

    • 权限请求:在应用访问用户的敏感信息(如摄像头、相册、位置等)之前,需要向用户请求权限,用户可以选择是否授权。
    • 数据沙盒:每个 iOS 应用都有自己的沙盒目录,应用只能访问自己沙盒内的数据,不能访问其他应用的数据,保证了数据的隔离和安全。
    • 加密存储:iOS 系统提供了加密存储功能,如使用 Keychain 来安全地存储用户的敏感信息。
  4. 如何防止应用被反编译

    • 代码混淆:使用代码混淆工具对代码进行混淆,改变代码的结构和变量名,增加反编译的难度。
    • 加壳:对应用进行加壳处理,在应用的外层添加一层保护壳,防止反编译工具直接解析应用的代码。
    • 服务器验证:在应用启动时,与服务器进行验证,确保应用是合法的版本,防止被篡改的应用运行。

十一、iOS 设计模式

  1. 简述 MVC(Model - View - Controller)设计模式

    • Model:负责处理数据和业务逻辑,如数据的存储、获取和处理。例如,在一个联系人管理应用中,联系人的信息(姓名、电话等)就是模型。
    • View:负责显示数据和用户交互界面,如 UIView 及其子类。例如,联系人列表界面就是视图。
    • Controller:作为模型和视图之间的桥梁,负责接收用户的输入,调用模型进行数据处理,并将处理结果显示在视图上。例如,联系人列表的视图控制器负责加载联系人数据并显示在列表中。
  2. 解释 MVVM(Model - View - ViewModel)设计模式

    • Model:与 MVC 中的模型类似,负责处理数据和业务逻辑。
    • View:负责显示数据和用户交互界面,通常是 UIViewControllerUIView
    • ViewModel:是视图和模型之间的中间层,负责将模型的数据转换为视图可以显示的格式,并处理视图的事件。ViewModel 与视图之间通过绑定机制进行数据同步。
  3. 简述单例模式在 iOS 开发中的应用场景

    • 全局配置管理:如应用的配置信息,使用单例模式可以确保整个应用中只有一个配置对象,方便进行统一的配置管理。
    • 共享资源访问:如网络请求管理器、数据库管理器等,使用单例模式可以避免多个实例同时访问共享资源导致的冲突问题。
  4. 解释观察者模式及其在 iOS 中的实现方式

    • 观察者模式:定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会收到通知并自动更新。

    • 在 iOS 中的实现方式

      • KVO(键值观察) :通过监听对象的属性变化,当属性值发生改变时,观察者会收到通知。例如:
[object addObserver:self forKeyPath:@"property" options:NSKeyValueObservingOptionNew context:nil];
  • 通知中心(NSNotificationCenter) :通过发布和订阅通知的方式实现观察者模式。例如:
// 发布通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
// 订阅通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"NotificationName" object:nil];

1. Object-c的类可以多重继承么?可以实现多个接口么?重写一个类的方式用继承好还是分类好?为什么?

Object-c:

  • 不可多重继承
  • 可实现多个接口

Category类别:

  • 一般情况用分类好

为什么分类好?:

  • 用Category去重写类的方法,仅对本Category有效,不会影响其他类与原有类的关系。

2. #import 跟#include 又什么区别,@class呢, #import 跟 #import””又什么区别?

#import:

  • Objective-C导入头文件的关键字

#include:

  • C/C++导入头文件的关键字

#import 跟 #include区别:

  • 使用#import头文件会自动只导入一次,不会重复导入,相当于#include和#pragma once;

@class:

  • 编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含;

#import 跟 #import””区别:

  • #import:

    • 包含系统的头文件
  • #import””:

    • 包含用户头文件

3. 属性readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?

readwrite:

  • 可读可写特性(需要生成getter方法和setter方法时)

readonly:

  • 只读特性(只会生成getter方法 不会生成setter方法 ;不希望属性在类外改变)

assign:

  • 是赋值特性(setter方法将传入参数赋值给实例变量;仅设置变量时)

retain:

  • 持有特性(setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1)

copy:

  • 赋值特性(setter方法将传入对象复制一份;需要完全一份新的变量时)

nonatomic:

  • 非原子操作(决定编译器生成的setter getter是否是原子操作,atomic表示多线程安全,一般使用nonatomic)

4.写一个setter方法用于完成@property (nonatomic,retain)NSString name,写一个setter方法用于完成@property(nonatomic,copy)NSString name

-?(void)?setName:(NSString*)?str
{
[str?retain];
[name?release];
name?=?str;
}
-?(void)setName:(NSString?*)str
{
id?t?=?[str?copy];
[name?release];
name?=?t;
}

5.对于语句NSString*obj = [[NSData alloc] init]; obj在编译时和运行时分别时什么类型的对象?

编译时:

  • NSString的类型

运行时:

  • NSData类型的对象

常见的object-c的数据类型有那些, 和C的基本数据类型有什么区别?如:NSInteger和int

object-c常见数据类型:

  • NSString
  • NSNumber
  • NSArray
  • NSMutableArray
  • NSData
    ...

C语言的基本数据类型:

  • int(一定字节的内存空间,用于存放数值)

NSInteger基本数据类型:

  • 不是NSNumber的子类
  • 不是NSObject的子类
  • 是Int或者Long的别名(NSInteger的定义typedef long NSInteger)

区别:

  • NSInteger会根据系统是32位还是64位来决定是本身是int还是Long

7.id 声明的对象有什么特性?

Id 声明的对象特性:

  • 运行时(可以指向任意类型的objcetive-c的对象)

8.Objective-C如何对内存管理的,说说你的看法和解决方法?

Objective-C的内存管理主要有三种方式:

  • ARC(自动内存计数)

    • (Garbage Collection)自动内存计数:
    • 这种方式和java类似,在你的程序的执行过程中。始终有一个高人在背后准确地帮你收拾垃圾,你不用考虑它什么时候开始工作,怎样工作。你只需要明白,我申请了一段内存空间,当我不再使用从而这段内存成为垃圾的时候,我就彻底的把它忘记掉,反正那个高人会帮我收拾垃圾。遗憾的是,那个高人需要消耗一定的资源,在携带设备里面,资源是紧俏商品所以iPhone不支持这个功能。所以“Garbage Collection”不是本入门指南的范围,对“Garbage Collection”内部机制感兴趣的同学可以参考一些其他的资料,不过说老实话“Garbage Collection”不大适合适初学者研究。
    • 方案:

      • 通过alloc – initial方式创建的, 创建后引用计数+1, 此后每retain一次引用计数+1, 那么在程序中做相应次数的release就好了.
  • 手动内存计数

    • (Reference Counted)手动内存计数:
    • 就是说,从一段内存被申请之后,就存在一个变量用于保存这段内存被使用的次数,我们暂时把它称为计数器,当计数器变为0的时候,那么就是释放这段内存的时候。比如说,当在程序A里面一段内存被成功申请完成之后,那么这个计数器就从0变成1(我们把这个过程叫做alloc),然后程序B也需要使用这个内存,那么计数器就从1变成了2(我们把这个过程叫做retain)。紧接着程序A不再需要这段内存了,那么程序A就把这个计数器减1(我们把这个过程叫做release);程序B也不再需要这段内存的时候,那么也把计数器减1(这个过程还是release)。当系统(也就是Foundation)发现这个计数器变 成员了0,那么就会调用内存回收程序把这段内存回收(我们把这个过程叫做dealloc)。顺便提一句,如果没有Foundation,那么维护计数器,释放内存等等工作需要你手工来完成。
    • 方案:

      • 一般是由类的静态方法创建的, 函数名中不会出现alloc或init字样, 如[NSString string]和[NSArray arrayWithObject:], 创建后引用计数+0, 在函数出栈后释放, 即相当于一个栈上的局部变量. 当然也可以通过retain延长对象的生存期.
  • 内存池

    • (NSAutoRealeasePool)内存池:
    • 可以通过创建和释放内存池控制内存申请和回收的时机.
    • 方案:

      • 是由autorelease加入系统内存池, 内存池是可以嵌套的, 每个内存池都需要有一个创建释放对, 就像main函数中写的一样. 使用也很简单, 比如[[[NSString alloc]initialWithFormat:@”Hey you!”] autorelease], 即将一个NSString对象加入到最内层的系统内存池, 当我们释放这个内存池时, 其中的对象都会被释放.

9. 原子(atomic)跟非原子(non-atomic)属性有什么区别?

atomic:

  • 提供多线程安全(防止在写未完成的时候被另外一个线程读取,造成数据错误)

non-atomic:

  • 在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值。

10. 看下面的程序,第一个NSLog会输出什么?这时str的retainCount是多少?第二个和第三个呢? 为什么?

NSMutableArray*?ary?=?[[NSMutableArray?array]?retain];
NSString?*str?=?[NSString?stringWithFormat:@"test"];
[str?retain];
[aryaddObject:str];
NSLog(@”%@%d”,str,[str?retainCount]);
[str?retain];
[str?release];
[str?release];
NSLog(@”%@%d”,str,[str?retainCount]);
[aryremoveAllObjects];
NSLog(@”%@%d”,str,[str?retainCount]);

str的retainCount创建+1,retain+1,加入数组自动+1 3

retain+1,release-1,release-1 2

数组删除所有对象,所有数组内的对象自动-1 1

11. 内存管理的几条原则是什么?按照默认法则.那些关键字生成的对象需要手动释放?在和property结合的时候怎样有效的避免内存泄露?

谁申请,谁释放

遵循Cocoa Touch的使用原则;

内存管理主要要避免“过早释放”和“内存泄漏”,对于“过早释放”需要注意@property设置特性时,一定要用对特性关键字,对于“内存泄漏”,一定要申请了要负责释放,要细心。

关键字alloc 或new 生成的对象需要手动释放;

设置正确的property属性,对于retain需要在合适的地方释放,

12.如何对iOS设备进行性能测试?

  • Profile

    • Instruments
    • Time Profiler

13. Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么?

线程创建有三种方法:

  • 使用NSThread创建、使用GCD的dispatch、使用子类化的NSOperation,然后将其加入NSOperationQueue;在主线程执行代码,方法是performSelectorOnMainThread,如果想延时执行代码可以用performSelector:onThread:withObject:waitUntilDone:

14. MVC设计模式是什么? 你还熟悉什么设计模式?

设计模式:

  • 并不是一种新技术,而是一种编码经验,使用比如java中的接口,iphone中的协议,继承关系等基本手段,用比较成熟的逻辑去处理某一种类型的事情,总结为所谓设计模式。面向对象编程中,java已经归纳了23种设计模式。

mvc设计模式 :

  • 模型,视图,控制器,可以将整个应用程序在思想上分成三大块,对应是的数据的存储或处理,前台的显示,业务逻辑的控制。 Iphone本身的设计思想就是遵循mvc设计模式。其不属于23种设计模式范畴。

代理模式:

  • 代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用.比如一个工厂生产了产品,并不想直接卖给用户,而是搞了很多代理商,用户可以直接找代理商买东西,代理商从工厂进货.常见的如QQ的自动回复就属于代理拦截,代理模式在iphone中得到广泛应用.

单例模式:

  • 说白了就是一个类不通过alloc方式创建对象,而是用一个静态方法返回这个类的对象。系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为,比如想获得[UIApplication sharedApplication];任何地方调用都可以得到 UIApplication的对象,这个对象是全局唯一的。

观察者模式:

  • 当一个物体发生变化时,会通知所有观察这个物体的观察者让其做出反应。实现起来无非就是把所有观察者的对象给这个物体,当这个物体的发生改变,就会调用遍历所有观察者的对象调用观察者的方法从而达到通知观察者的目的。

工厂模式:

public?class?Factory{
public?static?Sample?creator(int?which){
if?(which==1)
return?new?SampleA();
else?if?(which==2)
return?new?SampleB();
}
}

15 浅复制和深复制的区别?

浅层复制:

  • 只复制指向对象的指针,而不复制引用对象本身。

深层复制:

  • 复制引用对象本身。

意思就是说我有个A对象,复制一份后得到A_copy对象后,对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只不过是是一个指针,对象本身资源

还是只有一份,那如果我们对A_copy执行了修改操作,那么发现A引用的对象同样被修改,这其实违背了我们复制拷贝的一个思想。深复制就好理解了,内存中存在了

两份独立对象本身。

16. 类别的作用?继承和类别在实现中有何区别?

category 可以在不获悉,不改变原来代码的情况下往里面添加新的方法,只能添加,不能删除修改,并且如果类别和原来类中的方法产生名称冲突,则类别将覆盖原来的方法,因为类别具有更高的优先级。

类别主要有3个作用:

1).将类的实现分散到多个不同文件或多个不同框架中。

2).创建对私有方法的前向引用。

3).向对象添加非正式协议。

继承可以增加,修改或者删除方法,并且可以增加属性。

17. 类别和类扩展的区别。

category和extensions的不同在于 后者可以添加属性。
另外后者添加的方法是必须要实现的。
extensions可以认为是一个私有的Category。

18. oc中的协议和java中的接口概念有何不同?

OC中的代理有2层含义,官方定义为 formal和informal protocol。前者和Java接口一样。

informal protocol中的方法属于设计模式考虑范畴,不是必须实现的,但是如果有实现,就会改变类的属性。

其实关于正式协议,类别和非正式协议我很早前学习的时候大致看过,也写在了学习教程里

“非正式协议概念其实就是类别的另一种表达方式“这里有一些你可能希望实现的方法,你可以使用他们更好的完成工作”。

这个意思是,这些是可选的。比如我门要一个更好的方法,我们就会申明一个这样的类别去实现。然后你在后期可以直接使用这些更好的方法。

这么看,总觉得类别这玩意儿有点像协议的可选协议。”

现在来看,其实protocal已经开始对两者都统一和规范起来操作,因为资料中说“非正式协议使用interface修饰“,

现在我们看到协议中两个修饰词:“必须实现(@requied)”和“可选实现(@optional)”。

19. 什么是KVO和KVC?

KVC:键 – 值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法,直接或通过实例变量访问的机制。

很多情况下可以简化程序代码。apple文档其实给了一个很好的例子。

KVO:

  • 键值观察机制,他提供了观察某一属性变化的方法,极大的简化了代码。

具体用看到嗯哼用到过的一个地方是对于按钮点击变化状态的的监控。

比如我自定义的一个button

[self?addObserver:self?forKeyPath:@"highlighted"?options:0?context:nil];
#pragma?mark?KVO
-?(void)observeValueForKeyPath:(NSString?*)keyPath?ofObject:(id)object?change:(NSDictionary?*)change?context:(void?*)context
{
if?([keyPath?isEqualToString:@"highlighted"]?)?{
[self?setNeedsDisplay];
}
}

对于系统是根据keypath去取的到相应的值发生改变,理论上来说是和kvc机制的道理是一样的。

对于kvc机制如何通过key寻找到value:

“当通过KVC调用对象时,比如:[self valueForKey:@”someKey”]时,程序会自动试图通过几种不同的方式解析这个调用。首先查找对象是否带有 someKey 这个方法,如果没找到,会继续查找对象是否带有someKey这个实例变量(iVar),如果还没有找到,程序会继续试图调用 -(id) valueForUndefinedKey:这个方法。如果这个方法还是没有被实现的话,程序会抛出一个NSUndefinedKeyException异常错误。

(cocoachina.com注:Key-Value Coding查找方法的时候,不仅仅会查找someKey这个方法,还会查找getsomeKey这个方法,前面加一个get,或者_someKey以及_getsomeKey这几种形式。同时,查找实例变量的时候也会不仅仅查找someKey这个变量,也会查找_someKey这个变量是否存在。)

设计valueForUndefinedKey:方法的主要目的是当你使用-(id)valueForKey方法从对象中请求值时,对象能够在错误发生前,有最后的机会响应这个请求。这样做有很多好处,下面的两个例子说明了这样做的好处。“

因为我们知道button却是存在一个highlighted实例变量.因此为何上面我们只是add一个相关的keypath就行了,

可以按照kvc查找的逻辑理解,就说的过去了。

20. 代理的作用?

代理的目的是改变或传递控制链。允许一个类在某些特定时刻通知到其他类,而不需要获取到那些类的指针。可以减少框架复杂度。

另外一点,代理可以理解为java中的回调监听机制的一种类似。

21. oc中可修改和不可以修改类型。

可修改不可修改的集合类。这个我个人简单理解就是可动态添加修改和不可动态添加修改一样。

比如NSArray和NSMutableArray。前者在初始化后的内存控件就是固定不可变的,后者可以添加等,可以动态申请新的内存空间。

22. 我们说的oc是动态运行时语言是什么意思?

多态。 主要是将数据类型的确定由编译时,推迟到了运行时。
这个问题其实浅涉及到两个概念,运行时和多态。
简单来说,运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。

多态:

  • 不同对象以自己的方式响应相同的消息的能力叫做多态。意思就是假设生物类(life)都用有一个相同的方法-eat;

都继承了life后,实现各自的eat,但是调用是我们只需调用各自的eat方法。

也就是不同的对象以自己的方式响应了相同的消息(响应了eat这个选择器)。

因此也可以说,运行时机制是多态的基础?~~~

23. 通知和协议的不同之处?

协议有控制链(has-a)的关系,通知没有。

简单来说,通知的话,它可以一对多,一条消息可以发送给多个消息接受者。

24. 什么是推送消息?

推送通知更是一种技术。
简单点就是客户端获取资源的一种手段。
普通情况下,都是客户端主动的pull。
推送则是服务器端主动push。 测试push的实现可以查看该博文。

25. 关于多态性

多态,子类指针可以赋值给父类。

这个题目其实可以出到一切面向对象语言中,
因此关于多态,继承和封装基本最好都有个自我意识的理解,也并非一定要把书上资料上写的能背出来

26. 对于单例的理解

在objective-c中要实现一个单例类,至少需要做以下四个步骤:

1).为单例对象实现一个静态实例,并初始化,然后设置成nil,

2).实现一个实例构造方法检查上面声明的静态实例是否为nil,如果是则新建并返回一个本类的实例,

3).重写allocWithZone方法,用来保证其他人直接使用alloc和init试图获得一个新实力的时候不产生一个新实例,

4).适当实现allocWitheZone,copyWithZone,release和autorelease。

27. 说说响应链

事件响应链。 包括点击事件,画面刷新事件等。在视图栈内从上至下,或者从下之上传播。
可以说点事件的分发,传递以及处理。具体可以去看下touch事件这块。因为问的太抽象化了
严重怀疑题目出到越后面就越笼统。
可以从责任链模式,来讲通过事件响应链处理,其拥有的扩展性

一、Runtime 核心原理

Runtime(运行时)是 OC 的灵魂,负责对象的创建、方法调用、消息转发等底层操作。其开源源码可参考苹果官方的objc4仓库。

1. Runtime 内存模型(isa、对象、类、metaclass)

OC 的内存模型以isa 指针为核心,串联起实例对象、类对象、元类(metaclass)  三层结构,每层对应不同的结构体,存储不同信息。

(1)核心结构关系

plaintext

rust
 体验AI代码助手
 代码解读
复制代码
实例对象(Instance)-> 类对象(Class)-> 元类(Metaclass)-> 根元类(Root Metaclass)
  • 实例对象(Instance) :存储成员变量(ivar)  的值,其isa指针指向对应的类对象
    结构体简化:

    objc

    arduino
     体验AI代码助手
     代码解读
    复制代码
    struct Instance {
        Class isa; // 指向类对象
        // 成员变量的值(如NSString *name; int age;)
    };
    
  • 类对象(Class) :存储实例方法(-method)、属性(property)、协议(protocol) ,其isa指针指向元类,同时包含指向父类的superclass指针。
    结构体核心依赖class_data_bits_t,内部通过data()方法获取class_rw_t(可读写数据):

    objc

    arduino
     体验AI代码助手
     代码解读
    复制代码
    struct objc_class {
        Class isa;         // 指向元类
        Class superclass;  // 指向父类
        class_data_bits_t bits; // 存储类的核心数据
    };
    
  • 元类(Metaclass) :存储类方法(+method) ,其isa指针指向根元类(如NSObject的元类),superclass指向父类的元类。
    元类的本质是 “类的类”—— 因为类对象也是 OC 对象(可调用+method),需要元类来管理其方法。

  • 根元类(Root Metaclass) :所有元类的最终父类(如NSObject的元类),其isa指针指向自身superclass指向根类(如NSObject)。

(2)isa 指针的作用

  • 本质是Class类型的指针,用于定位对象的 “所属类”

    • 实例对象的isa → 类对象(确定实例能调用哪些实例方法);
    • 类对象的isa → 元类(确定类能调用哪些类方法)。
  • 64 位系统中,isa 指针通过位掩码存储额外信息(如对象是否在堆上、引用计数等),需通过ISA_MASK提取真实的类地址。

2. 为什么要设计 metaclass?

核心目的是解决 “类方法的存储归属” 问题

  • OC 中,实例方法的调用依赖实例对象的isa找到类对象,类对象存储实例方法列表;
  • 类方法(如+alloc)的调用者是 “类对象”,而类对象本身也是 OC 对象(可被isa指向),因此需要一个专门的 “类”(元类)来存储类方法列表。
  • 若没有 metaclass,类方法将无处存储,导致[NSObject alloc]这类调用无法实现。

3. class_copyIvarList & class_copyPropertyList 区别

两者均用于获取类的成员信息,但针对的对象和返回内容完全不同,核心区别如下:

对比维度class_copyIvarListclass_copyPropertyList
获取的内容成员变量(ivar)属性(property)
本质区别编译时定义的 “底层变量”(如_name封装后的 “属性”(含 setter/getter)
是否包含合成变量是(如@property合成的_name是(直接返回属性本身)
访问权限可获取私有 ivar(如类内部定义的int _age仅获取属性(私有 property 也可获取)
返回类型Ivar *(成员变量指针数组)objc_property_t *(属性指针数组)

示例
若类定义为@interface Person : NSObject { int _weight; } @property (nonatomic, copy) NSString *name; @end,则:

  • class_copyIvarList返回_weight_name(合成的 ivar);
  • class_copyPropertyList仅返回name(属性)。

4. class_rw_t 和 class_ro_t 的区别

两者均是类对象的核心数据结构,存储方法、属性、协议等信息,但核心区别在于读写权限和初始化时机

对比维度class_rw_t(Read-Write)class_ro_t(Read-Only)
读写权限可读写(运行时可修改)只读(编译时确定,不可修改)
初始化时机运行时(类第一次被使用时初始化)编译时(编译器生成,存储在 Mach-O 的__DATA段)
存储内容包含class_ro_t的指针 + 运行时添加的方法 / 属性 / 协议(如 Category 的内容)编译时确定的 “固定信息”:初始方法列表、属性列表、协议列表、成员变量信息
核心作用支持动态添加内容(如 Category、Method Swizzle)存储类的 “静态基础信息”,确保编译后不可篡改

关系class_rw_t内部有一个const class_ro_t *ro指针,指向类的只读基础数据;运行时动态添加的内容(如 Category 的方法)会直接存入class_rw_t

5. Category 加载流程 & 方法优先级

Category(分类)是 OC 中动态扩展类功能的核心机制,其加载和方法调用有严格的顺序规则。

(1)Category 加载流程(运行时阶段)

  1. 编译时:编译器将 Category 编译为category_t结构体,存储分类的方法列表、属性列表、协议列表,以及所属的类名。
    category_t结构体简化:

    objc

    arduino
     体验AI代码助手
     代码解读
    复制代码
    struct category_t {
        const char *name;       // 所属类名
        classref_t cls;         // 所属类(运行时绑定)
        struct method_list_t *instance_methods; // 实例方法
        struct method_list_t *class_methods;    // 类方法
        struct protocol_list_t *protocols;      // 协议
        struct property_list_t *properties;     // 属性
    };
    
  2. 运行时(map_images 阶段)

    • dyld(动态链接器)加载完所有类和分类后,调用_objc_init初始化 Runtime;
    • Runtime 通过_processCatlist遍历所有category_t,将分类的方法、属性、协议合并到所属类的class_rw_t(实例方法合并到类的instance_methods,类方法合并到元类的class_methods)。
  3. 合并规则
    分类的方法会插入到类原有方法列表的前面(而非替换),因此分类方法会 “覆盖” 类的同名方法(实际是优先调用)。

(2)Category 的 load 方法加载顺序

+load方法是 Category 中特殊的方法,不遵循消息转发机制,由 Runtime 直接调用,顺序规则如下:

  1. 类的 load 先于分类的 load:先调用所有类的+load(父类 → 子类),再调用所有分类的+load
  2. 同类分类的 load 按编译顺序:Xcode 编译时,后添加到项目的分类,其+load先被调用(可通过 “Build Phases → Compile Sources” 调整顺序);
  3. 不同类分类的 load 按类的加载顺序:依赖类的加载顺序(如 A 类依赖 B 类,则 B 类的分类+load先调用)。

(3)Category 同名方法的调用顺序

当多个分类(或类与分类)有同名方法时,调用顺序遵循 “后编译的分类优先”:

  1. 分类方法覆盖类的同名方法(因分类方法在方法列表前面);
  2. 多个分类的同名方法,后编译的分类方法先被调用(编译顺序可通过 Xcode 调整);
  3. 父类分类的方法优先级低于子类的分类(因子类的类加载晚于父类)。

注意:分类无法覆盖+load+initialize方法(+initialize遵循消息转发,会先调用父类的)。

6. Category & Extension 区别 + 能否给 NSObject 添加 Extension?

(1)核心区别

对比维度Category(分类)Extension(扩展)
能否添加成员变量不能(仅能添加方法、属性、协议,属性不会自动合成 ivar,需手动关联)能(可添加私有成员变量、方法、属性)
可见性公开(需在.h 中声明,或匿名分类在.m 中)私有(仅在定义的.m 文件中可见)
编译时机运行时合并到类中编译时作为类的一部分(与类同时编译)
是否需要实现可单独实现(.m 文件)必须在类的.m 文件中实现(否则编译报错)
核心用途扩展已有类的功能(如给 UIView 加分类)给类添加私有成员(如在.m 中隐藏细节)

(2)能否给 NSObject 添加 Extension?

不能直接添加,原因如下:

  • Extension 是类的 “一部分”,必须在类的定义文件(.m)中声明和实现
  • NSObject 是系统类,开发者无法修改其.m 文件,因此无法直接为其添加 Extension;
  • 若强行在自己的文件中声明@interface NSObject () { int _myVar; } @end,编译时会报错(“Category is not allowed on 'NSObject'”)。

替代方案:若需给 NSObject 添加私有成员,可通过 “匿名分类 + 关联对象” 实现,或自定义 NSObject 的子类。

7. 消息转发机制 + 与其他语言对比

OC 的方法调用本质是 “发送消息”(objc_msgSend),当消息无法被接收者处理时,会触发消息转发机制,避免崩溃。

(1)消息转发三阶段(完整流程)

在进入转发前,会先进行方法查找

  1. 从接收者的类的缓存(cache_t)  中查找方法(快速查找);
  2. 缓存未命中,从类的class_rw_t的方法列表中查找,若未找到则递归查找父类(直到根类NSObject);
  3. 若所有父类均未找到,进入动态方法解析 → 快速转发 → 慢速转发三阶段。

具体转发流程:

  1. 动态方法解析(Resolve)

    • 调用+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法),允许开发者动态添加方法实现
    • 示例:若[person run]未实现,可在resolveInstanceMethod中用class_addMethod添加run的实现;
    • 若返回YES,则重新发起消息查找;若返回NO,进入下一阶段。
  2. 快速转发(Fast Forwarding)

    • 调用-forwardingTargetForSelector:,允许开发者将消息转发给其他对象(“替身”);
    • 示例:返回self.otherObject,则消息会转发给otherObject处理;
    • 若返回非nil,则消息转发给该对象;若返回nil,进入下一阶段。
  3. 慢速转发(Slow Forwarding)

    • 调用-methodSignatureForSelector:,获取方法签名(返回值类型、参数类型);
    • 若返回nil,则触发崩溃(unrecognized selector sent to instance);
    • 若返回有效签名,调用-forwardInvocation:,开发者可在该方法中自定义消息处理逻辑(如转发给多个对象、记录日志)。

(2)与其他语言(如 Java)的消息机制对比

对比维度OC(消息转发)Java(方法调用)
绑定时机运行时绑定(动态)编译时绑定(静态,除非用反射)
方法不存在的处理触发消息转发,可自定义处理(避免崩溃)编译时报错(若未声明)或运行时抛NoSuchMethodError
灵活性高(支持动态添加方法、转发消息)低(需提前声明方法,反射仅能绕过编译检查)
性能略低(运行时查找和转发有开销)高(编译时确定方法地址)
崩溃风险可通过转发避免崩溃无法避免(除非用 try-catch 捕获异常)

8. 方法调用前的准备:消息查找流程

在 “动态解析→消息转发” 之前,Runtime 会先执行消息查找流程(分为快速查找和慢速查找),这是方法调用的核心前置步骤:

  1. 快速查找(缓存查找)

    • 调用objc_msgSend时,先从接收者的类的cache_t(缓存)中查找方法;
    • cache_t是哈希表,key 为SEL(方法选择器),value 为IMP(方法实现指针);
    • 若找到IMP,直接跳转到实现执行;若未找到,进入慢速查找。
  2. 慢速查找(方法列表查找)

    • 从类的class_rw_tmethod_list中遍历查找SEL(按方法列表顺序);
    • 若未找到,递归查找父类的method_list(直到根类NSObject);
    • 若找到,将SELIMP存入当前类的cache_t(缓存,供下次快速查找),然后执行IMP
    • 若所有父类均未找到,进入动态方法解析和消息转发。

9. IMP、SEL、Method 的区别和使用场景

三者是 Runtime 中描述 “方法” 的核心概念,关系为:Method包含SELIMPSEL是方法标识,IMP是方法实现地址。

概念定义本质核心作用使用场景
SEL方法选择器(typedef const struct objc_selector *SEL;字符串(方法名的哈希值)唯一标识一个方法(如@selector(run)方法调用(objc_msgSend(person, @selector(run)))、判断方法是否存在([person respondsToSelector:@selector(run)]
IMP方法实现指针(typedef id (*IMP)(id, SEL, ...);函数指针指向方法的具体实现代码直接调用方法(跳过消息查找,如IMP imp = [person methodForSelector:@selector(run)]; imp(person, @selector(run));)、Method Swizzle
Method方法结构体(typedef struct objc_method *Method;包含 SEL、IMP、方法签名封装方法的完整信息获取方法详情(如method_getName(method)获取 SEL、method_getImplementation(method)获取 IMP)、动态添加方法(class_addMethod

关系示例
Method method = class_getInstanceMethod([Person class], @selector(run));
SEL sel = method_getName(method); // 获取 SEL
IMP imp = method_getImplementation(method); // 获取 IMP

10. load、initialize 方法的区别(含继承关系)

+load+initialize是类初始化时的两个特殊方法,但触发时机、调用逻辑、继承行为完全不同。

(1)核心区别

对比维度+load 方法+initialize 方法
触发时机类 / 分类被加载到内存时(dyld 阶段)类第一次接收消息时(如[Person alloc]
调用方式Runtime 直接调用(不经过 objc_msgSend)经过消息转发(objc_msgSend)
是否自动调用父类是(父类 load 先于子类 load)否(仅当子类未实现时,才调用父类)
分类是否覆盖否(类和分类的 load 都会调用)是(分类的 initialize 会覆盖类的)
调用次数仅一次(类加载时)仅一次(类第一次使用时)
线程安全是(Runtime 加锁,串行调用)否(需手动加锁,避免多线程调用)

(2)继承关系中的区别

  • +load
    父类的+load先于子类的+load调用,且所有类的 load 调用完后,才调用分类的 load
    示例:NSObject → Person(子类) → Person+Category1 → Person+Category2(按编译顺序)。

  • +initialize
    仅当子类未实现+initialize时,才会调用父类的+initialize(因消息转发会先查找子类,子类未实现则找父类)。
    示例:

    objc

    less
     体验AI代码助手
     代码解读
    复制代码
    @interface Father : NSObject @end
    @implementation Father
    + (void)initialize { NSLog(@"Father initialize"); }
    @end
    
    @interface Son : Father @end
    @implementation Son
    // 未实现initialize
    @end
    
    // 调用 [Son alloc] 时,会先调用 Father 的 initialize(因Son未实现)
    

    若子类实现了+initialize,则仅调用子类的,父类的不会被调用(除非父类单独被使用)。

11. 消息转发机制的优劣

(1)优点

  1. 灵活性高:允许动态添加方法、转发消息,适配复杂场景(如 “多继承” 模拟、解耦);
  2. 容错性强:可捕获 “未实现的方法”,避免崩溃(如在forwardInvocation中记录日志或返回默认值);
  3. 支持 AOP(面向切面编程) :通过转发机制在方法调用前后插入逻辑(如埋点、权限校验)。

(2)缺点

  1. 性能开销:消息查找(缓存→方法列表→父类)+ 转发(三阶段)会增加运行时开销,频繁触发会影响性能;
  2. 调试难度大:方法调用链路长,崩溃时的调用栈可能不完整(如转发后崩溃,难以定位原始调用者);
  3. 可读性差:动态转发逻辑隐藏在底层,代码维护成本高(如新人难以理解 “为什么未实现的方法能执行”)。

二、内存管理

iOS 内存管理的核心是引用计数,Runtime 通过SideTableautoreleasepool等机制实现自动管理,ARC 则进一步简化了开发者的操作。

1. weak 的实现原理 + SideTable 结构

weak是 OC 中用于避免循环引用的弱引用机制,其核心是通过SideTable管理弱引用表,确保对象释放时自动将weak指针置为nil

(1)SideTable 结构

SideTable是 Runtime 中的全局哈希表,每个对象的引用计数和弱引用均由SideTable管理,结构简化如下:

objc

arduino
 体验AI代码助手
 代码解读
复制代码
struct SideTable {
    spinlock_t slock;          // 自旋锁(保证线程安全)
    RefcountMap refcnts;       // 引用计数表(key:对象指针,value:引用计数)
    weak_table_t weak_table;   // 弱引用表(存储所有指向该对象的weak指针)
};
  • spinlock_t:轻量级锁,适用于短时间持有(如修改引用计数时),避免线程竞争;
  • RefcountMapstd::unordered_map<DisguisedPtr<objc_object>, size_t>,存储对象的引用计数;
  • weak_table_t:弱引用表,结构为std::unordered_map<DisguisedPtr<objc_object>, weak_entry_t>weak_entry_t内部存储所有指向该对象的weak指针数组。

(2)weak 实现原理

  1. weak 指针赋值时(如__weak Person *weakP = person;):

    • Runtime 通过objc_storeWeak(&weakP, person)weakP添加到person对应的SideTableweak_table中;
    • personnil,则直接将weakP置为nil(不操作SideTable)。
  2. 对象释放时dealloc阶段):

    • 调用objc_clear_deallocating,从SideTable中找到该对象的weak_entry_t
    • 遍历weak_entry_t中的所有weak指针,将其置为nil
    • weak_table中删除该weak_entry_t,并清空引用计数表中的条目。
  3. 核心优势weak指针不会增加对象的引用计数,且对象释放时自动置为nil,避免野指针访问。

2. 关联对象的应用 + 系统实现 + 内存管理

关联对象(Associated Object)是 Category 中 “间接添加成员变量” 的机制,通过 Runtime API 将对象与另一个对象关联。

(1)关联对象的应用

  • 给 Category 添加 “成员变量” :Category 不能直接添加 ivar,但可通过关联对象存储数据;
  • 解耦数据存储:如给 UIView 关联一个NSString *identifier,无需继承 UIView;
  • 临时存储上下文:如网络请求回调中,将请求参数与回调 block 关联。

示例

objc

less
 体验AI代码助手
 代码解读
复制代码
// 给UIView添加分类,关联identifier
@interface UIView (Identifier)
@property (nonatomic, copy) NSString *identifier;
@end

@implementation UIView (Identifier)
static const void *kIdentifierKey = &kIdentifierKey;

- (void)setIdentifier:(NSString *)identifier {
    // 关联对象:key=kIdentifierKey,value=identifier,策略=OBJC_ASSOCIATION_COPY_NONATOMIC
    objc_setAssociatedObject(self, kIdentifierKey, identifier, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)identifier {
    return objc_getAssociatedObject(self, kIdentifierKey);
}
@end

(2)系统实现原理

关联对象的管理依赖 Runtime 内部的全局哈希表,核心结构如下:

  1. AssociationsManager:单例管理器,持有AssociationsHashMap,并通过自旋锁保证线程安全;
  2. AssociationsHashMapunordered_map<DisguisedPtr<objc_object>, ObjectAssociationMap>,key 是 “被关联的对象”(如 UIView 实例),value 是该对象的关联表;
  3. ObjectAssociationMapunordered_map<void *, ObjcAssociation>,key 是开发者定义的key(如kIdentifierKey),value 是ObjcAssociation(存储关联值和内存管理策略);
  4. ObjcAssociation:存储关联值(id _value)和内存管理策略(objc_AssociationPolicy)。

操作流程

  • objc_setAssociatedObject:通过AssociationsManager找到AssociationsHashMap,根据 “被关联对象” 找到ObjectAssociationMap,存入keyObjcAssociation
  • objc_getAssociatedObject:反向查找,根据 “被关联对象” 和key获取ObjcAssociation中的_value
  • objc_removeAssociatedObjects:删除 “被关联对象” 对应的ObjectAssociationMap

(3)关联对象的内存管理

关联对象的内存管理由objc_AssociationPolicy(关联策略)决定,策略对应 ARC 下的内存语义:

关联策略内存语义(ARC)作用
OBJC_ASSOCIATION_ASSIGNassign弱引用,不 retain,对象释放后变为野指针
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong(非原子)retain 关联值,线程不安全
OBJC_ASSOCIATION_COPY_NONATOMICcopy(非原子)copy 关联值,线程不安全
OBJC_ASSOCIATION_RETAINstrong(原子)retain 关联值,线程安全
OBJC_ASSOCIATION_COPYcopy(原子)copy 关联值,线程安全

释放时机

  • 当 “被关联对象” 释放时(dealloc),Runtime 会自动调用objc_removeAssociatedObjects,根据关联策略释放关联值(如retain策略会release关联值);
  • 也可手动调用objc_removeAssociatedObjects移除所有关联值。

(4)关联对象如何实现 weak 属性

关联对象本身不支持weak策略(OBJC_ASSOCIATION_ASSIGNassign,非weak),但可通过弱引用容器实现:

  1. 自定义一个WeakContainer类,内部用__weak持有目标对象;
  2. WeakContainer实例作为关联值,策略设为OBJC_ASSOCIATION_RETAIN_NONATOMIC
  3. 访问时从WeakContainer中获取__weak对象,实现弱引用效果。

示例

objc

less
 体验AI代码助手
 代码解读
复制代码
// 弱引用容器
@interface WeakContainer : NSObject
@property (nonatomic, weak) id value;
@end

@implementation WeakContainer
@end

// 关联时使用容器
- (void)setWeakValue:(id)weakValue {
    WeakContainer *container = [WeakContainer new];
    container.value = weakValue;
    objc_setAssociatedObject(self, kWeakValueKey, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)weakValue {
    WeakContainer *container = objc_getAssociatedObject(self, kWeakValueKey);
    return container.value; // 弱引用,对象释放后为nil
}

3. Autoreleasepool 原理 + 数据结构

Autoreleasepool(自动释放池)是 iOS 中管理临时对象内存的机制,通过延迟释放对象,避免频繁调用release

(1)核心原理

  • 作用:收集调用autorelease的对象,在Autoreleasepool销毁时,对池内所有对象调用release

  • 触发时机

    1. 主线程:RunLoop 的每个循环周期结束时(如kCFRunLoopBeforeWaiting),自动销毁并重建Autoreleasepool
    2. 子线程:需手动创建@autoreleasepool {},否则对象可能无法及时释放;
    3. 手动销毁:@autoreleasepool {}代码块执行完毕时,池内对象被release

(2)数据结构

Autoreleasepool基于双向链表实现,核心结构是AutoreleasePoolPage

  • AutoreleasePoolPage:每个 Page 是 4096 字节(一页内存),结构简化如下:

    objc

    arduino
     体验AI代码助手
     代码解读
    复制代码
    class AutoreleasePoolPage {
        static const size_t SIZE = 4096; // 4KB
        AutoreleasePoolPage *next;       // 下一个Page(链表节点)
        AutoreleasePoolPage *prev;       // 上一个Page
        id *begin;                       // Page内存储对象的起始地址
        id *end;                         // Page内存储对象的结束地址
        id *top;                         // 当前存储对象的下一个位置(栈指针)
        pthread_t thread;                // 所属线程(每个线程对应一个Page链表)
    };
    
  • Page 链表:当一个 Page 装满(top == end)时,创建新的 Page 并加入链表;

  • POOL_BOUNDARY:哨兵对象,标记Autoreleasepool的边界。@autoreleasepool {}会在开始时压入POOL_BOUNDARY,结束时从top向下遍历,直到遇到POOL_BOUNDARY,对中间所有对象调用release,并将top重置到POOL_BOUNDARY之后。

(3)操作流程

  1. 创建Autoreleasepool:调用objc_autoreleasePoolPush(),压入POOL_BOUNDARY,返回其地址;
  2. 对象调用autorelease:调用objc_autorelease(),将对象指针存入当前 Page 的top位置,top自增;若当前 Page 满,创建新 Page 并继续存储;
  3. 销毁Autoreleasepool:调用objc_autoreleasePoolPop(POOL_BOUNDARY),从top向下遍历,对每个对象调用release,直到遇到POOL_BOUNDARY,并调整top指针。

4. ARC 实现原理 + 优化

ARC(Automatic Reference Counting)是编译器和 Runtime 协作的自动内存管理机制,核心是 “编译器自动插入引用计数操作代码”。

(1)ARC 实现原理

  1. 编译器层面

    • 分析代码中对象的生命周期,在合适的位置自动插入retainreleaseautorelease

    • 例如:

      objc

      csharp
       体验AI代码助手
       代码解读
      复制代码
      // ARC代码
      - (void)test {
          Person *p = [[Person alloc] init]; // 编译器自动插入 [p retain](实际alloc返回的对象引用计数为1,无需retain)
      } // 函数结束时,编译器自动插入 [p release]
      
    • 遵循 “谁持有,谁释放” 原则:局部变量出作用域时释放,成员变量在对象dealloc时释放。

  2. Runtime 层面

    • 提供objc_retainobjc_releaseobjc_autorelease等 API,供编译器插入调用;
    • 通过SideTable管理引用计数,确保线程安全;
    • 处理weak指针(如对象释放时置为nil)。

(2)ARC 对 retain & release 的优化

ARC 通过编译器和 Runtime 优化,减少不必要的retain/release操作,提升性能:

  1. 返回值优化(NRVO - Named Return Value Optimization)
    若函数返回局部对象,编译器直接将对象的所有权转移给调用者,避免插入autoreleaseretain
    示例:

    objc

    ini
     体验AI代码助手
     代码解读
    复制代码
    - (Person *)createPerson {
        Person *p = [[Person alloc] init]; // 局部对象
        return p; // ARC优化:直接返回p,无需autorelease
    }
    // 调用者:Person *p = [self createPerson]; 无需retain
    
  2. Toll-Free Bridging 优化
    当 Core Foundation 对象(如CFStringRef)与 OC 对象(如NSString)桥接时,ARC 自动管理引用计数,避免手动调用CFRetain/CFRelease
    示例:NSString *str = (__bridge_transfer NSString *)CFStringCreateWithCString(...);__bridge_transfer让 ARC 接管 CF 对象的释放。

  3. 局部变量优化
    若局部变量仅在当前作用域使用,且无外部引用,编译器会省略retain/release(如循环内的临时对象)。

  4. 零成本异常处理
    MRC 中,异常抛出时需手动处理release;ARC 中,编译器通过@try/@finally自动插入release,且优化了异常处理的性能开销。

5. ARC 下的内存泄漏场景

ARC 虽自动管理内存,但仍存在以下常见泄漏场景:

  1. 循环引用

    • Block 与 self 循环引用self持有 block,block 持有self(如self.block = ^{ [self doSomething]; };);
      解决:用__weak typeof(self) weakSelf = self;打破循环。
    • ** delegate 循环引用 **:若delegatestrong修饰(如@property (nonatomic, strong) id<Delegate> delegate;),会导致委托方与被委托方循环引用;
      解决:delegateweak修饰。
    • 容器与对象循环引用:对象持有容器,容器存储对象(如self.array = @[self];);
      解决:用weak容器(如NSArray存储WeakContainer)。
  2. NSTimer 未 invalidate
    NSTimerretaintarget(如self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES];),若self持有timer,会形成循环引用;
    解决:在dealloc或页面销毁时调用[self.timer invalidate]; self.timer = nil;

  3. AFN 请求未取消
    AFN 的NSURLSessionDataTaskretain其回调 block,若 block 持有self,且请求未取消,self会一直被持有;
    解决:页面销毁时调用[task cancel];

  4. 缓存未清理
    全局缓存(如NSCache、单例中的NSDictionary)存储大量对象,且未设置过期策略,导致对象无法释放;
    解决:设置缓存上限(如cache.countLimit = 100;),或在内存警告时清理缓存。

  5. 子线程未退出
    子线程中开启 RunLoop 且未手动停止(如CFRunLoopRun();),导致子线程一直存活,持有其内部的对象;
    解决:调用CFRunLoopStop(CFRunLoopGetCurrent());停止 RunLoop。

三、NSNotification 机制

NSNotificationCenter(通知中心)是 iOS 中跨模块通信的核心机制,基于 “发布 - 订阅” 模式实现。

1. 实现原理(结构设计、存储关系)

NSNotificationCenter的核心是通知表,存储 “通知名 - 观察者 - 处理方法” 的映射关系,结构设计如下:

(1)核心结构

  • 通知表(_notificationTable)NSNotificationCenter内部维护一个哈希表,key 为**通知名(NSString * ,value 为NSMapTable(存储该通知名对应的所有观察者);
  • 观察者表(NSMapTable) :key 为观察者(id) ,value 为NSMutableArray(存储该观察者订阅该通知的所有 “处理条目”);
  • 处理条目(_NotificationObserver) :每个条目包含selector(处理方法)、object(通知发送者过滤条件)、queue(指定线程处理通知)、context(上下文)。

(2)name & observer & SEL 的关系

  • 多对多关系:一个通知名(name)可被多个观察者(observer)订阅,一个观察者可订阅多个通知名;
  • 过滤逻辑:订阅时指定object,则仅接收该object发送的通知;若objectnil,则接收所有发送者的该通知;
  • 处理逻辑:当通知被发布时,NSNotificationCenter根据通知名找到所有观察者,遍历处理条目,若object匹配,调用objc_msgSend(observer, selector, notification)

2. 通知的发送是同步还是异步?

默认是同步的

  • 调用postNotificationName:object:userInfo:时,NSNotificationCenter会在当前线程中立即遍历所有匹配的观察者,同步调用其selector
  • 若某个观察者的selector执行耗时,会阻塞当前线程(包括主线程,导致 UI 卡顿)。

异步发送方式
需手动将通知发布逻辑放入异步队列,例如:

objc

objectivec
 体验AI代码助手
 代码解读
复制代码
// 在子线程异步发布通知
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:nil];
});

// 或在主线程异步处理通知(观察者侧)
[[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
    // 主线程异步处理
}];

3. NSNotificationCenter 接收与发送是否在同一线程?如何异步发送?

(1)线程一致性

  • 默认情况:接收通知的线程与发送通知的线程完全一致
    示例:在子线程 A 调用postNotification,则所有观察者的selector会在子线程 A 执行;在主线程调用,则在主线程执行。
  • 例外情况:若订阅时指定了queue(如addObserverForName:object:queue:usingBlock:),则通知会在指定的queue对应的线程执行;
    示例:指定queue:[NSOperationQueue mainQueue],则无论通知在哪个线程发布,都会在主线程执行 block。

(2)异步发送通知的两种方式

  1. 发布侧异步:将postNotification放入异步队列,让发布操作不阻塞当前线程;

    objc

    less
     体验AI代码助手
     代码解读
    复制代码
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"AsyncPost" object:nil];
    });
    
  2. 接收侧异步:订阅时指定异步队列,让处理逻辑在后台线程执行;

    objc

    swift
     体验AI代码助手
     代码解读
    复制代码
    // 接收通知的block在全局队列执行(异步)
    [[NSNotificationCenter defaultCenter] addObserverForName:@"AsyncReceive" object:nil queue:[[NSOperationQueue alloc] init] usingBlock:^(NSNotification * _Nonnull note) {
        // 耗时处理(如解析数据)
    }];
    

4. NSNotificationQueue 是异步还是同步?在哪个线程响应?

NSNotificationQueue(通知队列)是对NSNotificationCenter的扩展,核心作用是延迟发送通知合并重复通知,其发送方式和线程规则如下:

(1)同步 / 异步特性

  • 默认是异步的NSNotificationQueue不会立即发送通知,而是将通知加入队列,在RunLoop 的特定模式下批量发送;

  • 支持三种发送模式(NSPostingStyle):

    • NSPostWhenIdle:RunLoop 空闲时发送(如无事件处理时);
    • NSPostASAP:RunLoop 下一次循环时发送(尽快发送,但不阻塞当前 RunLoop);
    • NSPostNow:立即发送(同步,等价于NSNotificationCenter的直接发布)。

(2)响应线程

  • 与发布通知的线程一致NSNotificationQueue的通知最终由NSNotificationCenter发送,因此响应线程与NSNotificationQueue所在的线程一致;

  • 示例:在主线程创建NSNotificationQueue并添加通知,则通知会在主线程的 RunLoop 空闲时发送,观察者在主线程响应;在子线程创建,则在子线程响应。