interview-Runtime

400 阅读9分钟

对象,类,元类关系图

一个 NSObject 对象占用多少内存空间?

NSObject 对象都会分配 16byte:实际上是 8 字节,但是会因为字节对齐的原因返回16字节的倍数

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
    //字节对齐
    return word_align(unalignedInstanceSize());
}

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
这个方法中,判断size是在字节对齐后是否小于16,如果小于16,就返回16 

字节对齐

即字节大小肯定为某个字节的倍数:以8字节对齐为例:7->8,13->16

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL //在arm64架构中是一个常数7
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif
/////////////////////////////////////////////////////
static inline uint32_t word_align(uint32_t x) {
    // 7+8 = 15
    // 0000 1111
    // 0000 1000
    //&
    // 1111 1000 ~7
    // 0000 1000 8
    
    // 0000 0111
    //
    // x + 7
    // 8
    // 8 二阶
    // 拓展 >> 3 << 3右移左移和(x + 7)功能一样
  
    return (x + WORD_MASK) & ~WORD_MASK; //这段代码的作用就是返回出去的字节大小一定是8的倍数(升对齐)
}

内存对齐

内存对齐是为了更好的节省内存空间:

//int 类型只需要占用4字节 char类型只需要占用1字节,如果进行8字节对齐,那么
// int  4 + 4 = 8
// char 1 + 7 = 8

而内存对齐后,就会将这两个数据放在同一内存段中,共用一个8字节
  • 8字节对齐----对象属性的内存空间优化;
  • 16字节对齐----对象的内存空间优化;

isa

  • isa_t 结构
union isa_t {
    isa_t() { }//初始话方法
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;//用于绑定关联类Class
    uintptr_t bits;
#if defined(ISA_BITFIELD) //结构体位域
    struct {
        ISA_BITFIELD;  // defined in isa.h,
    };
#endif
};
isa_t 是一个联合体
  • ISA_BITFIELD宏定义
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1; //表示是否对isa指针开启指针优化;0表示纯指针,1表示不仅仅是类的对象地址,还包含了类的信息,比如对象的引用计数                               \
      uintptr_t has_assoc         : 1; //是否有关联对象                                        \
      uintptr_t has_cxx_dtor      : 1; //是都有c++析构函数,有的话则需要做析构逻辑,没有的话释放对象速度更快                                        \
      uintptr_t shiftcls          : 44;// 存储类指针的值。在开启指针优化的情况下,在arm64架构中有33位用来存储类指针/*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6; //用于调试判断当前对象是真的对象还是没有初始化的空间                                        \
      uintptr_t weakly_referenced : 1; //是否被弱引用过,没有释放更快                                        \
      uintptr_t deallocating      : 1;  //对象是否正在释放                                      \
      uintptr_t has_sidetable_rc  : 1; //当对象引用计数大于10时,则需要借用改变量储存进位                                        \
      uintptr_t extra_rc          : 8//当前对象引用数据,实际上是引用计数-1;例如当对象引用计数为10时,extra_rc=9。大于10时就要用到上面的has_sidetable_rc
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)
  • 联合体 & 位域 用联合体和位运算节省空间

源码查看网址

openSource.apple.com/tarballs

clang

//通过clang命令将person.m文件编译成person.cpp文件
$ clang -rewrite-objc Person.m -o Person.cpp

方法名

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    {{(struct objc_selector *)"tureName", "@16@0:8", (void *)_I_Person_tureName},
    {(struct objc_selector *)"setTureName:", "v24@0:8@16", (void *)_I_Person_setTureName_}}
};

_I_Person_tureName : 函数名(函数指针)用于找到函数的具体实现 tureName: 方法名 @16@0:8 方法签名:

  • @:返回值类型 - id类型
  • 16:总共的量(偏移量)
  • @:参数一类型 - id类型 参数偏移范围0-7
  • : 参数二类型 - sel类型 参数偏移范围8-15

