IOS 底层面试题汇总 - AlvinSunny/OC-TheUnderlying GitHub Wiki
- 什么是类(类的定义)?
拥有类名、属性、方法就是一个类。
- 什么是数组?
数组就是在内存中一块连续的内存地址;
- 什么是字典?
co中字典就是通过key、value来存储数据的数据结构。
- 什么是指针?指针在内存中占用多少内存空间?
oc 中指针是指向内存地址的变量,指针在内存中占8个字节(64位);32位环境占4个字节,16位环境下是占2个字节
- 什么是对象?对象分几种?在内存中都存储了那些信息?
oc 中对象就是指向类对象的指针,对象分为三种:实例对象、类对象、元类对象
- 一个NSObject对象占用多少内存 ?
答:系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但NSObject对象内部值使用了8个字节的空间(64位环境下,可以通过class_getInstenceSize函数获得)
7 如果一个实例对象有自定义成员变量或属性;又或者有父类;又或者有自定义方法、协议;这个时候占多大内存空间呢 ?
这其中涉及到了结构体成员变量的内存对齐的问题,结构体内存对齐其中有一条要求结构体大小需要是最大成员变量大小的整数倍,而最大成员变量是指针变量(8个字节),结构体的最终的大小需要是8的整数倍。系统实际分配的大小也是16字节,所以分配的内存会是16的整数倍,父类的情况需要先加上父类的成员变量和属性所占用的内存。详细了解可以参考iOS底层原理(一):OC对象实际占用内存与开辟内存关系。至于自定义方法、协议是不存储在实例对象的内存中的,而是放在类对象的方法列表中。(原因是方法只需要存储一次就可以了,不管初始化多少个实例对象,但只要都是同一个类初始化出来的都需要相同的方法,这样看来放在类对象存储是很合理的)。
注意
不同的数据类型所分配的内存大小是不同的,比如一个OC对象分配的空间是8个字节,一个基本数据类型分配的空间是4个字节。下面提供一张一览表:
8 对象isa指针指向哪里? <1> instance(实例)对象的isa指向其class对象; <2> class对象的isa指针指向meta-class(元类)对象 <3> meta-class(元类)对象的isa指针指向基类(一般是指NSObject)的meta-class对象
但是被KVO监听的对象除外,因为KVO监听会在运行时生成新的派生类。isa指针指向的是这个派生类。
9
isa指针变量中存储了什么 ?superclass呢 ?
在arm64架构之前,存储了ISA指针指向对象的内存地址,但是在arm64之后需要在存储地址值 & ISA_MASK 这个值才是指向对象的内存地址。示意图如下

