写在前面
通过前面篇章的探索,我们已成功的从对象过渡到类了.本文就来讲讲实例出实例对象的类以及类的结构.
一、类的本质
① 类的本质
objc
源码下准备代码
利用clang
将OC文件
输出cpp文件
.(xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
)
发现类在底层用Class
接收,然后开始大海捞针(开天眼模式)找到了Class
的定义
想要找到objc_class
就搜不到了,但是总觉得它似曾相似,或许能在objc源码
中找到灵感
在源码中搜索代码的经验 "objc_class :"、 "objc_class {"
我们发现objc_class
继承于objc_object
结论:类的本质是objc_class类型的结构体,objc_class继承于objc_object,所以满足万物皆对象
② objc_class & objc_object 、objc_object和NSObject的关系
objc_object和NSObject
等等,为什么继承objc_object
就满足万物皆对象
了???
看过NSObject
的定义就知道了
仔细比较的话就能看出NSObject
和objc_object
有着说不清道不明的关系
- 其实
NSObject
是objc_object
的仿写,和objc_object
的定义是一样的,在底层会编译成objc_object
- 同理
NSObject类
是OC
版本的objc_class
objc_class & objc_object
前面使用clang
编译过main.m
文件,从编译后的c++
文件中可以看到如下c++
源码
-
NSObject
的底层编译是NSObject_IMPL
结构体- 其中
Class
是isa
指针的类型,是由objc_class
定义的类型 - 而
objc_class
是一个结构体.在iOS中,所有的Class
都是以objc_class
为模板创建的
- 其中
-
在
objc4源码
中搜索objc_class
的定义,源码中对其的定义有两个版本-
旧版 位于
runtime.h
中,已经被废除 -
新版位于
从新版的定义中,可以看到objc-runtime-new.h
,这个是objc4-818.2
最新优化的,我们后面的类的结构分析也是基于新版来分析的objc_class
结构体类型是继承自objc_object
的.
-
-
在
objc4源码
中搜索objc_object
或者objc_object {
,这个类型也有两个版本-
一个位于
objc.h
,没有被废除,从编译的main-arm64.cpp
中可以看到,使用的这个版本的objc_object
-
另一个位于
objc-privat.h
-
那么objc_class
与 objc_object
到底有什么关系?
通过上述的源码查找以及main-arm64.cpp
中底层编译源码,有以下几点说明:
- 结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa
属性 -
mian-arm64.cpp
底层编译文件中,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
-
objc_object
(结构体) 是 当前的根对象
,所有的对象都有这样一个特性objc_object
,即拥有isa
属性
引申问题:objc_object 与 对象的关系
- 所有的
对象
都是以objc_object
为模板继承
过来的 - 所有的对象是来自
NSObject(OC)
,但是真正到底层的是一个objc_object(C/C++)
的结构体类型 -
objc_object
与对象
的关系 是继承关系
③ Class isa
isa
明明是isa_t
类型的,为什么注释了一句Class ISA
?
- 万物皆对象,用继承于
objc_object
的Class
接收是没问题的 - 强转,方便
isa
走位时返回类的类型
④总结
- 所有的
对象 + 类 + 元类
都有isa
属性 - 所有的
对象
都是由objc_object
继承来的 - 简单概括就是
万物皆对象
,万物皆来源于objc_object
,有以下两点结论:- 所有以
objc_object
为模板创建的对象
,都有isa
属性 - 所有以
objc_class
为模板创建的类
,都有isa
属性
- 所有以
- 在结构层面可以通俗的理解为
上层OC
与底层
的对接:-
下层
是通过结构体
定义的模板
,例如objc_class、objc_object
-
上层
是通过底层的模板创建
的一些类型,例如TCJPerson
-
objc_class、objc_object、isa、object、NSObject
等的整体的关系,如下图所示
二、指针内存偏移
在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移
① 普通指针 - 值拷贝
我们观察上面的代码,虽然整型变量a
和b
都是被赋值为10,但是a
和b
内存地址是不一样的,这种方式被称为值拷贝
.
② 对象 - 指针拷贝或引用拷贝
通过运行结果,可以知道obj1
和obj2
对象不光自身内存地址不一样,连指向的对象的内存地址也不一样,这种方式被称为指针拷贝
或引用拷贝
.
③ 用数组指针引出 - 内存偏移
通过运行结果可以看到:
-
&a
和&a[0]
的地址是相同的.即首地址就代表数组的第一个元素的地址. - 第一个元素地址
0x7ffeefbff400
和第二个元素地址0x7ffeefbff404
相差4个字节,也就是int
的所占的4字节,因为他们的数据类型相同
. -
d
、d+1
、d+2
这个地方的指针相加就是偏移地址.地址加1就是偏移,偏移一个位数所在元素的大小. - 可以通过地址,取出对应地址的值.
三、类的结构
从objc_class
的定义可以得出,类有4个
属性:isa、superclass、cache、bits
① Class ISA
不但实例对象
中有isa指针
,类对象
中也有isa指针
关联着元类
Class
本身就是一个指针,占用8字节
② Class superclass
顾名思义就是类的父类
(一般为NSObject
)superclass
是Class
类型,所以占用8字节
③ cache_t cache
虽然对这个属性比较陌生(后面章节会详细介绍),但是cache
在英文中的意思是缓存
cache_t
是一个结构体,内存长度由所有元素决定:_bucketsAndMaybeMask
是long
类型,它是一个指针,占用8字节
;
mask_t
是个uint32_t
类型,_mask
占用4字节
;因_occupied
和_flags
都是uint16_t
类型,uint16_t
是 unsigned short
的别名,所以_occupied
占用2字节
;_flags
占用2字节
=>cache_t
占用16字节
④ class_data_bits_t bits
又是一个陌生的属性,但是苹果工程师还是蛮友好的,这一看就是存数据的地方那么问题来了,类的属性方法都去哪儿了?是在cache
还是在bits
? 其实前文中有提到一丢丢——objc_class
中有个class_rw_t *data()
方法
通过前面的分析可知,想要获取bits
的中的内容,只需通过类的首地址平移32字节
即可.
四、类的属性方法
为TCJPerson
添加hobby
成员属性、tcj_name
属性变量、sayNB
类方法、sayHello
实例方法
① 类的属性
x/4gx cls
打印当前类结构
bits
刚好是类的内存首地址
+isa、superclass、cache
的内存长度
=> 0x1000081f0+32字节 = 0x100008210
po
打印不出来,那就类型强转打印输出bits
的内存地址
根据class_rw_t *data() { return bits.data(); }
打印bits.data()
从$4
指针的打印结果中可以看出bits
中存储的信息,其类型是class_rw_t
,也是一个结构体类型.但我们还是没有看到属性列表、方法列表等,需要继续往下探索.
通过查看class_rw_t
定义的源码发现,结构体
中有提供
相应的方法
去获取属性列表、方法列表
等,如下所示
接下来继续探索 bits
中的属性列表,以下是 LLDB
探索的过程
那么我们的hobby
去哪了呢?难道我TCJPerson
类只配拥有“国籍”不配拥有“姓名”?让我静静...
由此我们知道class_rw_t
中的property_list
中只有属性,没有成员变量
,属性与成员变量的区别就是有没有set、get方法,如果有,则是属性,如果没有,则是成员变量.
在一顿猛如虎的操作(开天眼)之后,发现了class_rw_t
有个属性class_ro_t
.
在控制台输出ro
,跟class_ro_t
的结构类型一摸一样.
而我们的成员变量就在ro
的ivars
里面,好期待啊
如愿拿到了hobby
和tcj_name
,但是这个tcj_name
长得有点不一样.
仔细想想这不就是编译器会在底层自动将属性变量生成一个成员变量 _tcj_name(_前缀+属性变量),嗦嘎撕裂,还有谁...
小总结:
- 通过
{}
定义的成员变量,会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量 - 通过
@property
定义的属性,也会存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含属性
② 类的方法
-
类
的实例方法
存储在类的bits属性
中,通过bits --> methods() --> list
获取实例方法列表
,例如TCJPersong
类的实例方法sayHello
就存储在TCJPerson类的bits
属性中,类中的方法列表
除了包括实例方法
,还包括属性的set方法
和get方法
,还有系统在底层添加了一个c++的.cxx_destruct方法
.我们来打印验证一方 -
类
的类方法
存储在元类的bits属性
中,通过元类bits --> methods() --> list
获取类方法列表
,例如TCJPerson
中的类方法sayNB
就存储在TCJPerson类
的元类
(名称也是TCJPerson)的bits
属性中.打印验证一下 -
类的类方法可以理解成元类对象的实例方法,因此存在元类中.
③ 结论
- 成员变量存放在
ivar
- 属性存放在
property
,同时也会存一份在ivar
,并生成setter
、getter
方法 - 对象方法存放在
类
里面 - 类方法存放在
元类
里面
④ API验证
利用底层开放的API
可以验证以上结论
五、提出疑问
① 类存在几份?
由于类的信息
在内存
中永远只存在一份
,所以 类对象只有一份
.
② objc_object 与 对象的关系
- 所有的
对象
都是以objc_object
为模板继承
过来的 - 所有的对象 是 来自
NSObject(OC)
,但是真正到底层的 是一个objc_object(C/C++
)的结构体类型 -
objc_object
与对象
的关系是继承关系
③ 什么是 属性 & 成员变量 & 实例变量 ?
-
属性
(property
):在OC
中是通过@property
开头定义,且是带下划线成员变量
+setter
+getter
方法的变量 -
成员变量
(ivar
):在OC
的类中{}中定义
的,且没有下划线
的变量 -
实例变量
:通过当前对象类型,具备实例化的变量
,是一种特殊的成员变量
,例如NSObject、UILabel、UIButton
等
④ 成员变量 和 实例变量什么区别?
-
实例变量
(即成员变量
中的对象变量
就是实例变量
):以实例对象实例化来的,是一种特殊的成员变量
-
NSString
是常量
类型, 因为不能添加属性,如果定义在类中的{}
中,是成员变量
-
成员变量
中除去基本数据类型、NSString
,其他都是实例变量
(即可以添加属性
的成员变量
),实例变量主要是判断是不是对象
⑤ isKindOfClass 和 isMemberOfClass 的理解
这是一道涉及isa
走位图的面试题,大胆猜测下结果
先来探索一下isKindOfClass
和isMemberOfClass
的实现
上图中我已做了详细的解析,接下来结合isa
走位图(实线为父类走向)可以得出前面四个打印结果:
- NSObject元类与NSObject类不相等,NSObject元类的父类(指向NSObject类)与NSObject类相等——YES
- NSObject元类与NSObject类不相等——NO
- TCJPerson元类与TCJPerson类不相等,TCJPerson元类的父类与TCJPerson类不相等——NO
- TCJPerson元类与TCJPerson类不相等——NO
后面四个结果分析如下:
- NSObject类与NSObject类相等——YES
- NSObject类与NSObject类相等——YES
- TCJPerson类与TCJPerson类相等——YES
- TCJPerson类与TCJPerson类相等——YES
六、补充知识
strong & copy & weak 底层分析
在clang
编译的cpp文件
中可以发现 strong & copy & weak
修饰的属性在编译的底层代码中是有区别的
-
在
TCJPerson
中我们定义了两个两个属性,分别用copy
和strong
修饰 -
用
clang
将main.m
文件编译成main-arm64.cpp
,然后发现copy
和strong
修饰的属性的set
方法是有区别的
这里就有疑问了,为什么copy
修饰的属性使用了objc_setProperty
,而strong
修饰的没有?
- 在
LLVM
中搜索objc_setProperty
,找到如下所示的getOptimizedSetPropertyFn
方法
从这里即可看出,针对不同的修饰符,返回的是不同的
- 如果是
atomic & copy
修饰,name
为objc_setProperty_atomic_copy
- 如果是
atomic
且没有copy
修饰,name
为objc_setProperty_atomic
- 如果是
nonatomic & copy
修饰,name
为objc_setProperty_nonatomic_copy
- 其他剩余的组合,即
nonatomic、nonatomic & strong、nonatomic & weak
等,name
为objc_setProperty_nonatomic
上述的几个name
分别对应objc4-818.2
源码中的如下方法
然后通过汇编调试发现,最终都会走到objc_storeStrong
-
copy
修饰的属性汇编调试结果 -
strong
修饰的属性汇编调试结果 -
源码中搜索
objc_storeStrong
,有如下源码,主要也是retain
新值,release
旧值 -
llvm
编译源码中搜索objc_storeStrong
,找到EmitARCStoreStrongCall
方法,如下图所示,发现copy
和strong
修饰的属性执行的策略是不一致的 -
llvm
中搜索EmitARCStoreStrongCall
方法,在GenerateCopyHelperFunction
方法有调用,然后在这里发现了strong
和weak
的不同处理
其中BlockCaptureEntityKind
有如下的枚举值以及表示的含义
-
如果是
weak
修饰,执行EmitARCCopyWeak
方法,如下所示,weak
在底层的调用是objc_initWeak
-
如果是
strong
修饰,执行EmitARCStoreStrongCall
方法
结论
-
copy
和strong
修饰的属性在底层编译的不一致,主要还是llvm
中对其进行了不同的处理的结果.copy
的赋值是通过objc_setProperty
,而strong
的赋值时通过self + 内存平移
(即将指针通过平移移至name
所在的位置,然后赋值),然后还原成strong
类型 -
strong & copy
在底层调用objc_storeStrong
,本质是新值retain
,旧值release
-
weak
在底层调用objc_initWeak
② Type Encoding & Property Type String
Type Encoding-官方文档
Property Type String-官方文档
clang中的方法签名
Type encoding -- clang
编译后,方法列表的这些字符的含义是什么?
以@16@0:8
为例
-
@16
表示返回字符串占用16个字节
-- 第二个@
占8字节
,sel
占8字节
- 第一个
@
表示返回值
-
16
表示总共占用的字节数16字节
- 第二个
@
:第一个参数-
id
--@
统配类型 typedef struct objc_object *id
-
-
0
--从0开始
0-8
占8字节
-
:
-- 代表sel
,方法编号 -
8
--从8开始
8-16
占8字节
- 第一个
- 而
v24@0:8@16
中的v -- void
无返回值
clang编译后的属性的attribute
clang
编译输出了属性的attribute
,同样也可以通过property_getAttributes
方法获取
-
T
表示type
-
@
表示变量类型
-
C
表示copy
-
N
表示nonatomic
-
V
表示variable
变量,即下划线变量_tcj_name
写在后面
和谐学习,不急不躁.我还是我,颜色不一样的烟火.
网友评论