runtime相关面试题

一、runtime的内存模型

对象、类、元类之间的关系
1、对象
struct objc_object {
  Class_Nonnull  isa OBJC_ISA_AVAILABILITY;  //指向对象所属的类
}

当创建一个实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。
我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。C语言中void *为“不确定类型的指针”,void *可以用来申明指针 。

2、类对象
//类
struct objc_class: objc_object {
    Class superclass; //父类
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

class_data_bits_t bits 由它可以得到 class_rw_t,class_rw_t中存储了 方法列表,属性列表,协议列表, class_ro_t等相关信息。class_ro_t中存放了 成员变量列表。
class_ro_t 中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。

3、元类

类对象的isa指针指向了元类,super_class指针指向了父类的类对象,而元类的super_class指针指向了父类的元类。
元类中保存了创建类对象以及类方法所需的所有信息,而类对象中保存的是实例方法。

4、Method
struct objc_method {
    SEL _Nonnull method_name; // 方法名
    char * _Nullable method_types; // 方法类型
    IMP _Nonnull method_imp; // 方法实现
} 

在这个结构体中,我们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。

5、SEL(objc_selector)
typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL。

6、IMP
typedef id (*IMP)(id, SEL, ...);

指向最终实现程序的内存地址的指针。
Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现

question:runtime 如何通过 selector 找到对应的 IMP 地址
answer:每一个类对象中都有一个方法列表,方法列表中记录着方法的名称、方 法实现以及参数类型,selector本质就是方法名称,通过方法名称就可以在方法列表中找到对应的方法实现。

7、类缓存(objc_cache)

[cache 原理分析]https://www.jianshu.com/p/e5f99ed3d9f9
为了加速消息的分发,系统会对方法和对应的地址进行缓存,当我们调用一个方法时,会先从缓存中进行查找。
cache_t 是增量扩展的哈希表结构。哈希表内部存储的是bucket_t。
bucket_t 中存储的是 SEL 和 IMP 的键值对。

struct cache_t {
    struct bucket_t *_buckets;  //散列表
    mask_t _mask;  //散列表的长度 -1
    mask_t _occupied;  //已经缓存的方法数量,散列表的长度使大于已经缓存的数量的
    //...
}

struct bucket_t {
    cache_key_t _key;  //SEL 作为key @selector()
    IMP _imp;  //  函数的内存地址
    // ...
}
8、isa

现在的64位系统(arm64架构)中,苹果对isa做了优化,里面除了存储一个地之外还存储了很多其他信息。一个指针占8个字节,也就是64位,苹果只用了其中的33位来存储地址,其余31位用来存储其他信息。
isa结构源码如下,是一个联合体+位域的结构:
联合体union内部成员为互斥存在,即联合体所占内存大小决定于内部最大成员所占大小。结构体struct则是“有容乃大”

union isa_t {
    isa_t() { } // 构造方法
    isa_t(uintptr_t value) : bits(value) { } // 构造方法
    uintptr_t bits;
private:
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // 此宏为isa内部位域的分配情况,源码在下面
    };
    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif
    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};
# if __arm64__                                                                                                    
        uintptr_t nonpointer        : 1;  //(isa的第0位)表示是否对isa指针开启指针优化。0:纯isa指针,1:优化过的isa                                   
        uintptr_t has_assoc         : 1;  //(isa的第1位)记录这个对象是否是关联对象                                       
        uintptr_t has_cxx_dtor      : 1;  //(isa的第2位)记录是否有C++的析构函数                                       
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/    //(isa的第3-35位,共占33位)记录对象的地址值
        uintptr_t magic             : 6;  //(isa的第36-41位,共占6位)用于在调试时分辨对象是否完成初始化                                       
        uintptr_t weakly_referenced : 1;  //(isa的第42位)用于记录对象是否被弱引用或曾经弱引用过                                       
        uintptr_t unused            : 1;  //(isa的第43位)标志对象是否正在释放内存                                       
        uintptr_t has_sidetable_rc  : 1;  //(isa的第44位)用于标记是否有扩展的引用计数,当一个对象的引用计数比较少时,其引用计数就记录在isa中,当引用计数大于摸个值时就会采用sideTable来协助存储引用计数。                                       
        uintptr_t extra_rc          : 19  //(isa的第45-63位,共占19位)用来记录该对象的引用计数值-1.这里总共是19位,如果引用计数很大,19位存不下的话就会采用sideTable来协助存储。
