【一套iOS底层试卷-我想和你分享】答案详解
一、选择题(每题5分),有单选有多选哦
1. 在LP64下,一个指针的有多少个字节 分值5分
- A: 4
- B: 8
- C: 16
- D: 64
LP64指的是long(L) 占64位,pointer(P)占64位,因此答案选 (B)
2. 一个实例对象的内存结构存在哪些元素 分值5分
- A:成员变量
- B: supClass
- C: cache_t
- D: bit
实例对象的内存结构如下结构体所示,包含 isa指针,以及成员变量,成员变量紧跟在isa指针后面,通过对象首地址加偏移量获取,偏移量则存在对象的类中,因此答案选 A
struct objc_object {
private:
isa_t isa;
}
3. 下面sizeof(struct3) 大小等于 分值5分
struct LGStruct1 {
char b;
int c;
double a;
short d;
}struct1;
struct LGStruct2 {
double a;
int b;
char c;
short d;
}struct2;
struct LGStruct3 {
double a;
int b;
char c;
struct LGStruct1 str1;
short d;
int e;
struct LGStruct2 str2;
}struct3;
- A: 48
- B: 56
- C: 64
- D: 72
sizeof 用于在编译时判断数据类型需要占用多少字节。
参考:www.cnblogs.com/qiumingchen…
1、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
2、结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。
3、当结构体嵌套的时候,offset是根据嵌套结构体内部的最大成员的大小来计算的
4、最后计算结构体大小的时候,不能把嵌套结构体看成是一个整体,而要拆开来看
double a占用 8个字节,offset=0,size=8 int b占用4个字节,offset=8,size=12 char c占用1个字节,offset=12,size=13
【struct LGStruct1 str1 】占用24个字节,offset=13,不能被8整除,调整为offset=16,size=40
char b占用1个字节,offset=0,size=1 int c占用4个自己,offset=1,由于1不能被4整除,offset需要变成4,size=8 double a占用8个字节,offset=8,size=16 short d占用2个字节,offset=16,size=18
由于18不是结构体 LGStruct1 中的成员大小的最小公倍数,需要调整为24,size=24
short d占用2个字节,offset=40,size=42 int e占用4个字节,offset=42,不能被4整除,调整offset=44,size=48 【struct LGStruct2 str2】占用 16 字节,offset=48,size=64 double a占用8个字节,offset=0,size=8 int b 4字节,offset=8,size=12 char c 1字节,offset=12,size=13, short d 2字节,offset=13,调整为offset=14,size=16
由于16满足结构体 LGStruct2 各成员大小的最小公倍数,size=16
因此答案为 C: 64
4. 下列代码: re1 re2 re3 re4 re5 re6 re7 re8输出结果 分值5分
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
- A: 1011 1111
- B: 1100 1011
- C: 1000 1111
- D: 1101 1111
- (Class)class; 这个方法做了什么,看源码可知这个方法返回了自己
+ (Class)class { return self; }
那么 (id)[NSObject Class] 得到的是什么?首先要知道的是 id 是 objc_objecte *的别名,Class是objc_class *的别名,objc_object结构体只有一个成员变量 isa,而objc_class继承于objc_object。
因此强转id后,第一个成员变量isa还是没变的。
typedef struct objc_object *id;
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
// 其他结构
}
isKindOfClass:(BOOL)cls
判断这个类是否是另一个类或是其父类
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
isMemberOfClass:(BOOL)cls
判断一个类是否是另一个类
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
这题其实是考察对象isa指针和类对象isa指针的区别。对象的isa指针指向其类,类对象的isa指针指向它的metaClass,而metaclass的superClass最终指向NSObject 根类。
因此看题 BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; 这个比的是NSObject的metaClass 是否是 类NSobject的Kind,答案是YES
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; 这个比较的是 nsobject的metaClass和NSobject是否相等,当然不等。答案是NO
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; 这个比较的是LGPerson的metaClass是否为LGPerson类 的kind,当然为NO,
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; 这里也是NO
而alloc方法是申请内存用的,返回的是类的实例,它的isa指向的是它的类,而不是metaClass,因此按定义都是YES,
该题的答案为 1000 1111 ,选 C
5. (x + 7) & ~7 这个算法是几字节对齐 分值5分
- A: 7
- B: 8
- C: 14
- D: 16
这个对齐算法在苹果源码中随处可见,比如
define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
&:二进制与运算,当都是1是,为1,否则为0 ~:二进制取反,0为1,1为0
假设 x 为5,那么这题转为二进制就是:
5 + 7= 0101 + 0111=1100 ~7=1000 1100 & 1000=1000=8
在换一个x为2,转二进制: 2+7=0010+0111=1001 1001 & 1000=1000=8
可知这个算法是8字节对齐,答案为 B
6. 判断下列数据结构大小 分值5分
union kc_t {
uintptr_t bits;
struct {
int a;
char b;
};
}
- A: 8
- B: 12
- C: 13
- D: 16
union 联合体,特性就是所有成员占用同一段内存,这段内存大小等于联合体中最大的一个成员的大小 uintptr_t 是 unsigned long的别名,在64位下占用8个字节, struct大的小为8字节,因此这个union的内存占用总共为8字节
答案为 A
7. 元类的 isa 指向谁, 根元类的父类是谁 分值5分
- A: 自己 , 根元类
- B: 自己 , NSObject
- C: 根元类 , 根元类
- D: 根元类 , NSObject
元类的isa指向根元类,根元类的父类指向 NSObject
答案为 D
8. 查找方法缓存的时候发现是乱序的, 为什么? 哈希冲突怎么解决的 分值5分
- A: 哈希函数原因 , 不解决
- B: 哈希函数原因 , 再哈希
- C: 他存他的我也布吉岛 , 再哈希
- D: 他乱由他乱,清风过山岗 , 不解决
方法的缓存要用到objc_object中的cache_t cache 和 bucket_t
struct cache_t {
// 省略其他
void insert(SEL sel, IMP imp, id receiver);
extern "C" IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil);
}
struct bucket_t {
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
explicit_atomic _imp;
explicit_atomic _sel;
}
其中处理方法插入时主要有下面的逻辑,当计算出的hash值在bucket_t中不存在时,表示此位置没有插入过,直接插入,一旦出现冲突,那么重新计算hash值 cache_next(i, m)
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
// 计算hash
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
// 重新计算下一个位置的hash值
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
而查找步骤就是插入的反操作,这个是汇编写的,在源码的 objc-msg-x86_64.s 文件下。
此题答案 为 B
9. 消息的流程是 分值5分
- A: 先从缓存快速查找
- B: 慢速递归查找methodlist (自己的和父类的,直到父类为nil)
- C: 动态方法决议
- D: 消息转发流程
1、快速查找:objc_msgSend 查找 cache_t 缓存消息 2、慢速查找,递归自己和父类查找方法:lookUpImpOrForward 3、查找不到消息,进行动态方法解析:resolveInstanceMethod 4、消息快速转发:forwardingTargetForSelector 5、消息慢速转发:消息签名 methodSignatureForSelector 和分发 forwardInvocation 6、最终仍未找到消息:程序crash,报经典错误信息 unrecognized selector sent to instance
答案为: ABCD
10. 类方法动态方法决议为什么在后面还要实现 resolveInstanceMethod 分值5分
- A: 类方法存在元类(以对象方法形式存在), 元类的父类最终是 NSObject 所以我们可以通过resolveInstanceMethod 防止 NSObject 中实现了对象方法!
- B: 因为在oc的底层最终还是对象方法存在
- C: 类方法存在元类以对象方法形式存在.
- D: 咸吃萝卜,淡操心! 苹果瞎写的 不用管
此题考查isa指针方法查找和类的继承关系,答案为 A
二、判断题 (每题5分)
1. 光凭我们的对象地址,无法确认对象是否存在关联对象 分值5分
- 对
- 错
确定一个对象是否存在关联对象,可以通过读取objc_object的函数 bool hasAssociatedObjects(); 来判断对象是否存在关联对象。
原理就是 isa指针的定义,在isa_t中,有一位来存 has_assoc 表示是否存在关联对象。
// arm64
define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
}
union isa_t {
// 省略其他
uintptr_t bits;
struct {
ISA_BITFIELD; // defined in isa.h
};
}
而对象的底层结构是什么?是objc_object,它的第一个成员变量就是isa,因此当然可以通过已知的对象地址,确认对象是否存在关联对象。可以通过真机调试查看对应二进制位下的值是否为1确认(这里是第2位)。
因此答案为 错
2. int c[4] = {1,2,3,4}; int *d = c; c[2] = *(d+2) 分值5分
- 对
- 错
在c中,数组的首地址可以直接用数名表示,int *d = c,因此d指向数组首地址,由于有数组再内存中是连续的。因此 c[2] = *(d+2)
答案为 对
3. @interface LGPerson : NSObject{ UIButton *btn } 其中 btn 是实例变量 分值5分
- 对
- 错
非 property修饰,为成员变量,变量名为 btn
答案为 对
4. NSObject 除外 元类的父类 = 父类的元类 分值5分
- 对
- 错
具体关系图请参考 isa指针和类的继承关系图
答案为 对
5. 对象的地址就是内存元素的首地址 分值5分
- 对
- 错
注意题意,问的是内存元素。对象的内存元素有哪些,有:isa,成员变量。因此从对象的地址开始,第一个元素就是isa,占8字节,后面就是对象的成员变量
// 这里验证下
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
Person *p = [Person new];
p.name = @"我是昵称";
NSLog(@"对象指针的地址:%p",&p);
NSLog(@"对象地址:%p",p);
2021-09-13 15:02:08.597305+0800 HeaderMapDemo[9715:179712] 对象指针的地址:0x7ffeef7110b8
2021-09-13 15:02:08.597474+0800 HeaderMapDemo[9715:179712] 对象地址:0x600003af44b0
(lldb) x/4gx p
0x600003af44b0: 0x00000001004f47e8 0x00000001004ef020
0x600003af44c0: 0x00005cdcf1da44c0 0x000000010049004e
(lldb) po 0x00000001004ef020
我是昵称
(lldb)
可以很清楚的看到,Person对象p的内存结构,0x00000001004f47e8是isa,0x00000001004ef020是name属性值。
答案为 对
6. 类也是对象 分值5分
- 对
- 错
类是元类的对象
答案为 对
三、简单题 (每题 10分 合计 100分)
17、怎么将上层OC代码还原成 C++代码 分值10分
clang -rewrite-objc xxx.m
xcrun -sdk iphonesimulator clang -rewrite-objc xxxx.m
18、怎么打开汇编查看流程,有什么好处 ? 分值10分
1、通过断点 --》 左侧的 show the Debug navigator --〉切换堆栈信息可查看汇编
2、Xcode顶部菜单栏 --》 Debug --〉Debug workflow --》 Always Show Disassembly
好处:
可以从更深层次了解代码的运行流程,了解数据在寄存器中的走向和变化,还能发现一些系统私有的函数的符号,从而借此找到突破口
19、x/4gx 和 p/x 以及 p *$0 代表什么意思 分值10分
x 查看内存,同 memory read
x/[d]gx 查看内存分布,d表示查看几块,一块是8字节 。g : 格式化输出。x : 每一段以16进制打印(w :以16进制打印,但只输出8位。)
p:输出值、值类型、引用名、内存地址、常量的进制转换
p/x:以16进制格式打印(其他还有 p/o: 8进制打印 , p/t:二进制打印)
p *0:这个常用于lldb调试时数值的传递,0:这个常用于lldb调试时数值的传递,0:这个常用于lldb调试时数值的传递,0表示某个值的引用或内存地址,在这里表示地址,那么 p *$0 表示输出这个地址指向的内存的值。
int a = 10;
int *ap = &a;
输出:
(lldb) p ap
(int *) $0 = 0x00007ffeea2bb0b4
(lldb) p *$0
(int) $1 = 10
(lldb)
20、类方法存在哪里? 为什么要这么设计? 分值10分
类方法存在元类中。
为什么这么设计?
这个要说下对象和类,对象的方法存在类中,调用对象的方法会从它的类中查找。相应的,类的对象存在元类中,调用类方法会从它的元类中查找,这一设计保证了底层方法调用逻辑的一致性。
21、方法慢速查找过程中的二分查找流程,请用伪代码实现 分值10分
消息的慢速查找会进入 函数 lookUpImpOrForward,这个函数内存做了一些列逻辑判断后,最终沿着类的继承链,向上查找方法,知道找到或到达根类。
// 这里给出在类的方法列表中查找某一方法的源码,以二分法实现。
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
22、ISA_MASK = 0x00007ffffffffff8ULL 那么这个 ISA_MASK 的算法意义是什么? 分值10分
这个值怎么理解?拿出计算器将值输入,看二进制部分,可以看到底三位为0,中间44位为1,其他位为0。我们再来看看isa的位域结构(x_86_64),看看表示类信息的位域,从低位第3位开始,共44位。其实答案已经很明显了,就是为了得到isa中存储的 shiftcls ,也就是类信息。因此这个 ISA_MASK 算法的意义就是为了得到isa中的类信息。
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
23、类的结构里面为什么会有 rw 和 ro 以及 rwe ? 分值10分
在回答问题前,我们先来看下他们的结构和关系
struct class_rw_t {
class_rw_ext_t *ext() const
const class_ro_t *ro() const
// 省略其他
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const
// 省略其他
}
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
可以看到 rw 包括 ro和rwe。
ro表示只读,在编译时就确定了内存大小,我们直接读取 struct class_ro_t的instanceStart和instanceSize即可。
而rwe表示可读可写,里面有方法列表,属性列表,协议列表等,注意没有成员变量列表,方便我们在运行时改变内存结构。
【参考答案】
(1)ro 属于 clean memory,在编译即确定的内存空间,只读,加载后不会改变内容的空间;
(2)rw 属于 dirty memory,rw 是运行时结构,可读可写,可以向类中添加属性、方法等,
在运行时会改变的内存;
(3)rwe 相当于类的额外信息,因为在实际使用过程中,只有很少的类会真正的改变他
们的内容,所以为避免资源的消耗就有了 rwe;
(4)运行时,如果需要动态向类中添加方法协议等,会创建 rwe,并将 ro 的数据优先 attache 到 rwe 中,在读取时会优先返回 rwe 的数据,如果 rwe 没有被初始化,则返回 ro
的数据。
rw 中包括 ro 和 rwe,其中 rw 是 dirtymemory,ro 是 clean memory;为了让 dirty memory
占用更少的空间,把 rw 中可变的部分抽取出来为 rwe;
clean memory 越多越好,dirty memory 越少越好,因为 iOS 系统底层虚内存机制的原
因,内存不足时会把一部分内存回收掉,后面需要再次使用时从硬盘中加载出来即 swap 机制,clean memory 是可以从硬盘中重新加载的内存,iOS 中的 macho 文件动态
库都属于此类行;dirty memory 是运行时产生的数据,这部分数据不能从硬盘中重新
加载所以必须一直占据内存,当系统物理内存紧张的时候,会回收掉 clean memory 内
存,如果 dirty memory 过大则直接会被回收掉;所以 clean memory 越多越好,dirty
memory 越少越好;苹果对 rw、ro、rwe 进行这么细致的划分都是为了能更好更细致
的区别 cleanmemory 和 dirty memory;
24、cache 在什么时候开始扩容 , 为什么? 分值10分
这一部分位于类的cache_t的 insert函数中
void cache_t::insert(SEL sel, IMP imp, id receiver) {
// 省略其他
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
// 省略其他
}
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
看下面这段代码,第一次插入时,capacity=INIT_CACHE_SIZE为4,最大为1<<16。当 newOccupied > capacity * 3/4时,那么就扩容2倍。
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
25、objc_msgSend 为什么用汇编写 , objc_msgSend 是如何递归找到imp? 分值10分
汇编响应快,效率高。
objc_msgSend方法查找有两个流程。快速查找,慢查找。快查找通过hash方式,先根据sel和capacity-1计算出hash值,然后去数组buckets中查找对应下标的bucket_t是否为目标方法。如果不是,那么通过cache_next方法计算下一个hash值,当数组第一个bucket_t也不是时,那么就跳到数组最后一位,继续查找。
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
慢方法查找是通过类的继承关系,以二分法或者遍历查找类的方法列表,知道命中
26、一个类的类方法没有实现为什么可以调用 NSObject 同名对象方法 分值10分
假设我有一个类Person。
@interface Person : NSObject
+ (BOOL)isKindOfClass:(Class)aClass;
@end
@implementation Person
@end
当我们调用 [Person isKindOfClass:xxx]时,底层会有如下调用。先获取person类的meta class,找不到,继续往meta class的负累查找,最终找到根类就是 NSObject类,而 NSObject 类中是有同名方法的,所以可以调用到 NSObject的同名方法。
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
转载自:https://juejin.cn/post/7007359273692823566