iOS面试题
本面试题为个人使用版本,如后续流传出去,请转发的朋友务必注释一下,答案正确性有待商榷,本人的答案不代表权威,仅仅是个人理解。 文章内部有写混乱,将就着看吧。另外大部分图片加载不出来,,MARKDown格式也不太统一(各平台不一样),由于博主太懒不想改,不过不影响最终效果。
更新日志
- 2020年08月17日 更新了第23条的新的引申,关于NSTimer循环引用的根本原因, 以及优化方案
一、硬技术篇
1.对象方法和类方法的区别?
- 对象方法能个访问成员变量。
- 类方法中不能直接调用对象方法,想要调用对象方法,必须创建或者传入对象。
- 类方法可以和对象方法重名。
引伸1. 如果在类方法中调用self 会有什么问题?
- 在 实例方法中self不可以调用类方法,此时的self不是Class。
- 在类方法中self可以调用其他类方法。
- 在类方法中self不可以调用实例方法。
- 总结:类方法中的self,是class/ 实例方法中self是对象的首地址。
如果你正在面试,或者正准备跳槽,不妨看看我精心总结的iOS大厂面试资料:https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障
引申2. 讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?
- 对象的结构体当中存放着isa指针和成员变量,isa指针指向类对象
- 类对象的isa指针指向元类,元类的isa指针指向NSObject的元类
- 类对象和元类的结构体有isa,superClass,cache等等
引申3. 为什么对象方法中没有保存对象结构体里面,而是保存在类对象的结构体里面?
- 方法是每个对象相互可以共用的,如果每个对象都存储一份方法列表太浪费内存,由于对象的isa是指向类对象的,当调用的时候, 直接去类对象中去查找就可以了,节约了很多内存空间。
引申4. 类方法存在哪里? 为什么要有元类的存在?
- 所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。
引申5. 什么是野指针?
- 野指针就是指向一个被释放或者被收回的对象,但是对指向该对象的指针没有做任何修改,以至于该指针让指向已经回收后的内存地址。
- 其中访问野指针是没有问题的,使用野指针的时候会出现崩溃Crash!样例如下
__unsafe_unretained UIView *testObj = [[UIView alloc] init];
NSLog(@"testObj 指针指向的地址:%p 指针本身的地址:%p", testObj, &testObj);
[testObj setNeedsLayout];
// 可以看到NSlog打印不会闪退,调用[testObj setNeedsLayout];会闪退
复制代码
引申6. 如何检测野指针?
这是网友总结的,有兴趣的可以看下:www.jianshu.com/p/9fd4dc046… 本人,也就是看看乐呵,其原理啥的,见仁见智吧。开发行业太j8难了!
引申7. 导致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做后台服务器开发语言
引申8. 不使用第三方,如何知道已经上线的App崩溃问题, 具体到哪一个类的哪一个方法的?
大致实现方式如下。
- 使用NSSetUncaughtExceptionHandler可以统计闪退的信息。
- 将统计到的信息以data的形式 利用网络请求发给后台
- 在后台收集信息,进行排查
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
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(@"***********************************************");
}
复制代码
实现方式如: blog.csdn.net/u013896628/…
iOS中内省的几个方法?
- isMemberOfClass //对象是否是某个类型的对象
- isKindOfClass //对象是否是某个类型或某个类型子类的对象
- isSubclassOfClass //某个类对象是否是另一个类型的子类
- isAncestorOfObject //某个类对象是否是另一个类型的父类
- respondsToSelector //是否能响应某个方法
- conformsToProtocol //是否遵循某个协议
引申 2. ==、 isEqualToString、isEqual区别?
- == ,比较的是两个指针的值 (内存地址是否相同)。
- isEqualToString, 比较的是两个字符串是否相等。
- isEqual 判断两个对象在类型和值上是否都一样。
引申 3. class方法和object_getClass方法有什么区别?
- 实例class方法直接返回object_getClass(self)
- 类class直接返回self
- 而object_getClass(类对象),则返回的是元类
3.深拷贝和浅拷贝
- 所谓深浅指的是是否创建了一个新的对象(开辟了新的内存地址)还是仅仅做了指针的复制。
- copy和mutableCopy针对的是可变和不可变,凡涉及copy结果均变成不可变,mutableCopy均变成可变。
- mutableCopy均是深复制。
- copy操作不可变的是浅复制,操作可变的是深赋值。
4.NSString类型为什么要用copy修饰 ?
- 主要是防止NSString被修改,如果没有修改的说法用Strong也行。
- 当NSString的赋值来源是NSString时,strong和copy作用相同。
- 当NSString的赋值来源是NSMutableString,copy会做深拷贝,重新生成一个新的对象,修改赋值来源不会影响NSString的值。
5.iOS中block 捕获外部局部变量实际上发生了什么?__block 中又做了什么?
-
block 捕获的是当前在block内部执行的外部局部变量的瞬时值, 为什么说瞬时值呢? 看一下C++源码中得知, 其内部代码在捕获的同时
-
其实block底层生成了一个和外部变量相同名称的属性值如果内部修改值,其实修改的是捕获之前的值,其捕获的内部的值因代码只做了一次捕获,并没有做再一次的捕获,所以block里面不可以修改值。
-
如果当前捕获的为对象类型,其block内部可以认为重新创建了一个指向当前对象内存地址的指针(堆),操控内部操作的东西均为同一块内存地址,所以可以修改当前内部的对象里面的属性,但是不能直接修改当前的指针(无法直接修改栈中的内容)(即重新生成一个新的内存地址)。其原理和捕获基本数据类型一致。
-
说白了, block内部可以修改的是堆中的内容, 但不能直接修改栈中的任何东西。
- 如果加上__block 在运行时创建了一个外部变量的“副本”属性,把栈中的内存地址放到了堆中进而在block内部也能修改外部变量的值。
6.iOS Block为什么用copy修饰?
- block 是一个对象
- MRC的时候 block 在创建的时候,它的内存比较奇葩,非得分配到栈上,而不是在传统的堆上,它本身的作用于就属于创建的时候(见光死,夭折),一旦在创建时候的作用于外面调用它会导致崩溃。
- 所以,利用copy把原本在栈上的复制到堆里面,就保住了它。
- **ARC的时候 由于ARC中已经看不到栈中的block了。用strong和copy 一样 随意, 用copy是遵循其传统, **
7. 为什么分类中不能创建属性Property(runtime除外)?
-
分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。 Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。
-
在往深一点的回答就是 类在内存中的位置是编译时期决定的, 之后再修改代码也不会改变内存中的位置,class_ro_t 的属性在运行期间就不能再改变了, 再添加方法是会修改class_rw_t 的methods 而不是class_ro_t 中的 baseMethods
引伸:关联对象的原理?
- 关联对象并不是存储在关联对象本身内存中,而是存储在全局统一的一个容器中;
- 由 AssociationsManager 管理并在它维护的一个单例 Hash 表 AssociationsHashMap 中存储;
- 使用 AssociationsManagerLock 自旋锁保证了线程安全
引伸:分类可以添加那些内容?
- 实例方法,类方法,协议,属性
引伸:Category 的实现原理?
- Category 在刚刚编译完成的时候, 和原来的类是分开的,只有在程序运行起来的时候, 通过runtime合并在一起。
引申 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
- 不需要,被关联的对象的生命周期内要比对象本身释放晚很多, 它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。
引申 能否向编译后得到的类中增加实例变量, 能否向运行时创建的类中添加实力变量?
- 不能再编译后得到的类中增加实例变量。因为编译后的类已经注册在runtime中, 类结构体中objc_ivar_list 实例变量的链表和objc_ivar_list 实例变量的内存大小已经确定,所以不能向存在的类中添加实例变量
- 能在运行时创建的类中添加实力变量。调用class_addIvar 函数
引申 主类执行了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);
}
复制代码
8. load 和 initilze 的调用情况,以及子类的调用顺序问题?
① 调用时刻:+load方法会在Runtime加载类、分类时调用(不管有没有用到这些类,在程序运行起来的时候都会加载进内存,并调用+load方法); 每个类、分类的+load,在程序运行过程中只调用一次(除非开发者手动调用)。
② 调用方式: 系统自动调用+load方式为直接通过函数地址调用,开发者手动调用+load方式为消息机制objc_msgSend函数调用。
③ 调用顺序: 先调用类的+load,按照编译先后顺序调用(先编译,先调用),调用子类的+load之前会先调用父类的+load; 再调用分类的+load,按照编译先后顺序调用(先编译,先调用)(注意:分类的其它方法是:后编译,优先调用)。
① 调用时刻:+initialize方法会在类第一次接收到消息时调用。 如果子类没有实现+initialize方法,会调用父类的+initialize,所以父类的+initialize方法可能会被调用多次,但不代表父类初始化多次,每个类只会初始化一次。
② 调用方式: 消息机制objc_msgSend函数调用。
③ 调用顺序: 先调用父类的+initialize,再调用子类的+initialize (先初识化父类,再初始化子类)
- +initialize方法的调用方式为消息机制,而非像+load那样直接通过函数地址调用。
9. 什么是线程安全?
- 多条线程同时访问一段代码,不会造成数据混乱的情况
10. 你接触到的项目,哪些场景运用到了线程安全?
答: 举例说明,12306 同一列火车的车票, 同一时间段多人抢票! 如何解决 互斥锁使用格式
synchronized(锁对象) { // 需要锁定的代码 }
注意:锁定1份代码只用1把锁,用多把锁是无效的
Tips: 互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
互斥锁的使用前提:多条线程抢夺同一块资源
相关专业术语:线程同步,多条线程按顺序地执行任务
互斥锁,就是使用了线程同步技术
Objective-C中的原子和非原子属性
OC在定义属性时有nonatomic和atomic两种选择
atomic:原子属性,为setter/getter方法都加锁(默认就是atomic)
nonatomic:非原子属性,不加锁
atomic加锁原理:
property (assign, atomic) int age;
- (void)setAge:(int)age
{
@synchronized(self) {
_age = age;
}
}
- (int)age {
int age1 = 0;
@synchronized(self) {
age1 = _age;
}
}
原子和非原子属性的选择
nonatomic和atomic对比
atomic:线程安全,需要消耗大量的资源
nonatomic:非线程安全,适合内存小的移动设备
iOS开发的建议
所有属性都声明为nonatomic
尽量避免多线程抢夺同一块资源
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
atomic就一定能保证线程安全么?
不能,还需要更深层的锁定机制才可以,因为一个线程在连续多次读取某条属性值的时候,与此同时别的线程也在改写值,这样还是会读取到不同的属性值! 或者 一个线程在获取当前属性的值, 另外一个线程把这个属性释放调了, 有可能造成崩溃
复制代码
11. 你实现过单例模式么? 你能用几种实现方案?
1. 运用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这个函数,它可以保证整个应用程序生命周期中某段代码只被执行一次!
2. 不使用GCD的方式:
static Manager *manager;
implementation Manager
+ (Manager *)defaultManager {
if(!manager)
manager=[[self allocWithZone:NULL] init];
return manager;
}
end
3. 正常的完整版本
+(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时相同
-(id)copyWithZone:(NSZone *)zone{
return _instance;
}
// 方法3创建的目的的是 为了方式开发者在调用单例的时候并没有用shareInstance方法来创建 而是用的alloc 或者copy的形式创建造成单例不一致的情况
//
复制代码
引申1. 单例是怎么销毁的?
//必须把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了 就永远不会被调用
复制代码
引申2. 不使用dispatch_once 如何 实现单例
1.第一种方式,重写+allocWithZone:方法;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static id instance = nil;
@synchronized (self) { // 互斥锁
if (instance == nil) {
instance = [super allocWithZone:zone];
}
}
return instance;
}
2.第二种方式,不用重写+allocWithZone:方法,而是直接用@synchronized 来保证线程安全,其它与上面这个方法一样;
+ (instancetype)sharedSingleton {
static id instance = nil;
@synchronized (self) {
if (!instance) {
instance = [[self alloc] init];
}
}
return instance;
}
复制代码
12. 项目开发中,你用单例都做了什么?
答 :整个程序公用一份资源的时候 例如 :
- 设置单例类访问应用的配置信息
- 用户的个人信息登录后用的NSUserDefaults存储,对登录类进一步采用单例封装方便全局访问
- 防止一个单例对 应用 多处 对同意本地数据库存进行操作
13.APNS的基本原理
- 基本
- 第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
- 第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
- 第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。
- 详细说明
首先是注册
- Device(设备)连接APNs服务器并携带设备序列号(UUID)
- 连接成功,APNs经过打包和处理产生devicetoken并返回给注册的Device(设备)
- Device(设备)携带获取的devicetoken发送到我们自己的应用服务器
- 完成需要被推送的Device(设备)在APNs服务器和我们自己的应用服务器的注册
推送过程
- 1、首先手机装有当前的app,并且保持有网络的情况下,APNs服务器会验证devicetoken,成功那个之后会处于一个长连接。 (这里会有面试问? 如果app也注册成功了, 也下载了,也同意了打开推送功能, 这个时候在把App删除了, 还能接受推送了么? )
- 2、当我们推送消息的时候,我们的服务器按照指定格式进行打包,结合devicetoken 一起发送给APNs服务器,
- 3、APNs 服务器将新消息推送到iOS 设备上,然后在设备屏幕上显示出推送的消息。
- 4、iOS设备收到推送消息后, 会通知给我们的应用程序并给予提示
// 推送过程如下图 [图片上传失败...(image-2bbbef-1647873407855)]()
14. RunLoop的基础知识
- RunLoop模式有哪些?
答 : iOS中有五种RunLoop模式
NSDefaultRunLoopMode (默认模式,有事件响应的时候,会阻塞旧事件)
NSRunLoopCommonModes (普通模式,不会影响任何事件)
UITrackingRunLoopMode (只能是有事件的时候才会响应的模式)
还有两种系统级别的模式
一个是app刚启动的时候会执行一次
另外一个是系统检测app各种事件的模式
复制代码
- RunLoop的基本执行原理
答 : 原本系统就有一个runloop在检测App内部的行为或事件,当输入源(用户的直接或者间接的操作)有“执行操作”的时候, 系统的runloop会监听输入源的状态, 进而在系统内部做一些对应的相应操作。 处理完成后,会自动回到睡眠状态, 等待下一次被唤醒,
-
RunLoop和线程的关系
-
RunLoop的作用就是用来管理线程的, 当线程的RunLoop开启之后,线程就会在执行完成任务后,进入休眠状态,随时等待接收新的任务,而不是退出。
-
为什么只有主线程的
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];
复制代码
15. weak属性?
1\. 说说你理解weak属性?
复制代码
1.实现weak后,为什么对象释放后会自动为nil?
runtime 对注册的类, 会进行布局,
对于 weak 对象会放入一个 hash 表中。
用 weak 指向的对象内存地址作为 key,
Value是weak指针的地址数组。
当释放的时候,其内部会通过当前的key找到所有的weak指针指向的数组
然后遍历这个数组把其中的数据设置为nil。
稍微详细的说:在内部底层源码也同时和当前对象相关联得SideTable, 其内部有三个属性, 一个是一把自旋锁,一个是引用计数器相关,一个是维护weak生命得属性得表
**SideTable**这个结构体一样的东西,可以花半个小时看一眼。
复制代码
延伸
- objc中向一个nil对象发送消息将会发生什么?
首先 在寻找对象化的isa指针时就是0地址返回了, 所以不会有任何错误, 也不会错误
- 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 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
3. 如果没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。
复制代码
16.UIView和CALayer是什么关系?
- 两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以;
- 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
- 在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
- layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer
复制代码
16. @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 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
复制代码
17. static有什么作用?
static关键字可以修饰函数和变量,作用如下:
**隐藏**
通过static修饰的函数或者变量,在该文件中,所有位于这条语句之后的函数都可以访问,而其他文件中的方法和函数则不行
**静态变量**
类方法不可以访问实例变量(函数),通过static修饰的实例变量(函数),可以被类 方法访问;
**持久**
static修饰的变量,能且只能被初始化一次;
**默认初始化**
static修饰的变量,默认初始化为0;
复制代码
18. objc在向一个对象发送消息时,发生了什么?
- objc_msgSend(recicver, selecter..)
复制代码
19. runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
1\. runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
2\. runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。runloop在第一次获取时被创建,在线程结束时被销毁。
3\. 对于主线程来说,runloop在程序一启动就默认创建好了。
4\. 对于子线程来说, runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被开启,不然定时器不会回调。
复制代码
20. 如何手动触发一个value的KVO
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。
引申 0 如何给系统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;
}
}
复制代码
引申 1.通过KVC修改属性会触发KVO么?直接修改成员变量呢 ?
- 会触发KVO。即使没有声明属性,只有成员变量,只要accessInstanceVariablesDirectly返回的是YES,允许访问其成员变量,那么不管有没有调用setter方法,通过KVC修改成员变量的值,都能触发KVO。这也说明通过KVC内部实现了willChangeValueForKey:方法和didChangeValueForKey:方法
- 直接修改成员变量不会触发KVO。直接修改成员变量内部并没有做处理只是单纯的赋值,所以不会触发。
引申 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”。
21. 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上消失。
复制代码
22.网络协议
- TCP三次握手和四次挥手?
三次握手
1.客户端向服务端发起请求链接,首先发送SYN报文,SYN=1,seq=x,并且客户端进入SYN_SENT状态
2.服务端收到请求链接,服务端向客户端进行回复,并发送响应报文,SYN=1,seq=y,ACK=1,ack=x+1,并且服务端进入到SYN_RCVD状态 3.客户端收到确认报文后,向服务端发送确认报文,ACK=1,ack=y+1,此时客户端进入到ESTABLISHED,服务端收到用户端发送过来的确认报文后,也进入到ESTABLISHED状态,此时链接创建成功
- 哎!
- 嗯
- 给你
复制代码
为什么需要三次握手: 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。假设这是一个早已失效的报文段,但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
四次挥手
1.客户端向服务端发起关闭链接,并停止发送数据 2.服务端收到关闭链接的请求时,向客户端发送回应,我知道了,然后停止接收数据 3.当服务端发送数据结束之后,向客户端发起关闭链接,并停止发送数据 4.客户端收到关闭链接的请求时,向服务端发送回应,我知道了,然后停止接收数据
- 哎!
- 嗯
- 关了
- 好的
复制代码
为什么需要四次挥手: 因为TCP是全双工通信的,在接收到客户端的关闭请求时,还可能在向客户端发送着数据,因此不能再回应关闭链接的请求时,同时发送关闭链接的请求
引申
-
HTTP和HTTPS有什么区别?
- HTTP协议是一种使用明文数据传输的网络协议。
- HTTPS协议可以理解为HTTP协议的升级,就是在HTTP的基础上增加了数据加密。在数据进行传输之前,对数据进行加密,然后再发送到服务器。这样,就算数据被第三者所截获,但是由于数据是加密的,所以你的个人信息让然是安全的。这就是HTTP和HTTPS的最大区别。
-
HTTPS的加密方式?
-
Https采用对称加密和非对称加密结合的方式来进行通信。
-
Https不是应用层的新协议,而是Http通信接口用SSL和TLS来加强加密和认证机制。
- 对称加密: 加密和解密都是同一个钥匙
- 非对称加密:密钥承兑出现,分为公钥和私钥,公钥加密需要私钥解密,私钥加密需要公钥解密
-
HTTP和HTTPS的建立连接的过程?
HTTP
- 建立链接完毕以后客户端会发送响应给服务器
- 服务端接受请求并且做出响应发送给客户端
- 客户端收到响应并且解析响应给客户
HTTPS
- 在使用HTTPS是需要保证服务端配置了正确的对应的安全证书
- 客户端发送请求到服务器
- 服务端返回公钥和证书到客户端
- 客户端接受后,会验证证书的安全性,如果通过则会随机生成一个随机数,用公钥对其解密, 发送到服务端
- 服务端接受到这个加密后的随机数后,会用私钥对其进行揭秘,得到真正的随机数,然后调用这个随机数当作私钥对需要发送的数据进行对称加密。
- 客户端接收到加密后的数据使用私钥(之前生成的随机值)对数据进行解密,并且解析数据呈现给客户
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响应
23. 有没有使用过performSelector?
- 这题主要是想问的是有没有动态添加过方法
- 话不多说上代码
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
// 动态添加方法就不会报错
[p performSelector:@selector(eat)];
}
@end
@implementation Person
// **这里真是奇葩, 实在想不到什么时候才有这种使用场景, 我再外面找不到方法, 我再当前类里面直接在写一个方法就好咯,干嘛要在这里写这个玩意, 还要写一个C语言的东西, 既然面试想问, 那咱就要会!**
// void(*)()
// 默认方法都有两个隐式参数,
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];
}
@end
复制代码
- 当然面试的时候也可能问你这个
// 延时操作 和GCD的after 一个效果
[p performSelector:@selector(eat) withObject:nil afterDelay:4];
复制代码
-
你以为完了? 错了,大概率面试官会问你,*** 上面这段代码放在子线程中 是什么样子的?为什么?**
—首先 上面这个方法其实就是内部创建了一个NSTimer定时器,然后这个定时器会添加在当前的RunLoop中所以上面代码放到子线程中不会有任何定时器相关方法被执行,如果想要执行,开启当前线程即可 即
[[NSRunLoop currentRunLoop] run];
复制代码
// 完整调用
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];
});
// 由此我自行又做了一个测试, 把
[self performSelector:@selector(test)];
在子线程调用,是没有任何问题的。
// 我又测试了一下,
[self performSelector:@selector(test) withObject:nil afterDelay:2];
这个方法在主线程执行 打印线程是1
在子线程中调用打印线程 非1
复制代码
- 然后面试官开始飘了, 开始问你关于NSTimer相关问题?怎么办? 答: 搞他!
引申 NSTimer在子线程执行?
- NSTimer直接在在子线程是不会被调用的, 想要执行请开启当前的Runloop 。具体开启方案上面题有说,不赘述。
引申 为什么说NSTimer不准确?
- NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期 减少误差的方法 代码如下
// 在子线程中开启NStimer,或者更改当前Runloop的Mode 为NSRunLoopCommonModes
[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
// 利用CADisplayLink (iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// 利用GCD
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"GCD timer test");
});
dispatch_resume(_timer);
复制代码
引申: NStimer的循环引用?
- 有的人会说, NSTimer本身的target会引用这self, 而self又引用这Timer就造成了循环引用, 那如果timer用weak声明呢? 还会循环引用么? 答案:会的
- 原因是NTtimer和Runloop是一个相互存在的东西, 别的道理我就不多BB, 就是Runloop和tmier相互引用,而Runloop永远不会销毁,造成贷方面的“牵引” 所以苹果出来了一个invalid的方法。
- 优化的方案还有别的, 例如利用NSProxy这个专门做消息转发的虚类去优化循环引用(这里也经常会被问到。具体方案我不说, 自行百度,切记,如果兄弟你不知道这个玩意, 建议你看看,面试的时候被问到的概率还是挺大的。)
24. 为什么AFN3.0中需要设置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0却不需要?
- 功能不一样, 2.x是基于NSURLConnection的,其内部实现要在异步并发,所以不能设置1。 3.0 是基于NSURLSession其内部是需要串行的鉴于一些多线程数据访问的安全性考虑, 设置这个达到串行回调的效果。
AFNetworking 2.0 和3.0 的区别?
- AFN3.0剔除了所有的NSURLConnection请求的API
- AFN3.0使用NSOperationQueue代替AFN2.0的常驻线程
2.x版本常驻线程的分析
-
在请求完成后我们需要对数据进行一些序列化处理,或者错误处理。如果我们在主线中处理这些事情很明显是不合理的。不仅会导致UI的卡顿,甚至受到默认的RunLoopModel的影响,我们在滑动tableview的时候,会导致时间的处理停止。
-
这里时候我们就需要一个子线程来处理事件和网络请求的回调了。但是,子线程在处理完事件后就会自动结束生命周期,这个时候后面的一些网络请求得回调我们就无法接收了。所以我们就需要开启子线程的RunLoop来保存线程的常驻。
-
当然我们可以每次发起一个请求就开启一条子线程,但是这个想一下就知道开销有多大了。所以这个时候保活一条线程来对请求得回调处理是比较好的一个方案。
3.x版本不在常驻线程的分析?
-
在3.x的AFN版本中使用的是NSURLSession进行封装。对比于NSURLConnection,NSURLSession不需要在当前的线程等待网络回调,而是可以让开发者自己设定需要回调的队列。
-
所以在3.x版本中AFN使用了NSOperationQueue对网络回调的管理,并且设置maxConcurrentOperationCount为1,保证了最大的并发数为1,也就是说让网络请求串行执行。避免了多线程环境下的资源抢夺问题。
25. autoreleasePool 在何时被释放?
- ARC中所有的新生对象都是 自动加autorelese的, @atuorelesepool 大部分时候解决了瞬时内存暴增的问题 。
- MRC中的情况 关键词变了NSAutoreleasePool。
//来自Apple文档,见参考
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:urlencoding:NSUTF8StringEncoding error:&error];
}
// 如果循环次数非常多,而且循环体里面的对象都是临时创建使用的,就可以用@autoreleasepool 包起来,让每次循环结束时,可以及时释放临时对象的内存
// for 和 for in 里面是没有自动包装@autoreleasepool着的,而下面的方法是由@autoreleasepool自动包围的
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];
复制代码
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"666"] autorelease];
[pool drain];
// 其作用于为drain 和 init 之间
复制代码
-
回归正题@autoReleasePool什么时间释放?
- 一个被autoreleasepool包裹生成得对象,都会在其创建生成之后自动添加autorelease, 然后被autorelease对象得释放时机 就是在当前runloop循环结束的时候自动释放的
- 参考链接:blog.sunnyxx.com/2014/10/15/…
子线程中的autorelease变量什么时候释放?
- 子线程中会默认包裹一个autoreleasepool的, 释放时机是当前线程退出的时候。
autoreleasepool是如何实现的?
- @autoreleasepool{} 本质上是一个结构体:
- autoreleasepool会被转换成__AtAutoreleasePool
- __AtAutoreleasePool 里面有两个函数objc_autoreleasePoolPush(),objc_autoreleasePoolPop().,其实一些列下来之后实际上调用得是AutoreleasePoolPage类中得push 和 pop两个类方法
- push就是压栈操作,
- pop就是出栈操作于此同时对其对象发送release消息进行释放
26. iOS界面渲染机制? [这是很大的一个模块,里面牵扯很多东西, 耐心看下去]
- 先简单解释一下渲染机制
首先iOS渲染视图的核心是Core Animation,其渲染层次依次为:图层树->呈现树->渲染树
-
一共三个阶段
-
CPU阶段(进行Frame布局,准备视图和图层之间的层级关系)
-
OpenGL ES阶段(iOS8以后改成Metal), (渲染服务把上面提供的图层上色,生成各种帧)
-
GPU阶段 (把上面操作的东西进行一些列的操作,最终展示到屏幕上面)
-
稍微详细说明
-
首先一个视图由CPU进行Frame布局,准备视图和图层的层及关系。
-
CUP会将处理视图和图层的层级关系打包,通过IPC(进程间的通信)通道提交给渲染服务(OpenGL和GPU)
-
渲染服务首先将图层交给OpenGL进行纹理生成和着色,生成前后帧缓存,再根据硬件的刷新帧率,一般以设备的VSync信号和CADisplayLink(类似一个刷新UI专用的定时器)为标准,进行前后帧缓存的切换
-
最后,将最终 要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换, 应用纹理混合,最终显示在屏幕上。
程序卡顿的原因?
- 正常渲染流程
- CPU计算完成之后交给GPU,来个同步信号Vsync 将内容渲染到屏幕上
- 非正常(卡顿/掉帧)的流程
- CPU计算时间正常或者慢,GPU渲染时间长了, 这时候Vsync信号, 由于没有绘制完全,CUP开始计算下一帧,当下一帧正常绘制成功之后,把当前没有绘制完成的帧丢弃, 显示了下一帧,于是这样就造成了卡顿。
需要注意的是:Vsync时间间隔是固定的, 比如60帧率大的Vsync 是每16ms就执行一个一次,类似定时器一样
这里会出现一个面试题!!! 题目如下:
- 从第一次打开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渲染负担的一般是:离屏渲染,图层混合,延迟加载。
这里又会出现一个面试题!!! 一个UIImageView添加到视图上以后,内部如何渲染到手机上的?
图片显示分为三个步骤: 加载、解码、渲染、 通常,我们程序员的操作只是加载,至于解码和渲染是由UIKit内部进行的。 例如:UIImageView显示在屏幕上的时候需要UIImage对象进行数据源的赋值。而UIImage持有的数据是未解码的压缩数据,当赋值的时候,图像数据会被解码变成RGB颜色数据,最终渲染到屏幕上。
看完上面的又来问题了! 关于UITableView优化的问题?(真他妈子子孙孙无穷尽也~) 先说造成UITableView滚动时候卡顿的的原因有哪些?
- 隐式绘制 CGContext
- 文本CATextLayer 和 UILabel
- 光栅化 shouldRasterize
- 离屏渲染
- 可伸缩图片
- shadowPath
- 混合和过度绘制
- 减少图层数量
- 裁切
- 对象回收
- Core Graphics绘制
- -renderInContext: 方法
在说关于UITableView的优化问题!
基础的
- 重用机制(缓存池)
- 少用有透明度的View
- 尽量避免使用xib
- 尽量避免过多的层级结构
- iOS8以后出的预估高度
- 减少离屏渲染操作(圆角、阴影啥的)
-
**** 解释一下为什么减少离屏渲染操作?****
-
需要创建新的缓冲区
-
整个过程需要多次切换上下文环境, 显示从当前的屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果 显示到屏幕有上, 又要将上下文环境从离屏切换到当前屏幕,
-
****那些操作会触发离屏渲染?****
-
光栅化 layer.shouldRasterize = YES
-
遮罩layer.mask
-
圆角layer.maskToBounds = Yes,Layer.cornerRadis 大于0
-
阴影layer.shadowXXX
进阶的
- 缓存cell的高度(提前计算好cell的高度,缓存进当前的模型里面)
- 异步绘制
- 滑动的时候,按需加载
高阶的
- 你想不到 竟然不推荐用UILabel。哈哈哈~ 至于为什么 看下面的链接吧
至于上面的那些基础的,涉及到渲染级别的自己说的时候悠着点,面试官如果想搞你的话,考一考你最上面的那些,CUP和GUP,以及openGL相关, 在考一下你进程通信IPC,以及VSync信号啥的, 这些东西太鸡儿高深了,没点匠心 这东西还真搞不了,要想研究可以看看YYKit的作者写的一篇关于页面流畅的文章:blog.ibireme.com/2015/11/12/…
卡顿检测的方法
- 卡顿就是主线程阻塞的时间问题,可以添加Observer到主线程Runloop中,通过监听Runloop状态切换的耗时,以达到监听卡顿的目的
继续
既然都是图形绘制了,那就再研究一下事件响应链&原理
传统的问法来了:UIView和CALayer的区别? 通常我们这样回答:UIView可以响应用户事件,而CALayer不能处理事件
回答这个之前, 先回顾一下另外一个经典面试题:事件响应链和事件传递?
基本概念:
-
响应链: 是由链接在一起的响应者(UIResponse子类)组成的,一般为第一响应着到application对象以及中间所有响应者一起组成的。
-
事件传递: 获取响应链之后, 将事件由第一响应者网application的传递过程
-
[图片上传失败...(image-74d78b-1647873407847)]
-
[图片上传失败...(image-c53446-1647873407847)]
-
事件的分发和传递
-
当程序中发生触摸事件之后,系统会将事件添加到UIApplication管理的一个队列当中
-
UIApplication将处于任务队列最前端的事件向下分发 即UIWindow
-
UIWindow将事件向下分发,即UIView或者UIViewController
-
UIView首先看自己能否处理这个事件,触摸点是否在自己身上,自己的透明度是否大于0,01,userInteractionEnabled 是否是YES, Hidden实际是NO,如果这些都满足,那么继续寻找其子视图
-
遍历子控件,重复上面步骤
-
如果没有找到,那么自己就是改事件的处理者
-
如果自己不能处理,那么就不做任何处理 即视为没有合适的View能接收处理当前事件,则改事件会被废弃。
-
*** 怎么寻找当前触摸的是哪一个View?***
下面中两个方法
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
复制代码
事件传递给控件之后, 就会调用hitTest:withEvent方法去寻找更合适的View,如果当前View存在子控件,则在子控件继续调用hitTest:withEvent方法判断是否是合适的View, 如果还不是就一直遍历寻找, 找不到的话直接废弃掉。
// 因为所有的视图类都是继承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2\. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[I];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
复制代码
- 判断触摸点是否在视图内?
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
复制代码
- 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;
}
作者:执笔续春秋
链接:https://juejin.cn/post/6854573212165111822
网友评论