IOS面试题——OC之类与对象原理(2)

一 面试题汇总

  1. 类对象的结构,isa,superclass,cache,bits
  2. 什么是联合体(共用体),什么是位域,isa包含哪些信息,怎么获取isa指针地址
  3. class_rw_t,class_ro_t分别包含哪些信息,为什么这么设计
  4. method_t包含哪些信息,存储在什么位置,分类添加同名方法时会执行哪个
  5. property 和 ivars有什么区别,为什么说分类不能添加属性
  6. isKindOfClass 和 isMemberOfClass的区别
  7. objc_getClass,object_getClass,objc_getMetaClass区别
  8. 方法缓存cache_t是怎么存储的,hash计算与buckets扩容实现方式
  9. new与alloc/init的区别?

二 面试题解答(仅供参考)

2.1 类对象的结构,isa,superclass,cache,bits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在Objective-C中,类对象(Class Object)是描述类的元数据的结构体。
它包含了一些重要的信息,如类的方法列表、实例变量列表、父类引用等。
下面是类对象的一些重要成员:

1-isa指针(isa pointer):
isa指针指向该类的元类(Meta Class),元类描述了类对象的行为,如类方法等。
isa指针使得Objective-C中的对象能够动态地调用方法,实现了Objective-C的动态特性。
在继承链中,一个类的isa指针会指向其父类的类对象。

2-superclass指针(superclass pointer):
superclass指针指向该类的父类的类对象。
通过superclass指针,Objective-C的运行时系统能够沿着继承链找到一个类的父类,这是实现继承的基础。

3-方法缓存(cache):
方法缓存是一个哈希表,用于存储常用的方法调用结果,以提高方法调用的效率。
当一个方法被调用时,Objective-C会首先在方法缓存中查找,
如果找到了对应的方法实现,则直接调用,否则会沿着继承链向上查找。

4-附加的位字段(bits):
这是一个用于存储一些标志位的字段,它可以用来表示类的一些特性,
比如是否为元类、是否为根类、是否有C++的构造函数等。
这些标志位的具体含义可能会因为不同的Objective-C运行时版本而有所不同。

这些成员组合在一起构成了类对象的结构,它们共同描述了一个类在Objective-C运行时系统中的行为和特性

2.2 什么是联合体(共用体),什么是位域,isa包含哪些信息,怎么获取isa指针地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1-联合体(Union):
在C语言中,联合体是一种特殊的数据结构,它允许在同一内存空间中存储不同类型的数据。
联合体中的每个成员共享同一块内存,因此改变其中一个成员的值会影响其他成员。
联合体的大小等于其最大成员的大小。
联合体的定义类似于结构体,但是它的成员共享同一内存空间。

2-位域(Bit Fields):
位域是C语言的一个特性,允许在一个整数中以位的形式存储多个字段,并指定每个字段的位数。
位域可以用于在有限的内存中存储多个标志位或者字段,以节省内存空间。

3-isa指针包含的信息:
在Objective-C中,每个对象的类都有一个isa指针,指向该对象所属的类对象(Class Object)。
isa指针存储了关于该对象所属类的元数据信息,包括类的方法列表、父类引用等。
通过isa指针,Objective-C的运行时系统可以动态地查找方法的实现,并实现消息转发等动态特性。

4-获取isa指针地址:在Objective-C中,可以通过object_getClass()函数来获取一个对象的isa指针地址。

2.3 class_rw_t,class_ro_t分别包含哪些信息,为什么这么设计

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
在Objective-C中,class_rw_t和class_ro_t是两个结构体,分
别存储类对象的读写(read-write)信息和只读(read-only)信息。
它们的设计是为了将类对象的可变部分和不可变部分分离开来,从而更好地支持Objective-C的动态特性。

1-class_rw_t(Read-Write信息):

方法列表:包含了类的实例方法和类方法列表。
属性列表:包含了类的属性列表。
协议列表:包含了类遵循的协议列表。
其他可写信息:例如,关于类的一些可变状态信息,比如类的版本号、是否为元类等。
这些信息是可变的,因为在运行时,我们可以向类中添加新的方法、属性或协议,或者修改现有的方法等。

2-class_ro_t(Read-Only信息):

类名:类的名称。
父类:类的父类引用。
成员变量列表:类的实例变量列表。
方法列表:包含了类的实例方法和类方法列表。
属性列表:包含了类的属性列表。
协议列表:包含了类遵循的协议列表。
其他只读信息:例如,关于类的一些固定状态信息,比如是否为元类、是否有C++构造函数等。
这些信息是不可变的,因为它们描述了类的静态结构,通常在编译时确定,并在运行时保持不变。

将类对象的可变部分和不可变部分分离开来,有助于提高程序的性能和灵活性。
只读信息通常是在编译时确定的,因此可以在加载类时进行一次性的初始化,然后在运行时不再改变。
而可变信息通常是在运行时动态添加或修改的,因此将其与只读信息分离开来,
可以减少不必要的操作和内存消耗

2.4 method_t包含哪些信息,存储在什么位置,分类添加同名方法时会执行哪个

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
在Objective-C中,method_t结构体用于表示类的方法信息。
它包含了方法的名称、参数类型、返回类型、实现等信息。通常,method_t结构体的定义如下:

typedef struct objc_method *Method;
struct objc_method {
SEL method_name; // 方法名称
char *method_types; // 方法参数和返回类型
IMP method_imp; // 方法的实现
};
method_t结构体包含以下信息:

1-method_name:方法的名称,是一个SEL类型的指针,用于表示方法的选择器(Selector)。

