[toc]
主要是一些视频笔记和面试时候常问到的问题记录。(持续更新)
Runtime
什么是 Runtime?它的作用是什么?
Runtime 是 Objective-C 的运行时系统,它包含一系列的 API,允许在运行时创建类、调用方法、访问属性等。其作用是实现动态消息传递和运行时类型识别。
消息传递、消息转发
消息传递
objc_msgSend(obj, SEL/@selector(aMethod)/);
1、从消息缓存列表里面通过哈希表查找对应的方法(哈希冲突怎么办?应该是通过再哈希的方式解决的)
2、在当前类方法列表中查找
对于已排序好的列表,采用二分查找算法查找方法对应执行函数
对于没有排序的列表,采用一般遍历查找方法对应执行函数
类方法是没有排序的,所以是使用遍历查找方法
3、从父类逐级查找
判断父类是否是nil
有父类
在缓存中查找
缓存中无,则从父类方法列表中查找,直至父类为nil,进入消息转发流程
消息转发
当一个对象接收到无法解读的消息时,Runtime 会调用消息转发机制。这包括三个步骤:动态方法解析、备用接收者和完整转发。开发者可以通过重载 resolveInstanceMethod
和 forwardInvocation
方法来自定义消息的处理过程。
resolveInstanceMethod:
(resolveClassMethod:)
为类添加一个方法,返回YES
forwardingTargetForSelector:
返回一个其他对象去处理这个消息(备用receiver)
forwardInvocation:
如果上面两种情况没有执行,就会执行通过forwardInvocation进行消息转发
方法替换(Method-Swizzling)
Method Swizzing是发生在运行时的,在运行时将一个方法的实现替换成另一个方法的实现;
每个类都维护着一个方法列表,即methodList,methodList中有不同的方法,每个方法中包含了方法的SEL和IMP,方法交换就是将原本的SEL和IMP对应断开,并将SEL和新的IMP生成对应关系;
RunLoop
什么是RunLoop?
RunLoop是通过内部维护的事件循环
对消息/事件进行管理
的对象
**事件循环(Event Loop)
没有消息需要处理的时候,休眠以避免资源占用;【用户态】->【内核态】
有消息需要处理的时候,立刻被唤醒【内核态】->【用户态】
RunLoop的数据结构
Runloop和线程是一一对应的
主线程的runloop自动启动,而子线程的runloop需要手动启动
Timer与RunLoop的面试题
问题:定时器有个RunLoop mode,默认是在defaultMode,scrollView滚动的时候,主线程的RunLoop会转到UITrackingRunLoopMode,这时候定时器就会失效
解决:将定时器添加到CommonMode上
思考:为什么?
NSRunloopCommonModes
- CommonMode不是实际存在的一种Mode
- 是同步source/Timer/Observer到多个mode的一种技术解决方案
Block
这篇文章讲的挺透彻
iOS-Block本质
什么是Block?
- Block是将
函数
及执行上下文
封装起来的对象
block的几种形式?
堆block(__NSMallocBlock__)
,
栈block(__NSStackBlock__)
,使用外部变量并且未进行copy操作的block是栈block
全局block(__NSGlobalBlock__)
不使用外部变量的block是全局block
block变量截获?
- 局部变量(截获其值)
基本数据类型
对象类型(连同所有权修饰符一同截获)
- 静态局部变量(指针形式截获)
- 全局变量(不截获)
- 静态全局变量(不截获)
一般block会在栈区,经过copy之后,会拷贝到堆区,栈区的block的__forwarding指针指向拷贝后的堆区的block,而堆区的__forwarding指针会指向自己
为什么要用__block修饰局部变量?
__block修饰之后的局部变量实际变成了一个结构体,它内部有一个isa指针,这个结构体会被block捕获,成为其成员变量;block内部修改的时候,实际是通过这个结构体的isa指针去修改所修饰的局部变量的值的
弱引用管理
如何添加一个weak变量到弱引用表
一个被声明为__weak的对象指针,经过编译器编译之后,调用objc_initweak()
,经过一些列的函数调用(storeWeak()
),最终在weak_register_no_lock()
函数中进行弱引用变量的添加;具体添加的位置是通过哈希算法进行位置查找,如果说查找对应位置当中已经有当前对象对应的弱引用数组,那么就把新的弱引用变量添加到这个数组当中,如果没有,重新创建一个弱引用数组,然后第0个位置添加上最新的weak指针,后面的都初始化为0或者nil。
weak如何置nil
当一个对象被dealloc
之后,在dealloc
的内部实现当中,会调用弱引用清除的相关函数weak_clear_no_lock()
,在这个函数内部实现当中会根据 当前对象指针
查找弱引用表,把当前对象相对应的弱引用(数组)都拿出来,遍历数组当中所有的弱引用指针,置为nil。
weak自动置nil的原理(简书1,做参考)
runtime维护着一个weak表即hash表,用于存储指向对象的weak指针
Weak表是Hash表,Key是所指对象的地址,Value是Weak指针地址的数组
以对象的地址作为key,去找weak指针
触发调用arr_clear_deallocating 函数 ,根据对象的地址将所有weak指针地址的数组,遍历数组把其中的数据置为nil。
weak自动置nil的原理(简书2,做参考)
一 、实现
runtime在注册类时,会布局一个weak表(hash表),key是所指对象的地址,value是weak指针的地址的数组;当对象释放时,层层调用后,通过arr_clear_deallocating释放;
二、weak实现原理步骤:通过clang可以分析源码;
objc_initWeak//初始化weak;
objc_storeWeak()//修更新指针指向,创建对应的弱引用表;
clearDeallocating//通过key找到weak数组,然后对数组里的weak指针置nil,把这个entry(入口,记录)从weak表删除;
自动释放池问题
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
NSLog(@"%@",array);
}
Q: array在什么时候释放?
A: 在当前RunLoop将要结束的时候调用AutoreleasePoolPage::pop()来对其进行释放。
(实际上在每一次的RunLoop循环当中都会在将要结束的时候对前一次创建的AutoreleasePool进行pop操作,同时会push进来一个新的AutoreleasePool)
问题拓展:
要回答这个问题需要知道RunLoop和AutoReleasePool的关系。
Runloop每次循环都是被一个AutoReleasePool包围着的,具体说每次Runloop循环将要结束的时候会释放当前runloop的内存占用。再创建好一个AutoReleasePool给下一次Runloop循环使用。(慕课网6-7
)
ViewDidLoad是在主线程执行,在该方法中创建的array会加入到当次RunLoop的AutoReleasePool中,array会在当前RunLoop将要结束的时候得到内存释放。
一般错误的回答都是viewDidLoad方法结束就释放了。
AutoreleasePool原理?
数据结构:是以栈
为节点,通过双向链表
的形式组合而成。和线程
是一一对应的。
objc_autoreleasePoolPush()
objc_autoreleasePoolPop()
objc_autorelease()
AutoreleasePool为什么可以嵌套调用?
A:多层嵌套就是多次插入哨兵对象
AutoreleasePool使用场景?
在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool
组件化
组件化的好处?
- 业务分层、解耦,使代码变得可维护;
- 有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;
- 便于各业务功能拆分、抽离,实现真正的功能复用;
- 业务隔离,跨团队开发代码控制和版本风险控制的实现;
- 模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;
- 在维护好各级组件的情况下,随意组合满足不同客户需求;(只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App)
如何实现解耦?
-
分层
基础功能组件:按功能分库,不涉及产品业务需求,跟库Library类似,通过良好的接口供上层业务组件调用;不写入产品定制逻辑,通过扩展接口完成定制;
(网络组件、弹框组件、工具组件、)
基础UI组件:各个业务模块依赖使用,但需要保持好定制扩展的设计业务组件:业务功能间相对独立,相互间没有Model共享的依赖;业务之间的页面调用只能通过UIBus进行跳转;业务之间的逻辑Action调用只能通过服务提供;
中间件:target-action,url-block,protocol-class
https://www.jianshu.com/p/464a8f1ab949
CTMeditor
- 通过反射机制利用字符串找到相对应的target然后向它发送消息
AvoidCrash
Foundation框架潜在的崩溃的危险比如:
- 将 nil 插入可变数组中会导致崩溃。
- 数组越界会导致崩溃。
- 根据key给字典某个元素重新赋值时,若key为 nil 会导致崩溃。
- ......
利用runtime的特性,使用方法替换
,在即将发生崩溃的位置给它替换成默认实现,防止崩溃,同时上报这个错误到bugly
捕获到异常之后的处理(其实就是获取出现异常的堆栈,最后以通知的形式发送出去)
/**
* 提示崩溃的信息(控制台输出、通知)
*
* @param exception 捕获到的异常
* @param defaultToDo 这个框架里默认的做法
*/
+ (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo {
//堆栈数据
NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
//获取在哪个类的哪个方法中实例化的数组 字符串格式 -[类名 方法名] 或者 +[类名 方法名]
NSString *mainCallStackSymbolMsg = [AvoidCrash getMainCallStackSymbolMessageWithCallStackSymbols:callStackSymbolsArr];
if (mainCallStackSymbolMsg == nil) {
mainCallStackSymbolMsg = @"崩溃方法定位失败,请您查看函数调用栈来排查错误原因";
}
NSString *errorName = exception.name;
NSString *errorReason = exception.reason;
//errorReason 可能为 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds
//将avoidCrash去掉
errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""];
NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg];
NSString *logErrorMessage = [NSString stringWithFormat:@"\n\n%@\n\n%@\n%@\n%@\n%@",AvoidCrashSeparatorWithFlag, errorName, errorReason, errorPlace, defaultToDo];
logErrorMessage = [NSString stringWithFormat:@"%@\n\n%@\n\n",logErrorMessage,AvoidCrashSeparator];
AvoidCrashLog(@"%@",logErrorMessage);
//请忽略下面的赋值,目的只是为了能顺利上传到cocoapods
logErrorMessage = logErrorMessage;
NSDictionary *errorInfoDic = @{
key_errorName : errorName,
key_errorReason : errorReason,
key_errorPlace : errorPlace,
key_defaultToDo : defaultToDo,
key_exception : exception,
key_callStackSymbols : callStackSymbolsArr
};
//将错误信息放在字典里,用通知的形式发送出去
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AvoidCrashNotification object:nil userInfo:errorInfoDic];
});
}
多线程相关问题
参考:https://www.jianshu.com/p/361e8a0a4e7e
- iOS中的多线程
. NSThread
. GCD
. NSOperationQueue
NSThread - 轻量级别的多线程技术,需要我们自己管理线程
需要我们手动开辟子线程,如果使用init初始化方式则需要手动启动,如果使用构造器方式初始化则会自动启动。
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数"];
// 当使用初始化方法出来的主线程需要start启动
[thread start];
// 可以为开辟的子线程起名字
thread.name = @"NSThread线程";
// 调整Thread的权限 线程权限的范围值为0 ~ 1 。越大权限越高,先执行的概率就会越高,由于是概率,所以并不能很准确的的实现我们想要的执行顺序,默认值是0.5
thread.threadPriority = 1;
// 取消当前已经启动的线程
[thread cancel];
// 通过遍历构造器开辟子线程
[NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方式"];
performSelector:withObject:afterDelay:会在内部创建一个NSTimer,然后添加到当前的RunLoop中,如果当前线程没有开启RunLoop(子线程默认没有开启RunLoop),该方法会失效
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
performSelector:withObject:没有添加timer,所以不需要添加子线程RunLoop也可以执行
GCD对比NSOperationQueue
GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象。
它们的区别
- GCD执行效率更高,而且由于队列中执行的是由block构成的任务,是一个轻量级的数据结构,写起来更方便
- GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
- NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序
- NSOperationQueue因为面向对象,所以支持KVO,可以检测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
探讨
实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选 如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持 不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销
Q:假设有这么场景:有网络请求A、网络请求B,需要AB执行完之后继续进行下一步操作,怎么使用GCD实现?
A:
- 信号量(dispatch_semaphore)
- (void)GCD_Semaphore {
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"task1, %@",[NSThread currentThread]);
sleep(1);
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"2");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"task2, %@",[NSThread currentThread]);
sleep(1);
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"3, %@",[NSThread currentThread]);
}
打印结果
2021-01-05 23:15:33 1
2021-01-05 23:15:33 task1, <NSThread: 0x600000eecd00>{number = 3, name = (null)}
2021-01-05 23:15:34 2
2021-01-05 23:15:34 task2, <NSThread: 0x600000eecd00>{number = 3, name = (null)}
2021-01-05 23:15:35 3, <NSThread: 0x600000eb01c0>{number = 1, name = main}
这里的打印结果是1->task1->2->task2->3顺序执行,相当于加锁?
- dispatch_group(基于dispatch_semaphore实现的)
- (void)GCD_Group {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"task1");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"task2");
dispatch_group_leave(group);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"notify");
});
NSLog(@"===");
}
- dispatch_barrier_async(同时也可以用来实现多读单写、加锁、设置最大线程数)
- (void)GCD_barrier {
dispatch_queue_t queue = dispatch_queue_create("barrier_queue", DISPATCH_QUEUE_CONCURRENT);
// 注意dispatch_barrier_async只在自己创建的并发队列中才有效,在global_queue,串行队列上效果跟dispatch_(a)sync一样
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"task1");
});
dispatch_async(queue, ^{
NSLog(@"task2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"barrier");
});
NSLog(@"===");
dispatch_async(queue, ^{
NSLog(@"task3");
});
dispatch_async(queue, ^{
NSLog(@"task4");
});
}
多读单写
- (id)dataForKey:(NSString *)key {
__block id data;
//同步读取指定数据
dispatch_sync(self.concurrentQueue, ^{
data = [self.dict objectForKey:key];
});
return data;
}
- (void)setData:(id)data forKey:(NSString *)key {
// 异步栅栏调用设置数据
dispatch_barrier_async(self.concurrentQueue, ^{
[self.dict setObject:data forKey:key];
});
}
单例模式
这篇文章介绍的还不错
https://www.jianshu.com/p/a92c0283f243
什么是单例模式?
简单来说,一个单例类,在整个程序中只有一个实例,并且提供了类方法供全局调用,在编译时初始化这个类,然后一直保存在内存中,直到App退出时由系统自动释放这一部分内存
系统为我们提供的单例类有哪些?
- UIApplication(应用程序实例类)
- NSNotificationCenter(消息中心类)
- NSFileManager(文件管理类)
- NSUserDefaults(应用程序设置)
- NSURLCache(请求缓存类)
- NSHTTPCookieStorage(应用程序cookies池)
单例的存放位置
全局区
变量的存放位置
位置 | 存放的变量 |
---|---|
栈 | 临时变量(由编译器管理自动创建/分配/释放的,栈中的内存被调用时处于存储空间中,调用完毕后由系统系统自动释放内存) |
堆 | 通过alloc、calloc、malloc或new申请内存,由开发者手动在调用之后通过free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存,在ARC模式下,由系统自动管理。 |
全局区域 | 静态变量(编译时分配,APP结束时由系统释放) |
常量 | 常量(编译时分配,APP结束时由系统释放) |
代码区 | 存放代码 |
创建一个单例的方式
- 同步锁:NSLock
- @synchronized(self) {}
- 信号量 dispatch_semaphore_t
- 条件锁 NSConditionLock
- dispatch_once_t
单例注意事项-保证单例只被初始化一次
- 对alloc、new、copy、mutableCopy的处理
因为alloc] init 和 new都是调用的+ (instancetype)allocWithZone:(struct _NSZone *)zone
方法,那么我们可以重写这个方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
NSLog(@"allocWithZone");
@synchronized (self) {
if (instance == nil) {
instance = [super allocWithZone:zone];
return instance;
}
}
return nil;// 这里返回nil,那么后面初始化的对象就是nil了,返回instance的话其实就是同一个单例对象了
}
- 直接禁用对应的方法
+(instancetype) new __attribute__((unavailable("OneTimeClass类只能初始化一次")));
-(instancetype) copy __attribute__((unavailable("OneTimeClass类只能初始化一次")));
-(instancetype) mutableCopy __attribute__((unavailable("OneTimeClass类只能初始化一次")));
(用NS_UNAVAILABLE也可以,但是这个就没有提示了)
还要了解一下FMDB
NSMutableArray数据结构分析
普通C数组是是一段能被方便读写的连续内存空间,使用一段线性内存空间的一个最明显的缺点是,在下标0插入一个元素时,需要移动其它元素,即memmove的原理:
https://blog.csdn.net/qq_27909209/article/details/82689322
移除元素时同理也要移动其它元素;
当数组非常大的时候可能就会出现问题。
NSMutableArray是一个类簇,[NSMutableArray new]实际返回的是__NSArrayM
(lldb) po [[ NSMutableArray new] class]
__NSArrayM
__NSArrayM
使用了环形缓冲区 (circular buffer),这个数据结构相当简单,只是比常规数组或缓冲区复杂点。环形缓冲区的内容能在到达任意一端时绕向另一端。
环形缓冲区有一些非常酷的属性。尤其是,除非缓冲区满了,否则在任意一端插入或删除均不会要求移动任何内存。我们来分析这个类如何充分利用环形缓冲区来使得自身比 C 数组强大得多。我们在这里知道了几个有趣的东西:在删除的时候不会清除指针。最有意思的一点,如果我们在中间进行插入或者删除,只会移动最少的一边的元素。
NSDictionary数据结构
在内部,字典使用哈希表来组织其存储,并在给定相应键的情况下快速访问值
Crash类型
- Signal
- NSException
bugly需要使用符号表解析应该是用了捕捉了Signal异常,Signal异常是需要配合符号表才能解析的,NSException的话可以直接拿到崩溃信息
关于RunLoop防止崩溃
https://cloud.tencent.com/developer/article/1192474
这还有一篇文章可以参考(关于Crash收集)
·
http://www.cocoachina.com/articles/12301
图像显示原理
CPU生成位图(bitmap)经由总线在合适的时机传给GPU;GPU拿到位图之后会做相应位图的渲染,包括纹理的合成,之后把结果放到帧缓冲区(Frame Buffer
),由视频控制器,根据VSync信号在指定时间之前去提取帧缓冲区当中的内容,最终显示到手机屏幕上。
如何定位内存泄漏?
-
静态分析 cmd+shift+B
会报Warning,定位到对应位置修改即可
Instruments Leak(cmd+i)
首先需要对工程进行设置
Build Settings - Debug Infomation Format 设置成DWARF with dSYM File
其次需要在真机上运行
这样子才能定位到Xcode代码具体位置
具体操作这里就不记录了
冷启动
pre-main
1、减少动态库、合并一些动态库(定期清理不必要的动态库)
2、减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
3、减少C++虚函数数量
4、Swift尽量使用struct
5、用+initialize方法和dispatch_once取代所有的attribute((constructor))、>C++静态构造器、Objc的+load
main
1、在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在didFinishLaunching方法中
2、监控、埋点、基础功能设置 在willFinishLaunching
3、定位、网络配置、基础SDK 、必须的数据 在 didFinishLaunching
首页渲染
1、避免使用xib
2、首页一般关联业务较多,优先请求和渲染用户可见的页面
3、业务组件,业务相关配置等,在首页渲染完成之后
内存管理方案
NONPOINTER_ISA
散列表
TaggedPointer