OC底层原理03-对象的本质

1,161 阅读8分钟

对象的本质

上篇文章提到,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的三个重要步骤,

  1. 由系统计算出开辟的内存空间大小
  2. 申请开辟得出的大小内存空间,返回isa指针
  3. 将开辟的内存空间与要创建的对象进行关联

今天我们再深入了解下第三步,开辟空间得到的isa指针,是如何与类对象进行关联的?

union联合体

首先认识一个新的数据类型--union联合体(共用体)

  • 什么是联合体 联合体也是C语言中的一种数据类型,它与结构体的不同在于:
  1. 联合体中所有的成员共用一块内存,每次只能使用其中一个成员
  2. 联合体中各变量是互斥的,对某一个成员赋值,也会覆盖其他成员的值
  3. 顺序从低地址开始存放

它的优点是所有成员共用一段内存,对内存的使用更加精细灵活,同时也节省了内存空间,但是同样因为内存空间太小,导致它的包容性弱,没有可拓展的能力。

案例: 我们定义一个类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指针与对象关联的过程

  1. 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;初始化的结果:

  1. nonpointer赋值为1
  2. 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,与它的类关联在了一起.

参考推荐

isa与类关联的原理