iOS 类原理探索:类的结构分析
OC 类原理探索 系列文章
- OC 类原理探索:类的结构分析
- OC 类原理探索:类结构分析补充
- OC 类原理探索:属性的底层原理
前言
上一篇 OC 对象原理探索(三):对象的本质 & isa,介绍了isa
的结构,关联到了类,这篇文章主要对类的结构进行分析。
一、isa 分析到元类
先通过lldb
调试进行一波探索:
如上图:通过isa & isa的掩码
,最终得到了SSLPerson
,但是类中的isa & isa的掩码
也得到了SSLPerson
这是为什么呢?
是不是内存中存了很多个SSLPerson
类对象呢,下面我们来验证一下:
Class class1 = [SSLPerson class];
Class class2 = [SSLPerson alloc].class;
Class class3 = object_getClass([SSLPerson alloc]);
Class class4 = [SSLPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p-",class1,class2,class3,class4);
打印结果:
0x1000081f0-
0x1000081f0-
0x1000081f0-
0x1000081f0-
打印的地址都是相同的,可以判断SSLPerson
类对象在内存中只有一个。
我们用MachOView
打开本项目的MachO
文件 ->Symbol Table
->*Symbols
-> 搜索框输入class
-> 向下滑:
-
__OBJC_CLASS_RO_$_SSLPerson
是我们创建的SSLPerson
类。 -
__OBJC_METACLASS_RO_$_SSLPerson
并不是我们创建的,它是系统创建的SSLPerson
的METACLASS
(元类),类对象的isa
指针就指向了元类,那么元类的isa
指针又指向哪儿里呢。
二、isa 走位图和继承链
1. isa 的走位链
看SSLPerson
走位打印情况:
// SSLPerson 实例对象
SSLPerson *person = [SSLPerson alloc];
// SSLPerson 类对象
Class class = object_getClass(person);
// SSLPerson 元类
Class metaClass = object_getClass(class);
// SSLPerson 根元类
Class rootMetaClass = object_getClass(metaClass);
// SSLPerson 根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n实例对象:%p \n类:%p \n元类:%p \n根元类:%p \n根根元类:%p",person,class,metaClass,rootMetaClass,rootRootMetaClass);
打印结果:
实例对象:0x101044470
类:0x1000081f0
元类:0x1000081c8
根元类:0x7fff80843fe0
根根元类:0x7fff80843fe0
看打印结果,根元类
和根根元类
的地址都是0x7fff80843fe0
。可以得出SSLPerson
的isa
指向关系:实例isa
-> 类 isa
-> 元类 isa
-> 根元类 isa
-> 根元类
。
看NSObject
走位打印情况:
// NSObject 实例对象
NSObject *object = [NSObject alloc];
// NSObject 类
Class class = object_getClass(object);
// NSObject 元类
Class metaClass = object_getClass(class);
// NSObject 根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject 根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n实例对象:%p \n类:%p \n元类:%p \n根元类:%p \n根根元类:%p ",object,class,metaClass,rootMetaClass,rootRootMetaClass);
打印结果:
实例对象:0x100562780
类:0x7fff80844008
元类:0x7fff80843fe0
根元类:0x7fff80843fe0
根根元类:0x7fff80843fe0
看打印结果,元类
、根元类
、根根元类
的地址都是0x7fff80843fe0
。可以得出NSObject
的isa
指向关系:实例isa
-> 类 isa
-> 根元类 isa
-> 根元类
。可以发现NSObject
跟普通类有所不同,类
的isa
指针直接指向了根元类
。
根据上面的分析可以得到isa
的走位图:
2. 继承链
看下面的打印结果:
// SSLStudent 元类
Class tMetaClass = object_getClass(SSLStudent.class);
// SSLStudent 元类的父类
Class tSuperClass = class_getSuperclass(tMetaClass);
NSLog(@"SSLStudent 元类:%p",tMetaClass);
NSLog(@"SSLStudent 元类的父类:%p",tSuperClass);
// LGPerson 元类
Class pMetaClass = object_getClass(SSLPerson.class);
// LGPerson 元类的父类
Class pSuperClass = class_getSuperclass(pMetaClass);
NSLog(@"SSLPerson 元类:%p",pMetaClass);
NSLog(@"SSLPerson 元类的父类:%p",pSuperClass);
// NSObject 元类
Class nMetaClass = object_getClass(NSObject.class);
// NSObject 元类的父类
Class nSuperClass = class_getSuperclass(nMetaClass);
NSLog(@"NSObject 元类:%p",nMetaClass);
NSLog(@"NSObject 元类的父类:%p",nSuperClass);
// NSObject 父类
Class superClass = class_getSuperclass(NSObject.class);
NSLog(@"NSObject :%p",NSObject.class);
NSLog(@"NSObject 父类:%p",superClass);
打印结果:
SSLStudent 元类:0x100008178
SSLStudent 元类的父类:0x1000081c8
SSLPerson 元类:0x1000081c8
SSLPerson 元类的父类:0x7fff80843fe0
NSObject 元类:0x7fff80843fe0
NSObject 元类的父类:0x7fff80844008
NSObject :0x7fff80844008
NSObject 父类:0x0
-
SSLStudent 元类的父类
和SSLPerson 元类
的地址都是0x1000081c8
; -
SSLPerson 元类的父类
和NSObject 元类
的地址都是0x7fff80843fe0
; -
NSObject 元类的父类
和NSObject
的地址都是0x7fff80844008
; -
NSObject 父类
是空。
因此可以得到类的继承链:
image.png3.官方走位继承图
isa流程图.png三、源码分析类的结构
我们打开源码(objc4-818
版本)搜索struct objc_class
查看类结构。
1. 废弃的类结构
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在网上我们也会经常看到这段代码,但是我们可以看到OBJC2_UNAVAILABLE
,说明这个版本在2.0
中已经废弃了,我们就不用再看这个版本。
2. 源码类结构
现在用的类结构版本在objc-runtime-new.h
中,定义如下:
struct objc_class : objc_object {
xxx...
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
xxx...
}
可以看到类是一个objc_class
类型的结构体,继承于objc_object
,我们之前文章已经了解到objc_object
结构体中有个isa_t
类型的isa
成员变量。
objc_class
中的成员变量:
-
isa
:隐藏变量,继承于objc_object
; -
superclass
:类指针,指向父类; -
cache
:缓存; -
bits
:内存数据。
四、内存偏移(知识补充)
// 基本类型
int a = 10;
// 对象类型
SSLPerson *person = [SSLPerson alloc];
NSLog(@"%p -- %d",&a,a);
NSLog(@"%p -- %@",&person,person);
打印结果:
0x7ffeefbff3ac -- 10
0x7ffeefbff3a0 -- <SSLPerson: 0x100728a20>
a
指针指向10
的地址,person
指针指向[SSLPerson alloc]
开辟的内存地址,同时&person
指针又指向了person
指针的地址。
如图:
image.png// 数组指针
int c[4] = {1,2,3,4};
int *d = c;
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
for (int i = 0; i<4; i++) {
int value = *(d+i);
NSLog(@"%d",value);
}
打印结果:
0x7ffeefbff3a0 - 0x7ffeefbff3a0 - 0x7ffeefbff3a4
0x7ffeefbff3a0 - 0x7ffeefbff3a4 - 0x7ffeefbff3a8
1
2
3
4
-
d
、c
、c[0]
都指向了数组的首地址0x7ffeefbff3a0
; - 对地址进行
加1
操作就是地址偏移了sizeof(int)
的大小,得到下一个元素的地址0x7ffeefbff3a4
、0x7ffeefbff3a8
、0x7ffeefbff3ac
; - 通过
*(指针)
操作得到地址所存储的值。
五、类结构的内存计算
1. 计算 bits 偏移大小
上面我们知道类结构中有isa
、superclass
、cache
、bits
四个成员变量。isa
占8
个字节,superclass
是结构体指针也占8
个字节,下面来看下cache
占多少个字节。
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask;// 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;// 指针 8
};
}
-
cache_t
的大小为_bucketsAndMaybeMask
和union
的和。 - 点进
explicit_atomic<uintptr_t>
中可以看到初始化和方法都是范型
,所以_bucketsAndMaybeMask
的大小就是<uintptr_t>
的大小为8
字节; - 也可得出
union
的大小为8
字节,整个cache_t
结构体的大小就是16
字节。
所以得到,从类的首地址偏移到bits
需要8 + 8 + 16 = 32
个字节也就是0x20
。
2. 通过偏移获取 bits
用lldb
进行调试获取bits
中class_rw_t
的值:
3. bts 中 class_rw_t 结构分析
接下来我们要探究class_rw_t
中方法
和属性
的存储,打开源码,看一下class_rw_t
中有些什么:
struct class_rw_t {
xxx...
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
}
-
methods()
:获取方法函数,返回值为method_array_t
类; -
properties()
:获取属性,返回值为property_array_t
类;
看下method_array_t
和property_array_t
的定义:
class method_array_t :
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
xxx...
};
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
xxx...
};
template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
xxx...
}
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
struct property_t {
const char *name;
const char *attributes;
};
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
xxx...
}
struct method_t {
xxx...
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
xxx...
}
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
xxx...
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
xxx...
}
-
property_array_t
和method_array_t
继承自list_array_tt
; -
list_array_tt
中List
对应property_list_t
或method_list_t
; -
property_list_t
和method_list_t
继承自entsize_list_tt
; -
entsize_list_tt
中有get()
方法可以获取列表中元素; -
property_t
中有name
和attributes
成员变量; -
method_t
中有big
结构体,结构体中有name
、types
、imp
成员变量。
4. lldb 查看属性列表:property_list_t
image.pngproperties()
函数,最终得到了属性name
、age
,并没有获取到_hoby
成员变量;
5. lldb 查看方法列表:method_list_t
image.pngmethods()
函数,最终得到了方法name
、setName
、age
、setAge
、eat
,并没有获取到+ (void)run
;
6. lldb 查找类方法
类方法存储在元类中,lldb
验证:
7. 成员变量结构分析
image.png- 最终找到
ivar_list_t * ivars
变量; -
ivar_list_t
和method_list_t
、property_list_t
一样都继承自entsize_list_tt
。