# elif __x86_64__                      
        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 unused            : 1;                                         
        uintptr_t has_sidetable_rc  : 1;                                         
        uintptr_t extra_rc          : 8
9、Category
typedef 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;//  添加的所有属性
} category_t;

category 被添加在了 class_rw_t 的对应结构里。
category 实际上是 category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的category,添加了同一个方法,执行的实际上是最后编译的。
category 在刚刚编译完的时候和原来的类是分开的,只有在程序运行起来后,通过runtime,category和原来的类才会合并到一起。

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

一个NSObject对象都会分配16byte的内存空间。
但是实际上在64位下只使用了8byte,在32位下,只使用了4byte
一个NSObject实例对象成员变量所占的大小,实际上是8字节

Class_getInstanceSize([NSObject Class])

获取obj_C指针所指向的内存大小,实际上是16字节

#import 
malloc_size((__bridge const void *)obj);

对象在分配内存空间时,会进行内存对齐,所以在iOS中,分配内存空间都是16字节的倍数。

三、为什么要设计么他metaclass

1.简化了实例方法和类方法的调用流程,提高了消息发送的效率

四、class_copyIvarList 和 class_copyPropertyList区别

class_copyPropertyList:只能获取由property 声明的属性,包括.m中的,获取的属性名称不带下划线。
class_copyIvarList :能够获取.h和.m中的所有属性以及大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)

五、class_ro_t 与class_rw_t 的区别

class_rw_t:代表的是可读写的内存区,这块区域中存储的数据是可以更改的。
class_ro_t:代表的是只读的内存区,这块区域中存储的数据是不可以更改的。

OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过runtime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表。

六、当方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa指针找到这个对象的类对象,从类对象中的cache中查找是否存在SEL对应的IMP,若不存在,则会在 method_list中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发。

七、class、object_getclass、objc_getClass 这些方法有什么区别?

1、class 方法
class方法无论是类对象还是实例对象都可以调用,可以嵌套,返回永远是自身的类对象。

// 类方法,返回自身
+ (Class)class {
    return self;
}
 
// 实例方法,查找isa(类)
- (Class)class {
    return object_getClass(self);
}
 
// 类方法,返回自身
+ (Class)class {
    return self;
}
 
// 实例方法,查找isa(类)
- (Class)class {
    return object_getClass(self);
}

2、object_getClass 方法
object_getClass(id _Nullable obj) ;用于获取一个objc对象的isa指针指向的对象(即平时我们所说的类)
(1)传入参数:obj可能是instance实例对象、class类对象、meta-class元类对象
(2)返回值:
【1】如果是instance实例对象,返回class对象
【2】如果是class类对象,返回meta-class对象
【3】如果是meta-class元类对象,返回NSObject(基类)的meta-class对象

/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
/** 
 * Returns the class of an object.
 * 
 * @param obj The object you want to inspect.
 * 
 * @return The class object of which \e object is an instance, 
 *  or \c Nil if \e object is \c nil.
 */
