iOS底层高级面试题整理一(I)

iOS底层高级面试题整理【OC本质、KVC、KVO、Categroy、Block】(I)

1. 查看一个OC对象占用了多少内存

系统分配了16个字节给NSObject对象(通过malloc_size函数获得) 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

在64位CPU中,NSObject 对象占用大小为16字节,其中8字节为指针大小,8字节为实例对象结构体所占大小。 在32位CPU中,NSObject 对象占用大小为8字节,其中4字节为指针大小,4字节为实例对象结构体所占大小。

1
2
3
4
5
6
7
创建一个实例对象,至少需要多少内存?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);

创建一个实例对象,实际上分配了多少内存?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);

2. 常用的LLDB指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
print、p:打印
po:打印对象

// 读取内存
memory read/数量格式字节数 内存地址
x/数量格式字节数 内存地址
x/3xw 0x10010

// 修改内存中的值
memory write 内存地址 数值
memory write 0x0000010 10

bt 打印调用堆栈信息

call 调用方法

expression(执行表达式)

po(打印参数)

print(打印参数)

backtrace(调用栈信息)

frame(栈帧信息)
查看当前栈帧信息
选择栈帧

breakpoint(打印断点列表)
打印断点列表
函数地址下断
匹配特征字下断
对动态库函数下断
对某个OC方法下断
对某个OC类的所有方法下断
在某个OC所有同名方法设置断点
断点命令处理
对导出函数下断

3. OC对象

OC对象 可以分为3种:

1). instance对象 (实例对象) 2). class对象 (类对象) 3). meta-class对象 (元类对象)

3.1 instance对象 (实例对象)

instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象

1
2
3
4
5
6
7
8
9
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];

object1、object2是NSObject的instance对象(实例对象)
它们是不同的两个对象,分别占据着两块不同的内存

instance对象在内存中存储的信息包括
-isa指针
-其他成员变量

实例对象的内存分布.png

实例对象的内存分布

TIP:

  • isa 的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
union isa_t {    
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
  • isa的底层是isa_tisa_t的结构是联合体+位域
  • 联合体的大小取决于内部最大的元素的大小,所以isa的大小为8字节
  • 联合体内部的的元素在内存中的互相覆盖的,所以clsbits是不会同时存在的。

在联合体内又增加了位域的结构来使isa包罗万象。 8字节即为64个二进制位。每个二进制位都定义了存储的内容,即为位域。

1
2
3
4
5
6
7
8
9
10
#   define ISA_BITFIELD                                                
\ uintptr_t nonpointer : 1;
\ uintptr_t has_assoc : 1;
\ uintptr_t has_cxx_dtor : 1;
\ uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
\ uintptr_t magic : 6;
\ uintptr_t weakly_referenced : 1;
\ uintptr_t deallocating : 1;
\ uintptr_t has_sidetable_rc : 1;
\ uintptr_t extra_rc : 8
  • nonpointer:表示是否对 isa 指针开启指针优化(0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等)
  • has_assoc:关联对象标志位(0没有,1存在)
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
  • shiftcls: 存储类指针的值。开启指针优化的情况下,在arm64架构下有33位用来存储类指针
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存
  • has_sidetable_rc:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
  • extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。 如果引⽤计数⼤于 10,则需要使⽤ has_sidetable_rc
  • TaggedPointer

当对象为指针为TaggedPointer类型时,指针的值不是地址了,而是真正的值,直接优化了存储,提升了获取速度。

