建议先看下
IOS底层(三): alloc相关1.初探 alloc, init, new源码分析
IOS底层(八): alloc相关: isa与类关联源码分析
首先看个例子:
expXXX & 0x00007ffffffffff8ULL
这块先解释下, 之前我们讲过ISA源码
# define ISA_MASK 0x00007ffffffffff8ULL // x86_64下
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
这里 isa.bits & ISA_MASK
是返回类信息
我们分解看下上面例子
-
x/4gx test
读一下内存段, 第一个是isa
, 我们p/x一下isa & ISA_MASK
, 可看到有0x0000000100008388
, 我们接下来po
一下,po 0x0000000100008388
没问题返回自定义类SATest
(① x/4gx 读内存段, 打印内存情况, 第一个是isa指针地址
② p/xisa & ISA_MASK
是获取类信息
, 返回的是类的指针地址
, 此时类是SATest
③ po 是打印类信息 (都是lldb
调试命令)) -
既然是类信息, 我们再做一次
x/4gx
→p/x isa & ISA_MASK
→po
可看到po 0x0000000100008360(类isa指针地址)
也为SATest
-
既然是类信息, 我们再再做一次
x/4gx
→p/x isa & ISA_MASK
→po
可看到0x000000010036a140(类isa指针地址)
为NSObject
其实第二次时候就应该有疑问, 为什么还是SATest
, 而第三次是NSObject
第二次中 0x0000000100008360
是之前isa
中获取的isa
的指针地址, 即SATest类的类
, 在Apple中我们称SATest类的类
为元类
, 之后的NSObject
也称为元类
, 不过由于它源自根类
, 所以也可以成为根元类
(下面图片是新走一遍的结果, 之前不小心给关了, 打印地址可能有点差别, 原理不变)
元类探索
元类
-
首先了解一点, 对象
isa
指向的类
, 类也是一个对象( 所以有个流传, 万物皆对象:) ), 这个对象我们一般称为类对象
, 其isa
位域指向苹果定义的元类
-
元类
是系统
给的, 其定义
和创建
都是由编译器完成
,类
的归属
来自于元类
3.元类
是类对象
的类
, 每个类
都有一个独一无二的元类
用来存储类方法相关信息
4.元类
本身是没有名称的, 由于与类
相关联
, 所以使用了同类名一样的名称
由上面例子也可以得到关系
对象
→ 类
→ 元类
→ NSObject
, NSObject
元类指向自身
总结
元类走位图-
对象
的isa
是类
(也称类对象
) -
类
的isa
指向元类
-
元类
的isa
指向NSObject
(根元类
) -
NSObject
的isa指向本身
扩展个问题, 刚才看到自定义的一个类, 有元类有根元类依次循环, 那么岂不是, 创建一个类, 系统会自动帮我们创建多个NSObject?
我们可以这样验证下
Class cls1 = [SATest class];
Class cls2 = [SATest alloc].class;
Class cls3 = object_getClass([SATest alloc]);
NSLog(@"cls1: %p", cls1);
NSLog(@"cls2: %p", cls2);
NSLog(@"cls3: %p", cls3);
验证类1
可看到打印地址只有一个, 所以NSObject只有一份, 或者说NSObject(根元类)
在内存中只有一份。(类的信息在内存中永远只存在一份, 类对象只有一份
)
或者通过lldb验证也可以, 看下下面图片即可(根元类只是指向自己)
验证类2isa走势关系图
isa走势关系图(留意下虚线是isa, 实线是superclass)
-
isa走势:
实例对象(Instance)
isa指向 → Class(类)
isa 指向 → meta(元类)
isa 指向 → root (meta 根元类)
, 而root (meta 根元类)
isa 指向 → NSObject
父类实例对象也是一样走势
自定义类打印 NSObject打印如果是类NSObject, 会省去一个元类指向
-
superclass走势:
子类(SubClass)
继承 → 父类(SuperClass) →
继承 → 根类(Root Class)
根类即NSObject
无继承
子元类(meta SubClass)
继承 → 父元类(meta SuperClass) →
继承 → 根元类(meta Root Class)
继承 → NSObject
- 实例对象没有继承关系, 类才会有继承关系, NSObject继承nil
objc_class & objc_object
既然提到了类和对象, 我们看下objc_class
和objc_object
源码
1. typedef struct objc_class *Class;
2. 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;
/* Use `Class` instead of `struct objc_class *` */
// objc-runtime-new.h
3. struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// 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 getSuperclass() const {
#if __has_feature(ptrauth_calls)
// objc.h
1. struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
// objc-private.h
2. struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA(bool authenticated = false);
// rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
Class rawISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
uintptr_t isaBits() const;
// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
// initClassIsa(): class objects
// initProtocolIsa(): protocol objects
// initIsa(): other objects
void initIsa(Class cls /*nonpointer=false*/);
void initClassIsa(Class cls /*nonpointer=maybe*/);
void initProtocolIsa(Class cls /*nonpointer=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
// changeIsa() should be used to change the isa of existing objects.
// If this is a new object, use initIsa() for performance.
Class changeIsa(Class newCls);
...
首先可以看到objc_class
和objc_object
都有一个 isa
指针, 这个isa来自于objc_object
(struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };
), 且在新版objc-runtime-new.h
里面可看到objc_class
是继承于objc_object
的
总结:
-
Class
底层是objc_class
(typedef struct objc_class *Class;
结构体类型 ), 所有类都是以objc_class
为模板继承过来的。 -
objc_object
与对象的关系是继承关系
, 所有的对象都是以objc_object
为模板继承过来的, (但是真正到底层的是一个objc_object(C/C++)结构体类型) -
结构体类型
objc_class
继承自objc_object
, 其中objc_object
(结构体), 因为是继承关系且有一个isa
属性, 所以objc_class
也拥有了isa
属性 ( 其实的对象
,类
,元类
都有isa属性) -
NSObject
中的isa
在底层是由Class 定义的, 其中class
的底层编码来自objc_class
类型, 所以NSObject也拥有了isa属性 -
NSObject
是一个类,用它初始化一个实例对象objc,objc 满足objc_object
的特性(即有isa属性),主要是因为isa 是由 NSObject 从objc_class继承过来的,而objc_class继承自objc_object,objc_object 有isa属性。所以对象都有一个isa
,isa
表示指向,来自于当前的objc_object
(对象, 类, 元类都有isa
) -
objc_object
是根对象
,所有的对象都有这样一个特性 objc_object,即拥有isa属性 -
在结构层面可以通俗的理解为上层OC 与 底层的对接:
① 下层是通过结构体
定义的 模板,例如 objc_class、objc_object
② 上层是通过底层的模板创建的 一些类型,例如CATest
类结构分析
首先还是先看一个例子
例子1: 普通指针
普通指针定义2个变量a, b = 10, 打印两个变量值以及内存地址
普通指针地址指向-
a 和 b 为变量都指向10, 10是系统开辟的固定内存空间, 其他需要10的值的变量都可以指向内存固定生成的10
-
a 和 b 地址不一样, 这是一种拷贝, 属于
值拷贝
, 也成深拷贝
, 可发现a, b地址相差 4 个字节,这取决于a、b的类型
例子2: 对象指针
对象指针-
&p1/&p2 是
二级指针
, 指向对象的指针地址(0x7ffeefbff478, 0x7ffeefbff480 为对象指针
) -
p1/p2 是
一级指针
, 指向的 [SATest alloc] 开辟空间的内存地址 -
SATest
为[SATest alloc]
创建内存空间, [SATest alloc]开辟空间的isa
指向SATest
例子3: 数组指针
数组指针-
&arr == &arr[0] == 首地址, 其实他都是取的首地址, 数组地址其实就是数组第一个元素地址即
数组名为首地址
-
&arr[0]与%arr[1]相差
4字节
, 取决于数据类型
-
数组类型指针可以通过
首地址+偏移量
得到其他元素(偏移量为数组下标
) -
移动的字节数 等于 偏移量 * 数据类型字节数
, 这个根据&arr[0], &arr[1]看出, 两者相差4
有了上面的概念, 便于我们理解之后的探索
bits探索
再回头看下objc_class
源码(在 objc-runtime-new.h
)
objc-runtime-new.h
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// 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();
}
...
}
抛去那些删除的, ISA
占8字节
, superclass
为Class
类型占8字节
(这里的Class
是由objc_object
定义的,是一个指针), bits
只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits
(bits
有所以我们想要的信息), 那么我们接下来看下cache
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
...
}
先介绍几个定义
-
uintptr_t
定义typedef unsigned long uintptr_t;
,long
8字节 -
mask_t
定义typedef uint32_t mask_t;
,typedef unsigned int uint32_t;
, int类型占4字节 -
uint32_t
定义typedef unsigned int uint32_t;
, int类型占4字节 -
uint16_t
定义typedef unsigned short uint16_t;
short类型占2字节
拆开看一下
占8字节
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // long类型占8字节
占8字节
struct {
explicit_atomic<mask_t> _maybeMask; // int 4字节
#if __LP64__
uint16_t _flags; // short 占2字节
#endif
uint16_t _occupied; // short 占2字节
};
接下来看下 originalPreoptCache
看下 preopt_cache_t
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
/* dyld_shared_cache_builder and obj-C agree on these definitions */
struct preopt_cache_entry_t {
uint32_t sel_offs;
uint32_t imp_offs;
};
/* dyld_shared_cache_builder and obj-C agree on these definitions */
struct preopt_cache_t {
int32_t fallback_class_offset;
union {
struct {
uint16_t shift : 5;
uint16_t mask : 11;
};
uint16_t hash_params;
};
uint16_t occupied : 14;
uint16_t has_inlines : 1;
uint16_t bit_one : 1;
preopt_cache_entry_t entries[];
inline int capacity() const {
return mask + 1;
}
};
最后可计算出cache类的内存大小16字节
, 那么总共需要偏移 8 + 8 + 16 = 32
个字节。即首地址平移32位得到 bits
(bit
主要储存相关类信息)
创建2个类, SAPerson
继承NSObject
, SAStudent
继承SAPerson
-
p/x SAPerson.class
获取 SAPerson类首地址得到0x0000000100008260 SAPerson
-
x/4gx 0x0000000100008260
打印首地址isa指针的内存信息, -
将得到
0x100008260
平移32位得0x100008280
(此处为16进制, 逢16进1, 32则相当于进2个16即8260
变为8280
) -
bits
是这个类型class_data_bits_t bits
, 我们这边为了读取数据转一下 -
p $1 -> data
通过bits
地址得到bits
数据, 这里的data看源码可知是class_rw_t
类型的。留意下指针函数用->
((XXXX *)
结构), 结构体用.
class_rw_t *data() const {
return bits.data();
}
-
p *$2
打印bits
中数据信息
由于bits里面数据类型是class_rw_t
, 为了方便探索类里面的属性, 方法等, 我们接下来看一下class_rw_t
源码
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
...
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
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};
}
}
const protocol_array_t protocols() 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)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
};
通过查看class_rw_t
源码可看到, 首先class_rw_t也是个结构体类型, 结构体中有提供相应的方法去获取 properties 属性列表
、method 方法列表
、protocols协议列表
等
那么我们在SAPerson
定义几个属性, 方法
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SAPerson : NSObject {
NSString *hobby;
}
@property (nonatomic, copy) NSString *sa_name;
@property (nonatomic, assign) NSInteger sa_age;
- (void)sayYes;
+ (void)sayNo;
@end
属性列表
bits 数据信息 $3
在之前的例子我上面已经讲过了, 我们从读bits数据信息$3
之后开始
-
p $3.properties()
获得的属性列表
的list结构, 其中list
中的ptr
就是属性数组的参数指针地址。(p $3.properties()
命令中的propertoes
方法是由class_rw_t提供的, 方法中返回的实际类型为property_array_t
) -
p *$4.list.ptr
读一下指针地址, 可获取内存信息, count = 2, 也符合我们建的2个属性 -
p $5.get(0)
可获取到sa_name
对应属性(property_t) $6 = (name = "sa_name", attributes = "T@\"NSString\",C,N,V_sa_name")
-
p $5.get(1)
可获取到sa_age
属性(property_t) $7 = (name = "sa_age", attributes = "Tq,N,V_sa_age")
-
p $6.get(2)
数组越界, 因为我们只建立了2个属性
我们接下来看下方法
方法列表
-
p $3.methods()
获得的方法列表
的list结构, 接下来跟读属性类型, 依次读取指针地址, 读取列表对应项 -
p $5.get(0).name
读取出方法名 -
.cxx_destruct
由于底层是C++, 系统默认添加的方法 -
可以看到, 有自定义的方法
sayYes
, 系统自动生成的2个属性的set
,get
方法, 方法列表也有数组越界, 例如count = 6,p $5.get(6).name
读的时候也能看见数组越界报错
当然你要读协议列表
的list结构, 那里就p $3.protocols()
即可
其实到这里我们会有疑问, 属性
, 方法``协议
打印出来了没问题, 但是成员变量hobby
与类方法sayNo
并没有打印出来。我们再返回看一下struct class_rw_t
, 里面除了methods
、properties
、protocols
,还有一个ro
方法 const
class_ro_t *ro(), 看下
ro`底层
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
...
};
可看到const ivar_list_t * ivars;
, 有一个ivars
属性, 我们仿照下上面也读一下ro
可看到成员变量hoppy
储存在ivar_list_t
里面
总结
-
通过
XXXX {}
定义的成员变量
,会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表,除了包括成员变量
,还包括属性
的成员变量 -
通过
@property
定义的属性
,也会存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含property属性
接下来我们看下类方法存在哪里
类方法
-
x/4g SAPerson.class
获取类的内存信息, 以4片段打印 -
p/x 0x00000001000083b8 & 0x00007ffffffffff8UL
获取元类的首地址 -
p (class_data_bits_t *)0x00000001000083d8
转成class_data_bits_t
型便于我们获取bits
信息, 同时别忘了平移32位
, 元类的首地址平移32位
得到bits
信息 -
p $2->data()
通过元类地址获取bits
信息 -
p *$3
打印bits
数据 -
p $4.methods()
获取元类bits中的方法数组
-
p $5.list.ptr
/p *$6
(直接p *$5.list.ptr
也可以) 获取元类方法数组
中的方法列表
-
p $7.get(0).name
读出方法名, 可看到类方法是储存在元类中的
总结
-
类的
实例方法
储存在类的bits属性
中。 系统自动生成自定义属性@property
的set, get
方法, 也是存在这里。 -
类的
类方法
储存在元类的bits属性
中。
网友评论