iOS面试资料(二)
2022-03-10 本文已影响0人
JamieChen
iOS面试题
Block相关
Block
-
block本质
- block是将函数及其执行上下文封装起来的对象
关于block的截获特性,你是否有了解?block的截获变量特性是怎样的?
-
变量捕获机制分析:
- 对于“基本数据类型”的“局部变量”截获其值
- 对于“对象”类型的局部变量“连同所有权修饰符”一起截获
- 以“指针形式”截获局部静态变量(指针传递)
- 不截获全局变量、静态全局变量(直接访问)
-
改外部变量必要条件
- 将auto从栈copy到堆
原因:栈中内存管理是由系统管理,出了作用域就会被回收,堆中才是可以由程序员管理
- 将auto从栈copy到堆
对栈上的block进行copy之后,假如在mrc环境下,内存是否回泄漏?
- copy操作之后,堆上的block没有额外的成员变量指向它,正如我们alloc对象后,没有进行release,造成内存泄漏
为什么block会产生循环引用?
- 如果当前block对当前对象的某一成员变量进行截获,block会对当前对象有一个强引用
- 而当前block由于当前对象对其有一个强引用,产生了一个自循环引用的一个循环引用的问题
Block不允许修改外部变量的值原因
-
block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域---改变了作用域。
-
在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。
-
比如想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
-
所以 Apple 在编译器层面做了限制,如果在 block 内部试图修改 auto 变量(无修饰符),那么直接编译报错。
-
可以把编译器的这种行为理解为:对 block 内部捕获到的 auto 变量设置为只读属性---不允许直接修改。
如何实现对外部变量的捕获?
- 将变量设置为全局变量。原理:block内外可直接访问全局变量
- 加 static (放在静态存储区/全局初始化区)。原理是block内部对外部auto变量进行指针捕获
- 最优解:使用__block 关键字
__block
- 将auto变量封装为结构体(对象),在结构体内部新建一个同名的auto变量
- block内截获该结构体的指针
- 在block中使用自动变量时,使用指针指向的结构体中的自动变量
__block int var = 10;
void(^blk)(void) = ^{
var = 20;
};
blk();
转换后的代码:
struct __Block_byref_var_0 {
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var; // 10 => 20 该结构体持有相当于原来自动变量的成员变量
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_var_0 *var; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
block在修改NSMutableArray需不需要添加__block
- 不需要
- 原因:
- 当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。
- 所以,在block里面对指针指向内容做的修改,在block外面也一样生效。
block是如何捕获局部变量的?
- block捕获外界变量时,在内部会自动生成同一个属性来保存
UIView动画中block回调里self要不要弱引用?
- 不需要,它不会造成循环引用,因为它是类方法。
- 之所以需要弱引用本身,是因为怕对象之间产生循环引用,当前控制器不可能强引用一个类,所以循环无法形成。
block里面会不会存在self为空的情况(weak strong的原理)?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
__strong typeof(weakself) strongself = weakself;
[strongself.dataSource reloadDataWithCompletion:nil];
}];
- 有时候weakSelf在block里在执行reloadDataWithCompletion还存在
- 但在执行reloadDataWithCompletion前,可能会被释放了
- 为了保证self在block执行过程里一直存在,对他强引用strongSelf
__block与__weak的区别
- __block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型
- __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)
- __block对象可以在block中被重新赋值,__weak不可以。
多层block嵌套如何使用weakSelf?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
__strong typeof(weakself) strongself = weakself;
__weak typeof(self) weakSelf2 = strongself;
[strongself.dataSource reloadDataWithCompletion:^(BOOL result) {
__strong typeof(self) strongSelf2 = weakSelf2;
}];
}];
Masonry对于block内部引用self会不会造成循环引用?
- 不会
- 这个block没有copy,是在栈上,使用完直接释放了,
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
__weak来解决block中的循环引用,还有别的方法吗
- __block
- 将对象传进入修改
代理、Block利弊
-
与委托代理模式的代码相比,用block写出的代码更为整洁
-
代理优点:
- 代理语法清晰,可读性高,易于维护
- 它减少代码耦合性,使事件监听与事件处理分离
- 一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高
-
代理缺点:
- 实现代理的过程较繁琐
- 跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱
- 当多个对象同时传值时不易区分,导致代理易用性大大降低
-
block优点:
- 语法简洁,代码可读性和维护性较高
- 配合GCD优秀的解决多线程问题
-
block缺点:
- Block中得代码将自动进行一次retain操作,容易造成内存泄漏
- Block内默认引用为强引用,容易造成循环应用
-
运行成本:
- delegate运行成本低,block的运行成本高
- block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是假引用技术,使用完block置nil才会消除
- delegate只是保存了一个对象的指针,直接回调,没有额外的消耗。就像c的函数指针,只多了一个查表动作
Runtime
Runtime的相关术语
SEL、id、Class、Method、IMP、Cache、Property
介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
为什么要设计metaclass
class_copyIvarList & class_copyPropertyList区别
class_rw_t 和 class_ro_t 的区别
讲述一下runtime的概念,message send如果寻找不到相应的对象,会如何进行后续处理 ?
- objc向一个对象发送消息,runtime会根据对象的isa指针找到该对象实际所属类,然后在该类方法列表以及父类的方法列表中查找方法实现,如果向一个nil对象发送消息,在查找isa指针时就是0地址返回,不会出错
交互两个方法的现实有什么风险?
Runloop
- 什么是RunLoop?
- RunLoop 实际上是一个对象
- 这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)
- 从而保持程序的持续运行
- 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能
- 应用范畴
- 定时器(Timer)、PerformSelect
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
- 基本应用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
- runloop和线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
/*
* 从字典中获取,如果没有则直接创建
*/
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
NSTimer相关
NSTimer准吗?如果不准的话原因是什么?如何解决?
- 原因:
- NSTimer的触发时间到的时候,会在RunLoop中被检测一次;
- 如果在这一次的RunLoop中做了耗时的操作,会处于阻塞状态
- 时间超过了定时器的间隔时间,触发时间就会推迟到下一个runloop周期
- 解决方法:
- 在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作
- 使用CADisplayLink(一般用来做UI展示更新,同样存在runloop卡顿问题)
- 使用GCD定时器
使用NSTimer是如何处理循环引用的?
使用类方法
TODO(待填充);
谈谈常用的三种定时器优缺点(NSTimer、CADisplayLink、GCD定时器)✨✨✨✨✨
- NSTimer和CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
- 相比之下GCD的定时器会更加准时,因为GCD不是依赖RunLoop,而是由内核决定
- CADisplayLink和NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
在viewWillDisappear或者viewDidDisappear方法中将 timer = nil,是否还会造成循环引用?
- 问题一:如果只是想在离开此页时要释放,进入下一页时不要释放,场景就不适用了
- runloop->timer;controller->timer
如何利用runloop监控卡顿✨✨✨
NSThread中的Runloop的作用,如何使用
-
runloop是一个线程里运行循环,并对收到的事件进行处理,
-
保持线程长时间存活
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
- 用于定时器
KVO
KVO基础
KVO 的 全称Key-Value Observing,俗称“键值监听”,可以用于某个对象属性值的改变
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)
- 利用runtimeAPI动态生成一个子类(NSKVONotifying_XXXX),并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会动用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey
- 父类原来的setter方法
- didChangeValueForKey
- 内部触发监听器(
ObserveValueForKeyPath:ofObject:change:context
)
如何手动触发KVO?
- 手动调用
willChangeValueForKey
- 修改成员变量值
- 手动调用
didChangeValueForKey
直接修改成员变量会触发KVO么?
- 不会触发KVO(原因看KVO的本质)
KVC
KVC基础
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
通过KVC修改属性会出发KVO么?
- 能触发KVO()
- KVC在修改属性时,会调用
willChangeValueForKey:和didChangeValueForKey:
方法
KVC的赋值和取值过程是怎样的?
- 赋值过程
- 首先会按照setKey、_setKey的顺序查找方法,找到方法,直接调用方法并赋值;
- 未找到方法,则调用
+ (BOOL)accessInstanceVariablesDirectly;
- 若accessInstanceVariablesDirectly方法返回YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常;
- 若accessInstanceVariablesDirectly方法返回NO,则直接抛出异常;
- 取值过程
- 首先会按照getKey、key、isKey、_key的顺序查找方法,找到直接调用取值
- 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出异常;
- 若返回的YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到则取值;
- 找不到则抛出异常;
使用场景
- 单层字典模型转化:
[self.model setValuesForKeysWithDictionary:dict];
- 通过KVC修改未暴露的属性:
UILabel *placeholderLabel=[self.userTextField valueForKeyPath:@"placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];
- 使用valueForKeyPath可以获取数组中的最小值、最大值、平均值、求和
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
- 数组内部去重
[dataArray valueForKeyPath:@"@distinctUnionOfObjects.self"]
- 数组合并(去重合并:distinctUnionOfArrays.self、直接合并:unionOfArrays.self)
NSArray *temp1 = @[@3, @2, @2, @1];
NSArray *temp2 = @[@3, @4, @5];
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@distinctUnionOfArrays.self"]);
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@unionOfArrays.self"]);
输出两个数组:( 5, 1, 2, 3, 4 ), ( 3, 2, 2, 1, 3, 4, 5 )。
- 大小写转换(uppercaseString)及 打印字符串长度同样适用(length)
NSArray *array = @[@"name", @"w", @"aa", @"jimsa"];
NSLog(@"%@", [array valueForKeyPath:@"uppercaseString"]);
打印:
(NAME,W,AA,JIMSA)
简单介绍一下KVC和KVO,他们都可以应用在哪些场景?
- KVO:键值监听,观察某一属性的方法
- KVC:键值编码,间接访问对象的属性
Category
Category相关
Category的使用场合
- 将一个类拆成很多模块(其实就是解耦,将相关的功能放到一起)
Category的实现原理
- 通过runtime动态将分类的方法合并到类对象、元类对象中
- Category编译之后的底层结构是 struct_category_t , 里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将 Category 的数据,合并到类信息中(类对象、元类对象)
category和extension区别
- Extension在编译时,就把信息合并到类信息中
- Category是在运行时,才会将分类信息合并到类信息中
- 分类声明的属性,只会生成getter/setter方法声明,不会自动生成成员变量和getter/setter方法实现,而扩展会
- 分类不可用为类添加实例变量,而扩展可以
分类的局限性
- 无法为类添加实例变量,但可通过关联对象进行实现
- 分类的方法如果和类重名,会覆盖原来方法的实现
- 多个分类的方法重名,会调用最后编译的那个分类的实现
为什么category不能添加属性?使用Runtime就可以了?
- 分类没有自己的isa指针
- 类最开始生成了很多基本属性,比如IvarList,MethodList
- 分类只会将自己的method attach到主类,并不会影响到主类的IvarList
- 实例变量没有setter和getter方法。也没有自己的isa指针
- 关联对象都由AssociationsManager管理
- AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。
- 相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址
- 而这个map的value又是另外一个AssAssociationsHashMap,里面保存了关联对象的kv对
Category中有load方法么?load方法什么时候调用的?load方法能继承么?
-
有
-
+load方法会在runtime加载类、分类时调用;
-
每个类、分类的+load,在程序运行过程中只调用一次
-
调用顺序
-
先调用类的+load,(按照编译先后顺序,先编译,先调用),调用子类的+load之前会调用父类的+load
-
再调用分类的+load按照编译先后顺序调用(先编译,先调用)
test方法和load方法的本质区别?(+load方法为什么不会被覆盖)
- test方法是通过消息机制调用 objc_msgSend([MJPerson class], @selector(test))
- +load方法调用,直接找到内存中的地址,进行方法调用
load调用顺序
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序
- 先调用类的+load方法,之后按照编译先后顺序调用(先编译,先调用,调用子类的+load之前会先调用父类的+load)
- 再调用分类的+load,之后按照编译先后顺序调用(先编译,先调用)
不同Category中存在同一个方法,会执行哪个方法?如果是两个都执行,执行顺序是什么样的?
- 根据Build Phases->Compile Sources中添加的文件顺序,后面的会覆盖前面的
load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时他们之间的调用过程?🌟🌟🌟🌟🌟
区别:
- 调用方式不同
- load是根据函数地址直接调用
- initialize是通过objc_msgSend调用
- 调用时刻
- load是runtime加载 类/分类 的时候调用(只会调用1次)
- initialize是类第一次接收消息时调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
- 调用顺序
- load:先调用类的load。先编译的类,优先调用load(调用子类的load之前,会先调用父类的load)
- 再调用分类的load(先编译的分类,优先调用load)
- initialize:先初始化父类, 再初始化子类(可能最终调用的是父类的initialize方法)
分类中方法替换
- category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
- category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面
- 这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。
为什么不能动态添加成员变量?
- 方法和属性并不“属于”类实例,而成员变量“属于”类实例
- “类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。
- 假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用