iOS面试题
第一、KVO的底层原理
(http://tsb1119.blog.163.com/blog/static/2058393092018421593836/)
第二、KVC会触发set方法
(http://tsb1119.blog.163.com/blog/static/2058393092018421104046194/)
第三、block为什么要用copy属性修饰
Block分为全局Block、堆Block和栈Block
- (void)viewDidLoad {
[super viewDidLoad];
//__NSGlobalBlock__ 全局区的 (没有引用外部变量)
void (^DemoBlock)() = ^{
NSLog(@"DemoBlock");
};
NSLog(@"%@",DemoBlock);
int a = 6;
//__NSStackBlock__ 栈区 (内部使用了外部变量)(MRC模式下)
void (^DemoBlock2)() = ^{
NSLog(@"DemoBlock2 %d",a);
};
NSLog(@"DemoBlock2 %@",DemoBlock2);
//__NSMallocBlock__ 堆区 ([DemoBlock2 copy]后Block存放在堆区)
NSLog(@"DemoBlock2.Copy %@",[DemoBlock2 copy]);
void (^DemoBLock3)() = [DemoBlock2 copy];
NSLog(@"DemoBlock3 %@",DemoBLock3);
}
Log信息:
2016-06-13 19:11:03.712 02-Block为什么使用copy修饰[10956:548330] DemoBlock:<__NSGlobalBlock__: 0x100d2b0a0>
2016-06-13 19:11:03.713 02-Block为什么使用copy修饰[10956:548330] DemoBlock2 <__NSStackBlock__: 0x7fff5eed4950>
2016-06-13 19:11:03.713 02-Block为什么使用copy修饰[10956:548330] DemoBlock2.Copy <__NSMallocBlock__: 0x7ff1a9d19270>
2016-06-13 19:11:03.713 02-Block为什么使用copy修饰[10956:548330] DemoBlock3 <__NSMallocBlock__: 0x7ff1a9d9c590>
分析:
- DemoBlock内部没有调用外部变量时存放在全局区(ARC和MRC下均是)
- DemoBlock2使用了外部变量,这种情况也正式我们平时所常用的方式,Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.所以在使用Block属性时使用Copy修饰,而在ARC模式下,系统也会默认对Block进行copy操作
(http://tsb1119.blog.163.com/blog/static/20583930920185541625272/)
第四、objc使用什么机制管理对象内存?
通过 retainCount 的机制来决定对象是否需要释放。 每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了
第五、nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?
atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的
如使用[self.arr objectAtIndex:index]、[self.arr addObject:index]就不是线程安全的。好的解决方案就是加锁
第六、为什么@property声明(NString,NSArray,NSDictionary)时需要使用copy,使用strong有什么问题
(https://www.jianshu.com/p/1e1a6f9c26f8)
第七、category的底层原理
(http://tsb1119.blog.163.com/blog/static/205839309201843131148992/)
第七、响应链的应用
(http://tsb1119.blog.163.com/blog/static/205839309201843131148992/)
第八、深浅拷贝
(https://www.jianshu.com/p/1e1a6f9c26f8](https://www.jianshu.com/p/1e1a6f9c26f8)
第九、进程和线程的区别?
进程是资源调度的一个独立单元,而线程是CPU调度的基本单元
一个程序至少包含一个进程,一个进程至少包含一个线程
第十、循环引用案例
(http://tsb1119.blog.163.com/blog/static/20583930920171112114716388/)
第十一、如何访问并修改一个类的私有属性
(http://tsb1119.blog.163.com/blog/static/20583930920182250834973/)
第十二、atomic的实现原理
(https://www.jianshu.com/p/95cee7afc809)
第十二、消息转发+实现多delegate+实现多继承
(https://www.jianshu.com/p/2ac769dac779)
(https://www.jianshu.com/p/da1e95ee6fbb) (多delegate)
(https://www.jianshu.com/p/74a7b29a1282) (多继承)
第十三、performSelector:withObject:afterDelay:实现原理
https://www.jianshu.com/p/4cc207be6b94
第十四、为什么单例的全局变量要加static
http://tsb1119.blog.163.com/blog/static/20583930920185142747108/
第十四、MJExtension底层原理
http://tsb1119.blog.163.com/blog/static/20583930920184235441042/
第十五、pod update 和pod install 区别
https://www.jianshu.com/p/aed5d080dbae
第十六、解决NSTimer循环引用问题(消息转发)
https://www.jianshu.com/p/a6c4e0d74e04
第十六、监控卡顿函数
利用工具检测 https://www.jianshu.com/p/33570ba9feb0
第十七、用代码监控FPS值(CADisplayLink)
https://www.jianshu.com/p/ef206493abab
第十八、消息机制
https://www.jianshu.com/p/c8ce82d0e283
第十九、udp和tcp的区别
https://www.jianshu.com/p/104bdb6c43e3
第十九、SDWebImage 原理
https://www.jianshu.com/p/c38ffc290b11
第二十、如何同时重写get方法和set方法
https://www.jianshu.com/p/d947722e7e09
第二十、图片为什么要解码
为甚要解码?你问我为啥要解码?事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比,因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因
有一张large_leaves_70mp.jpg的图片,它在磁盘上的大小是8.3M,但它的像素是7033x10110的,也就是说图片解码后显示在屏幕上时所占的内存是7033x10110x4byte(一个像素是4byte),也就是271MB
当你用 UIImage 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。
iOS默认会在主线程对图像进行解码, 解码过程是一个相当复杂的任务,需要消耗非常长的时间, 不过由于解码后的图像太大,一般不会缓存到磁盘,
SDWebImage的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间
第二十一、@synchronized(self) 是递归锁 实现原理:一个hash表维护着传入对象地址与锁之间的映射关系
第二十一、如果不用锁如何实现线程同步,可以用信号量(dispatch_semaphore)
第二十二、对 atomic 不能保证线程安全的理解
https://www.jianshu.com/p/72bcd9fd6c5b
第二十三、野指针操作一定会crash吗
Student *stu = [[Student alloc] init];
[stu setAge:10];
[stu release];这里已经释放内存
[stu setAge:10];---》报错
Student对象接收到release消息后,会马上被销毁,所占用的内存会被回收。” 这里执行release只是标记对象占用的那块内存可以被释放,但是具体的释放的时间是不可控的,如果在release之后执行[stu setAge:10];不一定会野指针crash,如果对象内存已经被其他对象覆写占用,那么会crash,如果没有没覆写,调用依然可以正确执行。
内存回收的本质
1.申请一块空间,实际上是向系统申请一块别人不再使用的空间.
2.释放一块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用.
3.在这个个空间分配给别人之前 数据还是存在的.
(1) OC对象释放以后,表示OC对象占用的空间可以分配给别人.
(3) 但是再分配给别人之前 这个空间仍然存在 对象的数据仍然存在.
https://www.jianshu.com/p/30e59628b391
第二十三、函数(编译时) 和 方法(运行时发消息) 的区别
由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。
第二十四、 http状态码
500 服务器内部错误
406 请求格式错误
403 得到了授权,但是被服务器拒绝
401、407 没有访问权限
408 服务繁忙超时
404 找不到文件
503 服务不可用
第二十五、keyChain的使用
keyChain是ios中唯一可以存储安全可靠敏感数据的地方。而且应用被卸载,数据也不会被删除,所以非常可靠。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 获取
NSString * user = [SAMKeychain passwordForService:@"123.456.789" account:@"user"];
NSString * password = [SAMKeychain passwordForService:@"123.456.789" account:@"password"];
NSLog(@"user %@ password %@",user,password);
// 删除
// [SAMKeychain deletePasswordForService:@"123.456.789" account:@"user"];
// [SAMKeychain deletePasswordForService:@"123.456.789" account:@"password"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//新增
[SAMKeychain setPassword:@"123456" forService:@"123.456.789" account:@"user"];
[SAMKeychain setPassword:@"tsb1119" forService:@"123.456.789" account:@"password"];
}
第二十五、如何获取UUID、IDFA(可能被用户禁用导致无法获取)、IDFV、UDID(被禁用)
UUID(不唯一)
设备每次删除APP重新加载,每次获取的UUID都不一样,所以这个参数不能作为某台设备的唯一识别符了,最新的做法是把获取的UUID写入到keyChain这样的话,这个UUID就再也不会改变
https://www.jianshu.com/p/cf58d1b3ea12
CFUUIDRef puuid = CFUUIDCreate(nil);
CFStringRef uuidString = CFUUIDCreateString(nil, puuid);
NSString *result = (NSString *)CFBridgingRelease(CFStringCreateCopy(NULL, uuidString));
NSMutableString *tmpResult = result.mutableCopy;
// 去除“-”
NSRange range = [tmpResult rangeOfString:@"-"];
while (range.location != NSNotFound) {
[tmpResult deleteCharactersInRange:range];
range = [tmpResult rangeOfString:@"-"];
}
NSLog(@"UUID:%@",tmpResult);
屏幕快照 2020-08-24 下午7.38.58.png
屏幕快照 2020-08-24 下午7.38.24.png
IDFA(唯一,但是用户如果隐私设置禁用,则无法获取)
#import <AdSupport/AdSupport.h>
NSString * IDFA = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
屏幕快照 2020-08-24 下午8.02.27.png
IDFV(唯一,一定能拿到)
NSString * IDFV = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
屏幕快照 2020-08-24 下午7.54.22.png
第二十六、view 和 layer 的关系
其实说白了UIView和CALayer之间的关系就是MVC的关系,UIView就是C,CALayer就是M,负责绘图单元的笼统来说就是V
第二十七、如何判断当前机型是否有刘海
iPhone 自从 iPhone X(iOS11) 设备开始了 刘海屏幕,在之前项目中都是使用判断设备屏幕的大小来判断是否是刘海屏幕,但是随着苹果设备的更新,这种方式已经不合适继续使用了。
苹果在 iOS11 推出了一个 安全区域 的概念,我们可以通过这个安全区域来判断当前设备是否为刘海屏幕。
#define kIsBangsScreen ({\
BOOL isBangsScreen = NO; \
if (@available(iOS 11.0, *)) { \
UIWindow *window = [[UIApplication sharedApplication].windows firstObject]; \
isBangsScreen = window.safeAreaInsets.bottom > 0; \
} \
isBangsScreen; \
})
https://www.jianshu.com/p/87a1c2d138db
第二十七、MRC 下 pop VC 不会走 dealloc 方法
第二十八、block 访问 _str (实例变量) 依然会发生循环引用,因为访问 _str 的本质是 self->_str , block 依然强引用了 self
https://www.jianshu.com/p/0fce66ba25d3
第二十八、UILabel 如何自适应高度
(1)、通过计算str的高度 boundingRectWithSize
(2)、sizeToFit方法
创建一个分类
#import "UILabel+add.h"
@implementation UILabel (add)
-(instancetype)initWithFrame:(CGRect)frame Text:(NSString *)text
{
UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 0)];
label.text = text;
label.numberOfLines = 0;
[label sizeToFit];
return label;
}
@end
在VC中使用
UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 100, 0) Text:@"4584509438590438509843958439085904385903485093485403859034859043859034890583490580349"];
label.backgroundColor = [UIColor yellowColor];
[self.view addSubview:label];
屏幕快照 2020-08-29 下午4.50.44.png
第二十八、XIB 约束 中的 "<="
屏幕快照 2020-08-29 下午5.19.51.png 屏幕快照 2020-08-29 下午5.20.10.png 屏幕快照 2020-08-29 下午5.20.00.png
第二十九、UIResponder
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent: (nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
第二十九、手势冲突
屏幕快照 2020-08-30 下午8.27.25.png
btn和tableVeiw是同级的
为什么会出现这种情况btn可以响应、tableView无法响应
屏幕快照 2020-08-30 下午8.37.57.png可以清楚地看到,Button的事件直接就由UIApplication传递过来了,而TableView走的是正常的响应链
UIControl的响应机制不涉及响应链、由UIAPPlication直接分发
系统判别返回的 UIView是不是 UIControl的子类,如果是的话,直接将整个触摸事件交付 UIControl实例完成,不是的话,走正常的响应链流程
因为手势GestureRecognizer 是大哥,优先级更高,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view对事件的响应,响应链会终止,不会继续传递,所以tableView的didSelectRowAtIndexPath无法响应,;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权
那如何解决tableView无法响应的问题,就是让父类不去响应tap手势事件,就能够继续往下传递了
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return NO;
}
参考资料:https://www.jianshu.com/p/53e03e558cbd
第二十九、touchesBegan和UITapGestureRecognizer谁先调用
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
//给view添加tap手势
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction)];
[self.view addGestureRecognizer:tap];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesBegan");
}
-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesCancelled");
}
-(void)tapAction
{
NSLog(@"tapAction");
}
2020-09-07 15:59:30.786470+0800 手势[42537:10695492] touchesBegan
2020-09-07 15:59:30.865947+0800 手势[42537:10695492] tapAction
2020-09-07 15:59:33.535687+0800 手势[42537:10695492] touchesCancelled
首先调用响应链UIResponder的touchsBegan:withEvent方法,之后手势识别成功了,就会去cancel之前传递到的所有响应对象,于是就会调用它们的touchsCancelled:withEvent:方法
第三十、手势优先级(requireGestureRecognizerToFail)运用场景
(1)、识别单击、双击
- (void)viewDidLoad {
[super viewDidLoad];
// 单击的 Recognizer
UITapGestureRecognizer * singeTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(SingleTap:)];
singeTap.numberOfTapsRequired = 1;
[self.view addGestureRecognizer:singeTap];
// 双击的 Recognizer
UITapGestureRecognizer * doubleTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(DoubleTap:)];
doubleTap.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:doubleTap];
}
2020-09-08 18:06:51.644730+0800 手势[86353:11286838] 单击
2020-09-08 18:06:51.844263+0800 手势[86353:11286838] 双击
触发一个双击
但是 单击、双击都同时触发,这样导致无法识别单击、双击
[singeTap requireGestureRecognizerToFail:doubleTap];
只需要加上requireGestureRecognizerToFail让双击手势的优先级更高,这样就没问题了
(2)、scrollView和系统侧滑返回冲突
- (void)viewDidLoad {
[super viewDidLoad];
UIScrollView * scrollView = [[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].bounds];
scrollView.contentSize = CGSizeMake(1000, 0);
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];
}
这样的话侧滑滑动 scrollView 的时候会触发返回,并不会滑动到scrollView上,把scrollView上的手势优先级设置成高于 系统侧滑返的手势
[self.navigationController.interactivePopGestureRecognizer requireGestureRecognizerToFail:scrollView.panGestureRecognizer];
第三十一、系统侧滑手势和UISlider冲突
屏幕快照 2020-09-11 下午4.30.12.png
- (void)viewDidLoad {
[super viewDidLoad];
UISlider * slider = [[UISlider alloc]initWithFrame:CGRectMake(10, 100, 300, 50)];
[self.view addSubview:slider];
self.navigationController.interactivePopGestureRecognizer.delegate = self;
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if([touch.view isKindOfClass:[UISlider class]])
{
return NO;
}
else
{
return YES;
}
}
第三十二、请求头中 Content-Type 的类型
multipart/form-data (上传文件, 支持多个)
application/json (格式化后的json数据)
application/x-www-form-urlencoded(默认)
第三十三、NSOperation 、NSThread、GCD比较
NSOperation
(1)可以限制最大并发量
//1:创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
/*
系统默认的并发数是-1,所有任务全部并发执行
*/
queue.maxConcurrentOperationCount = 1;//(产生2条多线程)
//2:封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"1------%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"2------%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"3------%@",[NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"4------%@",[NSThread currentThread]);
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"5------%@",[NSThread currentThread]);
}];
NSBlockOperation *op6 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"6------%@",[NSThread currentThread]);
}];
//3:把操作添加到队列
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
[queue addOperation:op5];
[queue addOperation:op6];
self.queue= queue;
屏幕快照 2020-09-11 下午7.51.19.png
(2)支持暂停、取消
self.queue.suspended
[self.queue cancelAllOperations]
(3)、NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
(4)、支持优先级queuePriority
NSBlockOperation *blkop1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"执行blkop1");
}];
NSBlockOperation *blkop2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"执行blkop2");
}];
// 设置操作优先级
blkop1.queuePriority = NSOperationQueuePriorityLow;
blkop2.queuePriority = NSOperationQueuePriorityVeryHigh;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 操作添加到队列
[queue addOperation:blkop1];
[queue addOperation:blkop2];
2020-09-11 20:34:23.147442+0800 手势[5827:12822941] 执行blkop2
2020-09-11 20:34:23.147458+0800 手势[5827:12822946] 执行blkop1
第三十三、DNS解析过程
1、先从本地hosts文件里寻找是否有该域名的映射地址,有的话结束解析,没有的话第二步
2、从DNS解析器里查看是否有缓存,有的话结束解析,没有的话第三部
3、从DNS服务器里查看是否有缓存,有的话结束解析,没有的话第三部
4、向根DNS服务器发送查询请求,然后本地DNS服务器会接受到相应的IP地址,结束解析
第三十三、取数组最大最小平均值
obj.avgValue = [[obj.dataList valueForKeyPath:@"@avg.integerValue"]floatValue]; //kvc取数组最小值
obj.minValue = [[obj.dataList valueForKeyPath:@"@min.integerValue"]floatValue]; //kvc取数组最小值
obj.maxValue = [[obj.dataList valueForKeyPath:@"@max.integerValue"]floatValue]; //kvc取数组最小值
第三十四、离屏渲染的触发方式
shouldRasterize(光栅化)
masks(遮罩)
shadows(阴影)
edge antialiasing(抗锯齿)
group opacity(不透明)
复杂形状设置圆角等
渐变
第三十四、shouldRasterize 光栅化
光栅化有什么好处?
shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来(各种阴影遮罩等效果也会保存到位图中并缓存起来),如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用(下次就只是简单的从渲染引擎的cache里读取那张bitmap,节约系统资源)
画心率、血氧时候绘制了很多layer,并且使用了阴影,tableView滚动时候很卡顿,将cell的cell.layer.shouldRasterize = YES 就解决了这个问题
第三十五、查看SDK包支持的架构和生成某个架构的包 (支持的架构少,包的大小可以缩小)
PM15019411951:RealmeLink 15019411951$ cd /Applications/我的工作目录/最新8月代码/realmeLink-iOS/realmeLink/RealmeWatch/RealmeSaturnSDK/RealmeSaturnSDK/Products/RyeexSaturnBleService.framework
PM15019411951:RyeexSaturnBleService.framework 15019411951$ ls
Headers PrivateHeaders RyeexSaturnBleService
Info.plist README.md _CodeSignature
PM15019411951:RyeexSaturnBleService.framework 15019411951$ lipo -info RyeexSaturnBleService
Architectures in the fat file: RyeexSaturnBleService are: arm64 armv7
PM15019411951:RyeexSaturnBleService.framework 15019411951$ lipo RyeexSaturnBleService -thin arm64 -output ./RyeexSaturnBleService_arm64
PM15019411951:RyeexSaturnBleService.framework 15019411951$ ls
Headers RyeexSaturnBleService
Info.plist RyeexSaturnBleService_arm64
PrivateHeaders _CodeSignature
README.md
PM15019411951:RyeexSaturnBleService.framework 15019411951$ lipo -info RyeexSaturnBleService_arm64
Non-fat file: RyeexSaturnBleService_arm64 is architecture: arm64
第三十六、离屏渲染的检测
第三十六、离屏渲染 耗性能的原因
1、需要创建新的缓冲区;
2、离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
第三十七、atomic 的底层实现,老版本是自旋锁,新版本是互斥锁
第三十八、atomic一定是线程安全的吗
atomic 并不是绝对线程安全,它能保证代码进入 getter 和 setter 方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 getter 和 setter 方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系
第三十九、自旋锁与互斥锁的特点
互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行
第四十、PNG 是无损压损,JPEG能够是有损压缩(0-100% ),即损失部分信息来压缩图片,这样压缩以后的图片大小将会更小
第四十一、冷启动和热启动
冷启动:
App点击启动前,此时App的进程还不在系统里。
需要系统新创建一个进程分配给App。(这是一次完整的App启动过程)
热启动:
App在冷启动后用户将App退回后台,此时App的进程还在系统里。
用户重新返回App的过程。(热启动做的事较少)
第四十二、启动优化
(1) main函数前
如何查看main函数前所用时间
1、dylib loading time: 加载Mach-O文件(可执行文件、动态库等)减少动态库的使用,合并多个动态库
2、ObjC setup time: 注册类、分类,类、分类的数量不要过多,合并相似功能的类、分类
3、initializer time: 减少+load()方法的使用,用+initialize()代替、或者dispatch_once()代替
减少静态变量的使用
(2)main函数之后
会调用appDelegate的didFinishLaunchingWithOptions 指定rootVC 渲染第一个页面
1、将耗时操作放入到子线程,防止阻塞主线程
第四十三、系统的block、Masonry 、AFN的block为什么不会发生循环引用
1、系统的block
UIView动画block不会造成循环引用是因为这是类方法,vc不可能强引用一个类,所以不会造成循环引用
2、Masonry的block
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
//这里并不是self.block (self并没有持有block 所以不会引起循环引用)
block(constraintMaker);
return [constraintMaker install];
}
3、AFN的block
AFN请求回调block不会造成循环引用是因为在内部做了处理
block先是被AFURLSessionManagerTaskDelegate对象持有。而AFURLSessionManagerTaskDelegate对象被mutableTaskDelegatesKeyedByTaskIdentifier字典持有,在block执行完成后,mutableTaskDelegatesKeyedByTaskIdentifier字典会移除AFURLSessionManagerTaskDelegate对象,这样对象就被释放了,所以不会造成循环引用
#pragma mark - 添加代理
- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
delegate.manager = self;
//block被代理引用
delegate.completionHandler = completionHandler;
dataTask.taskDescription = self.taskDescriptionForSessionTasks;
//设置代理
[self setDelegate:delegate forTask:dataTask];
delegate.uploadProgressBlock = uploadProgressBlock;
delegate.downloadProgressBlock = downloadProgressBlock;
}
#pragma mark - 设置代理
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
forTask:(NSURLSessionTask *)task
{
NSParameterAssert(task);
NSParameterAssert(delegate);
[self.lock lock];
//代理被mutableTaskDelegatesKeyedByTaskIdentifier字典引用
self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
[delegate setupProgressForTask:task];
[self addNotificationObserverForTask:task];
[self.lock unlock];
}
#pragma mark - 任务完成
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask];
if (delegate) {
//任务完成,移除
[self removeDelegateForTask:dataTask];
[self setDelegate:delegate forTask:downloadTask];
}
if (self.dataTaskDidBecomeDownloadTask) {
self.dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask);
}
}
#pragma mark - 移除任务代理
- (void)removeDelegateForTask:(NSURLSessionTask *)task {
NSParameterAssert(task);
AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
[self.lock lock];
[delegate cleanUpProgressForTask:task];
[self removeNotificationObserverForTask:task];
//移除
[self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];
[self.lock unlock];
}
第四十四、UILabel 内容置顶
创建一个分类UILabel+extension
-(void)alignTop
{
// 对应字号的字体一行显示所占宽高
CGSize fontSize = [self.text sizeWithAttributes:@{NSFontAttributeName:self.font}];
// 多行所占 height*line
double height = self.frame.size.height;
// 显示范围实际宽度
double width = self.frame.size.width;
// 对应字号的内容实际所占范围
CGSize stringSize = [self.text boundingRectWithSize:CGSizeMake(width, 0) options:(NSStringDrawingUsesLineFragmentOrigin) attributes:@{NSFontAttributeName:self.font} context:nil].size;
// 剩余空行
NSInteger line = (height - stringSize.height) / fontSize.height;
// 用回车补齐
for (int i = 0; i < line; i++) {
self.text = [self.text stringByAppendingString:@"\n"];
}
}
-(void)alignBottom
{
CGSize fontSize = [self.text sizeWithAttributes:@{NSFontAttributeName:self.font}];
double height = fontSize.height*self.numberOfLines;
double width = self.frame.size.width;
CGSize stringSize = [self.text boundingRectWithSize:CGSizeMake(width, height) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.font} context:nil].size;
NSInteger line = (height - stringSize.height) / fontSize.height;
// 前面补齐换行符
for (int i = 0; i < line; i++) {
self.text = [NSString stringWithFormat:@" \n%@", self.text];
}
}
第四十五、事件传递和响应链
第四十五、为什么所有UI操作必须放在主线程?
因为UIKit框架是线程不安全的!
那么,就有人会问为什么不把UIKit框架设置为线程安全呢?
因为线程安全需要加锁,我们都知道加锁就会消耗性能,影响处理速度,影响渲染速度
而UI又是最追求速度流畅,体验无顿挫感的,给UI加锁是不可能的,这辈子都不可能的,想要UI极度流畅,但线程安全又得不到保障,怎么办呢?
苹果官方就强制规定所有UI操作必须在主线程中进行,避免多线程对UI进行操作,相当于人为给UIKit框架加锁,即高效高性能,又不会出现线程安全问题。
第四十五、RunLoop的运行模式?
RunLoop的运行模式共有5种、常用的就3种
-
kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
-
UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
-
kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式,是kCFRunLoopDefaultMode和UITrackingRunLoopMode的组合而已
-
UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
-
GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode
第四十六、RunLoop运行流程图