OBJC_EXPORT Class _Nullable
object_getClass(id _Nullable obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

3、objc_getClass方法
objc_getClass(const char * _Nonnull name)
(1)传入参数:字符串类名
(2)返回值:对应的类对象

/* Obtaining Class Definitions */
 
/** 
 * Returns the class definition of a specified class.
 * 
 * @param name The name of the class to look up.
 * 
 * @return The Class object for the named class, or \c nil
 *  if the class is not registered with the Objective-C runtime.
 * 
 * @note \c objc_getClass is different from \c objc_lookUpClass in that if the class
 *  is not registered, \c objc_getClass calls the class handler callback and then checks
 *  a second time to see whether the class is registered. \c objc_lookUpClass does 
 *  not call the class handler callback.
 * 
 * @warning Earlier implementations of this function (prior to OS X v10.0)
 *  terminate the program if the class does not exist.
 */

objc_getClass 无法嵌套,因为参数 是 char 类型,效果和 class 相同(因为不能嵌套,所以和class可以认为是相同的)

八、iOS 中内省的几个方法有哪些?内部实现原理是什么?

实现内省的方法包括:

  • isKindOfClass:Class
  • isMemberOfClass:Class
  • respondToSelector:selector
  • conformsToProtocol:protocol
+ (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;
}

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

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
  • isKindOfClass:(Class)cls方法内部,会先去获得 object_getClass 的类,object_getClass的源码实现去调用当前类的 obj -> getIsa().
  • 接着isKindOfClass中有一个循环,先判断class 是否等于 object_getClass返回的class,不等就继续循环判断是否等于 super class,不等的话再取 super class,如此循环下去。

isMemberOfClass 的源码实现是拿到自己的 isa指针和自己比较,是否相等。

九、load、initialize方法的有什么区别,继承关系中他们有什么区别

+load方法是在加载类和分类时系统调用。
每个类、分类的+load,在程序运行过程中只调用一次
调用顺序:
类要优先于分类调用+load方法。
子类调用+load方法时,要先调用父类的+load方法。
不同的类按照编译向后顺序调用+load方法(先编译,先调用)
分类的按照编译先后顺序调用+load方法(先编译,先调用)

Runtime 会调用 prepare_load_methods 方法准备好被调用的+load 方法,

其中 schedule_class_load(cls -> superclass) // 在调度类的 load 方法前, 要先调用父类的 load 方法(递归),决定了父类优先于子类调用

当prepare_load_methods 函数执行完之后,所有满足 +load 方法调用条件的类和分类就分别保存在全局变量中;
类保存 +load 方法 call_class_loads()
分类保存 +load 方法 call_category_loads()

+ load方法是系统根据方法地址直接调用,并不是 objc_msgSend函数调用
如果子类没有实现 +load 方法,当它被加载时runtime 是不会调用父类的 +load方法,除非父类也实现了 +load方法

initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次

  • 调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕
  • 子类和父类同时实现initialize,父类的先被调用,然后调用子类的
  • 本类与category同时实现initialize,category会覆盖本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)
  • initialize是主动调用的,只有当类第一次被用到的时候才会触发

十、Category 在编译过后,是在什么实际与原有的类合并到一起的 ?

image.png

十一、关联对象的应用,系统是如何实现关联对象的 | 关联对象的如何进行内存管理的以及关联对象如何实现weak属性?

关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象。

实现关联对象技术的核心对象有
1.AssociationsManager
2.AssociationsHashMap
3.ObjectAssociationMap
4.ObjcAssociation

所有的关联对象都由AssociationsManager管理,AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的k-v对。

一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。

美团技术团队参考文章
关联对象实现原理

十二、week的实现原理

Runtime 维护了一个哈希表,其key 为对象在内存的地址, value 是指向该对象的week 指针的地址的数组,所以当week 指针指向一个对象时,会从哈希表中查找该对象并在其 value数组末插入该指针,当该对象销毁时,会遍历该对象的value数组值,将其中指针全指向nil。

初始化时: runtime 会调用objc_initweak函数,初始化一个新的weak 指针指向对象的地址。
添加引用时: objc_initweak 函数会调用 objc_storeweak() 函数,objc_storeweak() 的作用是更新指针指向,创建对应的弱引用表。
释放时: 调用clearDeallocating 函数。clearDeallocating函数首先会根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为nil ,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

Objc_initweak 函数有一个前提条件: object 必须是一个没有被注册为 _weak 对象的有效指针。

你可能感兴趣的:(runtime相关面试题)