一.基础
1.对象方法和类方法的区别?
- 对象方法能个访问成员变量。
- 类方法中不能直接调用对象方法,想要调用对象方法,必须创建或者传入对象。
- 类方法可以和对象方法重名。
2.如果在类方法中调用self 会有什么问题?
- 在实例方法中self不可以调用类方法,此时的self不是Class。
- 在类方法中self可以调用其他类方法。
- 在类方法中self不可以调用实例方法。
总结:类方法中的self,是class/ 实例方法中self是对象的首地址。
3.讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?
- 对象的结构体当中存放着isa指针和成员变量,isa指针指向类对象
- 类对象的isa指针指向元类,元类的isa指针指向NSObject的元类
- 类对象和元类的结构体有isa,superClass,cache等等
4.为什么对象方法中没有保存对象结构体里面,而是保存在类对象的结构体里面?
方法是每个对象相互可以共用的,如果每个对象都存储一份方法列表太浪费内存,由于对象的isa是指向类对象的,当调用的时候直接去类对象中去查找就可以了,节约了很多内存空间。
5.类方法存在哪里?为什么要有元类的存在?
- 所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
- 为了调用类方法,这个类的`isa`指针必须指向一个包含这些类方法的一个`objc_class`结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。
6.什么是野指针?
- 野指针就是指向一个被释放或者被收回的对象,但是对指向该对象的指针没有做任何修改,以至于该指针让指向已经回收后的内存地址。
- 其中访问野指针是没有问题的,使用野指针的时候会出现崩溃Crash! 样例如下:
可以看到NSlog打印不会闪退,调用[testObj setNeedsLayout];会闪退
__unsafe_unretained UIView *testObj = [[UIView alloc] init];
NSLog(@"testObj 指针指向的地址:%p 指针本身的地址:%p", testObj, &testObj);
[testObj setNeedsLayout];
7.如何检测野指针?
8.导致Crash的原因有哪些?
1. 找不到方法的实现unrecognized selector sent to instance
2. KVC造成的crash
3. EXC_BAD_ACCESS
4. KVO引起的崩溃
5. 集合类相关崩溃
6. 多线程中的崩溃
7. Socket长连接,进入后台没有关闭
8. Watch Dog超时造成的crash
9. 后台返回NSNull导致的崩溃,多见于Java做后台服务器开发语言
9.不使用第三方,如何知道已经上线的App崩溃问题, 具体到哪一个类的哪一个方法的?
简单介绍
- 使用NSSetUncaughtExceptionHandler可以统计闪退的信息。
- 将统计到的信息以data的形式 利用网络请求发给后台 在后台收集信息,进行排查
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
return YES;
}
static void my_uncaught_exception_handler (NSException *exception) {
//这里可以取到 NSException 信息
NSLog(@"***********************************************");
NSLog(@"%@",exception);
NSLog(@"%@",exception.callStackReturnAddresses);
NSLog(@"%@",exception.callStackSymbols);
NSLog(@"***********************************************");
}
10.iOS中内省的几个方法?
首先要明白一个名词 `introspection` 反省,内省的意思,在iOS开发中我们会称它为反射.
1. isMemberOfClass //对象是否是某个类型的对象
2. isKindOfClass //对象是否是某个类型或某个类型子类的对象
3. isSubclassOfClass //某个类对象是否是另一个类型的子类
4. isAncestorOfObject //某个类对象是否是另一个类型的父类
5. respondsToSelector //是否能响应某个方法
6. conformsToProtocol //是否遵循某个协议
11.==、 isEqualToString、isEqual区别?
- == ,比较的是两个指针的值 (内存地址是否相同)。
- isEqualToString, 比较的是两个字符串是否相等。
- isEqual 判断两个对象在类型和值上是否都一样。
12.class方法和object_getClass方法有什么区别?
- 实例class方法直接返回object_getClass(self)
- 类class直接返回self
- 而object_getClass(类对象),则返回的是元类
13.深拷贝和浅拷贝
深拷贝:复制本身
浅拷贝:复制地址
- copy和mutableCopy针对的是可变和不可变,凡涉及copy结果均变成不可变,mutableCopy均变成可变。
- mutableCopy均是深复制。
- copy操作不可变的是浅复制,操作可变的是深赋值。
14.NSString类型为什么要用copy修饰?
- 主要是防止NSString被修改,如果没有修改的说法用Strong也行。
- 当NSString的赋值来源是NSString时,strong和copy作用相同。
- 当NSString的赋值来源是NSMutableString,copy会做深拷贝,重新生成一个新的对象,修改赋值来源不会影响NSString的值。
15.iOS中block 捕获外部局部变量实际上发生了什么?__block中又做了什么?
- block捕获的是当前在block内部执行的外部局部变量的瞬时值
- 为什么说瞬时值呢?看一下C++源码中得知,其内部代码在捕获的同时,其实block底层生成了一个和外部变量相同名称的属性值如果内部修改值,其实修改的是捕获之前的值,其捕获的内部的值因代码只做了一次捕获,并没有做再一次的捕获,所以block里面不可以修改值。
- 如果当前捕获的为对象类型,其block内部可以认为重新创建了一个指向当前对象内存地址的指针(堆),操控内部操作的东西均为同一块内存地址,所以可以修改当前内部的对象里面的属性,但是不能直接修改当前的指针(无法直接修改栈中的内容)(即重新生成一个新的内存地址)。其原理和捕获基本数据类型一致。
- 说白了, block内部可以修改的是堆中的内容, 但不能直接修改栈中的任何东西。如果加上__block 在运行时创建了一个外部变量的“副本”属性,把栈中的内存地址放到了堆中进而在block内部也能修改外部变量的值。
16.iOS Block为什么用copy修饰?
block 是一个对象
- MRC的时候 block 在创建的时候,它的内存比较奇葩,非得分配到栈上,而不是在传统的堆上,它本身的作用于就属于创建的时候(见光死,夭折),一旦在创建时候的作用于外面调用它会导致崩溃。
- 所以,利用copy把原本在栈上的复制到堆里面,就保住了它。
- ARC的时候 由于ARC中已经看不到栈中的block了。用strong和copy一样,用copy是遵循其传统
17.为什么分类中不能创建属性Property(runtime除外)?
- 分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
- Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。
- 通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。
- 在往深一点的回答就是 类在内存中的位置是编译时期决定的,之后再修改代码也不会改变内存中的位置,class_ro_t的属性在运行期间就不能再改变了, 再添加方法是会修改class_rw_t 的methods 而不是class_ro_t 中的 baseMethods
18.关联对象的原理?
- 关联对象并不是存储在关联对象本身内存中,而是存储在全局统一的一个容器中
- 由 AssociationsManager 管理并在它维护的一个单例 Hash 表 AssociationsHashMap 中存储;
- 使用 AssociationsManagerLock 自旋锁保证了线程安全
19.分类可以添加那些内容?
实例方法,类方法,协议,属性
20.Category 的实现原理?
- Category实际上是Category_t的结构体,在运行时,新添加的方法都被以倒序插入到原有方法列表的最前面,所以不同的Category添加了同一个方法,执行的实际上是最后一个
- Category在刚刚编译完的时候和原来的类是分开的,只有在程序运行起来后,通过Runtime,Category和原来的类才会合并到一起
21.使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
不需要,被关联的对象的生命周期内要比对象本身释放晚很多, 它们会在被 NSObject -dealloc 调用的 object_dispose()方法中释放。
对象的内存销毁时间表,分四个步骤
1. 调用 -release :引用计数变为零 对象正在被销毁,生命周期即将结束. 不能再有新的 __weak 弱引用,否则将指向 nil. 调用 [self dealloc]
2. 父类调用 -dealloc 继承关系中最直接继承的父类再调用 -dealloc 如果是 MRC 代码 则会手动释放实例变量们(iVars) 继承关系中每一层的父类 都再调用 -dealloc
3. NSObject 调 -dealloc 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
4. 调用 object_dispose() 为 C++ 的实例变量们(iVars)调用 destructors 为 ARC 状态下的 实例变量们(iVars) 调用 -release 解除所有使用 runtime Associate方法关联的对象 解除所有 __weak 引用 调用 free()
22.能否向编译后得到的类中增加实例变量,能否向运行时创建的类中添加实力变量?
- 不能向编译后得到的类中增加实例变量。
- 能向运行时创建的类中添加实例变量。
- 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量。
- 运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
23.主类执行了foo方法,分类也执行了foo方法,在执行的地方执行了foo方法,主类的foo会被覆盖么? 如果想只想执行主类的foo方法,如何去做?
- 主类的方法被分类的foo覆盖了,其实分类并没有覆盖主类的foo方法,只是分类的方法排在方法列表前面,主类的方法列表被挤到了后面, 调用的时候会首先找到第一次出现的方法。
- 如果想要只是执行主类的方法,可逆序遍历方法列表,第一次遍历到的foo方法就是主类的方法
- (void)foo{
[类 invokeOriginalMethod:self selector:_cmd];
}
+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector {
uint count;
Method *list = class_copyMethodList([target class], &count);
for ( int i = count - 1 ; i >= 0; i--) {
Method method = list[i];
SEL name = method_getName(method);
IMP imp = method_getImplementation(method);
if (name == selector) {
((void (*)(id, SEL))imp)(target, name);
break;
}
}
free(list);
}
24.load 和 initilze 的调用情况,以及子类的调用顺序问题?
+load
- 调用时刻:+load方法会在Runtime加载类、分类时调用(不管有没有用到这些类,在程序运行起来的时候都会加载进内存,并调用+load方法);
- 每个类、分类的+load,在程序运行过程中只调用一次(除非开发者手动调用)。
- 调用方式: 系统自动调用+load方式为直接通过函数地址调用,开发者手动调用+load方式为消息机制objc_msgSend函数调用。
- 调用顺序:
1. 先调用类的+load,按照编译先后顺序调用(先编译,先调用),调用子类的+load之前会先调用父类的+load
2. 再调用分类的+load,按照编译先后顺序调用(先编译,先调用)(注意:分类的其它方法是:后编译,优先调用)
+initialize
- 调用时刻:+initialize方法会在类第一次接收到消息时调用。
- 如果子类没有实现+initialize方法,会调用父类的+initialize,所以父类的+initialize方法可能会被调用多次,但不代表父类初始化多次,每个类只会初始化一次。
- 调用方式: 消息机制objc_msgSend函数调用。
- 调用顺序: 先调用父类的+initialize,再调用子类的+initialize (先初识化父类,再初始化子类)
+initialize方法的调用方式为消息机制,而非像+load那样直接通过函数地址调用。
25.什么是线程安全?
多条线程同时访问一段代码,不会造成数据混乱的情况
26.你实现过单例模式么? 你能用几种实现方案?
- 运用GCD:
import "Manager.h"
implementation Manager
+ (Manager *)sharedManager {
static dispatch_once_t onceToken;
static Manager * sharedManager;
dispatch_once(&onceToken, ^{
sharedManager=[[Manager alloc] init];
});
return sharedManager;
}
end
注明:dispatch_once这个函数,它可以保证整个应用程序生命周期中某段代码只被执行一次!
- 不使用GCD的方式:
static Manager *manager;
implementation Manager
+ (Manager *)defaultManager {
if(!manager)
manager=[[self allocWithZone:NULL] init];
return manager;
}
end
- 正常的完整版本
+(id)shareInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if(_instance == nil)
_instance = [MyClass alloc] init];
});
return _instance;
}
//重写allocWithZone,里面实现跟方法一,方法二一致就行.
+(id)allocWithZone:(struct _NSZone *)zone{
return [self shareInstance];
}
//保证copy时相同 目的是为了方式开发者在调用单例的时候并没有用shareInstance方法来创建 而是用的alloc 或者copy的形式创建造成单例不一致的情况
-(id)copyWithZone:(NSZone *)zone{
return _instance;
}
27.单例是怎么销毁的?
//必须把static dispatch_once_t onceToken; 这个拿到函数体外,成为全局的.
+ (void)attempDealloc {
onceToken = 0; // 只有置成0,GCD才会认为它从未执行过.它默认为0,这样才能保证下次再次调用shareInstance的时候,再次创建对象.
_sharedInstance = nil;
}
dispatch_once_t 的工作原理是,static修饰会默认将其初始化为0, 当且仅当其为0的时候dispatch_once(&onceToken, ^{})这个函数才能被调用,如果执行了这个函数,这个dispatch_once_t 静态变成-1了,就永远不会被调用
28.不使用dispatch_once 如何 实现单例
- 第一种方式,重写+allocWithZone:方法;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static id instance = nil;
@synchronized (self) { // 互斥锁
if (instance == nil) {
instance = [super allocWithZone:zone];
}
}
return instance;
}
- 第二种方式,不用重写+allocWithZone:方法,而是直接用@synchronized 来保证线程安全,其它与上面这个方法一样;
+ (instancetype)sharedSingleton {
static id instance = nil;
@synchronized (self) {
if (!instance) {
instance = [[self alloc] init];
}
}
return instance;
}
29.项目开发中,你用单例都做了什么?
整个程序公用一份资源的时候
- 设置单例类访问应用的配置信息
- 用户的个人信息登录后用的NSUserDefaults存储,对登录类进一步采用单例封装方便全局访问
- 防止一个单例对 应用 多处 对同意本地数据库存进行操作
30.APNS的基本原理
步骤
- 第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
- 第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
- 第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。
详细说明
注册
1. Device(设备)连接APNs服务器并携带设备序列号(UUID)
2. 连接成功,APNs经过打包和处理产生devicetoken并返回给注册的Device(设备)
3. Device(设备)携带获取的devicetoken发送到我们自己的应用服务器
4. 完成需要被推送的Device(设备)在APNs服务器和我们自己的应用服务器的注册
推送过程
1、首先手机装有当前的app,并且保持有网络的情况下,APNs服务器会验证devicetoken,成功那个之后会处于一个长连接。 (这里会有面试问? 如果app也注册成功了, 也下载了,也同意了打开推送功能, 这个时候在把App删除了, 还能接受推送了么? )
2、当我们推送消息的时候,我们的服务器按照指定格式进行打包,结合devicetoken 一起发送给APNs服务器,
3、APNs 服务器将新消息推送到iOS 设备上,然后在设备屏幕上显示出推送的消息。
4、iOS设备收到推送消息后, 会通知给我们的应用程序并给予提示
31.说说你理解weak属性?
指向但不持有,指向的内存地址释放后会自动置为nil
32.实现weak后,为什么对象释放后会自动为nil?
在 Runtime 中专门维护了一个用于存储 weak指针变量的 weak 表,这实际上是一个 Hash 表。这个表 key 是 weak指针 所指向的内存地址,value 是指向这个内存地址的所有 weak指针,实际上是一个数组。
过程可以总结为3步
初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak指针 指向对象的地址。
添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。
释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从weak表 中删除,最后清理对象的记录。
33.__weak 和 __unsafe_unretain 的区别?
__weak 是 __unsafe_unretain升级版,__unsafe_unretain 在指向的内存地址销毁后,指针本身并不会自动销毁,这也就造成了野指针,之后容易造成 Crash。__weak 在指向的内存销毁后,可以将指针变量置为 nil,这样更加安全。
34.objc中向一个nil对象发送消息将会发生什么?
在寻找对象化的isa指针时就是0地址返回了,所以不会有任何错误,也不会错误
35.objc在向一个对象发送消息时,发生了什么?
首先是通过obj的isa指针找到对应的class,先去操作对象中的缓存方法列表中objc_cache中去寻找当前方法,如果找到就直接实现对应IMP
如果在缓存中找不到,则在class中找到对应的Method list中对应的foo
如果class中没有找到对应的foo,就会去superClass中去找,如果找到了对应的foo, 就会实现foo对应的IMP
缓存方法列表,就是每次执行这个方法的时候都会做如此繁琐的操作这样太过于消耗性能,所以出现了一个objc_cache,这个会把当前调用过的类中的方法做一个缓存
当前method_name作为key,method_IMP作为Value
当再一次接收到消息的时候,直接通过objc_cache去找到对应的foo的IMP即可,避免每一次都去遍历objc_method_list
如果一直没有找到方法, 就会专用消息转发机制,机制如下:
动态方法解析和转发
上面的例子如果foo函数一直没有被找到,通常情况下,会出现报错,但是在报错之前,OC的运行时给了我们三次补救的机会
Method resolution
Fast forwarding
Normal forwarding
1.Runtime会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve(重启) 这个消息;
2.如果resolve方法返回NORuntime就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
3.如果没有新的目标对象返回,Runtime就会发送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。
4.发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。
36.UIView和CALayer是什么关系?
1.两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以;
2.每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIVie 的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,layer内部有SubLayers,View 内部有SubViews.但是Layer比View多了个AnchorPoint
3.在View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
4.CALayer是默认修改属性支持隐式动画的,在给UIView的Laye 做动画的时候,View 作为Layer的代理,Layer通过actionForLayer:forKey:向View请求相应的 action(动画行为)
5.layer内部维护着三份layer tree
presentLayer Tree(动画树),
modeLayer Tree(模型树),
Render Tree (渲染树)
在做 iOS动画的时候,我们修改动画的属性,在动画的其实是Layer的presentLayer的属性值,而最终展示在界面上的其实是提供View的modelLayer
37.@synthesize 和 @dynamic 分别有什么作用
@property有两个对应的词@synthesize,@dynamic
如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
@synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
@dynamic 告诉编译器:属性的setter与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。
假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。
编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
38.static有什么作用?
通过static修饰的函数或者变量,在该文件中,所有位于这条语句之后的函数都可以访问,而其他文件中的方法和函数则不行
类方法不可以访问实例变量(函数),通过static修饰的实例变量(函数),可以被类方法访问
static修饰的变量,能且只能被初始化一次;
static修饰的变量,默认初始化为0;
39.runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。runloop在第一次获取时被创建,在线程结束时被销毁。
对于主线程来说,runloop在程序一启动就默认创建好了。
对于子线程来说, runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被开启,不然定时器不会回调。
40.如何手动触发一个value的KVO
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。
41.如何给系统KVO设置筛选条件?
举例:取消Person类age属性的默认KVO,设置age大于18时,手动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (void)setAge:(NSInteger)age {
if (age >= 18) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}else {
_age = age;
}
}
42.通过KVC修改属性会触发KVO么?直接修改成员变量呢 ?
会触发KVO。即使没有声明属性,只有成员变量,只要accessInstanceVariablesDirectly返回的是YES,允许访问其成员变量,那么不管有没有调用setter方法,通过KVC修改成员变量的值,都能触发KVO。
这也说明通过KVC内部实现了willChangeValueForKey:方法和didChangeValueForKey:方法
直接修改成员变量不会触发KVO,直接修改成员变量内部并没有做处理只是单纯的赋值,所以不会触发。
43.KVC的底层实现?
赋值方法setValue:forKey:的原理
(1)首先会按照顺序依次查找setKey:方法和_setKey:方法,只要找到这两个方法当中的任何一个就直接传递参数,调用方法;
(2)如果没有找到setKey:和_setKey:方法,那么这个时候会查看accessInstanceVariablesDirectly方法的返回值,如果返回的是NO(也就是不允许直接访问成员变量),那么会调用setValue:forUndefineKey:方法,并抛出异常“NSUnknownKeyException”;
(3)如果accessInstanceVariablesDirectly方法返回的是YES,也就是说可以访问其成员变量,那么就会按照顺序依次查找 _key、_isKey、key、isKey这四个成员变量,如果查找到了,就直接赋值;如果依然没有查到,那么会调用setValue:forUndefineKey:方法,并抛出异常“NSUnknownKeyException”。
取值方法valueForKey:的原理
(1)首先会按照顺序依次查找getKey:、key、isKey、_key:这四个方法,只要找到这四个方法当中的任何一个就直接调用该方法;
(2)如果没有找到,那么这个时候会查看accessInstanceVariablesDirectly方法的返回值,如果返回的是NO(也就是不允许直接访问成员变量),那么会调用valueforUndefineKey:方法,并抛出异常“NSUnknownKeyException”;
(3)如果accessInstanceVariablesDirectly方法返回的是YES,也就是说可以访问其成员变量,那么就会按照顺序依次查找 _key、_isKey、key、isKey这四个成员变量,如果找到了,就直接取值;如果依然没有找到成员变量,那么会调用valueforUndefineKey方法,并抛出异常“NSUnknownKeyException”。
44.ViewController生命周期
按照执行顺序排列:
1. initWithCoder:通过nib文件初始化时触发。
2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
3. loadView:开始加载视图控制器自带的view。
4. viewDidLoad:视图控制器的view被加载完成。
5. viewWillAppear:视图控制器的view将要显示在window上。
6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
9. viewDidAppear:视图控制器的view已经展示到window上。
10. viewWillDisappear:视图控制器的view将要从window上消失。
11. viewDidDisappear:视图控制器的view已经从window上消失。
45.TCP连接的三次握手和四次挥手
45.1.三次握手
1.第一次握手:客户端给服务端发送一个SYN报文,并指明客户端的初始化序列号ISN(c);此时客户端处于SYN_Send状态
2.第二次握手:服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也是指定了自己的初始化序列号ISN(s),同时会把客户端的ISN+1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务器处于SYN_REVD的状态
3.第三次握手:客户端收到SYN报文之后,会发送一个ACK报文,当然,也是一样把服务器的ISN+1作为ACK的值,表示已经收到了服务器的SYN报文,此时客户端处于established状态
4.服务器收到ACK报文之后,也处于established状态,此时,双方建立了连接
45.2.三次握手的作用
1.确认双方的接受能力,发送能力是否正常
2.指定自己的初始化序列号,为后面的可靠传送做准备
3.如果是https协议的话,三次握手这个过程还会进行数字证书的验证以及加密密钥的生成
45.3.ISN是固定的吗
动态生成的,如果是固定的,攻击者很容易猜出后续的确认号
45.4.什么是半连接队列?
服务器第一次收到客户端的SYN之后,就会处于SYN_RCVD状态,此时双方还没有完全建立起连接,服务器会把此种状态下请求连接放在一个队列里,这个叫做半连接队列
45.5.三次握手可以携带数据吗
第一,第二次不可以携带数据,第三次可以
前面两次不可以是防止攻击者在前两次握手中放入大量垃圾数据
第三吃可以是因为客户端已经处于established状态,对客户端来说连接已经建立成功了,所以发送能力是正常的
45.6.四次挥手
1.第一次挥手:客户端发送一个FIN报文,报文中会指定一个序列号,此时客户端处于CLOSED_WAIT1状态
2.第二次挥手:服务端收到FIN之后,会发送ACK报文,且把客户端的序列号值+1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT2状态
3.第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发送FIN报文,且指定一个序列号,此时服务端处于LAST_ACK的状态
4.第四次挥手:客户端收到FIN之后,一样发送一个ACK报文作为应答,且把服务端的序列号值+1作为ACK报文的序列号值,此时客户端处于TIME_WAIT状态,需要过一阵子以保证服务端收到自己的ACK报文之后才会进入CLOSED状态
5.服务端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态
45.7.为什么客户端发送ACK之后不直接关闭,而要等一阵子才关闭?
要确保服务器是否收到了ACK报文,如果没有收到,服务器会重新发FIN报文给客户端,
客户端再次收到FIN报文之后,就知道之前的ACK报文丢失了,然后再次发送ACK报文
46.HTTP和HTTPS有什么区别?
HTTP协议是一种使用明文数据传输的网络协议。
HTTPS协议可以理解为HTTP协议的升级,就是在HTTP的基础上增加了数据加密。
在数据进行传输之前,对数据进行加密,然后再发送到服务器。
这样,就算数据被第三者所截获,但是由于数据是加密的,所以你的个人信息让然是安全的。
这就是HTTP和HTTPS的最大区别。
47.HTTPS的加密方式?
Https采用对称加密和非对称加密结合的方式来进行通信。
Https不是应用层的新协议,而是Http通信接口用SSL和TLS来加强加密和认证机制。
对称加密: 加密和解密都是同一个钥匙
非对称加密:密钥承兑出现,分为公钥和私钥,公钥加密需要私钥解密,私钥加密需要公钥解密
48.HTTP和HTTPS的建立连接的过程?
HTTP
建立链接完毕以后客户端会发送响应给服务器
服务端接受请求并且做出响应发送给客户端
客户端收到响应并且解析响应给客户
HTTPS
在使用HTTPS是需要保证服务端配置了正确的对应的安全证书
客户端发送请求到服务器
服务端返回公钥和证书到客户端
客户端接受后,会验证证书的安全性,如果通过则会随机生成一个随机数,用公钥对其解密, 发送到服务端
服务端接受到这个加密后的随机数后,会用私钥对其进行揭秘,得到真正的随机数,然后调用这个随机数当作私钥对需要发送的数据进行对称加密。
客户端接收到加密后的数据使用私钥(之前生成的随机值)对数据进行解密,并且解析数据呈现给客户
49.HTTP协议中GET和POST的区别
GET在特定的浏览器和服务器对URL的长度是有限制的。但是理论上是没有限制的
POST不是通过URL进行传值,理论上不受限制。
GET会把请求参数拼接到URL后面, 不安全,
POST把参数放到请求体里面,会比GET相对安全一点,但是由于可以窥探数据,所以也不安全,想更安全用加密。
GET比POST的请求速度快。原因:Post请求的过程,会现将请求头发送给服务器确认,然后才真正的发送数据,而Get请求 过程会在链接建立后会将请求头和数据一起发送给服务器。中间少了一步。所以get比post快
post的请求过程
三次握手之后 第三次会把post请求头发送
服务器返回100 continue响应
浏览器开始发送数据
服务器返回200 ok响应
get请求过程
三次握手之后 第三次会发送get请求头和数据
服务器返回200 ok响应
50.有没有使用过performSelector?(有没有动态添加过方法)
Person *p = [[Person alloc] init];
// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
// 动态添加方法就不会报错
[p performSelector:@selector(eat)];
// 默认方法都有两个隐式参数,
void eat(id self,SEL sel){
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(eat)) {
// 动态添加eat方法
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
51.performSelector的实现原理
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
52.performSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?
不起作用,子线程默认没有 Runloop,也就没有 Timer。
解决的办法是可以使用 GCD 来现:Dispatch_after
// 完整调用
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// [[NSRunLoop currentRunLoop] run]; 放在上面执行是不可以的,因为当前只是开启了runloop 里面没有任何事件(source,timer,observer)也是开启失败的
[self performSelector:@selector(test) withObject:nil afterDelay:2];
[[NSRunLoop currentRunLoop] run];
});
53.NSTimer在子线程执行?
NSTimer直接在在子线程是不会被调用的, 想要执行请开启当前的Runloop 。具体开启方案上面题有说,不赘述。
54.NStimer的循环引用?
有的人会说, NSTimer本身的target会引用这self, 而self又引用这Timer就造成了循环引用, 那如果timer用weak声明呢? 还会循环引用么? 答案:会的 原因是NTtimer和Runloop是一个相互存在的东西, 别的道理我就不多BB, 就是Runloop和tmier相互引用,而Runloop永远不会销毁,造成贷方面的“牵引” 所以苹果出来了一个invalid的方法。 优化的方案还有别的,例如利用NSProxy这个专门做消息转发的虚类去优化循环引用
55.为什么AFN3.0中需要设置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0却不需要?
功能不一样
2.x operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。
3.x operationQueue是用来接收NSURLSessionDelegate回调的,鉴于一些多线程数据访问的安全性考虑,设置了maxConcurrentOperationCount = 1来达到串行回调的效果。
55.1 AFNetworking 2.0 和3.0 的区别?
AFN3.0剔除了所有的NSURLConnection请求的API
AFN3.0使用NSOperationQueue代替AFN2.0的常驻线程
55.2 2.x版本常驻线程的分析
在请求完成后我们需要对数据进行一些序列化处理,或者错误处理。
如果我们在主线中处理这些事情很明显是不合理的,不仅会导致UI的卡顿,甚至受到默认的RunLoopModel的影响,我们在滑动tableview的时候,会导致时间的处理停止。
这里时候我们就需要一个子线程来处理事件和网络请求的回调了,但是,子线程在处理完事件后就会自动结束生命周期,这个时候后面的一些网络请求得回调我们就无法接收了。
所以我们就需要开启子线程的RunLoop来保存线程的常驻。
当然我们可以每次发起一个请求就开启一条子线程,但是这个想一下就知道开销有多大了,所以这个时候保活一条线程来对请求得回调处理是比较好的一个方案。
55.3 3.x版本不在常驻线程的分析?
在3.x的AFN版本中使用的是NSURLSession进行封装。对比于NSURLConnection,NSURLSession不需要在当前的线程等待网络回调,而是可以让开发者自己设定需要回调的队列。
在3.x版本中AFN使用了NSOperationQueue对网络回调的管理,并且设置maxConcurrentOperationCount为1,保证了最大的并发数为1,也就是说让网络请求串行执行。避免了多线程环境下的资源抢夺问题。
55.autoreleasePool 在何时被释放?
一个被autoreleasepool包裹生成得对象,都会在其创建生成之后自动添加autorelease, 然后被autorelease对象得释放时机 就是在当前runloop循环结束的时候自动释放的
56.子线程中的autorelease变量什么时候释放?
子线程中会默认包裹一个autoreleasepool的,释放时机是当前线程退出的时候。
57.autoreleasepool是如何实现的?
@autoreleasepool{} 本质上是一个结构体:
autoreleasepool会被转换成__AtAutoreleasePool,里面有两个函数objc_autoreleasePoolPush(),objc_autoreleasePoolPop().
其实一些列下来之后实际上调用得是AutoreleasePoolPage类中得push和pop两个类方法
push就是压栈操作,
pop就是出栈操作于此同时对其对象发送release消息进行释放
58.iOS界面渲染机制?
渲染机制
首先iOS渲染视图的核心是Core Animation,其渲染层次依次为:图层树->呈现树->渲染树
一共三个阶段
CPU阶段(进行Frame布局,准备视图和图层之间的层级关系)
OpenGL ES阶段(iOS8以后改成Metal),(渲染服务把上面提供的图层上色,生成各种帧)
GPU阶段(把上面操作的东西进行一些列的操作,最终展示到屏幕上面)
稍微详细说明
首先一个视图由CPU进行Frame布局,准备视图和图层的层及关系。
CUP会将处理视图和图层的层级关系打包,通过IPC(进程间的通信)通道提交给渲染服务(OpenGL和GPU)
渲染服务首先将图层交给OpenGL进行纹理生成和着色,生成前后帧缓存,再根据硬件的刷新帧率,一般以设备的VSync信号和CADisplayLink(类似一个刷新UI专用的定时器)为标准,进行前后帧缓存的切换
最后,将最终 要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换, 应用纹理混合,最终显示在屏幕上。
59.程序卡顿的原因?
正常渲染流程
CPU计算完成之后交给GPU,来个同步信号Vsync 将内容渲染到屏幕上
非正常(卡顿/掉帧)的流程
CPU计算时间正常或者慢,GPU渲染时间长了, 这时候Vsync信号, 由于没有绘制完全,CUP开始计算下一帧,当下一帧正常绘制成功之后,把当前没有绘制完成的帧丢弃, 显示了下一帧,于是这样就造成了卡顿。
Vsync时间间隔是固定的, 比如60帧率大的Vsync 是每16ms就执行一个一次,类似定时器一样
60.从第一次打开App到完全开始展现出UI,中间发生了什么?或者App是怎么渲染某一个View的?
1.Core Animation
Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。
这个 Observer 的优先级是 2000000,低于常见的其他 Observer。
当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;
这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。
当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。
这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。
2.CPU渲染职能
布局计算:如果视图层级过于复杂,当试图呈现或者修改的时候,计算图层帧率就会消耗一部分时间,
视图懒加载:iOS只会当视图控制器的视图显示到屏幕上才会加载它,这对内存使用和程序启动时间很有好处,但是当呈现到屏幕之前,按下按钮导致的许多工作都不会被及时响应。比如,控制器从数据局中获取数据, 或者视图从一个xib加载,或者涉及iO图片显示都会比CPU正常操作慢得多。
解压图片:PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对 图层内容赋值的时候(直接或者间接使用 UIImageView )或者把它绘制到 Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。
Core Graphics绘制:如果对视图实现了drawRect:或drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
图层打包:当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示 屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环 转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须 要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层 级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用 程序可控的。
3.GPU渲染职能
GPU会根据生成的前后帧缓存数据,根据实际情况进行合成,其中造成GPU渲染负担的一般是:离屏渲染,图层混合,延迟加载。
61.一个UIImageView添加到视图上以后,内部如何渲染到手机上的?
图片显示分为三个步骤: 加载、解码、渲染、
通常,我们程序员的操作只是加载,至于解码和渲染是由UIKit内部进行的。
例如:UIImageView显示在屏幕上的时候需要UIImage对象进行数据源的赋值。而UIImage持有的数据是未解码的压缩数据,当赋值的时候,图像数据会被解码变成RGB颜色数据,最终渲染到屏幕上。
62.解释一下为什么减少离屏渲染操作?
需要创建新的缓冲区
整个过程需要多次切换上下文环境, 显示从当前的屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果 显示到屏幕有上, 又要将上下文环境从离屏切换到当前屏幕
63.那些操作会触发离屏渲染?
光栅化 layer.shouldRasterize = YES
遮罩layer.mask
圆角layer.maskToBounds = Yes,Layer.cornerRadis 大于0
阴影layer.shadowXXX
64.iOS保持界面流畅的技巧
65.事件响应链和事件传递?
响应链: 是由链接在一起的响应者(UIResponse子类)组成的,一般为第一响应着到application对象以及中间所有响应者一起组成的。
事件传递: 获取响应链之后, 将事件由第一响应者网application的传递过程
事件的分发和传递
当程序中发生触摸事件之后,系统会将事件添加到UIApplication管理的一个队列当中
UIApplication将处于任务队列最前端的事件向下分发 即UIWindow
UIWindow将事件向下分发,即UIView或者UIViewController
UIView首先看自己能否处理这个事件,触摸点是否在自己身上,自己的透明度是否大于0,01,userInteractionEnabled 是否是YES, Hidden是否是NO,如果这些都满足,那么继续寻找其子视图
遍历子控件,重复上面步骤
如果没有找到,那么自己就是该事件的处理者
如果自己不能处理,那么就不做任何处理,即视为没有合适的View能接收处理当前事件,则改事件会被废弃。
66.怎么寻找当前触摸的是哪一个View?
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
事件传递给控件之后, 就会调用hitTest:withEvent方法去寻找更合适的View,如果当前View存在子控件,则在子控件继续调用hitTest:withEvent方法判断是否是合适的View, 如果还不是就一直遍历寻找, 找不到的话直接废弃掉。
67.tableView 加一个tap的手势,点击当前cell的位置,哪个事件被响应?为什么?
tap事件被响应, 因为tap事件添加之后,默认是取消当前tap以外的所有事件的, 也就是说, tap事件处于当前响应者链的最顶端
// 解决的办法执行tap的delagete
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if([touch.view isKindOfClass:[XXXXcell class]]) {
return NO;
}
return YES;
}
68.SDWebImage是怎么做缓存的?
缓存采用了二级缓存策略。图片缓存的时候,在内存有缓存,在磁盘中也有缓存,其中内存缓存是用NSCache做的
一、如何做缓存的步骤:
0、下载图片
1、将图片缓存在内存中
2、判断图片的格式png或jpeg,将图片转成NSData数据
3、获取图片的存储路径, 其中图片的文件名是通过传入Key经过MD5加密后获得的
4、将图片存在进磁盘中。
二、如何获取图片的?
1、在内存缓存中找
2、如果内存中找不到, 会去默认磁盘目录中寻找, 如果找不到,在去自定义磁盘目录中寻找
3、如果磁盘也找不到就会下载图片
4、获取图片数据之后, 将图片数据从NSData转化UIImage。其中转化根据图片的类型进行转化
5、默认对图片进行解压缩,生成位图图片
6、将位图图片返回
三、图片是如何被解压缩的?
1、判断图片是否是动态图片,如果是,不能解压缩
2、判断图片是否透明,如果是,不能解压缩
3、判断图片的颜色空间是不是RGB如果不是、不能解压缩
4、根据图片的大小创建一个上下文
5、将图片绘制在上下文中
6、从上下文中读取一个不透明的位图图像,该图像就是解压缩后的图像
7、将位图图像返回
NSCache
这个NSCache说白了就是做缓存专用的一个系统类
类似可变字典一样,但是NSCache是线程安全的,系统类自动做好了加锁和释放锁等一系列的操作,还有一个重要的是如果内存不足的时候NSCache会自动释放掉存储的对象,不需要开发者手动干预。
来看一眼NSCache提供的属性和相关方法
//名称
@property (copy) NSString *name;
//NSCacheDelegate代理
@property (nullable, assign) id<NSCacheDelegate> delegate;
//通过key获取value,类似于字典中通过key取value的操作
- (nullable ObjectType)objectForKey:(KeyType)key;
//设置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
/*
设置key、value
cost表示obj这个value对象的占用的消耗?可以自行设置每个需要添加进缓存的对象的cost值
这个值与后面的totalCostLimit对应,如果添加进缓存的cost总值大于totalCostLimit就会自动进行删除
感觉在实际开发中直接使用setObject:forKey:方法就可以解决问题了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
//根据key删除value对象
- (void)removeObjectForKey:(KeyType)key;
//删除保存的所有的key-value
- (void)removeAllObjects;
/*
当NSCache缓存的对象的总cost值大于这个值则会自动释放一部分对象直到占用小于该值
非严格限制意味着如果保存的对象超出这个大小也不一定会被删除
这个值就是与前面setObject:forKey:cost:方法对应
*/
@property NSUInteger totalCostLimit;
/*
缓存能够保存的key-value个数的最大数量
当保存的数量大于该值就会被自动释放
非严格限制意味着如果超出了这个数量也不一定会被删除
*/
@property NSUInteger countLimit;
/*
这个值与NSDiscardableContent协议有关,默认为YES
当一个类实现了该协议,并且这个类的对象不再被使用时意味着可以被释放
*/
@property BOOL evictsObjectsWithDiscardedContent;
@end
//NSCacheDelegate协议
@protocol NSCacheDelegate <NSObject>
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时被回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end
countLimit注意一下这个属性,这个属性就是设置最大缓存数量,啥意思呢?
这玩意就和栈差不多,先进先出(叫什么FIFO?)原则。
比如你countLimit设置为5 那么当你缓存第6个对象的时候,原本第一个就被移除了。
所以这便就有有一个风险,也可能会是面试点,为什么,通过key去取值的时候,一定要判断一个获取的对象是否为nil?
答:就因为很有可能某些对象被释放(顶)掉了
69.NSCache里面缓存的对象,在什么场景下会被释放?
NSCache自身释放了,其中存储的对象也就释放了。
手动调用释放方法removeObjectForKey、removeAllObjects
缓存对象个数大于countLimit
缓存总消耗大于totalCostLimit
程序进入后台
收到内存警告
70.SDWebImage实现原理是什么? 它是如何解决tableView的复用时出现图片错乱问题的呢?
原理如上
解决错乱是在UIImageView+WebCache文件中这个方法每次都会调用 [self sd_cancelCurrentImageLoad];
71.为什么刷新UI要在主线程操作
UIKit并不是一个线程安全的类,所以涉及多个线程同时对UI进行操作会造成影响。
71.1 为什么不把UIKit框架设置为线程安全呢?
因为线程安全需要加锁
我们都知道加锁就会消耗性能,影响处理速度,影响渲染速度,我们通常自己在写@property时都会写nonatomic来追求高性能高效率。
1.假设能够异步设置view的属性,那我们究竟是希望这些改动能够同时生效,还是按照各自runloop的进度去改变这个view的属性呢?
2.假设UITableView在其他线程去移除了一个cell,而在另一个线程却对这个cell所在的index进行一些操作,这时候可能就会引发crash。
3.如果在后台线程移除了一个view,这个时候runloop周期还没有完结,用户在主线程点击了这个“将要”消失的view,那么究竟该不该响应事件?在哪条线程进行响应?
在Cocoa Touch框架中,UIApplication初始化工作是在主线程进行的。
而界面上所有的视图都是在UIApplication 实例的叶子节点(内存管理角度),所以所有的手势交互操作都是在主线程上才能响应
72.NSNotification
NSNotificationCent子线程中发出通知,也要在主线程中刷新UI
// 比如
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新UI
});
NSNotificationCenter用完之后不移除,会崩溃么?
有时候会导致crash
比如在通知事件中处理数据或者UI事件,但是由于通知的不确定性造成事件的不确定,有异步操作在通知事件中处理等都可能造成崩溃。
而且通知的崩溃很难检测。
73.什么情况使用 weak 关键字,相比 assign 有什么不同?
weak 这个词儿解决了一件事情,就是内存的事情
在ARC中weak的出现解决了一些循环引用的问题,比如delegate,xib连线出来的控件一般也是weak(也可以用strong )
weak表明了一种“非拥有的关系”,不保留新值,也不释放旧值。
assign也是如此,但常用的assign一般用于基本数据类型(CGFloat 或 NSlnteger等)
assign可以用于非OC对象,也可以用于OC对象(MRC时代使用), 但是weak必须用在OC对象。
74.关键字copy 的用法?
block用Copy是MRC时代留下来的传统。
在MRC中方法内部的block是在栈区的,使用copy可以把它放到堆区。
在ARC中写不写都行,用Strong也是可以的。
NSString、NSArray、NSDictionary也经常使用copy,因为里面有对应的可变的子类型,为了确保安全性,建议使用copy修饰
75.@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的。
@property = ivar(实例变量) + getter + setter
自动合成的
76.说说内存管理?
粗糙版本
版本一
内存中每一个对象都有一个属于自己的引用计数器。
当某个对象A被另一个对象引用时,A的引用计数器就+1,如果再有一个对象引用到A,那么A的引用计数器就再+1。
当其中某个对象不再引用A了,A的引用计数器会-1。
直到A的引用计数减到了0,那么就没有人再需要它了,就是时候把它释放掉了
版本二
对象通过 alloc copy new 生成得得对象在MRC年代需要手动管理内存,
利用得技术是returnCount引用计数器,来管理对象得释放时机
alloc创建对象引用计数器+1
retain持有关系引用计数器+1
release 引用计数器-1。
如果当前对象得returnCount = 0 对象就会被在dealloc方法里面适当时机进行释放(啥时候释放?)如果当前returnCount大于0得时候,就会一直被持有。
版本三
当 alloc copy new 生成得对象里面在内部会关联SideTable,
SideTable有三个属性,一个是一把自旋锁,一个是引用计数器相关
一个是维护weak生命得属性得表,其中retain、release 对利用键值对会对当前对象得引用计数器进行加减操作(位移),如果当前引用计数器为0得时候,其dealloc内部会删除当前的引用计数器,并且释放当前对象。
详细版本:www.jianshu.com/p/ef6d9bf8f…
77.imageName与imageWithContentsOfFile区别?
imageWithContentsOfFile: 加载本地目录图片,并不会缓存,占用内存小, 不能加载image.xcassets里面的图片资源。相同的图片会被重复加载到内存中
imageName:加载到内存中,会缓存起来,占用内存较大,相同的图片不会被重复加载到内存当中,会读取image.xcassets的图片图片资源。
使用 imageNamed 创建的 UIImage 会被立即加入到 NSCache 中(解码后的 Image Buffer),直到收到内存警告的时候才会释放不使用的 UIImage。
imageWithContentsOfFile 会每次重新申请内存,相同图片不会缓存
如果不断重复读取同一个图片,则使用imageName
如果不需要重复读取同一个图片,并且需要低内存,则使用imageWithContentsOfFile
78.IBOutlet连出来的视图属性为什么可以被设置成weak?
因为链接之Xcode 内部把链接的控件 放进了一个_topLevelObjectsToKeepAliveFromStoryboard的私有数组中,这个数组强引用这所有top level的对象 所以用weak也无伤大雅。
79.id 为什么不能用点语法?
点语法就是setter和getter方法, 然而id类 无法确定所指的类是什么类型, 寻不到setter个getter方法,id类型的对象 只能用【】方法调用方法
80.id和NSObject的区别?
id是struct objc_object结构体指针,可以指向任何OC对象,当然不包括NSInteger等类型,因为这些数据类型不是OC对象。
另外OC的基类不止有NSObject一个,还有个NSProxy虚类。所以说id类型和NSObject并不是等价的。
81.OC中 Null 与 nil的区别
NULL是指指针是空值,用来判断C 指针;
nil是指一个OC对象(指针)为空;
Nil是指一个OC类为空;
NSNull则用于填充集合元素;这个类只有一个方法null,并且是单例的;
82.自旋锁和互斥锁
相同点
都能保证同一时间只有一个线程访问共享资源,都能保证系统安全
不同点
互斥锁:如果共享数据已经有了其他线程加锁了,线程会进行休眠状态等待锁,一旦被访问的资源被解锁,则等待资源的线程会被唤醒。信号量dispatch_semaphore 为互斥锁 @synchronized是NSLock的封装 属于互斥锁 互斥锁一般用于等待时间较长的情况
适用于:线程等待锁的时间较长
自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。OSSpinLock 属于自旋锁 自旋锁一般用于时间较短的情况,OSSpinLock
适用于:线程等待锁的时间较端
83.进程和线程的区别
进程是指在系统中正在运行的一个应用程序
线程是进程中的一个实体,一个进程想要执行任务, 必须至少有一条线程,应程序启动的时候会默认开启一条线程,也就是主线程
一个进程拥有多个线程
84.LayoutSubviews和drawRect调用时机
LayoutSubviews调用时机
init初始化不会调用LayoutSubviews方法
addsubView 时候会调用
改变一个View的frame的时候调用
滚动UIScrollView导致UIView重新布局的时候会调用
手动调用setNeedsLayout或者layoutIfNeeded
drawRect调用时机
drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的
85.cocoaPods里面pod install和update的区别?
pod install
一般是第一次想要为项目添加pod的时候使用的,当然也可以在添加和移除库使用
每次pod install的时候,pod install 回为每一个安装的pod库在Podfile.lock文件中写入其版本号,并且锁定当前版本号。
如果pod install的时候,不会更新其版本库,而是去下载新的或者移除当前版本
pod update
当执行了pod update的时候,cocoaPods不会考虑Podfile.lock中的版本。直接去更新当前所有的库到最新,然后Podfile.lock会更新这一次的版本号。
85.frame和masonry哪个性能好?为什么
有的相对布局最终都会转换成Frame绝对布局,中间多了一层转换的操作
86.iOS从iOS9 - 13的特性
iOS9
从HTTP升级到HTTPS
App瘦身 下面有讲 这里不赘述( App瘦身 )
新增UIStackView
iOS10
新增通知推送相关的操作。自定义通知弹窗,自定义通知类型(地理位置,时间间隔,日历等)
iOS11
无线 调试
齐刘海儿,导航条,安全距离等
iOS12
启动速度优化
应用启动速度提升40%
键盘响应速度提升50%
相机启动速度提升70%
iOS13
黑暗模式 详情请查阅 www.jianshu.com/p/0da3b107f…
87.App如何瘦身?
1.删除陈旧代码、删除陈旧xib/sb,删除无用的图片资源(检测未使用图片的工具LSUnusedResources )
2.无损压缩图片,本地音视频压缩;以直接减少图片大小
3.使用webP格式的图片(加载速度比较慢,但可以达到瘦身的效果)
4.减小类名称的长度(高性能的话可以试一试)
5.减少使用静态库
一些主题类的东西提供下载功能,不直接打包在应用包里面,按需加载资源
iOS9之后的新特性
应用程序切片(App Slicing)
Slicing:这个过程是iOS9出来之后不需要程序员干预的一个瘦身的过程,简单来说就是我们再上传IPA包到iTunesConnect,然后AppStore会对app进行切片,切成特定的机型想要的数据,比如@3x给max用,@2x就自动剔除了。 是一个自动的过程
中间代码(Bitcode)
Bitcode:是一种中间码,如果配置了Bitcode(Xcode7以后默认开启)的程序会在App Store Connect上被重新编译等一系列操作,进而苹果内部会对可执行文件进行优化,也就是说不需要我们干预什么东西,也操作不了, 如果后面苹果有更牛逼的优化操作,也是苹果的事情, 跟我们个人开发者一毛钱关系没有。
按需加载资源(On Demand Resources)
On Demand Resources:按需加载, 是程序员自己手动操作,说白了就是在用的时候去下载某些资源, 但是我们自己在配置的时候都需要配置,要额外写一些代码啥的,等我们提交到市场的时候, 苹果内部会把我们按需加载的资源从包里面做了一些抽离操作啥的, 让我们的包在下载的时候更小,举个例子,就是吃鸡里面沙漠地图如果玩家不自己下载, 就玩不了沙漠。
88.APP的生命流程
1.APP的启动流程(pre-main)
2.APP的初始化流程(main)
3.APP的运行时生命周期
APP的启动流程
1.iOS系统首先会加载解析该APP的Info.plist文件,因为Info.plist文件中包含了支持APP加载运行所需要的众多Key,value配置信息,例如APP的运行条件(Required device capabilities),是否全屏,APP启动图信息等。
2.创建沙盒(iOS8后,每次启动APP都会生成一个新的沙盒路径)
3.根据Info.plist的配置检查相应权限状态
4.加载Mach-O文件读取dyld路径并运行dyld动态连接器(内核加载了主程序,dyld只会负责动态库的加载,dyld说明:
dyld叫做动态链接器,主要的职责是完成各种库的连接。dyld是苹果用C++写的一个开源库)
4.1 首先dyld会寻找合适的CPU运行环境
4.2 然后加载程序运行所需的依赖库和我们自己写的.h.m文件编译成的.o可执行文件,并对这些库进行链接。
4.3 加载所有方法(runtime就是在这个时候被初始化并完成OC的内存布局)
4.4 加载C函数
4.5 加载category的扩展(此时runtime会对所有类结构进行初始化)
4.6 加载C++静态函数,加载OC+load
4.7 最后dyld返回main函数地址,main函数被调用
Mach-O文件说明:
Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,类似于windows的 PE 文件。像我们编译产生的.o文件、程序可执行文件和各种库等都是Mach-O文件。
Mach-O文件主要有3部分组成:
1.Header:保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等等。Headers的主要作用就是帮助系统迅速的定位Mach-O文件的运行环境,文件类型。保存了一些dyld重要的加载参数
2.LoadCommands:可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。
3.Data: 每一个segment的具体数据都保存在这里,这里包含了具体的代码、数据等等。
APP的初始化流程
1.main 函数
2.执行UIApplicationMain
2.1 创建UIApplication对象
2.2 创建UIApplication的delegate对象
2.3 创建MainRunloop
2.4 delegate对象开始处理(监听)系统事件(没有storyboard)
3.根据Info.plist获得最主要storyboard的文件名,加载最主要的storyboard(有storyboard)
程序启动完毕的时候, 就会调用代理的application:didFinishLaunchingWithOptions:方法
在application:didFinishLaunchingWithOptions:中创建UIWindow
创建和设置UIWindow的rootViewController
最终显示第一个窗口
89.App启动优化
1.是减少系统依赖库
2.减少自己需要加入的各种三方库(库越少dyld加载的速度越快,就能越早的返回程序入口main函数的地址)
3.有一些自己加入的库,能选择静态库就选择静态库,少用动态库,因为动态库的加载方式比静态库慢。如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库。
4.自己加入的各种framework库根据情况设为optional和required,如果该framework在当前App支持的所有iOS系统中都存在(模拟器,真机),那么就设为required,否则就设为optional。(Required(强引用)的framework一定会被加载到内存中,及时不使用也会被加载到内存中。Optional(弱引用)的framework启动的时候并不会加载,在使用的时候才会进行加载,可以减少启动时加载动态库的时间。)
5.将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)。
6.减少项目文件中Category,静态变量等的使用数量
7.使用appCode检查项目中,那些类和方法没有使用到。 把没有使用到的删除
8.让UI大佬尽量把给的资源压缩到最小,因为在启动加载时会加载资源图片进行IO操作。所以图片小加载速度也会响应提升。
9.内存上优化:类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响,及影响加载速度也消耗内存;因为OC的动态特性,都是加载后通过类/方法名反射找到这个类/方法进行调用,OC的对象模型会把类/方法名字符串都保存下来(压缩算法TinyPNG)。
APP初始化流程的优化点
1.尽量使用纯代码而不是xib或者storyboard来进行UI框架的搭建,尤其是使用的TabBarController这种,尽量避免使用xib和storyboard,因为xib和storyboard也还是要解析成代码来渲染页面,并且官网为了满足更多的需求,必定做了更多的适配判断处理,会多很多步骤。会增加代码的执行效率从而增加启动时长。
2.尽量在application:didFinishLaunchingWithOptions:中代码的执行时间。能多线程就多线程,能后台执行就后台执行。部分加载可以选择懒加载或者后台加载。不要阻塞主线程从而造成启动时间加长。
通常优化的一般来说,还是从AppDelegate先入手优化
didFinishLaunchingWithOptions
applicationDidBecomeActive
优化的核心思想就是,能延时的延时,不能延时的尽量放到后台去优化。
1.日志、统计等必须在APP一启动就最先配置的事件,仍然把它留在 didFinishLaunchingWithOptions 里启动。
2.项目配置、环境配置、用户信息的初始化 、推送、IM等事件,这些功能在用户进入 APP 主体的之前是必须要加载完的,把他放到广告页面的viewDidAppear启动。
3.其他 SDK 和配置事件,由于启动时间不是必须的,所以我们可以放在第一个界面的 viewDidAppear 方法里,这里完全不会影响到启动时间。
4.每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
5.尽量不要在didFinishLaunchingWithOptions 里面创建和开启多线程
高频面试题
1.你在开发过程中常用到哪些定时器,定时器时间会有误差吗,如果有,为什么会有误差?
iOS中常NSTimer、CADisplayLink、GCD定时器,其中NSTimer、CADisplayLink基于NSRunLoop实现,故存在误差,GCD定时器只依赖系统内核,相对一前两者是比较准时的。
误差原因是:与NSRunLoop机制有关, 因为RunLoop每跑完一次圈再去检查当前累计时间是否已经达到定时设置的间隔时间,如果未达到,RunLoop将进入下一轮任务,待任务结束之后再去检查当前累计时间,而此时的累计时间可能已经超过了定时器的间隔时间,故会存在误差。 详情:juejin.cn/post/686345…
2. NSTimer、CADisplayLink会产生循环引用吗?如果会,你是如何解决的?
#import <Foundation/Foundation.h>
@interface LXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "LXProxy.h"
@interface LXProxy ()
/** weak target*/
@property (nonatomic, weak) id target;
@end
@implementation LXProxy
+ (instancetype)proxyWithTarget:(id)target{
LXProxy *proxy = [LXProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
// 调用
_timer = [NSTimer scheduledTimerWithTimeInterval:2 target:[LXProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];
二.Runloop
1.Runloop概念
RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。
没有消息处理时,就会进行休眠避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
有消息需要处理时,立刻被唤醒,由内核态切换到用户态
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
1-1.Event Loop模型
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
1-2.RunLoop的数据结构
RunLoop 相关的主要涉及五个类:
CFRunLoop:RunLoop对象
CFRunLoopMode:运行模式
CFRunLoopSource:输入源/事件源
CFRunLoopTimer:定时源
CFRunLoopObserver:观察者
1-2-1.CFRunLoop
pthread:线程对象,说明RunLoop和线程是一一对应的
currentMode:当前所处的运行模式
modes:多个运行模式的集合
commonModes:模式名称字符串集合
commonModelItems:Observer,Timer,Source集合
1-2-2.CFRunLoopMode
由name、source0、source1、observers、timers构成
1-2-2-1.五种CFRunLoopMode运行模式
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
UIInitializationRunLoopMode:在刚启动App时进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
1-2-3.CFRunLoopSource
source0:即非基于port的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户态
source1:基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件,具备唤醒线程的能力
1-2-4.CFRunLoopTimer
基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。
1-2-5.CFRunLoopObserver
1-3.各数据结构之间的联系
线程和RunLoop:一对一
RunLoop和Mode:一对多
Mode和source、timer、observer:一对多
1-4.RunLoop和线程的关系
RunLoop的作用就是用来管理线程的, 当线程的RunLoop开启之后,线程就会在执行完成任务后,进入休眠状态,随时等待接收新的任务,而不是退出
线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
自己创建的线程默认是没有开RunLoop的
2.怎么创建一个常驻线程?
为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
// 先创建一个线程用于测试
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(play) object:nil];
[thread start];
// 保证一个线程永远不死
[[NSRunLoop currentRunLoop] addPort:[NSPort port] -forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 在合适的地方处理线程的事件处理
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
三.Runtime
结构组成
1.类的结构和isa指针
2.class_copyIvarList与class_copyPropertyList的区别?
class_copyIvarList:能够获取.h和.m中的所有属性以及大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)。
class_copyPropertyList:只能获取由property声明的属性,包括.m中的,获取的属性名称不带下划线。
3.class_ro_t和class_rw_t的区别?
class_rw_t提供了运行时对类拓展的能力,
class_ro_t存储的大多是类在编译时就已经确定的信息。
二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。简单的说class_rw_t存储列表使用的二维数组,class_ro_t使用的一维数组。
class_ro_t存储于class_rw_t结构体中,是不可改变的。保存着类的在编译时就已经确定的信息。
运行时修改类的方法,属性,协议等都存储于class_rw_t中
知识点
1.Method Swizzle注意事项