对象的本质
上篇文章提到,OC中对象的本质是结构体,那么这个结论我们要如何去验证呢?这时候我们需要用到Clang。
Clang是一个由Apple主导编写,基于LVVM的C、C++、OC语言的轻量级编译器
@interface LGPerson : NSObject
@end
@implementation LGPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return NSApplicationMain(argc, argv);
}
使用Clang编译上边的代码:
clang -rewrite-objc main.m -o main.cpp,然后打开main.cpp文件,搜到LGPerson可以看到
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
这里就可以看到,Clang编译后,LGPerson就被编译成了了LGPerson_IMPL的结构体。而且其中初始化就有一个struct NSObject_IMPL NSObject_IVARS的数据,它表明了一种继承关系(虽然严格来说结构体没有继承,这样做实际上是结构体类型的强制转换,linux内核源码中有很多这样的方法。父结构体变量必须放在子结构体的首位)。我们再找到LGPerson_IMPL结构体的‘父类’
struct NSObject_IMPL {
Class isa;
};
我们看到NSObject_IMPL中只有一个isa指针。isa我们下边做讨论,看到这里就验证了,对象在编译运行后,确实是以结构体的类型存在的。
类属性
类在实际使用过程中,肯定会包括一些属性,那么在LGPerson中添加一个属性name,再来编译看下结果。
@interface LGPerson : NSObject
@property(nonatomic, copy)NSString *name;
@end
@implementation LGPerson
@end
编译后我们再找到LGPerson看有没有什么变化:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
// @property(nonatomic, copy)NSString *name;
/* @end */
// @implementation LGPerson
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1);
}
// @end
这里看到了两处变化:
结构体LGPerson_IMPL多了一条数据:
NSString *_name;_I_LGPerson_name和_I_LGPerson_setName_分别是属性name的get和set方法
主要来探索下set的底层实现过程:
在之前配置的源码代码中,查找objc_setProperty的底层实现。
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
...
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
其中我们可以看到,属性的set方法,在底层实现中只做了两件事旧值release:objc_release(oldValue); 新值retain:newValue = objc_retain(newValue);
objc_setProperty
objc_setProperty方法是采用了
适配器设计模式,这种设计模式主要应用于“希望复用,但接口与复用环境要求不一致的情况”。在实际使用过程中,属性set方法的调用会有很多种情况(setName,setAge),而它们都会去做相同的事情旧值release,新值retain,但是属性set方法的调用也是各有不同,如果直接调用底层进行"旧值release,新值retain",会产生非常多中间临时变量,类库迁移复杂等问题,所以应该将这一过程抽取剥离出来进行共用,。objc_setProperty方法作为了一个中间层进行包装处理,本质可以认为是个
接口,供上层的set方法调用,通过接口参数:SEL _cmd, id newValue等,使得不同的set方法在下层不会相互不影响,做到上下层接口的隔离。
isa与对象的关联
在OC底层原理01:alloc方法底层探索中,alloc的三个重要步骤,
- 由系统计算出开辟的内存空间大小
- 申请开辟得出的大小内存空间,返回isa指针
- 将开辟的内存空间与要创建的对象进行关联
今天我们再深入了解下第三步,开辟空间得到的isa指针,是如何与类对象进行关联的?
union联合体
首先认识一个新的数据类型--union联合体(共用体)
- 什么是联合体 联合体也是C语言中的一种数据类型,它与结构体的不同在于:
- 联合体中所有的成员共用一块内存,每次只能使用其中一个成员
- 联合体中各变量是互斥的,对某一个成员赋值,也会覆盖其他成员的值
- 顺序从低地址开始存放
它的优点是所有成员共用一段内存,对内存的使用更加精细灵活,同时也节省了内存空间,但是同样因为内存空间太小,导致它的包容性弱,没有可拓展的能力。
案例: 我们定义一个类Car,它有四个属性表示四个方向
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
那么在创建这个类对象时,会为每一个BOOL类型的属性分配一个字节的内存空间,但是BOOL类型使用"0或1"就可以表示清楚,四个字节会存在内存浪费。使用union联合体,表示这四个数据只需要四位,也就是一个字节足够了。
union {
char bits;
// 位域
struct { // 1代表了属性占据的位数,从低位向高位排序
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
isa指针
通过alloc第三步initInstanceIsa的源码我们可以看到isa与对象关联的底层实现。

源码内容:
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 = isa_t((uintptr_t)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; // bits数据的初始化
// 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;
}
}
initIsa方法中传入的nonpointer为true,所以initIsa调用到了else里。其中可以看到,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
};
而isa_t就是一个联合体类型的数据,其中位域部分ISA_BITFIELD:
- nonpointer:表示是否对isa指针开启指针优化。0:纯isa指针;1:isa中包含了类对象地址、类信息、对象的引用计数等
- has_assoc:关联对象标志位,0没有,1存在
- has_cxx_dtor:该对象是否有C++或objc的析构器,如果有析构函数,则可以做析构逻辑,如果没有,则可以更快的释放对象。(objc中析构函数为dealloc)
shiftcls:存储类指针的值,对象继承自类,类也是有自己的isa,这里shiftcls中,保存的就是类指针的值- magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced:志对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放。
- deallocating:标志对象是否正在释放内存
- has_sidetable_rc:当对象引⽤计数⼤于10时,则需要借⽤该变量存储进位
- extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减1, 例如,如果对象的引⽤计数为10,那么extra_rc为9。如果引⽤计数⼤于10,则需要使⽤到下⾯的has_sidetable_rc。
他们在内存中的存储为:
isa指针与对象关联的过程
- isa位域的初始化
newisa.bits = ISA_MAGIC_VALUE;
这里我们也可以看到注释里的提示:
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
ISA_MAGIC_VALUE 对应的是MacOS中的0x001d800000000001ULL,下图是初始化的前后对比
其中因为联合体中,cls和bits是互斥关系,单独为cls赋值时,不会为bits赋值,但是在对bits赋值中,也会对cls的值进行追加.
上图分析下cls赋一个默认的初值0x001d800000000001.
所以说,newisa.bits = ISA_MAGIC_VALUE;初始化的结果:
- nonpointer赋值为1
- magic赋值为59
2.newisa.has_cxx_dtor = hasCxxDtor;因为我们没有对Person类中添加析构函数dealloc,所以这里会传一个flase
3.newisa.shiftcls = (uintptr_t)cls >> 3;
这句代码,就是将cls与对象关联起来的关键部分,在介绍shiftcls我们已经提到了,这里保存的是类的指针值,类同样也有自己的isa,所以类的信息同样保存在自己的shiftcls中,(uintptr_t)cls将cls强转后的数据,右移3位,摒弃掉类isa中的nonpointer、has_assoc、has_cxx_dtor前三个数据后,赋值给对象isa的shiftcls.

ISA_MASK验证
利用isa_t位域中的ISA_MASK,& 与上得到obj结果的isa.即当shiftcls关联结束后,会回到obj->initInstanceIsa(cls, hasCxxDtor);
这里可知,alloc方法的底层实现中,申请由系统开辟的内存空间得到的isa,与它的类关联在了一起.