前面从对象的本质中 对象的本质,就分析了isa指针的底层是什么,以及它的位运算。那么接下来,我们再去探寻下,isa中还需要我们注意和了解的地方。
1. 从ISA
分析到元类
1.1.类的ISA指针指向一个未知的东西
老规矩,带入到代码里面去操作。创建一个TestPerson
类,并在main.m
文件里面进行初始化:
通过断点调试,查看地址。通过p/x p
获取TestPerson
的指针地址,再通过x/4gx
拿到当前对象的isa
指针地址。拿到这个isa
指针地址后,再&
上isa
指针的掩码,就能获取对应类的信息(思路:根据objc
源码得到:return (Class)(isa.bits & ISA_MASK);
)。
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);
//这里与ISA_MASK这个掩码是将isa指针shiftcls的前面3位以及后面的位置0,这样得到的就是isa指针的类信息,这样就将isa指针和类关联起来了。
#endif
}
复制代码
掩码:(ULL
是无符号长整型unlonglong
标志)
下面根据刚刚说的步骤,通过lldb
调试:
通过这么一通操作,得到的是TestPerson
类的地址,总感觉过程太平淡,得搞点事,激动的心,颤抖的手,喊起响亮的口号:搞事、搞事、搞事。。。?
那么如果直接用x/4gx
查看最后得到TestPerson
地址(0x0000000100008260)
,是不是也能得到相应的内存结构,如果能拿到这个内存结构,再执行一次上面所进行的&操作,会是怎么样的一个结果了?
对比两次&操作的结果:第一次:0x0000000100008260
,第二次:0x0000000100008238
,这两者是不一样的,但是所指向的类,都是TestPerson
。
那么我们是不是可以有这么一个猜想:当前的这个类,就和对象一样,可以无限开辟,也就是说,在内存中不止有一个类。
有了这么一个猜想之后,就进行验证下,创建了void verTestPersonNum(void)
方法,通过打印结果:
通过打印结果,发现,所得到的TestPerson
类的指针地址都是0x100008260
,和第一次&操作得到的指针地址0x0000000100008260
是一样的。
到这里,就能得出一个结果:第二次&
操作得出的指针地址:0x0000000100008238
不是类。那么,它是什么了?是不是有可能是苹果系统所给出的一个新的结构?
因为苹果系统的设计是如下图所示的路线,那么接下来该是什么,就需要进一步探究了:
1.2.通过MachOView
分析是元类
接下来的分析,就要用到MachOView
这个工具了
把工程中的machO
文件,拖入这个工具里面:
通过这个工具,就能分析machO
的符号表了,我们在symbol Tabel
的Symbols
里面,搜索class
,就能找到TestPerson
类的对应信息(如下图),在TestPerson
类前面,还有一个metaClass
的类。
我们只是创建TestPerson
类,但是在编译的可执行文件里面,还多了一个metaclass
,也就是元类
。这个是系统或者编译器自己创建的。
2.ISA
的走位图和类的继承链
2.1.根元类的探究
经过分析,对象的isa
指针指向类,类的指针指向元类,那么,会不会有元类的isa
指针指向其他的类了?
继续分析,通过lldb
调试,寻找答案:
那么,isa
指针的走位图,应该是:
既然得到了根元类是NSObject
,那么,是不是可以直接用NSObject
类去找他的根元类了?
根据lldb
调试,发现,根类NSObject
的isa
指针直接指向的是根元类的isa
指针。
2.2.ISA
指针的走位图
先通过代码,来获取类、元类、根元类、根根元类的地址,以TestPerson
做个对比,来分析他们之间的关系:
#pragma mark - NSObject 元类链
void lgTestNSObject(void){
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// 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 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// TestPerson元类
Class pMetaClass = object_getClass(TestPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);//父类
NSLog(@"%@ - %p",psuperClass,psuperClass);
复制代码
在main
函数里面,调用该方法,打印的结果:
按照我们正常的推断流程,TestPerson
通过class_getSuperclass
是找到的父类NSObject
的类对象,但是,打印的结果,却是元类的地址。
那么我们是不是可以得出一个结论:任何对象,他的元类的父类,就是其根元类。就如下面这张示意图:
就比如:子类实例对象的isa
指针指向子类,子类的isa
指针指向子元类,子元类的isa
指针指向根元类。
2.3.类的继承链
我们的任何类,都会满足一条继承关系。那么,我们在工程里面,再创建一个TestSon
类,来继承于TestPerson
类
那么在lgTestNSObject
方法里,再增加几行代码:
#pragma mark - NSObject 元类链
void lgTestNSObject(void){
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// 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 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// TestPerson元类
Class pMetaClass = object_getClass(TestPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);
Class sonMetaClass = object_getClass(TestSon.class);
Class sonSuperClass = class_getSuperclass(sonMetaClass);
NSLog(@"%@ - %p",sonSuperClass,sonSuperClass);
}
复制代码
再次运行,得到打印结果:
发现,TestSon
类的父类,是TestPerson
类。那么,就验证了刚刚得出的那个结论(任何对象,他的元类的父类,就是其根元类)是错误的。
我们都知道,TestSon
继承于TestPerson
,而TestPerson
继承于NSObject
。根据我们前面的打印结果,TestPerson
的元类,是指向NSObject
的元类的,但是TestSon
的父类指向的却是TestPerson
,那么意味着,元类,也有一条继承链。就是:子元类继承于父元类,父元类继承于根元类。
然而,好像一到NSObject
,总会有特殊情况,那么,我们再针对NSObject
做一个探究:
#pragma mark - NSObject 元类链
void lgTestNSObject(void){
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// 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 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// TestPerson元类
Class pMetaClass = object_getClass(TestPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);
//TestSon
Class sonMetaClass = object_getClass(TestSon.class);
Class sonSuperClass = class_getSuperclass(sonMetaClass);
NSLog(@"%@ - %p",sonSuperClass,sonSuperClass);
// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
}
复制代码
再次运行,得到结果:
根据打印结果,发现NSObject
的父类是nil
,而根元类的父类,竟然是NSObject
类,那么这就回到了原点,也可以得出结论:所有的类,都是来源于NSObject
。那么就得到下面这个isa
指针链和superclass
继承链关系图:
这里再附上苹果官方文档提供的图:
3.通过源码分析类的结构
刚刚探究完类的isa
指针链和类的继承链,那么类里面,到底是怎样组成的了?就比如:
我们直接查看TestPerson
类的内存情况,发现,是有内容存在的。就好比对象,内存里面存放的是isa
指针和成员变量,那么类了?内存里面存放的什么了?
那么就需要分析类的底层原理了。通过前面章节,我们知道,类的底层就是objc_class
的结构体。那么我们直接在objc
的源码中(iOS底层探究之alloc)全文索引struct objc_class
来找到其声明的地方。搜索到的结果,发现有两处声明,第①
处声明已经明确表示,只能在objc2
环境下,才能使用,但是我们目前不是在这个环境下的,所以,这个声明不用管,所以,直接看另外一个就成。
那么我们看另外一个声明:
从底层,能够看出,类的底层是objc_class
,是继承于objc_object
,里面也包含好些内容,比如:cache
、bits
。。。等等,那么像这类内容到底代表着什么了?那就需要进一步的探究了。
那么可以得出类的结构图:
4.类的结构内存计算
在objc
源码中,创建一个TestPerson
类,在里面添加一个name
的成员变量。然后在main.m
文件里面初始化。首先通过x/4gx
把TestPerson
(继承于NSObject
)类的内存结构情况打印出来:
我们刚刚通过类的源码分析,那么打印出来的内存结构里面,isa
指针地址在最前面,isa
:0x0000000100008480
,推断下,0x000000010036a140
就应该是Class superclass
了(根据源码,类的结构体里面的内容)
通过lldb
调试打印下,果然是父类NSObject
。
那么接下来,0x000000010161d470
就应该是cache_t cache
了,这个探究先放一放,先来探究第四
个地址0x0001801800000003
,那么第四
个地址,应该就是class_data_bits_t bits
了。想要获取第四
个地址所对应的值,就要把首地址指针
平移到这个位置上来,平移的长度,就是isa
(8
字节) + superclass
(8
字节) + cache
,这三者的长度之和。现在已知isa
和 superclass
的长度,但是cache
的长度不知道。我们可以看下cache_t
底层的类型。
cache_t
底层的类型是一个结构体,结构体的内存大小,是根据结构体里面的内容来决定的。因为,结构体里面的方法、函数的内存不存储在结构体的内存区域里面,还有全局变量的内存是在全局区域,也不占结构体的内存,通过查看源码(源码太长,不粘贴出来了),所以,最后只剩下:
那么只剩下一个explicit_atomic<uintptr_t> _bucketsAndMaybeMask
和一个联合体
。而在explicit_atomic
的声明,发现
struct explicit_atomic : public std::atomic<T> {***}
复制代码
是一个public
类型,所以他的内存大小取决于<uintptr_t>
的大小。那么uintptr_t
是一个无符号的长整型,所以是8
字节大小(64
位)。
typedef unsigned long uintptr_t;
复制代码
在联合体里面,同理,explicit_atomic<mask_t> _maybeMask;
的大小取决于<mask_t>
,而mask_t
,是
typedef uint32_t mask_t;
复制代码
所以explicit_atomic<mask_t> _maybeMask
内存大小为4
;uint16_t _flags
;和uint16_t _occupied
;都是2
,它们三者的总和是8
。
同理explicit_atomic<preopt_cache_t *> _originalPreoptCache
;的大小取决于<preopt_cache_t *>
,而preopt_cache_t *
是一个指针,所以内存大小为8
。
由于联合体
是互斥的,所以联合体
的内存大小是8
。所以,最终算得cache
的内存大小是8 + 8 = 16
.
所以,要看第四个地址的值,那么首地址就要平移 8 + 8 + 16 = 32
字节大小的位置。转变成二进制,就是0x20
。首地址是:0x1000084a8
,接下来就通过lldb
调试:
之所以使用class_data_bits_t
,原因是:当前是在进行取地址,而这个地址指向的空间是bits空间,所以就能强转成(class_data_bits_t *)
这么一个指针地址。
5.lldb分析类的结构
刚刚在源码分析中,知道了bits
的计算流程,接着,就通过lldb来分析类的结构。通过源码,知道bits
里面包含了class_rw_t
,返回了class_rw_t
的内容,那么猜想出:class_rw_t
里面就有对外的接口。
根据源码里面的提示,可以先取出bits
里面的data()
:
这样子,就把bits
的data
打印出来了。查看所打印的信息,发现,在TestPerson
类里面的name成员变量根本就没出现在这里面。那么还需要接着找。
回想下,刚刚在打印data()
的时候,在源码里面,这个data
是class_rw_t
,那么就进入到class_rw_t
的底层,去寻找答案。既然能在外面获取data
,那么在class_rw_t
的底层,就有提供对外的方法。刚好印证了之前的猜想。所以可以从这里入手。在class_rw_t的底层源码里面,有一段代码:
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};
}
}
复制代码
因为我们在声明TestPerson
类的成员变量使用的方式是@property (nonatomic, copy) NSString *name;
,那么,找property_array_t properties()
应该是可以的。通过lldb
调试:
此时,我们目前得到的,还是property_array_t
,还需要进入到property_array_t
里面去继续跟踪。进入其底层源码:
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};
复制代码
可以看到,是分两层拷贝的(就是最外层是一个数组,这个数组里面的元素还是数组),一个property_t
,另外一个是property_list_t
,再进入list_array_tt
,查看其底层(源码太多,不粘出来了),这里面就有很多的迭代器(就如数组遍历,就是一个迭代器)。这里面就有我们要找的name
信息。再进行lldb
调试:
这么一层层的找下来,最后通过get
方法,把name
这个成员变量的信息找出来了。
s
6.类的bits
数据分析
通过lldb
分析,找到了需要找的name
信息,在TestPerson
类里面添加更多的成员变量,再添加一个实例方法和一个类方法:
然后再运行objc
源码,到断点处,再进行lldb
调试:
最后,我们发现,我们只能找到name
和hobby
这个两个成员变量,还有一个subject
没发现。还有定义的两个方法。这是我们通过property_array_t
,只找到name
和hobby
这个两个成员变量。
那么接下来,再试试通过methods()
,再去挖掘下,看能不能发现新的东西。
找到了5
个方法,但是方法都为空,这是为什么了?
那就要到底层源码中去寻找答案了。
在class_rw_t
底层中,有
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};
}
}
复制代码
而property_array_t
的底层的property_list_t
是没有实现的
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};
复制代码
源码中,property_list_t底层没有实现
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
复制代码
而我们所需要的信息是在property_t
里面,课以查看下property_t
的声明,全文索引 property_t {
,可以看到是结构体,我们之所以通过data()
,逐级查询,能够获取name
和hobby
的信息,就在此了。
再看看method_array_t
的底层的method_list_t
是有实现的
class method_array_t :
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;
public:
method_array_t() : Super() { }
method_array_t(method_list_t *l) : Super(l) { }
const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
return beginLists();
}
const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};
复制代码
method_list_t 的底层实现
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
bool isUniqued() const;
bool isFixedUp() const;
void setFixedUp();
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
ASSERT(i < count);
return i;
}
bool isSmallList() const {
return flags() & method_t::smallMethodListFlag;
}
bool isExpectedSize() const {
if (isSmallList())
return entsize() == method_t::smallSize;
else
return entsize() == method_t::bigSize;
}
method_list_t *duplicate() const {
method_list_t *dup;
if (isSmallList()) {
dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
dup->entsizeAndFlags = method_t::bigSize;
} else {
dup = (method_list_t *)calloc(this->byteSize(), 1);
dup->entsizeAndFlags = this->entsizeAndFlags;
}
dup->count = this->count;
std::copy(begin(), end(), dup->begin());
return dup;
}
};
复制代码
那么同理,我们所需要的信息是在method_t
里面,课以查看下method_t
的声明,全文索引 method_t {
,看源码(部分)
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
private:
bool isSmall() const {
return ((uintptr_t)this & 1) == 1;
}
// The representation of a "small" method. This stores three
// relative offsets to the name, types, and implementation.
struct small {
// The name field either refers to a selector (in the shared
// cache) or a selref (everywhere else).
RelativePointer<const void *> name;
复制代码
有个big()的对外输出,也就是可以通过方法.big()就能够打印出对应的方法信息
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
复制代码
那么就能通过lldb
调试
能够把部分的方法打印出来,但是还是缺少subject
成员变量,和+ (void)say666;
的类方法。这些将在后面的文章中,进行相应的补充。
补充内容—指针和内存平移
如何获取当前的内存结构—–之内存偏移
- 普通指针
比如:新起一个工程,打印两个int
变量,来查看其内存情况,数据和地址:
int a = 10; //
int b = 10; //
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
复制代码
根据这个打印的结果,看出,在内存某个区域里面,有两个指针,指向同一个的值区域。
有两个指针,同时指向某一个值为10
的区域,意味着这个值区域能被任何指针访问。这一个现象有点类似于值拷贝(不是真的拷贝啊),把这个值区域的值,给到a
和b
。
- 对象指针
接下来,创建一个TestPerson类,两次初始化,形成两个对象:
TestPerson *p1 = [TestPerson alloc];
TestPerson *p2 = [TestPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
复制代码
从打印的结果来看,他们两者的地址不同,并且他们所指向的空间也不同。
- 数组指针
// 数组指针
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);
复制代码
打印的结果:
从打印结果看出,&c
和&c[0]
是同一个地址,那是因为&c[0]
是首地址,数组中,地址的排列是顶针存储的,还是连续的存储。
而d、d+1、d+2
,其中d
就是&c
的指针地址,d+1
就是按照现有的数据结构平移1
个间隔大小为4
的单位。同理,d+2
就是平移2
个间隔大小为4
的单位。如果是对象,间隔单位大小就为8
.
按照上面的分析结果,可以有个大胆的构想,通过for
循环把数组c[4]
的元素遍历出来,就可以把原本的int value = c[i]
,改成 int value = *(d+i);
,那么同样能够遍历出来:
int c[4] = {1,2,3,4};
int *d = c;
for (int i = 0; i<4; i++) {
// int value = c[i];
int value = *(d+i);
NSLog(@"%d",value);
}
复制代码
打印结果:
得出结论:内存是可以进行平移,平移后,取地址里面的值,就可以得到当前内存里面的值。
刚刚我们是获取了类的地址, 那么这个类的地址,就是当前内存结构里面的首地址。有了首地址,就可以按照刚刚内存平移的步骤,平移一些大小,就能得到里面的内容。
到了此处,欧耶,大功告成,类的原理的底层探究,就完成了,有木有点收获啊,(不许没有啊<( ̄▽ ̄)/)
感谢各位的光临