2-method_types:方法的参数和返回类型,是一个C字符串,采用Objective-C的方法签名格式进行表示,
例如"v@:@@"表示没有返回值(void),接受两个参数,并且都是对象类型。

3-method_imp:方法的实现,是一个函数指针,指向了方法的具体实现代码。

这些方法信息通常存储在类对象的method_list中,method_list是一个method_t类型的数组,
存储了该类的所有方法信息。Objective-C运行时系统在加载类时会初始化这个列表,
并在需要时通过选择器(Selector)来查找对应的方法实现。

当给一个类添加同名方法时,会执行最后一个被添加的同名方法。
这是因为在方法列表中,后添加的方法会覆盖先添加的同名方法,
所以在查找方法实现时,会找到最后一个添加的方法实现并执行

2.5 property 和 ivars有什么区别,为什么说分类不能添加属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在Objective-C中,property和ivars是两种用于描述类成员的概念,它们之间有一些区别。

1-Property(属性):

属性是通过@property关键字声明的。
属性提供了一种简洁的语法来声明类的实例变量,并自动生成对应的 getter 和 setter 方法。
属性可以指定存储特性(如strong、weak等)和访问特性(如readonly、readwrite等)。
属性通常用于定义类的公共接口,提供了一种封装成员变量的方式。

2-Instance Variables(实例变量,简称 ivars):

实例变量是类中直接声明的变量。
实例变量的访问权限取决于其声明的位置,通常是@interface部分的大括号中。
实例变量直接暴露了类的内部实现细节,因此不够封装。

虽然属性可以看作是对实例变量的封装,但它们是不同的概念。
属性提供了一种更高级别的接口,隐藏了实例变量的细节,并且可以自动生成 getter 和 setter 方法。

关于分类不能添加属性的问题,这是因为在 Objective-C 中,分类无法添加实例变量(即 ivars)。
而属性本质上是依赖于实例变量存在的,因此在分类中不能直接添加属性。
尽管在 Objective-C 2.0 中,我们可以在分类中声明属性,
但实际上编译器并不会为这些属性生成对应的实例变量和存取方法,
因此这些属性只是在编译时存在,而在运行时并不会起作用。

2.6 isKindOfClass 和 isMemberOfClass的区别

1
2
isKindOfClass:用于判断对象的实际类型,考虑其是否为指定类或其子类的实例。
isMemberOfClass:用于判断对象的实际类型,只考虑其是否为指定类的实例,不考虑其子类。

2.7 objc_getClass,object_getClass,objc_getMetaClass区别

1
2
3
objc_getClass 用于根据类名获取类对象。
object_getClass 用于获取对象所属的类对象。
objc_getMetaClass 用于根据类名获取类对象的元类。

2.8 方法缓存cache_t是怎么存储的,hash计算与buckets扩容实现方式

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
在Objective-C中,方法缓存(Method Cache)以及其存储方式对于方法的快速查找至关重要。
方法缓存是一个哈希表,用于存储类的方法调用结果,以提高方法调用的效率。
让我们来看看它的存储方式以及如何处理哈希冲突和扩容

1-存储方式:
方法缓存采用哈希表来存储方法调用结果。
哈希表中的每个桶(Bucket)都包含一个指向方法的指针,以及该方法对应的选择器(Selector)。
具体存储方式可能是类似于下面的结构:

struct cache_t {
struct bucket_t *buckets; // 指向哈希表的指针
unsigned int mask; // 哈希表大小的掩码
unsigned int occupied; // 已占用的桶数
};
每个桶包含一个指向方法的指针和该方法对应的选择器,这样可以在进行方法查找时快速定位到对应的方法。

2-哈希计算:
当需要查找某个方法时,Objective-C 运行时系统会根据选择器计算出哈希值,
并使用哈希值来确定该方法在方法缓存中的位置。哈希值的计算通常是通过选择器的地址等信息来进行的,
以确保在不同的运行时场景下能够得到相对均匀的哈希分布。

3-处理哈希冲突和扩容:
在进行方法查找时,可能会出现哈希冲突,即不同的选择器计算出了相同的哈希值。
为了处理这种情况,方法缓存采用了开放寻址法(Open Addressing)来解决冲突。
当发生冲突时,系统会在哈希表中顺序查找下一个空桶,并将方法存储在该位置。

当方法缓存中的桶数达到一定阈值时,可能需要对哈希表进行扩容,以保持哈希表的性能。
扩容的具体方式可能涉及重新计算哈希值、创建新的更大的哈希表,
并将现有的方法重新插入到新的哈希表中,最后释放旧的哈希表。

通过以上方式,方法缓存能够快速地存储和查找类的方法调用结果,提高了方法调用的效率。

2.9 new与alloc/init的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在Objective-C中,new 和 alloc/init 都是用来创建对象的方法,但它们之间有一些区别:

1-alloc/init:

1.1-alloc 方法用于分配内存空间,但不会初始化对象属性。
1.2-init 方法用于初始化对象,即设置对象的初始状态,如设置属性的初始值等。
1.33-通常情况下,你需要在调用 alloc 之后立即调用 init 来完成对象的初始化。

MyClass *object = [[MyClass alloc] init];

2-new:

2.1-new 是一个类方法,结合了 alloc 和 init 的功能,它会同时分配内存空间并且初始化对象。
2.2-在使用 new 创建对象时,你不需要再显式调用 init 方法。

MyClass *object = [MyClass new];

总的来说,new 相对于 alloc/init 更加简洁,
因为它们将两个步骤合并成一个步骤,但是在使用上没有太大的区别。

三 参考

  • 简书—OC之类与对象原理