一、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 在编译过后,是在什么实际与原有的类合并到一起的 ?
十一、关联对象的应用,系统是如何实现关联对象的 | 关联对象的如何进行内存管理的以及关联对象如何实现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 对象的有效指针。