跟isa不同,superclass指针是直接指向父类的类对象,superclass指针中存储的内容就是父类的类对象的地址值。
10 为什么几乎所有的实例对象都有一个isa指针 ?
简单回答:OC 实例对象基本上都继承自NSObject,而NSObject自带就有一个成员变量isa . 所以实例对象基本上都有这个isa 指针。
有深度的回答:OC 实例对象基本上都继承自NSObject,而NSObject自带就有一个成员变量isa . 另外isa指向实例对象的类对象、类对象的元类对象的,通过isa可以找到相关的方法、协议进行调用,这在OC运行时通过object_getClass()方法 ,是通过isa指针来查找对象(查找实例对象的类对象,查找类对象的元类对象,查找元类对象返回基类的元类对象)。都需要用到的一个重要的指针变量,所以所有继承自NSObject的实例对象都有一个isa指针变量。
总结:
1.Class objc_getClass(const char *aClassName)
1> 传入字符串类名
2> 返回对应的类对象
2.Class object_getClass(id obj)
1> 传入的obj可能是instance对象、class对象、meta-class对象
2> 返回值
a) 如果是instance对象,返回class对象
b) 如果是class对象,返回meta-class对象
c) 如果是meta-class对象,返回NSObject(基类)的meta-class对象
3.- (Class)class、+ (Class)class
1> 返回的就是类对象
- (Class)class {
return self->isa;
}
+ (Class)class {
return self;
}
11 isa 指针是否安全 ?
- isa 指针不是安全的。
- 原因:isa指针是用来查找class的,但是一个实例对象添加了KVO监听后,在运行时的时候会衍生出新的类,这个时候isa就会从指向原有类改变为指向新生成的类,同时新生成的类是原来类的子类,两者是继承关系,而子类没办法继承父类私有属性和方法的,
- 这时在下面的应用场景中不安全:
- KVC键值编码赋值 可能会造成Crash
- Runtime修改私有属性值和私有方法可能会造成Crash,即通过ISA直接调用底层方法会造成Crash 如 :self->isa然后再去操作对应的方法,就不一定是安全。
12 OC 对象的信息存放哪里 ?
- <1> 成员变量的具体值存放在实例(instance)对象中.
- <2> 对象方法、属性、成员变量、协议信息都是存放在类(class)对象中的.
- <3> 类方法存放在元类(meta-class)对象中.
13 一个实例对象调用copy方法,返回的是同一对象吗 ?
这个取决于你的copy协议内部是怎么实现的,如果遵循了 协议 ;实现了 copyWithZone方法并且返回了通过alloc、new方法创建的对象那是不同对象了。
14 OC 对象内存分配的注意点或特点是什么 ?
OC对象的本质是结构体,结构体在分配内存时是以16个字节为单位为OC对象在堆空间分配存储空间的;在这个过程中以内存对齐为原则进行内分配。比如一个实例对象的成员变量总共需要28字节的存储空间那么系统就会分配32个字节的内存给它。
- iOS用什么方式实现对一个对象的KVO?(KVO 的本质是什么 ?)
替换原来的setter方法实现保留成员变量赋值是在原来类中进行,利用Runtime API 动态生成一个子类,并且让instance对象的ISA指向这个全新的子类;
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
1> 执行 willChangeValueForKey:
2> 父类原来的setter方法
3> 执行 didChangeValueForKey: ,内部会触发监听器(Observe)的监听方法--> observeValueForKeyPath:ofObject:change:context
- 如何触发KVO
KVO一般来说是自动触发的,当属性值发生改变时就通过监听方法收到消息,那手动触发的场景是不是需要在属性值没有被修改的时候,依然需要收到KVO的监听回调 ,比如轮询查询状态值 ?如果是那就需要通过实例对象调用willChangeValueForKey:和didChangeValueForKey:方法 这样就可以实现了。
疑问 :如果不调用willChangeValueForKey:只调用didChangeValueForKey:可以吗 ?
这是不可以的,因为didChangeValueForKey:内部实现是会检查willChangeValueForKey:方法是否被调用了,如果没有被调用是不会触发监听方法的。
- 通过KVC修改属性会触发KVO么?直接修改成员变量呢 ?
- 通过KVC修改属性会触发KVO, KVC的全称是
Key-Value Coding,俗称键值编码,可以通过一个key来访问某个属性,本质上来说KVC内部在修改成员变量的同时是会主动的调用调用willChangeValueForKey:只调用didChangeValueForKey:去触发KVO的,所以才会触发。 - 直接修改成员变量不会触发KVO。直接修改成员变量内部并没有做处理只是单纯的赋值,所以不会触发。
- KVC的赋值和取值过程是怎样的?原理是什么?
- Category 的使用场合是什么 ?
为实现项目需求,把一个类拆解成多个模块管理时用到分类。
- Category 的实现原理 ?
category 编译之后的底层结构是struct category_t 的一个结构体,里面存储着分类的对象方法、类方法、属性、协议信息;
在程序运行的时候,runtime会将Category的数据合并到类信息中(类对象、元类对象中)
- Category 和 Class Extension 的区别是什么 ?(Extension 扩展)
时机
Class Extension 在编译器编译完成时,它的数据就已经包含在类信息中。
Category是在运行时,才会将数据合并到类信息中。
存在形式
Class Extension只存在与.m文件中声明私有变量和方法。
Category是拥有.h和.m文件的
作用
Category可以重写方法、自定义方法声明和实现;不能添加成员变量
Class Extension 一般用于声明私有方法,私有属性,私有成员变量;
Category中有load方法吗 ?load方法是什么时候调用的 ? load 方法能继承吗 ?
有的,load方法在runtime加载类或分类时候调用;load方法可以继承,但是我们在开发中基本上不会主动去调用load方法。load方法一般由系统自动调用。
5.
load、initialize方法的区别是什么 ?它们在category中的调用的顺序 ?以及出现继承时他们之间的调用过程 ?
区别:
-
调用方式:
+ load是根据函数地址直接调用;+initialize是通过obje_magSend调用。 -
调用时机:
+load是在runtime加载类、分类的时候调用,一般情况下只会调用一次,且一定会调用;+initialize是在第一次接收消息时调用,每个类只会initialize一次(父类的initialize方法可能会被多次调用,在子类没有实现initialize的时候)如果一直没有用到这个类是不会触发的,所以不一定会调用。
调用顺序和继承: ###load 1>先调用类的load a) 先编译的类,优先调用load方法 b) 调用子类的load之前,会先调用父类的load 2> 再调用分类的load 先编译的分类,优先调用load方法
###initialize 1 > 先初始化父类 2> 再初始化子类(可能最终调用的是父类的initialize方法,但即使调用的是父类的返回的依然是子类的对象)
- Category能否添加成员变量 ?如果可以,如何给Category添加成员变量 ?不可以为什么 ?
- 不能直接添加成员变量,但是可以间接实现Category有成员变量的效果。
- Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:
objc_class结构体的定义如下:
在上面的objc_class结构体中,ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。
7 category中能添加属性吗?
Category不能添加成员变量(instance variables),那到底能不能添加属性(property)呢?
这个我们要从Category的结构体开始分析:
从Category的定义也可以看出Category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。
但是为什么网上很多人都说Category不能添加属性呢?
实际上,Category实际上允许添加属性的,同样可以使用@property,但是不会生成_变量(带下划线的成员变量),也不会生成添加属性的getter和setter方法的实现,所以,尽管添加了属性,也无法使用点语法调用getter和setter方法(实际上,点语法是可以写的,只不过在运行时调用到这个方法时候会报方法找不到的错误,如下图)。但实际上可以使用runtime去实现Category为已有的类添加新的属性并生成getter和setter方法。详细内容可以看峰哥之前的文章:《iOS Runtime之四:关联对象》
给category添加property
调用category中property的setter(报方法找不到的错误)
调用category中property的getter(报方法找不到的错误)
结论:
分类可以添加属性,但是不会自动生成成员变量的声明、set方法实现、get方法实现。这样对其赋值操作是无效的。而且从分类的底层结构可以看出,其中并不包含有成员变量列表,也就是说在设计之初根本就没有设计成员变量的存储。所以没有成员变量的
声明、set方法实现、get方法实现的属性添加是无效的。如果一定要实现可用的属性可以通过关联对象技术实现。
- load方法真的只会调用一次吗 ?
一般情况下只调用一次,但不能保证绝对只调用一次;比如:有开发者在其他地方手动来调用load方法,也是有可能会再次被调用成功;
所以建议如果要在load方法中做一些事情,尽量加上GCD的dispatch_once函数来确保自己的实现只执行一次
block的原理是什么 ? 本质是什么 ?
原理:block是封装了函数调用(也可以理解为一种闭包),在合适的时机进行调用;
本质:block本质上是一个OC对象,它内部也有一个isa指针,是封装了函数调用以及函数调用环境的OC对象。
__block的作用是什么 ?有什么使用注意点 ?
作用:解决block内部无法直接访问auto变量的问题
注意点:主要是关于内存管理方面的问题,如__block仅限于ARC时会对所引用变量产生强引用,在MRC环境下是不会产生强引用。
block的属性修饰词为什么是copy? 使用block有哪些使用注意 ?
原因:block一旦没有进行copy操作,就不会在堆空间;而是在栈空间,栈空间的内存管理程序员是无法干涉的,block可能随时被释放。在堆空间就可以由程序员自己对其内存管理。
注意:循环引用的问题
block内部在修改NSMutableArray元素,需不需要添加__block?
不需要,修改可变数组并不会影响数组本身,对数组进行添加删除元素其实际上可以认为是在使用这个数组。且如果能不加__block就不要加,因为添加__block修饰后会使数据结构变得复杂,对运行效率会有一定程度的影响。
1. 讲一下OC 的消息机制
-
OC中的方法调用其实都是转成
objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selecter方法名); -
objc_msgSend底层实现是通过消息发送(当前类、父类的缓存和方法列表中查找)、动态方法解析、消息转发来实现
2. 消息转发机制流程
前提:在当前消息接收者体系中找不到方法的具体实现(自己没有能力来处理,将消息转发给别人来处理)
-
进入
int __forwarding__(void *frameStackPointer, int isStret) 函数 -
调用
forwardingTargetForSelector:方法 返回值为空:表示没有提供消息接收者,这时就需要调用methodSignatureForSelector:方法 返回值不为空:objc_msgSend(返回值,SEL)forwardingTargetForSelector:用来返回一个可以接收当前消息的消息接收者 -
调用
methodSignatureForSelector:方法,要求返回方法签名 (所谓的方法签名:方法的返回值类型和参数类型)
返回值为空:调用doesNotRecognizeSelector:方法,表示这是未识别的选择器,抛出 unrecognized selector sent to instance (该方法找不到)
返回值不为空:调用forwardInvocation:方法
- forwardInvocation:方法中开发者可以自定义任何逻辑处理,这样就不会因为方法找不到而崩溃
3. 什么是runtime?平时项目中有用过么 ?
- runtime是一套C语言的API , 内部通过c/c++、汇编代码实现;是OC的底层实现;为OC提供动态性支持,允许很多操作推迟到运行时再进行;平时编写代码都转换成runtime调用;其核心是消息机制
具体应用包括
-
利用关联对象(
AssociatedObject)给分类添加属性 -
遍历类的所有成员(修改
textfield的占位文字颜色、字典转模型、自动归档解档) -
交换方法实现:交换系统的方法-->
method_exchangeImplementations(),相当于是拦截了系统实现,交换成自己的实现,比如:数组添加元素过滤掉空值、网络请求给字典中添加值时,过滤掉空值防止崩溃 -
利用消息转发机制解决方法找不到的异常问题,防止程序崩溃 实现思路:创建一个NSObject的分类,在load方法中用dispatch_once中替换掉 methodSignatureForSelector:和 forwardInvocation:方法的实现,并过滤掉已经实现了这两个方法的类;之后可以把错误信息处理保存并上传以方便随时知道哪里出了问题;具体用到的runtime接口有: // hook:钩子函数 //获取方法对应的Method class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:)); //交换方法 method_exchangeImplementations(method1, method2);
4. 以下代码输出结果是什么 ?为什么?
-
结果为:当前类、父类、当前类、父类
-
原因:
[super message]的底层实现
1.消息接收者仍然是子类对象
2.从父类开始查找方法的实现
5. 以下布尔值得输出结果是什么 ?
输出结果:YES NO NO NO
原因:
1\. [[NSObject class] isKindOfClass:[NSObject class]]
// 这句代码的方法调用者不管是哪个类(只要是NSObject体系下的)在右边传入的是[NSObject class]不变的情况下,都返回YES;
假设NSObject类对象的元类是(A)
(A)的父类是NSObject类对象: NSObject的元类的父类指向NSObject的类对象
NSObject类对象的元类还是(A)
(A) = (A)
2\. [[NSObject class] isMemberOfClass:[NSObject class]]
NSObject的元类和NSObject类对象肯定不是同一对象,返回NO
3\. [[XYHHEHEHE class] isKindOfClass:[XYHHEHEHE class]]
XYHHEHEHE的元类和XYHHEHEHE类对象肯定不是同一对象,XYHHEHEHE的元类的父类和XYHHEHEHE类对象也不会是同一对象,所以返回NO
4. [[XYHHEHEHE class] isMemberOfClass:[XYHHEHEHE class]]
XYHHEHEHE的元类和XYHHEHEHE类对象肯定不是同一对象,所以返回NO
5. 以下代码可以输出结果吗?为什么?
@interface XYHPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)print;
@end
@implementation XYHPerson
- (void)print
{
NSLog(@"my name is %@", self.name);//self->_name
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *test = @"hello word";
id cls = [XYHPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
@end
- print为什么能够调用成功?
- 为什么self.name变成了ViewController等其他内容
问题1 解读
cls指针指向XYHPerson类对象,cls指针内部就存储着XYHPerson类对象的地址值
void *obj 指向cls指针地址,obj指针内部就存储着cls的地址值
obj -- > cls -- > [XYHPerson class]
通过一段代码来对比分析:
XYHPerson *person = [XYHPerson alloc]init];
person --> isa -- > [XYHPerson class]
对比可以看出题中代码逻辑和上面这段代码一样最终都访问的是XYHPerson类对象 所以print方法可以被调用成功
问题2 解读
// 局部变量分配在栈空间
// 栈空间分配,从高地址到低地址
void test()
{
long long a = 4; // 0x7ffee638bff8
long long b = 5; // 0x7ffee638bff0
long long c = 6; // 0x7ffee638bfe8
long long d = 7; // 0x7ffee638bfe0
NSLog(@"%p %p %p %p", &a, &b, &c, &d);
}
//XYHPerson 的内存结构是
struct XYHPerson_IMP
{
Class isa;
NSString *_name;
}
从XYHPerson的底层结构可以看出_name是位于isa指针前面的8个字节中,而栈空间存储的数据是连续的;就是说读取的会是isa之前的8字节中的数据;
利用ojb指针找到XYHPerson的这块内存,从跳过前面的8字节找到8~16字节中的数据;汇编代码的逻辑就是找到cls挑8个字节找到高8个字节中存储的数据. 栈空间访问是通过地址值访问;这时就会变成访问cls前面的那8个字节的数据,在viewDidLoad方法内存中test字符串就位于cls高8字节的位置,所以输出的是“hello word”;
- 疑问 如果 NSString *test = @"hello word";不写的话会输出什么 ?
会输出 my name is <ViewController: 0x7fbe9705320 >
因为viewDidLoad方法内部调用了 [super viewDidLoad]
其结构是
objc_msgSendSuper({
self, //当前对象
[UIViewController class] //当前对象的父类
},@selector(viewDidLoad));
[super viewDidLoad] 方法调用最终会转换成以下代码
//定义一个结构体
struct objc_super = {
self,
[ViewController class]
};
传入参数,发送消息
objc_msgSendSuper2(objc_super, sel_registerName("viewDidLoad"));
id cls = [XYHPerson class];
void *obj = &cls;
[(__bridge id)obj print];
从代码中可以看出在cls之前声明了objc_super结构体变量,内存中的表现就是:
objc_super结构体变量中有两个成员,按照栈空间的分配规则self是紧挨着cls的;所以最终输出的是 self ,而self就是当前控制器, 输出:‘my name is <ViewController: 0x7fbe9705320 > ’
- 讲讲 RunLoop,项目中有用到吗?
-
RunLoop顾名思义就是运行循环,是在程序运行中循环做一些事情; - iOS系统中有两个关于Runloop的对象:
NSRunLoop和CFRunLoopRefNSRunloop是Foundation框架提供的,是对CoreFoundation框架提供的CFRunloopRef的封装。 -
CoreFoundation提供的是纯C语言的API,都是线程安全的,Foundation不是线程安全的。 - iOS中RunLoop是开源的
- 项目中用到的:
线程保活,比如:在APP退到后台持续的做事情、
解决NSTime在滑动时停止工作的问题,标记runloop模式为
kCFRunLoopCommonModes
- runloop内部实现逻辑?
- RunLoop通知
Observers:将要进入loop做事情了 - 通知Observers:
将要处理Timers - 通知Observers:
将要处理Sources - 开始处理
Blocks - 开始处理
Source0 - 如果存在
Source1就通知Observers先不要休眠,先把Source1处理一下 - 事情做完了,开始休眠
- 休眠中,等待被某个消息唤醒;
Timer、GCD、Source1那个唤醒我,我就处理那个 - 再次处理
Blocks - 根据前面的处理结果,决定是循环从‘将要处理Timers’再来一遍;或者退出loop
- 退出loop分为:
切换mode、当前线程销毁、程序从内存中移除这几种情况
3.
runloop和线程的关系?
- 每条线程都有唯一的一个与之对应的
RunLoop对象 -
RunLoop保存在一个全局的字典里,线程作为key,RunLoop作为value - 线程刚创建时是没有
RunLoop对象的(主线程的RunLoop有系统控制获取),RunLoop会在第一次获取它时创建 -
RunLoop会在线程结束时销毁掉 - 子线程默认是不会开启
RunLoop的,需要开发者自己获取(创建)
timer与runloop的关系?
Runloop的创建需要设定运行模式CFRunLoopMode,在运行模式的数据结构中有一个_timers成员,_timers成员中就保存着定时器事件;而定时器事件的实现由runloop的API---> __CFRunLoopDoTimer()中的__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__()函数实现
5.程序中添加每3秒响应一次的
NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
标记的定时器的Runloop运行模式为kCFRunLoopCommonModes;
6.runloop 是怎么响应用户操作的, 具体流程是什么样的?
响应用户操作是属于Source1事件,首先会捕捉到系统事件,Source1会把事件包装成Source0里面去处理该事件。
7.说说runLoop的几种状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};
8.runloop的mode作用是什么?
mode可以让runloop轻松的切换不同的模式,去处理不同组的Source0/Source1/Timer/Observe分隔开,互不影响。同时在处理这个模式的loop,是不可以再去处理其他的模式;切换需要先退出当前loop,在重新进入;
- 你理解的多线程?
- 多线程:即
multithreading, 是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。 - IOS 通常多线程方案有四种:
pthread、NSThread、GCD、NSOperation - 开发中常用的多线程方法首选:
GCD、NSOperation
- iOS的多线程方案有哪几种?你更倾向于哪一种?
- 你在项目中用过 GCD 吗?
用过呀 !利用GCD把任务添加到队列中执行是开发中常见的方式;因为GCD执行速度快且线程安全所以称为开发中开启多线程的首选。
- GCD 的队列类型
GCD的队列类型可以分为:串行队列和并发队列;
-
串行队列一般需要手动创建,这种队列根据不同情况async会开启新的线程,sync不会开启新线程,但不论哪种都是串行执行任务
-
并发队列:可以获取系统的全局队列,也可以自己创建;async会开启新线程并发执行任务;sync不会开启新线程串行执行任务
- 说一下
NSOperationQueue和GCD的区别,以及各自的优势
-
GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本封装。 -
GCD只支持FIFO的队列,只有使用dispatch_semaphore多线程加锁时可以设置并发量,NSOperationQueue可以设置priority(优先级)很方便的调整执行顺序和设置最大并发数量 -
线程安全:GCD是线程安全的,NSOperationQueue是无法保证线程安全 -
依赖关系:GCD想要实现相对复杂,NSOperationQueue可以在轻松在Operation间设置。 -
线程状态监测:GCD不支持, NSOperationQueue支持KVO,可以监测operation是否正在执行(isExecuted).是否结束(isFinished).是否取消(isCanceld) -
任务管理:GCD未开始的任务很难去停止,NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了) -
执行速度:GCD的执行速度比NSOperationQueue快 -
继承:GCD无法实现,NSOperationQueue可以继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。
总结:NSOpertation是更为高级的多线程抽象,其在做框架时应该被优先考虑,结构会更好。关于GCD的的执行效率方面,不会比NSOperation快太多且线程安全,自己的话还是用GCD多些。
NSOperation中start与main的区别是什么?
- start和main.
- 按照官方文档所说,如果是非并发就使用main,并发就使用start。
- 那现在并发和非并发已经没有区别了,start和main的区别在哪里呢?
- main方法的话,如果main方法执行完毕,那么整个operation就会从队列中被移除。如果你是一个自定义的operation并且它是某些类的代理,这些类恰好有异步方法,这是就会找不到代理导致程序出错了。然而start方法就算执行完毕,它的finish属性也不会变,因此你可以控制这个operation的生命周期了。 然后在任务完成之后手动cancel掉这个operation即可。
- 线程安全的处理手段有哪些?
- 采用线程同步技术、尽量合理的开启子线程,避免同时访问同意快资源
- 线程同步技术中推荐:
dispatch_semaphore(ios 4开始)、pthread_mutex加锁来实现
- OC你了解的锁有哪些?在你回答基础上进行二次提问; 追问一:自旋和互斥对比? 追问二:使用以上锁需要注意哪些? 追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!
- 自旋锁和互斥锁是两种线程同步技术的加锁方案,目前自旋锁已经不推荐使用,因为可能会引起优先级翻转等问题,同时对内存的消耗较大影响性能;互斥锁在以上两方面没有问题,所以推荐使用互斥锁。
- 自旋锁注意点:不在安全,有条件的情况建议使用互斥锁
- 互斥锁注意点:建议使用性能高的方案
dispatch_semaphore(ios 4开始)、pthread_mutex,比如:@synchronized就不推荐使用。
- 下面代码输出结果是什么 ?为什么 ?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
NSLog(@"4");
NSLog(@"5");
}
- (void)test
{
NSLog(@"2");
}
输出结果:1、3、4、5、2 原因:viewDidLoad是在主线程,performSelector:withObject: afterDelay: 底层调用是把test添加主线程RunLoop的定时任务(Timer)中0.0秒后调用objc_msgSend(self,@selector(test))方法,添加到Timer再调用是需要时间的且是异步操作,所以会继续把viewDidLoad后面的代码执行完毕;最后后再执行定时任务。
- test-->函数调用栈
打印`-[ViewController test](self=0x00007fb525d08430, _cmd="test") at ViewController.m:28:5
frame #1: 0x0000000104506151 Foundation`__NSFireDelayedPerform + 414
frame #2: 0x00000001053fe3e4 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
frame #3: 0x00000001053fdff2 CoreFoundation`__CFRunLoopDoTimer + 1026 //定时器任务
frame #4: 0x00000001053fd85a CoreFoundation`__CFRunLoopDoTimers + 266
frame #5: 0x00000001053f7efc CoreFoundation`__CFRunLoopRun + 2220
frame #6: 0x00000001053f7302 CoreFoundation`CFRunLoopRunSpecific + 626
frame #7: 0x000000010da552fe GraphicsServices`GSEventRunModal + 65
frame #8: 0x0000000108204ba2 UIKitCore`UIApplicationMain + 140
frame #9: 0x00000001041646c0 Interview01-打印`main(argc=1, argv=0x00007ffeeba9af38) at main.m:14:16
frame #10: 0x0000000106d75541 libdyld.dylib`start + 1
frame #11: 0x0000000106d75541 libdyld.dylib`start + 1
- 下面代码输出结果是什么 ?为什么 ?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// 这句代码的本质是往Runloop中添加定时器
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
- (void)test
{
NSLog(@"2");
}
输出结果:1、3 原因:异步并发队列中开辟了新线程,performSelector:withObject: afterDelay:方法本质:会把test添加到runloop的定时任务中,而新线程在创建时默认是不会启动runloop,只有在第一次调用后才会启动;这里没有实现runloop的调用;所以根本就不会触发到test方法;
- 加上这句代码就可以执行test方法 NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture;
- 下面代码输出结果是什么 ?为什么 ?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
NSLog(@"2");
});
}
输出:1、2
原因:dispatch_after本质是添加任务到RunLoop中,在一段时间后执行block中的任务;这个时间即便是0秒钟,那也依然需要一个过程。
为什么不会死锁不是要到主线程执行吗 ?
原因:添加到RunLoop这个操作是正常执行的,不会阻塞线程。和普通代码一行一行执行没有区别。RunLoop调度的任务会等到当前任务完成再执行dispatch_after,所以肯定不会死锁。
- 下面代码输出结果是什么 ?为什么 ?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil];
NSLog(@"3");
});
}
- (void)test
{
NSLog(@"2");
}
输出结果:1、2、3
原因:performSelector: withObject这句代码的本质是objc_msgSend(),不影响执行顺序;
-
performSelector: withObject底层源码
- (id)performSelector:(SEL)sel withObject:(id)obj {
//判断如果方法为空,直接抛出方法不存在异常错误
if (!sel) [self doesNotRecognizeSelector:sel];
//objc_msgSend消息发送
return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
- 下面代码输出结果是什么 ?为什么 ?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test
{
NSLog(@"2");
}
输出结果1,程序直接crach;崩溃在performSelector:onThread:withObject:waitUntilDone方法
原因:
-
执行到
[thread start],start会做两件事:1. 开启新线程,2. 执行block中的任务; 执行完毕会马上退出, 此时thread已经执行结束自动退出了,再让thread做事情肯定会报:target thread exited while waiting for the perform(在等待执行时退出目标线程); -
如何才能执行到test中的方法 ?
在block中添加启动runloop的代码实现
//1.添加Port对象到RunLoop的NSDefaultRunLoopMode模式
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//2. 启动RunLoop的NSDefaultRunLoopMode模式
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//3. 发现没事做会休眠
//4. 题中的`performSelector`调用就会唤醒runloop执行任务,这样test方法就会被执行
总结:线程的任务一旦执行完毕,生命周期就结束了,无法再次使用;开启RunLoop添加Port是为了保持线程处于激活状态。
- 为什么要启动RunLoop,用强指针指向不行吗?
不行,因为强指针指向线程只能够保证线程不被释放;并不能保证线程处于激活状态;这两者还是有区别的。
- 使用
CADisplayLink、NSTimer有什么注意点?
CADisplayLink: 保证调用频率和屏幕的刷帧频率一致,60FPS(帧每秒)
###内存管理方面:
-
CADisplayLink、NSTimer会对target产生强引用,如果target有对它们产生强引用,那么就会引发循环引用,会造成内存泄露。如果一直泄露最终会导致程序crash。 解决办法: -
IOS10之后NSTimer可以使用block, 在block外部使用__weak typeof()修饰target-
block的意义:计时器的执行主体;在执行时,计时器本身作为参数传递给这个块,以帮助避免循环引用
-
-
CADisplayLink、NSTimer可以使用代理对象--NSProxy
block
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
NSProxy
封装--
@implementation XYHProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
XYHProxy *proxy = [XYHProxy alloc];
proxy.target = target;
return proxy;
}
//消息转发到target
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
使用--
//CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:[XYHProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
-----------------------------------------
//NSTimer 的 scheduledTimerWithTimeInterval方法内部会启动Runloop,自动开始定时任务; 但如果我们调用的是timerWithTimeInterval接口,就需要自己加入runloop。这点在使用上需要注意
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[XYHProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
###使用方法方面:
NSTimer在加入RunLoop时mode设置应为kCFRunLoopCommonModes或NSRunLoopCommonMode;且CADisplayLink、NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时;若想定时更加精准可以考虑使用GCD的dispatch_source
- 介绍下内存的几大区域
代码段、数据段的内存是在编译阶段进行分配的 堆空间、栈空间的内存是在运行时进行分配的
- 讲一下你对 iOS 内存管理的理解
内存管理原则:
- 在iOS中,使用
引用计数来管理OC对象的内存 - 一个新创建的OC对象引用计数默认是
1,当引用计数减为0时;OC对象就会销毁,释放其占用的内存空间 - 调用
retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理经验:
- 当调用
alloc、new、copy、mutableCopy方法返回了一个对象,当不需要这个对象时,就要调用release或者autorelease来释放它 - 想拥有某个对象,就让它的引用计数
+1; 不想再拥有时,就让它的引用计数 -1
内存管理范围:
引用计数内存管理范围仅限OC对象;
- ARC 都帮我们做了什么?
-
LLVM编译器和Runtime相互协作的结果 -
LLVM编译器:负责生成内存管理相关代码 -
Runtime:负责消息发送、弱引用实现、copy等功能,基本上没有它不干的
weak指针的实现原理
简单描述:
- 存储:对象有被弱指针指向时,此弱引用会被存储到由iOS维护的全局
SideTables哈希表中 - 释放:在实现
dealloc的底层函数clearDeallocating()中通过哈希查找,找到对应的弱引用并清除
详细讲解:
- iOS全局维护了一个
SideTables哈希表,用SideTable来存储和释放OC对象的弱引用指针 - 一旦对象有被弱指针指向就会被存储进SideTable的
weak_table_t属性中,weak_table同样是一个哈希表 - 当一个对象执行到dealloc方法时,检查到有弱引用指针指向该对象;
- 拿到
当前对象地址值通过哈希查找(按位&上弱引用表的mask值, 最终得到索引-- >index),找到对应的弱引用指针表,调用weak_entry_for_referent函数传入当前对象和弱引用表,执行内部的weak_entry_remove函数将其从全局弱引用表中移除
autorelease对象在什么时机会被调用release
-
autorelease对象什么时候调用release方法,是由RunLoop来控制的;在某次Runloop循环中,RunLoop休眠之前调用release; -
可否具体:runloop的
Observer2监听到kCFRunLoopBeforeWaiting事件,触发就会调用objc_autoreleasePoolPop(),由其内部实现对autorelease对象的release
- 方法里有局部对象, 出了方法后会立即释放吗
ARC环境下,出了方法后立即被释放;因为ARC环境下,编译器是在方法结束之前插入了release代码;
MRC环境下,如果在创建时是调用了autorelease,可能会延迟释放;因为会有一定的延迟,它需要等到某次runloop休眠之前才会进行释放,runloop执行任务多少时间都不确定,所以可能会有延迟;
- 以下代码会发生什么事情 ?有什么区别 ?
第一段
@property (copy, nonatomic) NSString *name;
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
}
- 可能会引起坏内存访问,从而导致崩溃 !运气好的话可能不会出问题。
原因:
- 异步任务中的代码相当于是执行
setter方法[self.name setName:@"abcdefghijk"] -
nonatomic是不会对赋值操作进行加锁的,所以很有可能出现多条线程同时对_name进行release操作,这就会造成对一个对象过度释放,这会直接抛出异常:坏内存访问坏内存访问:尝试向一个块已经不能执行这个消息的内存块发送消息就可以被认为是坏内存访问;
- (void)setName:(NSString *)name
{
if (_name != name) {//新值和旧值不同就赋值
[_name release];//先释放旧值,引用计数器减一
//retain: 声明的时候用的strong关键字
//copy : 声明的时候用的copy关键字
_name = [name copy];
}
}
疑问:setter方法中 if 判断有什么意义 ?
- 如果值没有变化,就没必要再去赋值;这样可以简化代码可以提高执行速度
- 相同的对象赋值,一旦遇到把这个对象重复多次传入;这时
_name就是name,可以看到代码中会先对_name进行release,这就相当于对传进来的对象 release,假如传进来对象的引用计数刚好是1,进行一次release后该对象就会释放掉;接下来再进行retain操作肯定会报坏内存访问:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)
第二段
@property (copy, nonatomic) NSString *name;
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
}
- 会正常执行结束;
原因:
IOS中引用了Tagged Pointer技术专门用来优化NSString对象为NSTaggedPointerString这样的类型,如果赋值数据可以直接存储在指针里面去了,就不会动态分配内存。说明self.name是一个Tagged Pointer指针,赋值的话是指针赋值,不是OC对象赋值;不会走setName:方法,也就不会出现过度释放的问题。
两段代码的区别是:第二段的self.name是Tagged Pointer,第一段不是