  • TaggedPointer的特点专门用来存储小对象,例如NSNumber和部分NSString指针不在存储地址,而是直接存储对象的值。
  • ② 所以,它不是一个对象,而是一个伪装成对象的普通变量。
  • ③ 内存也不在堆,而是在栈,由系统管理,不需要malloc和free在内存读取上有着3倍的效率,创建时比以前快106倍。(少了malloc流程,获取时直接从地址提取值)

3.2 Class对象 (类对象)

我们平时说的类,其实也是对象,称为类对象, 每个类在内存中有且只有一个class对象

class对象在内存中存储的信息主要包括 isa指针 superclass指针 类的属性信息(@property)、类的对象方法信息(instance method) 类的协议信息(protocol)、类的成员变量信息(ivar) ……

类对象的内存分布.png

类对象的内存分布

3.3 meta-class对象 (元类对象)

每个类在内存中有且只有一个meta-class对象

meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括 isa指针 superclass指针 类的类方法信息(class method) ……

1
2
3
4
5
// 将类对象当做参数传入,获得元类对象
Class objectMetaClass = object_getClass(objectClass5);
// objectMetaClass是NSObject的meta-class对象(元类对象)
// 查看是否为元类对象:
Bool result = class_isMetaClass(objectMetaClass)

L9TGZk.png

元类对象的内存分布

3.4 三者之间的关系

L9TLXe.png

类对象的内存分布.png

instance的isa指向class 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用

class的isa指向meta-class 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

iOS对象的内存分布

对象的内存分布

iOS对象的isa的指向过程.png

isa 和 superClass 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
instance的isa指向class

class的isa指向meta-class

meta-class的isa指向基类的meta-class

class的superclass指向父类的class
-如果没有父类,superclass指针为nil

meta-class的superclass指向父类的meta-class
-基类的meta-class的superclass指向基类的class

instance调用对象方法的轨迹
-isa找到class,方法不存在,就通过superclass找父类

class调用类方法的轨迹
-isa找meta-class,方法不存在,就通过superclass找父类

总结

  • OC对象本质是结构体,都有 isa 指针
  • 实例对象:内存只存储 isa 和 成员变量的值
  • 类对象:superclass 指针,对象方法列表、属性信息等都是存放在类对象中的
  • meta-class对象:是一种特殊的类对象,内存结构相同
  • 通过 isa 和 superclass 指针,把对象都串联起来了

4. KVO实现原理

KVO(Key-Value Observing)是一套事件观察 & 通知机制,开发者可以使用它来监测对象属性的变化并做出响应。

延展 KVO和NSNotificatioCenter都是 iOS 观察者模式的一种实现,两者的区别在于: 相对于被观察者和观察者之间的关系,KVO是一对一的NSNotificatioCenter是一对多的 KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听

4.1 基本实现:

KVO使用三部曲:

  • 注册观察者
1
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew  NSKeyValueObservingOptionOld) context:NULL];
  • 实现回调
1
2
3
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
}
  • 移除观察者
1
2
3
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
}

苹果官方推荐的方式是——在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,这是一种比较理想的使用方式

4.2 底层实现原理

L9Mypi.png

KVO底层实现图解

L9Mk1T.png

NSKVONotifying_XXX 的内部结构及方法调用顺序

  • 利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,并且让instance对象的isa指向这个全新的子类NSKVONotifying_XXX

  • 当修改对象的属性时,会在子类NSKVONotifying_XXX调用Foundation的_NSSetXXXValueAndNotify函数

  • _NSSetXXXValueAndNotify函数中依次调用: 1)、willChangeValueForKey 2)、父类原来的setter 3)、didChangeValueForKeydidChangeValueForKey:内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath: ofObject: change: context:)

扩展

  • NSKVONotifying_Person 重写了 class 方法,直接return class _getSuperClass(object_getClass(self))是为了隐藏 NSKVONotifying_Person 不被外界看到

  • Foundation 框架中很多例如 _NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify 等类簇。可以通过命令查询

    1
    nm Foundation  grep ValueAndNotify

5. KVC实现原理

KVC(key-Value coding) 键值编码,指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。

L9O8vh.png

KVC底层实现图解

L9O8vh.png

KVC底层设值流程

1
2
3
4
5
// 常见的API有
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值

NSKeyValueCoding类别中的其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。

问答延展:

1). 如何手动触发KVO? 手动调用willChangeValueForKey:didChangeValueForKey: 2). 直接修改成员变量会触发KVO么? 不会触发KVO 3).通过KVC修改属性会触发KVO么? 会触发KVO


部分来源参考: 稀土掘金作者:iSwifter 链接:https://juejin.cn/post/6844903920322494478


iOS底层高级面试题整理一(I)
https://blog.qzl-coding.top/2022/04/18/iOS底层高级面试题整理一(I)/
作者
Long Chiu
发布于
2022年4月18日
许可协议