iOS 常用面试题
1,对于OC的动态性你有什么理解
- 动态绑定
在运行时而不是在编译时确定方法的调用和函数的应用,通过msg send 实现动态方法的绑定 - 动态类型
对象的类型可以在运行时确定而不是编译时固定,使用id 类型是可以在运行时决定对象的实际类型,通过 iskindof respondsToSelector 确保类型的安全执行 - 动态加载
可以在运势是按需加载动态库或者框架 - 动态方法交换
可以通过class_replaceMethod 可以在运行时替换对象的特定方法,还以为类或者对象添加方法,扩展功能 - 支持元编程
以在运行时检查和操作代码的结构行为和类型,主要表现为
-- 1.反射
类反射,可以在运行时函数检查类的信息。例如objc_getclasslist获取当前进程中的类,class_getName 获取类的名称
方法反射,使用class_copyMethodlist 获取一个类的所有方法
属性反射,使用class_copyproertylist获取类的属性列表
--2.动态方法的添加和替换
class_addMethod, 为一个类新增方法,便于扩展
class_replaceMedhtod,对方法的实现进行替换,实现对方法的拦截或修改
perfromSelector 等动态调用方法
--3.动态类的创建和修改
ocj_allocateclasspair objc_registercallpair
--4.动态属性的操作,包括添加删除修改
class_addProperty 添加新的属性
objc_getAssociatedObject objc_setAssiciateObject 动态获取和设置
2,有那些与动画卡帧相关的问题
1.cpu 过载
复杂的计算,大量的数据处理,同步任务的阻塞主线程造成,cpu处理过多的计算任务,导致动画无法按照预期帧率进行
- 通过减少主线程的负载,将计算任务和数据处理移到后台线程
- 使用异步方法处理长时间人任务,避免阻塞主线程
- 减少不必要的计算,使用缓存和预处理等方式
2.GPU 过载
高复杂的图形操作,纹理资源过多,过度绘制等
- 避免过度绘制,确保UI层次结构简单
- 减少复杂的图形操作,优化图形资源的使用
- 使用合适的图形框架和技术,比如metal和core aniamation ,提高渲染效率
3.内存压力
过多的内存分配,内存的释放不及时,以及资源的泄漏等
- 减少内存的分配,及时释放资源
- 使用autoreleasepool 控制内存使用,避免内存泄漏
- 确保图形资源的的有效管理,避免不必要纹理和缓存
4.主线程的阻塞
长时间的同步任务,网络请求,文件的I/O 等
- 避免在主线程进行同步操作,比如网络请求,文件的IO等
- 将主线程任务移动到后台线程
- 使用GCD是处理异步任务等
5.过度绘制 以及一些错误的动画参数
复杂的视图层级,冗余的UI元素等,一次错误的参数导致的冲突等
- 确保参数的合理,避免动画时间过长或过短
- 避免动画冲突,确保动画的顺序和逻辑
- 使用合适的动画工具,如core Animaiton uikit 动画等,确保动画流畅
使用instruments 进行性能分析
选择合适的模板
- time profiler:用于分析CPU时间的消耗 查看方法或函数的对cpu时间的占用
- Acitvey monitor 监控cpu和内存的总体使用情况
- allocactions 分析内存分配,查看内存泄漏和过度分配
- leaks 检测内存泄漏
- core animation 分析动画的性能,查找动画卡针的原因
- gpu Frame Capture 分析gpu 性能适合渲染和图形相关的性能分析
3.你知道线程保活的方式吗
线程保活是指保持线程在一段时间内持续运行,以便能够快速响应任务或请求。在 iOS 中,线程的创建和销毁是具有一定成本的。如果应用频繁创建和销毁线程,可能会导致性能下降。因此,使用线程保活技术,可以提高应用的性能和响应速度。
iOS 中线程保活的方式
- Grand Central Dispatch(GCD)提供了全局队列,允许你将任务分派到系统级线程池。这些线程可以在需要时自动扩展和收缩,并在没有任务时保持活动状态。这是一种高效的线程保活方式。
- OperationQueue 是一种高级的任务调度方式。可以通过设置队列的最大并发数和任务等待时间,实现线程保活。
- 通过创建自定义线程,并让线程在特定的条件下保持活动,可以实现线程保活。这通常通过使用 Run Loop 来维持线程的活动状态。
在使用中需要注意
- 资源的管理 避免浪费和泄漏
- 线程的安全
- 避免阻塞主线程
4.你使用过哪些锁,能具体介绍一下吗
首先锁是在多线程环境下,防止数据竞争确保线程同步的一种方式。
常用的锁包括
- NSLock 适用于简单的通途,是一个互斥锁,适用于防止多个线程同时访问共享资源
- NSRecursiueLock 如果涉及递归操作或者多次获取,使用的递归锁,允许同一线程多次获取不会应发死锁
- NSCondition NSConditionLock,适用于基于条件进行同步时 适合生产和消费的模型
使用wait 或者 waituntilDate 等待条件满足,使用sigal 或者boardcast通知继续执行
#import <Foundation/Foundation.h>
NSCondition *condition = [[NSCondition alloc] init];
BOOL ready = NO;
// 生产者线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
ready = YES; // 生产者准备好了
[condition signal]; // 通知等待的线程
[condition unlock];
});
// 消费者线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
while (!ready) { // 等待条件满足
[condition wait];
}
// 执行消费者任务
// ...
[condition unlock];
});
NSconditionlock适合更复杂的条件控制
lockWhenCondition
unlockWithCondition
#import <Foundation/Foundation.h>
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:0];
// 线程 1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[conditionLock lockWhenCondition:0]; // 等待条件为 0
// 执行操作
[conditionLock unlockWithCondition:1]; // 设置条件为 1
});
// 线程 2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[conditionLock lockWhenCondition:1]; // 等待条件为 1
// 执行操作
[conditionLock unlock]; // 释放锁
});
- os_unfair_lock 高性能的无偏锁,通常是替代就的OSSPinLock
#import <os/lock.h>
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&lock); // 获取锁
// 执行操作
os_unfair_lock_unlock(&lock); // 释放锁
- dispatch Semaphore 需要控制线程并发
#import <Foundation/Foundation.h>
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 初始信号量为 1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量
// 执行操作
dispatch_semaphore_signal(semaphore); // 释放信号量
});
- @synchronized
oc 的同步语法糖
- (void)threadSafeMethod {
@synchronized(self) { // 加锁
// 执行线程安全的操作
// ...
} // 解锁
}
5.多种计时器的使用有哪些问题
ios开发中最常用的计时器nstimer 需要注意的问题,
- 确保在不需要时通过 invalidate() 来停止计时器
- 当timer 作为闭包进行回调时,注意循环引用
- 默认情况下,time 是在起创建runloop上运行,如果对应的runloop 被阻塞或者没有运行会导致计时器不工作
- nstimer 不是实时精确地,受到其runloop中的其他任务的影响,考虑精度可以使用 dispatchSourceTimer 或者CADisplaylink
- 如果设置的时间间过短 可能会导致性能下降
- 避免在后台运行计时器,可能会被系统中止
立即运行:如果需要计时器立即开始运行,使用 scheduledTimerWithTimeInterval(:target:selector:userInfo:repeats:) 或 scheduledTimerWithTimeInterval(:repeats:block:)。
会立即将计时器添加到当前的runloop 中,并开始执行
稍后运行:如果需要稍后启动计时器或添加到特定的 RunLoop,使用 init(timeInterval:target:selector:userInfo:repeats:) 或 init(fire:interval:target:selector:userInfo:repeats:)
dispatchsouretimer 是基于GCD的定时器,用于更精确的定时任务,不受到runloop的影响,尤其在多线程环境中使用,需要注意手动管理生命周期,以及注意UI刷新任务需要回调到主线程执行
let queue = DispatchQueue(label: "com.myapp.timerQueue")
let dispatchTimer = DispatchSource.makeTimerSource(queue: queue)
// 配置定时器
dispatchTimer.schedule(deadline: .now() + 2.0, repeating: 2.0) // 2秒后开始,每2秒执行
// 添加回调
dispatchTimer.setEventHandler { [weak self] in
self?.doSomething()
}
// 启动定时器
dispatchTimer.resume() // 默认处于暂停状态,必须 resume 才会开始
//DispatchSourceTimer 需要手动取消,确保计时器不会泄漏。
dispatchTimer.cancel() // 取消计时器
CADisplayLink 用户屏幕刷新通的任务常于动画或者视图渲染,由于与屏幕刷新同步资源消耗高,不适用于延迟执行
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(updateFrame)];
// 将 DisplayLink 添加到 RunLoop
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[displayLink invalidate]; // 停止 DisplayLink
displayLink = nil; // 确保清除引用
6.如何检测bug和崩溃,包括线上的问题
1.bugly分析 符号表分析 在xcode编译过程中。编译器会生成一个符号表,包含了所有代码中的符号名称和地址映射,在使用时可以讲崩溃日志中的地址信息转换为人类刻度的信息,例如函数名称 文件名等 能都快速定位
2.日志分析
3.异常捕获
4.功能限制开关
5.xcode 调试,debug instrments
7.你有哪些使用KVO 和 KVC的经验?
kvo key-value observing
是一种观察属性变化的机制,能够在对象属性变化时接收到通知,分为3个步骤
1.注册观察者
[self addObserver:self forKeyPath:@"propertyName" options:NSKeyValueObservingOptionNew context:nil];
2.观察属性变化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"propertyName"]) {
NSLog(@"Property changed: %@", change[NSKeyValueChangeNewKey]);
}
}
3.取消注册
[self removeObserver:self forKeyPath:@"propertyName"];
使用的是oc 的运行时动态特性,主要实现步骤为,当你为某个对象添加观察者时,系统会创建对应的子类,并且重写其setter方法,在调用原setter 方法之前发出通知
kvc key-value-codeing
是一种通过键访问属性的方法,允许使用字符串键来获取和设置对象的属性
通过valueforkey setvalue forkey 来实现
还支持嵌套路径
NSString *city = [myObject valueForKeyPath:@"address.city"];
底层实现
1,动态方法查找,查找是否有常规的getter( get<Key>、is<Key>、<Key>) 和 setter 方法
2,访问实例变量,如果类允许的话
3,valueForUndefinedKey setValue:forUndefinedKey:
accessInstanceVariablesDirectly 决定是否能直接访问实例变量
如果键是以 countof enumertorof memberof 等前缀命名,kvc 会假设这是一个集合,并调用相关方法
8,除了加锁,多线程的安全问题还有哪些
1,多个线程同时访问修改共享数据,可能导致数据不一致的问题,解决办法可以通过加锁
2,死锁,多个线程相互等待导致的无法继续,大概是因为锁的使用不当
3,资源竞争,多个线程竞争有限的系统资源(如内存、文件句柄等)可能导致性能下降或者死锁。
4,内存管理问题,多个线程访问数据,一个线程可能释放了另一个线程仍在使用的内存,导致内存访问错误或崩溃。
5,频繁的线程切换可能会带来性能开销
考虑使用合适的同步机制,锁定策略,原子操作(原子操作是一种不可分分割的操作,一单开始就会完成执行,在多线程中其他线程无法终端或者干预这个操作),确保线程的正确和资源的管理
9.什么是runloop ,你怎么理解他,他有什么用处
runloop 还是一个不断循环的机制,负责处理应用程序中的各种事件,每个线程有一个runloop.
runloop 循环包括三个阶段
- 等待事件, RL进入休眠,等待事件,不占资源
- 处理事件,有事件发生,唤醒并处理
- 回到等待
runloop 和线程的关系 - 每条线程都有唯一一个与之对应的runloop
- runloop 保存在一个全局字典中,key是线程,value 是runloop
- 线程创建是没有runloop 对象,只有在第一次获取时才能创建 [NSRunLoop currentRunLoop]
- runloop 会在线程结束是销毁
- 主线程的runloop 已经自动穿件,子线程默认没有开启
应用
- 线程保活
- 解决nstimer 滑动式停止的问题
nstimer 默认绑定到 NSDefaultRunLoopMode,当用户滚动视图时,runloop 会切换到 UITrackingRunloopModel ,解决办法是将timer添加到NSRunLoopCommonModes - 监控应用卡顿
runloop一共有多个状态其中包括
1.kCFRunLoopEntry 这个表示Runloop 刚刚开始运行,还没处理时间准备好进入循环了
可以进行一些初始化操作,或者设置一些环境
2.kCFRunLoopBeforeTimers 这个状态表示即将检查计时器,timers,可以在这里检查或者调整计时器确保正确的时间间隔
3.kCFRunloopBeforeSources 这个表示即将处理输入源,包括用户 系统时间,在这里可以设置或者修改输入源
4.kCFRunloopBeforeWaitng 这个表示即将进去休眠,这里可以执行一些非紧急任务,
5.kCFRunLoopAfterWaiting 这个表示刚刚从休眠中唤醒,可以在这里执行一些准备操作
6.KCFRunLoopExit 这个表示已经完成循环,即将推出或者开始新的循环,可以在这里做一些清理操作
使用 RunLoop 观察者来监控 RunLoop 的状态。当 RunLoop 在 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 之间的时间超过超时时间时,表明应用可能出现卡顿。
NSTimeInterval timeoutInterval = 0.5; // 超时时间
CFRunLoopObserverRef observer;
CFRunLoopActivity activities = kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting;
observer = CFRunLoopObserverCreateWithHandler(NULL, activities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
static NSDate *entryTime = nil;
if (activity == kCFRunLoopBeforeSources) {
entryTime = [NSDate date]; // 记录 RunLoop 进入状态的时间
} else if (activity == kCFRunLoopAfterWaiting) {
if (entryTime && -[entryTime timeIntervalSinceNow] > timeoutInterval) {
NSLog(@"Application is stuck"); // 如果超过超时间,表示可能卡顿
}
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
- 性能优化
RunLoop 即将休眠时执行一些非紧急任务,以确保这些任务不会阻塞主线程。这有助于提高应用的响应能力。
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 在 RunLoop 即将休眠前执行一些任务
[self performBackgroundTasks];
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
10,你有使用Coredata的经验吗,可以分享一下你的使用体验和注意事项吗
ios开发中的数据持久包括NSUserDefault 文件存储,SQlite ,Core Data,Keychain等,
- userdefault
一般用于存储用的配置等小型,使用简单,不适用于大量数据或者复杂的数据结构 - 文件存储
可以直接将数据保存到文件系统中,通常用于存储比较大的数据 图片等,缺点需要手动管理文件名和路径等,不适合管理复杂的数据结构 - SQLite
轻量数据库,支持SQL语法,适合存储结构化数据和执行复杂的查询。需要熟悉SQL语句和数据库操作,手动管理数据库的链接和事务 - Keychain
用于存储铭感数据的安全方式,适合存储密码和秘钥等,有点就是数据加密且有系统级别的安全,应用删除后任可保留,可以在不同应用之间共享,使用比较复杂,不适合大量数据 - coredata
iOS 上的持久化框架,提供了高级的数据管理功能,适合复杂的数据结构和关系映射,还支持数据迁移和批处理,查询和过滤功能
在使用上需要
1.首先需要配置coredata 栈,通常会创建一个单例,来负责配置和管理core data栈,使用NSPersistentContainer 初始化,并且提供viewcontext 来操作
2.执行操作
创建数据,需要创建一个NSManagerObject 对象并且插入到NSManagerObjectContext中
读取数据,使用NSFetchRequest进行查询
更新数据,需要先查询到对象,在修改属性并保存
删除数据,需要先查询对象,再从上下文中删除 并保存
需要注意的是在多线程环境中,每个线程需要使用独立的NSManagerObjectContext,不要共享以防止错误
在设置实体的关联的时候,需要注意避免循环引用
11,你知道哪些性能优化的方法,能具体介绍实现原理吗
- 内存优化
内存分配需要系统进行内存管理和垃圾收集,频繁的内存分配会增加系统负担
可以通过复用例如tableview collectview ,不要频繁的创建一些变量,及时的释放资源,对数据进行一些缓存等
适当的使用autoreleaseppol 在循环或者后台线程中需要创建大量的的零时变量时使用,避免内存的过度增长 - cpu使用优化
减少循环和冗余计算,尝试使用更高效的算法,降低计算复杂度,避免不必要的循环,地丢等
将一些耗时操作放到后台线程,避免主线程的阻塞,充分发挥多核处理器的优势
使用instruments分析方法函数的调用时间 - ui渲染优化
减少重绘次数,延迟加载不急需的UI元素,对图片或者其他资源按需加载
使用instruments工具检测离屏渲染,避免过度使用 圆角 阴影等特性
减少负责层级的试图层级机构 合并重复试图
使用shouldResterize 进行缓存 - 网络性能优化
合并小的网络请求,肩上网络通信次数,使用批量请求和数据压缩来优化网络
将频繁访问的数据缓存在本地,避免重复请求
确保网络请求在后台线程执行,避免主线程的的阻塞 - 文件或者数据库的优化
避免频繁的读写文件,对SQL或者coredata 等持久化的数据,使用事务和批处理来优化性能,减少查询次数,使用索引等来优化查询 - 使用instrument或者日志来,分析性能 具体问题具体解决
12,如何检测卡顿,能介绍具体实现原理吗
- 上文提到的runloop
- 主线程中添加timer 定期检查时间间隔是否大于预期
- 使用instruments 分析工具分析
- 或者一些第三方工具
14,内存占用过高的检测方法是什么,你能介绍具体实现原理吗,根据你的项目经验,可以分享一下你项目中的难点和优点,以及架构设计的优缺点吗
instruments中的内存分析工具,包括memory leaks (主要是通过引用计数和生命周期,判断其引用计数为0的时候,是否被释放,),allocaction等工具
Xcode debug模式下可以直接查看内存使用
使用建议,正确的使用weak 避免循环引用和内存泄漏,在执行大量的临时对象创建时,使用autoreleasepool 来及时释放内存,在试图页面消失时,检测内存变化,对应的对象dealloc 方法是否执行等
15,多线程的实现方式,GCD和NSOperation的优缺点
常见的就是gcd NSOperation 还有NSThread
GCD是通过队列和任务来实现的并发操作,他的优点包括高性能,简单易用,使用灵活
缺点是缺乏高级功能调试难度较高
GCD的是建立才GCD之上的,提供了更加丰富的功能入任务的依赖关系,优先级,任务的取消等,比GCD相对复杂,由于支持了更多特性,性能略低于GCD
16,你知道哪些第三方库,你有看过哪些源码,他们使用了哪些技术,他们有什么优缺点,他们的架构设计师是怎么样的
- AFNetworking
AFNetworking 是最流行的 Objective-C 网络库之一,简化了 HTTP 网络请求和响应处理。
使用的技术
-
基于 NSURLSession:AFNetworking 在 NSURLSession 之上构建,提供更高级的功能。
-
操作队列:使用 NSOperationQueue 来管理并发网络请求。
-
序列化:支持 JSON、XML、图片等多种数据格式的序列化和反序列化。
优缺点 -
优点:强大的网络功能,支持链式调用,易于使用,社区支持广泛。
-
缺点:随着 Swift 的流行,其 Objective-C 版本的维护力度下降,但仍有许多应用依赖于此。
架构设计
AFNetworking 的架构包括以下主要部分: -
AFHTTPSessionManager:处理网络请求和响应,基于 NSURLSession。
-
AFURLSessionManager:处理低级别的 URLSession 操作。
-
AFSecurityPolicy:提供 SSL/TLS 安全策略。
-
AFImageDownloader:用于异步下载图片。
- SDWebImage
SDWebImage 是一个流行的图片加载和缓存库,常用于优化图片处理。
使用的技术
-
异步加载:支持异步下载和缓存图片。
-
缓存机制:提供内存和磁盘缓存,减少网络请求。
-
图像解码:优化图像解码以提高性能。
优缺点 -
优点:简单易用,提供丰富的功能,如图片缓存、异步加载等。
-
缺点:可能会增加内存占用,特别是在处理大量图片时。
架构设计
SDWebImage 的架构包括以下主要部分: -
SDWebImageManager:管理图片下载和缓存。
-
SDImageCache:提供内存和磁盘缓存。
-
SDWebImageDownloader:处理图片的异步下载。
-
UIImageView+WebCache:为 UIImageView 提供扩展,支持直接加载网络图片。