1. OC的本质是什么?
- OC的面向对象是基于C/C++的结构体实现的。
2. 一个NSObject对象占用多少内存?
- 系统分配了16个字节给NSObject对象。
//通过malloc_size函数获得,该函数返回的是系统给实例对象分配的内存大小,在64bit环境下,所获得的数值是16个字节的倍数。
Person *person = [[Person alloc] init];
NSLog(@"%zu", malloc_size((__bridge const void *)person));
//可以通过allocWithZone查看源码,会调用calloc(1, size)函数;size_t size = cls->instanceSize(extraBytes);
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- 但NSObject对象只使用了8个字节的空间。
//64bit环境下,可以通过class_getInstanceSize函数获得;
NSLog(@"%zu", class_getInstanceSize([Person class]));
//通过class_getInstanceSize检查源码,是通过alignedInstanceSize的内存对齐原理,返回实例对象的成员变量在内存中所占字节的大小;
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
sizeof
- sizeof既是一个关键字,也是一个运算符(操作符),但它不是函数。它主要用来计算某一个量在当前系统中所需占用的字节数。
//64bit
size_t i = sizeof(int);//4个字节
size_t longInt = sizeof(NSInteger);//8个字节
NSLog(@"%zu, %zu", i, longInt);
3. 对象的isa指针指向哪里?
- interface(实例)对象的isa指针指向class对象。
- class对象的isa指针指向meta-class对象。
- meta-class对象的isa指针指向基类的meta-class对象。
// instance对象,实例对象
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
// class对象,类对象
// class方法返回的一直是class对象,类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = object_getClass(object1);
Class objectClass4 = object_getClass(object2);
Class objectClass5 = [NSObject class];
// meta-class对象,元类对象
// 将类对象当做参数传入,获得元类对象
Class objectMetaClass = object_getClass(objectClass5);
isa详解
isa详解 – 位域
当通过isa指针获取到类或者元类对象在内存中的地址时,isa要进行位运算,才能得到类或者元类在内存中的地址。
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
//isa进行位运算:isa & ISA_MASK
return (Class)(isa.bits & ISA_MASK);
#endif
}
4.对象的superclass指针指向哪里?
- subclass类对象的superclass指针指向superclass类对象,superclass类对象的superclass指针指向rootclass类对象,rootclass类对象的superclass指针指向nil。
- subclass元类对象的superclass指针指向superclass元类对象,superclass元类对象的superclass指针指向rootclass元类对象,rootclass元类对象的superclass指针指向rootclass类对象,rootclass类对象的superclass指针指向nil。
5. OC类信息存放在哪里?
- interface(实例) 对象中,存放着类的成员变量的具体值
- class 类对象中,存放着成员变量,属性,协议,实例方法的信息。
- meta-class 元类对象,存放着类方法的信息。
OC对象分为:interface(实例)对象,class(类)对象,meta-class(元类)对象。
6. IOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 给对象添加KVO监听,
- IOS在运行过程中,通过runtime API动态生成该对象的子类(NSKVONotifiying_类名),并且让interface(实例)对象的isa指针,指向这个子类。
- 当修改interface (实例)对象的属性时,会调用Foundation的_NSSetxxxValueAndNotifiy函数。
- NSSetxxxValueAndNotifiy函数中,会调用willChangeValueForKey, 然后是父类的set方法,再然后是didChangeValueForKey.
- didChangeValueForKey中,内部会触发Observer 的监听方法(observeValueForKeyPath: ofObject: change: context:)。
7. 如何手动触发KVO?
- 手动调用willChangeValueForKey:和didChangeValueForKey:。
8. 直接修改成员变量会触发KVO吗?
- 不会触发,因为KVO的本质是触发set方法,成员变量没有set方法。
9. 通过KVC修改属性会触发KVO么?
- 会触发KVO。
-[1] 当对象中是有属性的,属性是有set和get方法的。KVO的本质是触发set方法。
-[2] 当对象中没有属性,只有成员变量时。当通过KVC修改成员变量的值时,runtime会自动触发willChangeValueForKey: 和 didChangeValueForKey:. 在didChangeValueForKey:中会触发Observe的监听方法。
10. KVC的赋值和取值过程是怎样的?原理是什么?
-
KVC 赋值:setValue: ForKey:
-
KVC取值: valueForKey:
11. Category的使用场合是什么?
- 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处;
- 可以减少单个文件的体积;
- 可以把不同的功能组织到不同的category里;
- 可以由多个开发者共同完成一个类;
- 可以按需加载想要的category 等等。
- 声明私有方法。比如在父类中,该方法是私有的,但是想在子类中调用该方法时,可以给这个子类添加一个分类,并在分类中声明该私有方法。
- 模拟多继承。
- 把framework的私有方法公开。
12. Category的实现原理?
- Category编译之后的底层结构是struct category_t,里面存储着分类的实例方法、类方法、属性、协议信息。
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。
- 在类中和分类中都包含有相同的方法时,优先调用分类中的方法(后编译,先调用),因为在程序运行时首先合并所有分类的数据(实例方法、类方法、属性、协议),然后把合并后的分类数据,插入到类原来的数据的前面。运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止。
13. Category和Class Extension的区别是什么?
- Class Extension在编译的时候,它的数据就已包含在类信息中。
- Category是在运行时,才会将数据合并到类信息中。
注意:
- extension可以添加实例变量,而category是无法直接添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)
14. Category有+load方法吗?+load方法是什么时候调用的?+load方法能继承吗?
- 有+load方法。
- +load方法是在runtime加载类、分类的时候调用的。
- 每个类、分类的+load方法,在程序运行过程中只调用一次。
- 调用顺序:
- 先调用类的+load方法。按照编译先后顺序调用(先编译,先调用);调用子类的+load方法之前先调用父类的+load方法。
- 再调用分类的+load方法。按照编译先后顺序调用(先编译,先调用)。
- +load方法可以继承。但是一般情况下不会主动去调用+load方法,都是让系统自动调用。
注意:+load方法是直接通过函数指针,获取到这个函数地址,分开来直接调用。所以在类中和分类都会调用load方法。不是通过消息发送机制(objc_msgSend)调用的。
15. Category有+initialize方法吗? +initialize方法是什么时候调用?+initialize方法能继承吗?
- 有+initialize方法。
- +initialize方法是在类第一次收到消息时调用。
- 先调用父类的+initialize方法,再调用子类的+initialize方法。(先初始化父类,再初始化子类,每个类只会初始化一次)
- 如果子类没有实现+initialize方法,会调用父类的+initialize方法,所以父类的+initialize方法可能会被调用多次,因为初始化子类时,先调用父类的+initialize方法,再调用子类的+initialize方法,但是+initialize方法在子类中没有实现,就会通过superclass指针找到父类,父类再在方法列表中找到该方法,并调用。
- 如果分类实现了+initialize方法,则该类在调用该方法时,直接调用分类的+initialize方法。
- +initialize方法可以继承,优先调用父类的+initialize方法。
注意:+initialize方法是通过objc_msgSend(消息发送机制)进行调用的。
16. +load方法和+initialize的区别是?
- 调用方式:
- +load方法是通过函数地址,来调用。
- +initialize方法是通过objc_msgSend(消息发送机制)来调用。
- 调用时刻:
- +load方法是在runtime时,加载类/分类时被调用,每个类/分类在程序运行过程中只调用一次。
- +initialize方法是在类第一次收到消息时调用,在程序运行过程中,如果该类没有收到消息,那么+initialize方法也不会被调用。
- category调用顺序:
- +load方法
- 先调用类的+load方法。先编译的类,优先调用+load方法;调用子类的+load方法之前,会先调用父类的+load方法。
- 再调用分类的+load方法。先编译的分类,优先调用+load方法。
- +initialize方法
- 先调用父类的+initialize方法。(父类的+initialize方法可能最终被多次调用)
- 再调用子类的+initialize方法。(类的+initialize方法,可以被分类覆盖)
- +load方法
17. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果。
- 则需要使用runtime API 中的关联对象objc_setAssociatedObject添加关联对象,objc_getAssociatedObject 是获取关联对象的值。如果想移除关联对象,可以给该关联对象传nil. 或者想移除所有关联对象,可以调用该objc_removeAssociatedObject函数。
18. block的原理是怎样的?本质是什么?
- 是封装了函数调用以及调用环境的OC对象。
- 本质上也是一个OC对象,它内部也有isa指针。
知识扩展:
- block有三种类型:(最终都是继承自NSBlock)
- NSGlobalBlock 在内存的数据区域.data区,是没有访问auto局部变量,当调用copy后,没有任何的改变,依然在数据区域。
- NSStacklBlock 在内存的栈中,是访问了auto局部变量,当调用copy后,从栈中复制到堆中。
- NSMallocBlock 在内存的堆中,__NSStacklBlock__调用了copy,当调用copy后,引用计数器+1.
19. __block的作用是什么?有什么使用注意点?
- 作用:
- __block可以用于解决block内部无法修改auto 局部变量值的问题。(因为auto局部变量在block中是值传递捕获,auto局部变量是在栈中,程序执行完该作用域,就会被销毁,所以需要通过__block把auto局部变量放在堆中,当auto变量加上__block修饰符时,会把该变量封装成一个block结构体,在这个结构体中有个forwarding指针,指向自身,并且这个结构体中包含有auto变量)
- 编译器会将__block变量包装成一个对象。
- 注意点:
- __block不能修饰全局变量和static静态变量。
- 当__block变量在栈上时,不会对指向的对象产生强引用。
- 当__block变量被copy到堆时:
1. 会调用__block变量内部的copy函数。
2. copy函数内部会调用_Block_object_assign函数。
3. _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。(注意:这里仅限于ARC是会强引用(retain),MRC时不会retain) - __block变量从堆上移除:
1. 会调用__block变量内部的dispose函数。
2. copy函数内部会调用_Block_object_dispose函数。
3. _Block_object_dispose函数会自动释放指向的对象。(release)
20. block的属性修饰词为什么是copy?使用block有哪些使用注意?
- block一旦没有进行copy操作,就不会在堆上,随时会被销毁。在堆上可以控制它的生命周期。
- 使用注意:防止循环引用的问题。
- 循环引用:在对象中持有block属性,并在block中又持有该对象本身,会造成循环引用。
- 循环引用的问题解决:
1. 在ARC中
方法一:可以使用__weak修饰符(不会产生强引用,指向的对象销毁时,会自动让指针置为nil)。
方法二:可以使用__unsafe_unretained修饰符(不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变)。
方法三:可以使用__block修饰符(必须调用block,并在执行完block中的内容后,把修饰的该对象置为nil)。
2. 在MRC中(在MRC情况下,不支持弱引用__weak)在这里插入代码片
方法一:可以使用__unsafe_unretained修饰符(不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变)。
方法二:可以使用__block修饰符(因为在MRC中,__block不会对持有对象进行强引用retain)。
21. block在修改NSMutableArray,需不需要添加__block?
- 不需要,修改内容也是对数组的使用,只有对对象赋值的时候才需要__block
22. OC的消息机制?
- OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名),objc_msgSend([NSObject class], @selector(init));
- objc_msgSend底层有3大阶段:
- 消息发送(当前类、父类中查找):
- 动态方法解析:
“v@:” 请访问IOS Type encode
- 消息转发:
23. 以下打印结果分别是什么?
分别有两个对象:
-
问题1
打印结果为:MJStudent,MJStudent,MJPerson,MJPerson。- 消息接收者仍然是子类对象
- 从父类开始查找方法实现
- (Class)class{ return object_getClass(self);//self为消息接收者 } - (Class)superClass{ return class_getSuperclass(object_getClass(self));//self为消息接收者 }
-
问题2
打印结果:1,0,0,0
因为消息接收者都是类对象,所以通过以下源码可知,当消息接收者为类对象时,是来比较它们的元类对象。所以res2,res3,res4的结果都是0,因为比较者(cls)是类对象而不是元类对象。
res1的结果是1,是因为基类的元类对象通过superclass指针获取到的是基类的类对象,NSPerson,NSObject的基类的元类对象通过superclass指针获取到的是基类的类对象NSObject。
-
问题3:
打印结果为:my name’s <ViewController: 0x7fada5f06d50>
在栈中存储情况请看下图:
24. isMemberOfClass 和 isKindOfClass的区别:
- isMemberOfClass:是判断消息接收者(self)是不是这个类、元类对象。
- isKindOfClass:是判断消息接收者(self)是不是这个类、元类对象和它们的类、元类的父类。
25. 什么是Runtime?平时项目中有用过嘛?
- OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时进行;OC的动态性就是由Runtime来支撑的,Runtime是一套C语言的API,封装了很多动态性相关的函数;平时编写的OC代码,底层都是转换成了Runtime API进行调用。
- 用法:
- 利用关联对象(AssociatedObject)给分类添加属性
- 遍历类的所有成员变量、属性(修改UI控件的私有控件、字典转模型、自动归档解档)
- 交换方法实现(交换系统自带的方法)
- 利用消息转发机制解决方法找不到的异常问题
- 等等。
26. 讲讲RunLoop, 项目中有用到吗?
- 控制线程生命周期(线程保活)。
- (instancetype)init { self = [super init]; if (self) { self.isStoped = NO; __weak typeof(self) weakSelf = self;//防止循环引用 self.thread = [[CCThread alloc] initWithBlock:^{ //--------OC语言运行RunLoop---------- //往RunLoop中添加source //因为Mode中没有任何Source0\Source1\Timer\Observer,RunLoop就会立马退出。 [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; //运行RunLoop //[[NSRunLoop currentRunLoop] run];//会进入无休止的loop,无法停止,它专门用于开启一个永不销毁的线程(NSRunLoop) while (weakSelf && !weakSelf.isStoped) {//增加while循环,使RunLoop不会执行完就退出 //[NSDate distantFuture]代表很长的时间,使RunLoop永不过期 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } //--------C语言运行RunLoop----------- //创建上下文(需要初始化一下结构体,因为auto局部变量,如果不初始化,会造成乱码的情况) CFRunLoopSourceContext context = {0}; //创建source CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context); //往RunLoop中添加source //因为Mode中没有任何Source0\Source1\Timer\Observer,RunLoop就会立马退出。 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); //销毁source CFRelease(source); //启动RunLoop /** 第二个参数:RunLoop过期时间 第三个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop; 设置为false,代表执行完source后不会退出当前loop。 */ CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false); NSLog(@"----end------"); }]; [self.thread start]; } return self; }
CFRunLoopStop(CFRunLoopGetCurrent());//停止/退出RunLoop。
- 解决NSTimer在滑动时停止工作的问题。
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
- 开启一个NSTimer时,它是在RunLoop的kCFRunLoopDefaultMode模式;当UI界面ScrollView滑动的时候,RunLoop的currentModel就切换为UITrackingRunLoopMode模式;
- RunLoop启动时只能选择一种模式运行,并且顶层的 RunLoop 的”commonModeItems”会被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。又因为kCFRunLoopDefaultMode和UITrackingRunLoopMode这两种模式的commonModes都被标记为NSRunLoopCommonModes。
- CommonModes:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
所以有以下两种解决方案:
- 将NSTimer都加入到这两种模式中。
- 将NSTimer标记为NSRunLoopCommonModes。
- 监控应用卡顿。
- 性能优化。
27. RunLoop内部实现逻辑?
28. RunLoop和线程的关系?
29. timer与RunLoop的关系?
- Runloop里面有多种mode,每种mode包含多个Timer,Source,Observer,一般timer属于common mode,它是一种标记,包含default和Tracking两种mode。
30. 程序中添加每3秒响应一次的NSTimer, 当拖动tableview时timer可能无法响应要怎么解决?
31. RunLoop是怎么相应用户操作的,具体流程是什么样的?
- Source1捕捉用户操作,然后把这个事件包装成事件队列EventQueue,然后放到source0中处理
32. 说说RunLoop的几种状态
33. RunLoop的mode作用是什么?
34. 你理解的多线程是?
- 在一个任务中,可以开启多个线程同时进行相应的操作。
35. 请简单说明多线程技术的优点和缺点?
- 优点:
- 能适当提高程序的执行效率。
- 能适当提高资源利用率(CPU、内存利用率)。
- 缺点:
- 创建线程需要时间开销:大约需要90毫秒的创建时间。
- 创建线程需要空间开销:IOS下主要成本包括,内核数据结构(大概1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K)。
- 如果开启大量的线程,反而会降低程序的性能。
- 线程越多,CPU的调度线程上的开销就越大。
- 线程设计更加复杂;比如线程之间的通信、多线程的数据共享。
36. 请简单说明线程和进程,以及他们之间的关系?
- 进程:是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
- 线程:线程是CPU调度的最小单位,1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程),进程(程序)的所有任务都在线程中执行。
- 进程和线程的关系:
- 进程是CPU调度(执行任务)和分配资源的基本单位。
- 线程是CPU调度的最小单位,线程没有资源。
- 一个进程中可以开启多条线程,且至少要有一条线程(主线程)。
- 所有的线程均共享进程的资源。
37. IOS多线程方案有哪几种?你更倾向于哪一种?
说明:
1.在c语言中,没有对象的概念,对象类型是以-t/Ref结尾的;
2.c语言中的void * 和OC的id是等价的;
3.在混合开发时,如果在 C 和 OC 之间传递数据,需要使用 __bridge 进行桥接,桥接的目的就是为了告诉编译器如何管理内存,MRC 中不需要使用桥接;
4.在 OC 中,如果是 ARC 开发,编译器会在编译时,根据代码结构, 自动添加 retain/release/autorelease。但是,ARC 只负责管理 OC 部分的内存管理,而不负责 C 语言 代码的内存管理。因此,开发过程中,如果使用的 C 语言框架出现retain/create/copy/new 等字样的函数,大多都需要 release,否则会出现内存泄漏
-
pthread:
-
NSThread:
-
GCD:
队列:- 并发队列:多个任务并发(同时)执行。
全局并发队列:
创建并发队列:
全局并发队列和并发队列的区别:- 全局并发队列在整个应用程序中本身是默认存在的并且对应有高优先级、默认优先级、低优先级和后台优先级一共四个并发队列,我们只是选择其中的一个直接拿来用。而Create函数是实打实的从头开始去创建一个队列。
- 在IOS6.0之前,在GCD中凡是使用了带Create和retain的函数在最后都需要做一次release操作。而主队列和全局并发队列不需要我们手动release。当然了,在IOS6.0之后GCD已经被纳入到啦ARC的内存管理范畴中,即便是使用retain或者create函数创建的对象也不再需要开发人员手动释放,我们像对待普通OC对象一样对下GCD。
- 在使用栅栏函数的时候,苹果官方明确规定栅栏函数只有在和使用create函数自己创建的并发队列一起使用的时候才有效。
- 串行队列:一个任务执行完毕后,再执行下一个任务。
- 主队列:所有的任务都在主线程中串行执行
同步函数:dispatch_sync
- 在当前线程执行任务,不具备开新线程的能力。
- 任务执行的方式: 同步。
- 无论是添加到并发队列中,还是手动创建的串行队列中,都是按照串行队列执行任务。
- 当往当前线程串行/主队列线程中加入dispatch_sync函数时,会卡主当前线程,产生死锁。因为当前队列是串行队列,任务执行是按照一个任务执行完后,再继续执行下一个任务,但是dispatch_sync函数是需要立即执行任务,串行队列中任务还未执行完,所以会造成死锁,卡主当前线程。
异步函数:dispatch_async
- 在新的线程执行任务,具备开启新的线程。
当在主线程调用dispatch_async异步函数并放在串行/并发队列中时,开辟新的线程。
当在子线程调用dispatch_async异步函数并放在串行队列中时,不会开辟新的线程。
GCD快速迭代函数:dispatch_apply
- 注意,迭代函数不能放在主队列中,如果放在主队列中,会造成死锁。
GCD异步栅栏函数:dispatch_barrier_async
- 注意不能使用串行队列和全局并发队列,如果使用啦全局并发队列,异步栅栏失效,不起作用,效果等同于异步函数dispatch_async。
GCD队列组函数:dispatch_group_async
- 队列组是把放入该group的队列中的任务都执行完成后,收到通知,然后执行新的任务。
- 并发队列:多个任务并发(同时)执行。
-
NSOperation
队列:- 主队列:[NSOperationQueue mainQueue];
特点:串行队列,和主线程相关(凡是放在主队列中的任务都在主线程中执行)。 - 自定义队列:NSOperationQueue *queue = [[NSOperationQueue alloc] init];
特点:默认是并发队列,但是可以控制让它变成一个串行队列。队列中可以设置最大并发数量(maxConcurrentOperationCount),默认值为-1(表示为一个最大值,不受限制),可以设置最大并发数量为1,就该队列变成串行队列啦,当最大并发数量为0时,不执行任务。当任务数量>1时,设置最大并发数量则无效,因为当任务数量>1时,就会开启子线程和当前线程一起运行。
NSInvocationOperation:
NSBlockOperation:
//暂停 YES //只能暂停当前操作后面的操作,要等待当前操作完成后,才能暂停。 [queue setSuspended:YES]; //恢复 NO [queue setSuspended:NO]; //取消 //只能取消等待中的操作,当前的操作无法取消。 [queue cancelAllOperations];
- 主队列:[NSOperationQueue mainQueue];
38. 请简单说明主线程的作用,以及使用注意点?
- 主线程的作用:
- 显示\刷新UI界面。
- 处理UI事件(比如点击事件、滚动事件、拖拽事件等)。
- 主线程使用注意点:
- 不要将耗时操作放在主线程中执行,不然可能会卡主UI,导致UI不流畅,出现卡顿的情况。
- UI操作必须放在主线程中处理。
39. 请简单列出NSThread线程的几种状态,并说明状态转换的逻辑?
- 线程的状态:
新建-就绪-运行-阻塞-死亡 - 切换条件:
当"创建"了线程对象之后,线程处于“新建状态”,此时会在内存中创建线程对象分配资源。
当“启动”线程后,线程处于“就绪状态”,此时线程对象会被移到可调度线程池中。“新建->就绪”
当CPU“调度到该线程”的时候,线程处于“运行状态”,当CPU调度其他线程的时候(时间片过完)线程会从运行状态切换到就绪状态。就绪<->运行
当线程需要“等待同步锁或者是被调用了sleep方法”的时候,线程会从运行状态切换到“阻塞状态”,此时在内存中线程对象会被从可调度线程池中移出,处于阻塞状态的线程无法工作。“运行->阻塞”
当线程“解除同步锁或者是sleep方法到时间”,那么线程会从阻塞状态切换到“就绪状态”,此时线程对象会被移入到可调度线程中。“阻塞->就绪”
当线程“任务执行完毕或者是异常退出”的时候,线程进入到“死亡状态”,在内存中分配的资源会被收回,线程进入到死亡状态之后,再也不能有任何的其他状态切换,是为终结态。
40. 请简单说明如何简单的解决多线程访问同一块资源造成的线程安全的问题,以及注意点?
- 解决方法:对特定的代码进行加锁。
如何加互斥锁:@synchronized(锁对象){//需要加锁的代码}。 - 加锁的注意点:
- 加锁需要消耗大量的CPU资源。
- 注意加锁的位置。
- 注意加锁的前提条件。
- 注意加锁的锁对象。锁定一份代码只用一把锁,用多把锁是无效的。
41. GCD和NSOperation的对比?
- CGD是纯C语言的API,而操作队列则是OC的对象对象。
- 在GCD中,任务用块(block)来表示,而块是个轻量级的数据结构;相反操作队列中的(操作)NSOperation则是个更加重量级的OC对象。
42. NSOperation和NSOperationQueue的好处有?
- NSOperationQueue可以方便调用cancel方法来取消某个操作,而GCD中的任务是无法被取消的(安排好任务之后就不管了)。
- NSOperation可以方便的指定操作间的依赖关系。
- NSOperation可以通过KVO提供对NSOperation对象的精细控制(如监听当前操作是否被取消或是否已经完成等)。
- NSOperation可以方便的指定操作优先级。操作优先级表示此操作与队列中其他操作之间的优先关系,优先级高的操作先执行,优先级低的后执行。
- 通过自定义NSOperation的子类可以实现操作重用。
43. 线程安全,线程同步方案?
操作同一个资源,必须要保持是同一把锁,如果不是同一把锁,则加锁无效。并且加锁后,在任务执行完后,必须解锁,要不然会造成死锁。
- OSSpinLock(导入头文件#import <libkern/OSAtomic.h>):叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。目前已经不再安全,可能会出现优先级反转问题。(如果等待锁的线程的优先级较高,它会一直占用CPU资源,优先级低的线程就无法释放锁)
- os_unfair_lock(导入头文件#import <os/lock.h>):叫做“互斥锁”,用于取代不安全的OSSpinLock,从IOS10开始才支持,从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。
- pthread_mutex_t(导入头文件#import <pthread.h>):叫做“互斥锁”,等待锁的线程会处于休眠状态。
递归锁:允许同一个线程对一把锁进行重复加锁。 - NSLock:对pthread_mutex_t的锁封装。
- NSRecursiveLock::对pthread_mutex_t的的递归锁封装。
- NSCondition:对pthread_mutex_t的cond条件和mutex锁封装。
- NSConditionLock:对pthread_mutex_的cond条件和mutex锁封装。会根据内部存储的条件condition的值,进行加锁。
- 在同步串行队列中开启子线程执行任务。
- 使用信号量dispatch_semaphore_t控制线程最大并发数量:
- @synchronized:是封装了mutex的递归锁。
45. 自旋锁与互斥锁的比较?
46. atomic和nonatomic的区别?
- atomic
在IOS中,是默认的。系统自动生成的getter/setter方法会进行加锁,同步操作,在getter/setter方法内部中保证线程安全,但是不能保证多个线程访问时的安全。会保证 CPU 能在别的线程来访问这个属性之前,先执行完当前流程。速度不快,因为要保证操作整体完成。 - nonatomic
在IOS中,不是默认的。系统自动生成的getter/setter方法不会进行加锁,同步操作,更快,线程不安全,如有两个线程访问同一个属性,会出现无法预料的结果。
47. 多线程文件读写安全,多读单写?
- 使用读写锁pthread_rwlock_t:是“互斥锁”。
- 异步函数dispatch_async进行读的操作和异步栅栏dispatch_barrier_async进行写的操作:读写操作必须保证在同一个手动创建的并发队列中。
48. 请问下面题的打印结果是?
- 打印结果为:1,3。
因为performSelector: withObject: afterDelay: 方法的本质是往Runloop中添加定时器,子线程默认没有启动Runloop,所以定时器不工作。
- 打印结果为:1。
因为[thread start]执行后,就直接进入thread的block,然后block调用完,销毁。当再次往thread线程中添加方法test调用时,该thread线程已被销毁,所以不执行test的方法。
要使test方法能够在线程thread中执行,要在thread中添加Runloop,使线程处于保活的状态,不会被销毁。
49. 使用CADispaly、NSTimer有什么注意点?
// 保证调用频率和屏幕的刷帧频率一致,60FPS
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
- CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用.
解决方案:- 对于CADisplayLink、NSTimer的target对象为一个弱引用对象,但是不能使用__weak typeof(self) weakSelf = self,因为target传入的只是对象的地址,self本身就是一个强引用,所以无效;
创建一个代理对象(NSProxy)的子类,子类中有个弱引用属性,该属性持有self对象,然后用消息转发机制。
该子类继承NSProxy的性能比继承NSObject的性能好,因为NSProxy在进行消息发送时,不会遍历父类,直接进入动态方法解析和消息转发。 - 但是该语句在block中时有效的,因为block会根据对象的修饰符来产生相应的强引用和弱引用对象。
- 对于CADisplayLink、NSTimer的target对象为一个弱引用对象,但是不能使用__weak typeof(self) weakSelf = self,因为target传入的只是对象的地址,self本身就是一个强引用,所以无效;
- NSTimer和CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer和CADisplayLink不准时。
解决方案:
可以使用GCD定时器,会更加的准时,因为GCD不是依赖RunLoop,而是由内核决定
50. 介绍下内存的几大区域?
内存地址由低到高的顺序是:
- 内存保留区。
- 代码段:编译之后的代码。
- 数据段:
- 字符串常量:比如NSString *str = @“123”
- 已初始化数据:已初始化的全局变量、静态变量等
- 未初始化数据:未初始化的全局变量、静态变量等
- 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大。
- 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小。
- 内核区。
51. 什么是TaggedPointer?
52. 以下两端代码能发生什么事?又什么区别?
- 以下这段代码中self.name是__NSCFString类对象。
该段代码执行,程序崩溃,报野指针异常(Thread 7: EXC_BAD_ACCESS),因为self.name的赋值是调用-(void)setName()方法,当调用该方法进行赋值时首先对_name进行release,然后在对_name进行copy或retain。但以上代码是在多个子线程中,同时访问一块资源,就会造成野指针的情况。
解决方案:- 就是使用线程同步,对这个资源进行加同步锁。(建议使用)
- 设置该属性为原子性(atomic)属性。(不建议使用,因为原子性是自动给set/get方法里面加上了线程同步锁,只能保证访问set/get方法时的线程安全,但是不能保证在外部访问该属性是安全的。)
- 以下这段代码中self.name是NSTaggedPointerString类对象。
这段代码能够正常的执行,不会出现任何问题。因为self.name是NSTaggedPointerString类对象,TaggedPointer的指针里面存储的数据是:Tag + Data,也就是将数据直接存储在指针中。
53. 讲一下你对IOS内存管理的理解?
- 在IOS中,使用引用计数来管理OC对象的内存。
- 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间.
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1.
- 内存管理的经验总结:
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放。
- 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1。
- 可以通过以下私有函数来查看自动释放池的情况
- extern void _objc_autoreleasePoolPrint(void);
- 在arm64之后,引用计数器是直接存储在优化过的isa指针中的extra_rc(引用计数器减1)中,当extra_rc存储满后,就把has_sideTable_rc变成1,然后把引用计数存储在SideTable中的refcnts的散列表中。
54. 什么是自动释放池(@autoreleasepool)?
- 自动释放池是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放,自动释放池本事销毁的时候,池子里面所有的对象都会做一次release操作。
- 自动释放池的主要底层数据结构是:_AtAutoreleasePool、AutoreleasePoolPage。
- 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址。
- 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
- 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。
- 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
- id *next指向了下一个能存放autorelease对象地址的区域。
55. 自动释放池(autoreleasepool)什么时候创建,什么时候销毁?
- App启动后,系统在主线程RunLoop 里注册两个Observser,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件是:
Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush() 创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。 - 第二个 Observer 监视了两个事件:
1. _BeforeWaiting(准备进入休眠) 时,调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
2. _Exit(即将退出Loop) 时,调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 优先级最低,保证其释放池子发生在其他所有回调之后。
- 第一个 Observer 监视的事件是:
56. ARC都帮我们做了什么?
- ARC利用LLVM(编译器)的特性,自动在合适的地方帮我们生成retain、release、autorelease的代码。
- 像弱引用的存在,需要runtime的支持的,在程序运行时中,监控弱引用的存在,然后销毁这些弱引用。
- LLVM和Runtime相互协作。
57. 方法里有局部对象,出了方法后会立即释放吗?
- 如果ARC生成的代码是在这个方法结束之前对对象添加release代码时,是会被立即释放。
- 如果ARC生成的代码是给对象加了autorelease代码时,那么对象不会立即释放,要等Runloop睡眠或结束之前才被释放。
58. Copy和MutableCopy的区别?
- Copy:对不可变对象进行copy操作是指针拷贝,不会产生新的对象,是浅拷贝;对可变对象进行copy操作是值拷贝,会产生新的对象,是深拷贝。当copy作为可变对象的修饰符时,就会把这个可变对象变成不可变对象,然后调用可变对象的方法时会报该方法找不到。
- MutableCopy:对不可变对象和可变对象进行mutableCopy时,会产生新的对象,是深拷贝。
59. copy和strong区别的区别?
- copy:当属性的修饰符为copy时,该属性的set方法中的内存管理就是:
//不可变字符串为例 - (void)setName:(NSString*)name{ if (_name != name){ [_name release]; _name = [name copy];//浅拷贝,指针拷贝,指向同一块数据,保持数据唯一性。 } } //可变字符串为例 - (void)setName:(NSMutalbeString*)name{ if (_name != name){ [_name release]; _name = [name copy];//深拷贝,内容拷贝,生成新的对象,且_name的类型变为不可变类型(NSString)。 } }
- strong:当属性的修饰符为strong时,该属性的set方法中的内存管理就是:
//不可变字符串为例 - (void)setName:(NSString*)name{ if (_name != name){ [_name release]; _name = [name retain];//深拷贝,内容拷贝,生成新的不可变对象。 } } //可变字符串为例 - (void)setName:(NSMutalbeString*)name{ if (_name != name){ [_name release]; _name = [name retain];//深拷贝,内容拷贝,生成新的可变对象。 } }
60. weak指针的实现原理是?
- 当一个对象obj被weak指针指向时,这个weak指针会以obj作为key,被存储到sideTable类的weak_table这个散列表上对应的一个weak指针数组里面。
- 当一个对象obj的dealloc方法被调用时,Runtime会以obj为key,从sideTable的weak_table散列表中,找出对应的weak指针列表,然后将里面的weak指针逐个置为nil。
61. weak和assign的区别?
- weak:只可以修饰对象。如果修饰基本数据类型,编译器会报错-“Property with ‘weak’ attribute must be of object type”。适用于delegate和block等引用类型,不会导致野指针问题,也不会循环引用,非常安全。
- assign:可修饰对象(只适用于MRC),和基本数据等值类型。在ARC中,如果修饰对象,会产生野指针问题,修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。如果修饰基本数据类型则是安全的。因为值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要我们自己手动管理内存或通过ARC管理。