类结构体模型
// objc_object对象模型
struct objc_object {
private:
isa_t isa;
public:
Class ISA();
Class getIsa();
// 省略其余方法
...
}
// objc_class类模型
struct objc_class : objc_object {
Class isa //所属类的指针
Class super_class//指向父类的指针
const char *name //类名
long version // 版本
long info //供运行期使用的一些位标识。
long instance_size //实例大小
struct objc_ivar_list *ivars//成员变量数组
struct objc_method_list **methodLists//方法列表
struct objc_cache *cache//指向最近使用的方法.用于方法调用的优化
struct objc_protocol_list *protocols//协议的数组
}
在objc_class结构体中定义了对象的method list,
protocal,ivar list等,表示对象的行为。
为什么说类也是对象
类被定义为objc_class结构体,objc_class结构体继承自
objc_objct,对象是用 objc_object 结构体表示的,所以类也
是对象。
oc中的类和元类也是一样,都是结构体构成的,由于类的结构体定义
继承自objc_object所以其也是一个对象,并且具有对象的isa特
征。
在应用程序中,类对象只会被创建一份。
isa指针指向Meta Class,因为Objc的类的本身也是一个
Object,为了处理这个关系,runtime就创造了Meta Class,
当给类发送[NSObject alloc]这样消息时,实际上是把这个
消息发给了Class Object
因为一个类是一个对象,它是元类(metaclass)的对象。
元类是关于类对象的描述,
就像类是普通实例对象的描述一样。实际上,元类的方法列表正
是#类方法#:该类对象响应的选择器。
什么是isa指针
每个对象都有一个标识对象类的isa实例变量。
运行时使用此指针来确定对象需要时的实际类。
1、 id类型是一个objc_object结构体的指针。
2、objc_object结构体包含一个Class 类型的变量isa。
3、 Class是objc_class结构体的指针。
id
objc_msgSend第一个参数类型为id,大家对它都不陌生,
它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以
顺藤摸瓜找到对象所属的类。
为什么id类型可以指向OC中任意对象
id类型被定义为指向对象的指针
NSObject只有一个Class对象isa,而objc_object也是只有一个Class对
象isa,也就是说id等价于NSObject*。所以id是一个一个比较灵活的对象指
针,并且是一个指向任何一个继承了Object(或者NSObject)类的对象
为什么不能用isa判断一个类的继承关系
isa指针不总是指向实例对象所属的类,不能依靠它来确定
类型,而是应该用class方法来确定实例对象的类。
因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类
类结构关系
实例对象在运行时被表示成objc_object类型结构体,结构体内部
有个isa指针指向objc_class结构体。objc_class内部保存了类
的变量和方法列表以及其他一些信息,并且还有一个isa指针。这个
isa指针会指向meteClass(元类),元类里保存了这个类的类方法
列表。
runtime中设计了meta class, 通过meta class来创建类对
象,所以类对象的isa指向对应的meta class。而meta class也
是一个对象,所有元类的isa都指向其根元类,根元类的isa指针指
向自己,通过这种设计,isa的整体结构形成了一个闭环。为了完整
性,其实元类里也有一个isa指针,这个isa指针,指向的是根元
类,根元类的isa指针指向自己
实例对象--(runtime)-->objc_object--(isa)--
>objc_class--(isa)-->元类--isa-->根元类--isa-->自
己。
类和元类都有自己的继承体系,但它们都有共同的根父类
NSObject,而NSObject的父类指向nil。Root
Class(Class)是NSObject类对象,而Root Class(Meta)
是NSObject的元类对象。对于像NSObject这样的类来说,它其实
代表的是一个类对象,本质上还是一个普通的实例对象,类对象的、
self指针应该指向的是这个类对象自身。
继承实现
当我们调用某个类的方法时,如果这个类的方法列表里没有该方法,
则会去找这个类的父类的方法列表。这种机制就是通过
objc_class的第二个变量super_class指针实现的。
并且这种继承关系会扩展到元类。
内存布局:
类的本质时结构体,在结构体中包含一些成员变量,
如method list,ivar list等,这些都是结构体的一部分,
method protocol,property的实现这些都可以放到类中,
所有对象调用同一份即可,但对象的成员变量不可以放在一起,
因为每个对象的成员变量值都是不同的。
为什么不能添加变量
创建实例对象时,会根据其对应的Class分配内存,内存构成
是ivar+isa_t.并且实例变量不只是包含当前class的ivars,
也会包含其继承链的ivars,ivars的内存布局在编译时就已经
决定,运行时需要根据ivars内存布局创建对象,所以runtime
不能动态修改ivars,会破坏已有的内存布局。
不能向编译后得到的类中增加实例变量
因为编译后的类已经注册在runtime中,类结构体中的
objc_ivar_list实例变量的链表和instance_size
实例变量的内存大小已经确定,同时runtime会调用
class_setIvarLayout或class_WeakIvarLayout
来处理strong weak引用,所以不能向存在的类中添加实例变量
运行时创建的类时可以添加实例变量,调用class_addIvar函数
,但是得在调用objc_allocateClassPari之后,
objc_registerClassPair之前,原因同上。
系统如何解决新增实例冲突
类的结构体在编译时都是固定的,如果想修改类的结构需要重新编译
原来UIViewController的结构体中增加了
childViewControllers属性,这个时候和子类的内存偏移就
发生了冲突,只不过,runtime有检测内存地址冲突的机制,
在类生成实例变量时,会判断实例变量是否有地址冲突,
如果发生冲突则调整对象的地址偏移。
ivars methodLists
在objc_class结构体中:ivars是objc_ivar_list指针;
methodLists是指向objc_method_list指针的指针。
也就是说可以动态修改 *methodLists 的值来添加成员方法,
这也是Category实现的原理.
IMP:
//结构
typedef void (*IMP)(void /* id, SEL, ... */ );
在runtime中IMP本质上就是一个函数指针
在IMP中有两个默认的参数id和SEL,id也就是方法的self
这和objc_msgSend()函数传递的参数一样;
Runtime提供很多对于IMP的操作API,我们很多Runtime函数都
是IMP操作
Method:
//结构
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
};
用来表示方法,其中包含SEL和IMP
Property:
在runtime中定义了属性的结构体,用来表示对象中定义的属性。
@property修饰符用来修饰属性,修饰后的属性为
objc_property_t类型,其本质是property_t结构体
struct property_t {
const char *name;
const char *attributes;
};
ivar读写
实例变量的isa_t指针会指向其所属的类,对象中并不会包含
method,property,protocol,ivar等信息,这些信息在编
译时都保存在只读结构体class_ro_t中。在class_ro_t中ivar
时const只读的,在image load时copy到class_rw_t中时,
是不会copy ivars的,并且class_rw_t中并没有定义ivars
的字段。
在防伪某个成员变量时,直接通过isa_t找到对应的objc_class
并通过其class_ro_t的ivar list做地址偏移,查找对应的对象
内存,正是由于这种方式,所以对象的内存地址是固定不可改变的。
方法传参:
当调用实例变量的方法时,会通过objc_msgSend()发起调用。
调用是会传入self和SEL,函数内部通过isa在类的内部查找方法
列表对应的IMP,传入对应的参数并发起调用,如果调用的方法涉
及到当前对象的成员变量的访问,这是就是通过objc_msgSend()
内部,通过类的ivar list判断地址偏移,取出ivar并传入调
用的IMP中的。
调用super的方式时则调用objc_msgSendSuper()函数实现
isa_t:
对于对象指针也是一样,在oc1.0时,isa时一个真的指针,
指向一堆区的地址,而在oc2.0时代,一个指针长度时八个字节
也就是64位,在64位中直接存储着对象的信息。当查找对象所属
的类时,直接在isa指针中进行位运算即可,而且由于在栈区进
行操作,查找速度非常快。
例如,isa_t本质时一个结构体,如果创建结构体再用指针指向这
个结构体,内存占用是很大的,但是Tagged Pointer特性中,
直接把结构体的值都存储到指针中,这样就相当节省内存了。
苹果不允许直接访问isa指针,和Tagged Pointer也是有关
系的,因为tagged Pointer的情况下,isa并不是一个指针指
向另一块内存区,而是直接表示对象的值,所以通过直接访问
isa获取到的信息是错误的。
SEL
它是selector在Objc中的表示类型(Swift中是
Selector类)。selector是方法选择器,
可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
从源码中看其实是一个const *的常量字符串,只代表名字而已
其实它就是个映射到方法的C字符串,你可以用 Objc
编译器命令@selector()或者 Runtime 系统的
sel_registerName函数来获得一个SEL类型的方法选择器。
常见来两个相同的类,并定规两个相同的方法,通过@selector
获取SEL并打印,我们发现SEL都是同一个对象,地址都是相同的,
不同类的相同SEL是同一个对象。
在runtime中维护一个SEL列表,这个表不按照类存储,
只要相同SEL就算作一个,并存储到表中。
Protocol
协议存储在protocol_t结构体中,而protocol_t继承objc_object,所以也就具备对象特征
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
};
既然具备了对象的特征,那么也有isa指针,在Protocol中所有的isa都指向同一个类Protocol。Protocol类并没有元类
runtime如何实现weak变量的自动置nil
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表
中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时
候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键,
在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil
map_images 函数Runtime 初始化操作
在runtime中所有类都存在一个哈希表中,在table的buckets中存储
1. 加载所有类到类的gdb_objc_realized_classes表中
2. 对所有类做重映射
3. 将所有SEL都注册到namedSelectors表中
4. 修复函数指针遗留
5. 将所有Protocol添加到protocol_map表中
6. 将所有Protocol重映射
7. 初始化所有非懒加载的类,进行rw,ro操作
8. 便利所有懒加载类,执行初始化
9. 处理所有Category包括Class和MetaClass
10. 初始化所有未初始化类
load方法
Load方法是在类加载的时候调用的,是在类加载到运行时的时候调用的,
在mian函数之前,由系统调用,所以比较适合在load中调用钩子方法。只调用
一次,注意load方法是直接调用的,并没有走运行时的objc_msgSend函数
load方法顺序应该是,父类->子类->分类
initialize
initialize是由Runtime调用的,与load不同的是,initialize方法是第
一次调用类所属方法时才会调用,如果当前类方法永远不被调用的话
initialize有可能永远不会执行。