method_t

method_t是对方法\函数的封装

struct method_t {
    SEL name; //函数名
    const char *types; //编码(返回值类型、参数类型)
    IMP imp;//指向函数的指针(函数地址)
};

类的结构

NSObjectd定义

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

Class定义

typedef struct objc_class *Class;

objc_class定义

struct objc_class : objc_object {
    // Class ISA;    //8个字节
    Class superclass;//8个字节
    cache_t cache;    //16个字节 pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

    void setInfo(uint32_t set) {
        assert(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }
    ...
    ...
    ...

objc_object定义

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

上述源码总结:

  1. Class是objc_class结构体的指针(注意看*)
  2. objc_class 又是继承于objc_object的结构体
  3. objc_object结构体中只有isa 一个属性

class_rw_t

  • rw代表可读可写
  • 类的属性、方法、协议打等信息存放于class_rw_t

class_ro_t

存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

类的结构图解

isa

isa类型:

  • 纯指针,指向内存地址
  • NON_POINTER_ISA,除了内存地址,还存有一些其他信息

isa_t联合体

cache_t

cache_t结构 bucket_t结构 cache_t是结构体,里面有bucket_t散列表,每个bucket_t存储的 SEL 和 IMP 的键值对

cache散列表查找过程,在 objc-cache.mm 文件中

上面是查询散列表函数,其中cache_hash(k, m)是静态内联方法,将传入的key和mask进行&操作返回uint32_t 索引值。do-while 循环查找过程,当发生冲突 cache_next 方法将索引值减 1。所以是无序的;

  • LRU算法

LRU - (Least Recently Used):最近最少使用策略——先淘汰最近最少使用的内容,在方法缓存中也用到了这种算法

cache_t图解

cache_t

实例对象的数据结构

本质上 objc_object 的私有属性只有一个 isa 指针。指向 类对象 的内存地址。

类对象的数据结构

什么时候会报 unrecognized selector 的异常

objc 在向一个对象发送消息时,runtime 库会根据对象的 isa 指针找到该对象实际所属的类,然后在该类中 的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会 进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常 unrecognized selector sent to XXX

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量? 为什么?

不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量; 原因

  • 1.因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setvarlayout 或 class_setWeaklvarLayout 来 处理 strong weak 引用.所以不能向存在的类中添加实例变量。
  • 2.运行时创建的类是可以添加实例变量,调用 class_addIvar 函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.

objc 中向一个 nil 对象发送消息将会发生什么?

如果向一个 nil 对象发送消息,首先在寻找对象的 isa 指针时就是 0 地址返回了,所以不会出现任何错误。 也不会崩溃

Category & initialize

分类的底层结构

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

Category 在编译过后,是在什么时机与原有的类合并到一起的

    1. 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
    1. 然后会 map_images。
    1. 接下来调用 map_images_nolock。
    1. 再然后就是read_images,这个方法会读取所有的类的相关信息。
    1. 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。
    1. 在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体 中。
  • 在运行时,新添加的方法,都被以倒序插入到原有方法列 表的最前面,所以不同的 Category,添加了同一个方法,执行的实际上是最后一个。

分类方法加载顺序

  • load方法
  1. 主类的load方法会先于分类load方法
  2. 父类的load方法会先于子类的load方法
  • 非load方法(子类/父类/分类)
  1. 主类和分类拥有相同方法时,分类会被调用,因为每个分类的方法会通过attachlist方法以数组的形式插入到二维数组的前面,所以会造成分类覆盖主类的假象。
  2. 如果子类也拥有相同的方法,会覆盖主类和分类的方法。

load方法加载时机

load方法在程序启动装载类信息的时候就会调用

load_images(const char *path __unused, const struct mach_header *mh) {
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        //准备非懒加载类load方法,将其放在一个数组中
        prepare_load_methods((const headerType *)mh);
    }
   //遍历加载load方法
    call_load_methods();
}

如何修改分类load方法顺序

target - Bulid Phases - Complies Sources,shuoebi 就是调整编译顺序

initialize在什么时候调用

