likes
comments
collection
share

【一套iOS底层试卷-我想和你分享】答案详解

作者站长头像
站长
· 阅读数 32

一、选择题(每题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;

}

【一套iOS底层试卷-我想和你分享】答案详解

这题其实是考察对象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分

【一套iOS底层试卷-我想和你分享】答案详解

这个值怎么理解?拿出计算器将值输入,看二进制部分,可以看到底三位为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
评论
请登录