《目录-iOS & OpenGL & OpenGL ES & Metal》
之前探索alloc底层的时候,遗留了一个问题,initInstanceIsa
方法是怎么关联的。是不是有不少小伙伴一直困扰isa到底是什么?我们今天就来探索一下~
一、isa是什么?
前面2篇文章都提到过,打印一个对象的内存地址,排在第一段的值是一个isa
。那我们今天就来探究一下!
我们都知道oc中有一个特别重要类:NSObject
,我们追踪进去看,会发现它里面只有一个isa,它是一个指针
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
what?这么nb的类怎么就只有一个isa呢?是不是隐藏了一些其他的信息呢?我们再看源码中的objc_object
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
的确只有一个isa。由此,我们可以看出每一个类都有一个初始且必有的属性-isa指针
。这个isa的类型是class,也就意味着,它是一个类指针。
继续前两篇的探索,来到initInstanceIsa
方法
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
来到initIsa
方法
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
//在这个地方,绑定isa的cls指向传进来的cls
//我们可以直接从这里看到isa的源码
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
assert(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
** isa是有一个联合体结构(union),它里面还有位域**
当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union),利用union可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间。
//联合体
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
//位域
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
联合体里面的属性是互斥的,比如说,设置了cls,就不用设置bits了。
我们从前面知道了isa是一个8字节,也就是64位,那isa里面到底存放了些什么?
二、isa里存放的有什么
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
以真机下,arm64为例:从0~64位,依次存储的信息:
nonpointer
:占1位,表示是否对 isa 指针开启指针优化【0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等】
has_assoc
:占1位,关联对象标志位【0没有,1存在】
has_cxx_dtor
:占1位,该对象是否有 C++ 或者 Objc 的析构器【0如果没有,则可以更快的释放对象;1如果有析构函数,则需要做析构逻辑】
shiftcls
:占33位,存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。
magic
:占6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced
:占1位,表示对象是否被指向或者曾经指向⼀个 ARC 的弱变量,
没有弱引⽤的对象可以更快释放。
deallocating
:占1位,表示对象是否正在释放内存
has_sidetable_rc
:占1位,当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
extra_rc
:占19位,当表示该对象的引⽤计数值,实际上是引⽤计数值减 1。【例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到刚提到的 has_sidetable_rc】
** 我们可以看出,isa中其实包含了很多信息,其实就是便于我们优化内存**
这里找到一张图,很形象:
三、isa关联对象和属性
我们现在已经知道了一个对象的第一个属性是isa。继续上面的思路去调试,需要根据位域的信息进行一系列位运算有点太繁琐了。我们换另一个思路:
根据object_getClass(object)
方法是可以拿到这个对象的类信息的,通过对象找到类,必然是通过isa,我们可以从这个方法入手,看isa是怎么把对象和类关联起来的。
//测试代码
LGPerson *object = [LGPerson alloc];
object_getClass(object);
来到object_getClass
方法
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
来到getIsa()
方法
inline Class
objc_object::getIsa()
{
//直接来到这一步,一般都不是TaggedPointer,是的话走下面
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
来到ISA()
方法
inline Class
objc_object::ISA()
{
//...省略一些代码,只看关键代码
//#define ISA_MASK 0x00007ffffffffff8ULL __x86_64__
//我们是在mac上跑的,所以用的x86的,arm64的是0x0000000ffffffff8ULL
//给isa联合体的位域 加上一层蒙版,也就是 与 ISA_MASK
//蒙版什么意思呢,就是遮住一部分,只让我们看有用的
return (Class)(isa.bits & ISA_MASK);
}
我们来LLDB调试看一下:
可以说,isa就是通过与上一个遮罩ISA_MASK,将对象与类关联起来。
也可以说,isa关联了对象和类,对象的isa指向这个对象的类。
四、isa的走位
在分析isa走位之前,有个小知识点测试: 对象可以创建多个,那类可以创建多个吗?
对象可以创建多个,但类只能创建一个那我们就可以放心地去一步步向上找isa的指向了。
(ps:图片中,没有放p/x isa的地址 & isa_mask
这一步,是因为我在正常工程中跑的,没在源码跑,发现p/x这一步打印的值和isa的值一样,就怕看起来太恶心,免去了多余的一步)
我们可以通过x/4xg object
拿到isa,然后p/x isa的地址 & isa_mask
,拿到指向地址。一步步向上找出isa的走位。
我们再看一下苹果官方文档的图。
我们来解读一下,里面有2条线:isa指向一条线。继承关系一条线。
isa指向:实例对象 --> 类 --> 元类 --> 根元类 --> 根元类。
细分:
- 子类实例对象 --> 子类 --> 子元类 --> 根元类 --> 根元类
- 父类实例对象 --> 父类 --> 父元类 --> 根元类 --> 根元类
- 根类实例对象 --> 根类 --> 根元类 --> 根元类
* 对象是由类创建的
* 类其实也是一个对象(万物皆对象),类对象是由元类创建的
* 根类/根元类其实就是NSObject
* 如果继续查,后面就无限递归指向 根元类
继承关系:
- 子类 ---> 父类 ---> 根类 ---> nil
- 子元类 ---> 父元类 ---> 根元类 ---> 根类 ---> nil
* 根元类继承自根类
* 根类继承自nil
这里或许会有个疑问:类和元类是什么时候创建的呢?
其实,它们是在编译器编译的时候创建的。可以通过两种方法去查证:
- LLDB调试,断点在alloc方法执行之前,打印类的class,根据isa和isa_mask看是不是能拿到父类
- command+b,生成一个编译文件,放在machoView中看class相关信息,这时候程序还没有跑起来,但是已经有了相应的父类,即可证明
五、总结
- 每个对象创建的时候都会有一个默认属性isa指针,指向创建这个对象的类
- isa是一个联合体,里面包含了位域信息,主要目的是为了内存优化
- isa的属性之间互斥
- isa的走位:实例对象 --> 类 --> 元类 --> 根元类 --> 根元类