  1. 晚于load方法调用
  2. +initialize方法通过消息机制调用(objc_msgSend),即类第一次接收到消息的时候调用。
  3. 每一个类只会initialize一次(父类的initialize方法可能会被调用多次),当子类没有实现+initialize方法时,会调用父类的initialize方法
  4. 先调用父类的+initialize方法,后调用子类的+initialize方法
  5. 如果一个类有分类,那么会调用最后编译的分类实现的。即:如果分类实现了+initialize,就覆盖类本身的+initialize调用。

消息发送和转发

消息发送

_objc_msgSend函数的实现

上图核心 _class_lookupMethodAndLoadCache3函数 _class_lookupMethodAndLoadCache3 实现(下图)

如果消息发送阶段没有找到方法,就会进入消息转发机制

消息转发

处理消息的一个机制:防止程序崩溃

OC的运行时在程序崩溃前提供了三次拯救程序的机会。当向someObject发送某消息,但runtime sys在本类和其父类都找不到对应的方法时runtime并不会马上报错:

  1. 动态方法解析:向当前类发送resolveInstanceMethod: 检查是否动态添加方法
  2. 备援接收者:检查该类是都实现了 forwardingtargetForSelector:方法 看看能不能把选择子转发给其他接收者处理
  3. 完整的消息转发 nsinvocation :runtime发送methodSignatureForSelector消息获取Selector对应的方法签名。返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出。

isKindOfClass、isMemberOfClass

BOOL re1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [[Person class] isKindOfClass:[Person class]];
BOOL re4 = [[Person class] isMemberOfClass:[Person class]];
NSLog(@"\nre1:%hhd\nre2:%hhd\nre3:%hhd\nre4:%hhd",re1,re2,re3,re4);
    
BOOL re5 = [[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [[Person alloc] isKindOfClass:[Person class]];
BOOL re8 = [[Person alloc] isMemberOfClass:[Person class]];
NSLog(@"\nre5:%hhd\nre6:%hhd\nre7:%hhd\nre8:%hhd",re5,re6,re7,re8);

2020-02-11 15:44:25.843470+0800 Test[44114:837672] 
re1:1
re2:0
re3:0
re4:0
2020-02-11 15:44:25.844011+0800 Test[44114:837672] 
re5:1
re6:1
re7:1
re8:1

解析:

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

####
object_getClass((id)self)
这个方法中:如果self是对象,那返回类,如果self是类对象,那返回元类

1-4是类方法,5-8是对象方法

method swizzling 方法交换

改变selector和imp的映射关系

  • 利用 method_exchangeImplementations 交换两个方法的实现
  • 利用 class_replaceMethod 替换方法的实现
  • 利用 method_setImplementation 来直接设置某个方法的 IMP

使用注意点

  • 使用dispatch_once_t 防止他人手动有调用+ (void)load,导致方法又被交换回来了
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [RuntimeTool best_MethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(studentInstanceMethod)];
    });
}
  • 交换之前判断原始方法有没有实现,如果没有那就添加一个空的block实现,你可以在block中,进行bug上传
  • 如果交换的方法是自己已经有的,那么就需要替换(更新)方法
  • 防止子类没有实现,而父类实现。相当于交换了父类的方法,交换后就会崩溃,因为父类没有实现被交换的方法。所以要给本类先添加class_addMethod(),添加如果成功,那就class_replaceMethod();如果添加失败,就证明存在方法,那就进行正常的交换
+ (void)best_MethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {//判断原始方法有没有实现,如果没有那就添加一个空的block实现,你可以在block中,进行bug上传
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            //这里面进行bug上传
        }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod
    //防止子类没有实现,而父类实现,相当于交换了父类的方法
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}