前言
一步一个脚印地探索iOS的OC底层原理,通过前面的文章可以大概了解了OC对象创建的alloc原理
和OC对象的内存字节对齐
,但是这也只是知道了对象创建的底层过程和开辟内存空间的,这篇文章将介绍对象的本质
和对象与类的关联---isa
1.isa的初始化
isa指针:在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。也就是说在对象创建的时候就会有isa指针初始化了。为了搞清楚还是需要用到OC对象创建的alloc原理里面源码的_class_createInstanceFromZone
的方法的部分源码,然后跟着流程进去得到如下的部分源码
//_class_createInstanceFromZone的部分代码
//初始化实例的isa指针
obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
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;
}
}
其中的nonpointer
字面的意思是没有指针的,一般情况下nonpointer是为true的,只有在例如实现了allocwithzone方法,retain,release等的时候会是false。如果为false是直接将传进来的cls为isa的关联的cls赋值。
其他的剩下的部分就是对isa
的初始化赋值了。但是具体的isa
内部是怎样的还是不知道的,从源码中isa_t
点击进去可以查看。
2.isa的内部结构
通过源码可以知道isa
的isa_t
类型的内部结构
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
};
#ifndef _UINTPTR_T
#define _UINTPTR_T
typedef unsigned long uintptr_t;
#endif /* _UINTPTR_T */
从中可以知道,isa
是一个联合体(union),里面有关联的类cls和long型的bits。
什么是联合体(union)呢?联合体是一种特殊的类,也是一种构造类型的数据结构。完全就是共用一个内存首地址,并且各种变量名都可以同时使用,操作也是共同生效。所以也叫
共用体
。并且联合体(union)中是各变量是“互斥”的
,但是内存使用更为精细灵活,也节省了内存空间。
由上面的概念可以知道,cls
和bits
之间是互斥的,即有cls
就没有bits
,有bits
就没有cls
。这就很好地解释了为什么上面的源码在初始化isa
的时候会用nonpointer
来区分开。所以isa
的大小占8个字节,64位。其中这64位中分别存储了什么呢?通过ISA_BITFIELD
位域源码:
# 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
# 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
这两种是分别在arm64
和x86
系统架构下的,但是都是64位的,本文的说明是在x86
下介绍的。
- nonpointer:占1位,表示是否对
isa
指针开启指针优化,0:表示纯指针;1:表示不止是类对象地址,isa中包含了类信息、对象的引用计数等。 - has_assoc:占1位,表示关联对象标志位,0没有,1存在。
- has_cxx_dtor:占1位,表示该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有则可以更快的释放对象。
- shiftcls:占44位,存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针。(由上面的初始化源码也可以很好的说明,关联的类指针向右移3位)
newisa.shiftcls = (uintptr_t)cls >> 3;
- magic:占6位,用于调试器判断当前对象是真的对象还是没有初始化的空间。
- weakly_referenced:占1位,表示对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
- deallocating:占1位,表示对象是否正在释放内存。
- has_sidetable_rc:占1位,表示当对象引用技术大于10时,则需要借用该变量存储进位。
- extra_rc:占8位,表示该对象的引用计数值,实际上是引用计数总值减1。例如,如果对象的引用计数为10,那么extra_rc为9,如果引用计数大于10,则需要使用到下面的has_sidetable_rc。
3.isa是对象与类的连接
为了方便介绍下面的内容需要定义一个什么属性都没有的TestJason类,然后通过objc4-756.2
苹果官方的源码。通过object_getClass
这个方法可以获取到类。
TestJason *test2 = [TestJason alloc];
Class testClass = object_getClass(test2);
NSLog(@"%@",test2);
通过源码找到object_getClass
的方法
/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
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];
}
}
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
}
# define SUPPORT_INDEXED_ISA 0
# define ISA_MASK 0x00007ffffffffff8ULL
从源码中可以知道返回的isa
最终是(Class)(isa.bits & ISA_MASK)
。
其中源码有一个判断isTaggedPointer()
,其中苹果对于Tagged Pointer
的概念引入,是为了节省内存和提高执行效率,对于 64 位程序,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。如果想了解这部分的内容可以看看深入理解 Tagged Pointer
。
下面就是用lldb
的指令来验证一下的。首先用x/4gx test2
来打印出地址
然后用
p/x TestJason.class
打印出TestJason
类的内存值
由源码知道类Class的最终返回是
(Class)(isa.bits & ISA_MASK)
的,所以将x/4gx test2
打印出来的0x001d800100001749
& ISA_MASK
的值得到如下:
最终发现
$3
和$4
的内存值是一样的,所以isa
是关联着对象与类的。由前面的文章知道由于内存的优化对象的其他属性的位置实际会发生变化的,所以对象的第一个属性就是isa
。
4.isa的走位原理
通过上面的介绍,可以知道了isa是关联着对象与类的,并且对象的isa指向类,因为万物皆对象,那么类的isa指向的是谁呢?可以通过苹果官方的isa的走位流程图
isa流程图.png
其中虚线是代表
isa
的走位,实线代表的是继承关系
的走位。图中有一个meta class
元类的概念。
什么是元类?在OC中,对象的方法并没有存储于对象的结构体中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。
当对象的实例方法被调用时,它通过自己的isa来查找对应的类,然后在所属类的 class_data_bits_t结构体中查找对应方法的实现。同时,每一个objc_class 也有一个指向自己的父类的指针superclass用来查找继承的方法。
而当调用 类方法 时,它的查找流程是怎样的呢?对此OC的解决方案就是引入元类,来保证类方法也能通过相同的机制查找到。对于元类的解释转自OC源码分析之isa
是不是看着这张图的各种箭头指向一脸懵逼呢?下面就是来对这张图的isa
的走位验证一下。
4.1 isa的走位
还是用TestJason
这个什么属性都没有的类来介绍。通过lldb
的指令来验证。由上面的图可以知道,对象的isa
指向类,类的isa
指向元类。
由图可以知道,先用
x/4gx test2
来打印出isa
的内存值,然后用isa
的内存值&ISA_MASK
得到$9
,然后po $9
此时得到的是类。然后再用x/4gx
来打印$9
的值,此时就是相当于打印出类的isa
的内存值了。最后可以看到两个TestJason
,但是这两个的内存值是不一样的,分别是类和元类。但是中可以看到元类
里面还有isa
值,那么就继续打印。
从中可以看到,元类的
isa
指向了NSObject
,但是这个NSObject
到底是类呢?还是元类呢?为了搞清楚,可以用x/4gx NSObject.class
来打印类的isa
来验证。
从中可以看到
$13
打印的内存值是等于$16
的,那么就可以知道元类
的isa
指向的是根元类
的isa
。那么根元类
的isa
指向谁呢?继续打印
从中可以看到
根元类
的isa
指向了它的本身,这就形成了一个闭环。所以整体来说的话,就是
对象的isa-->类的isa-->元类的isa-->根元类的isa-->根元类的isa(根元类本身)
这就很好地验证了苹果的官网的isa
走位图。
4.2 对象,类之间的继承的走位
为了介绍方便,添加多一个TestSuperJason
的类,并且让TestJason
是继承TestSuperJason
的。还是通过lldb
的指令来的打印class_getSuperclass
方法。
这是类的继承关系的打印
这是元类的继承关系打印
所以它们的继承关系是
类:
TestJason-->TestSuperJason-->NSObject-->nil
元类:
TestJason元类-->TestSuperJason元类-->NSObject元类-->NSObject类-->nil
5.对象的本质
通过上面的知识可以大概了解了isa
的原理,但是对象的本质是什么还不是很了解的,可以通过clang
编译成cpp
文件来查看。实现的代码如下:
@interface Jason : NSObject{
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@end
@implementation Jason
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"123");
}
return 0;
}
在文件的路径下,可以终端输入命令,就可以查看main.cpp文件
clang -rewrite-objc main.m -o main.cpp
从中可以看到
#ifndef _REWRITER_typedef_Jason
#define _REWRITER_typedef_Jason
typedef struct objc_object Jason;
typedef struct {} _objc_exc_Jason;
#endif
extern "C" unsigned long OBJC_IVAR_$_Jason$_name;
struct Jason_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *nickName;
NSString *_name;
};
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation Jason
static NSString * _I_Jason_name(Jason * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Jason$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Jason_setName_(Jason * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Jason, _name), (id)name, 0, 1); }
// @end
struct NSObject_IMPL {
Class isa;
};
从中可以看到,对象最终会被编译成结构体struct
,NSObject_IMPL
里面包含着isa
,在类里面定义的属性name
和成员变量nickName
也是在Jason_IMPL
结构体里面,但是属性变量name
是有getter
方法_I_Jason_name
和setter
方法_I_Jason_setName_
的,而成员变量nickName
是没有的。并且这些方法里面都有默认带有两个参数id self
和SEL _cmd
,这样就很好解释了我们在方法中可以直接调用self
。
5.最后
通过上面的内容可以了解到isa
是一个isa_t
类型的联合体(union),并且里面的属性是互斥的,isa
的大小占8字节。一般情况下都是在bits
下的位域来存储内容,其中ISA_BITFIELD
在x86
和arm64
架构下都是64位,但是里面的属性的占位有点区别的。isa
是关联着对象与类的,并且对象的本质就是一个结构体。至此有关isa
的原理介绍到此结束。