对象的底层探索(上)

572 阅读7分钟

准备工作

汇编基础须知

  • b bl:跳转指令,方法调用
  • ret:函数的返回
  • ; : 注释

三种找寻源码的探索思路

  1. 断点

    使用真机在alloc下断点,按住control点击step in,发现alloc方法在libobjc.A.dylib

  2. 符号断点

    在alloc下断点,断点停住时,Xcode左下角点击“+”,选择Symbolic Breakpoint,输入alloc,继续运行,发现alloc方法在libobjc.A.dylib

  3. 通过汇编

    在alloc下断点,断点停住时,在Xcode菜单栏中选择Debug->Debug WorkFlow->Always Show Disassembly,按住control点击step in,最终发现alloc方法在libobjc.A.dylib,

alloc方法探索

查看objc源码进行分析

通过查询objc源码,跳转到alloc方法实现,来查看对象调用alloc时,调用了底层的哪些方法。

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

通过源码的跳转,初步分析alloc的调用顺序为alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_objc_rootAllocWithZone

接下来就单步调试一下alloc方法,看看实际的调用顺序是否如我们所分析。

调试objc源码验证分析

通过断点调试objc源码,发现当对象调用alloc方法时,最先进入的是callAlloc,然后才是到源码中的alloc方法里,然后才按照上一步设想的调用顺序执行。

探索最先callAlloc的原因

首先在调用[对象 alloc]出打断点,然后打开Always Show Disassembly。

image.png 发现在调用alloc时,先调用了objc_alloc,这个方法并没有在我们之前查看源码时所分析的调用过程中。control+单步调试,发现确实进入了objc_alloc方法。那继续在objc源码中搜索objc_alloc方法。

id objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

objc_alloc方法里调用了callAlloc,那调试时最先进入callAlloc方法就说的通了。但是为什么是objc_alloc呢?

在objc的源码objc-runtime-new中,有一个fixupMessageRef方法,当方法名为alloc时,调用的imp设为objc_alloc方法的地址。先调用objc_alloc的谜题也随之解开。

image.png

alloc方法的实际调用顺序

objc_aaloc->callAlloc->->alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_objc_rootAllocWithZone

汇编调试验证实际调用顺序

添加一个符号断点objc_alloc,再次调试alloc方法。

objc_alloc

首先进入到objc_alloc方法中,发现其汇编中跳转到了objc_msgSend方法中。读取x0寄存器,发现返回值是LGPerson,代表是调用者是LGPerson,再查看x1寄存器,其值为alloc,说明objc_msgSend函数是通过LGPerson调用alloc。

接下来,我们就对alloc下一个符号断点继续跟踪。 image.png

alloc

alloc中调用了_objc_rootAlloc,step into进入该函数继续调试。 image.png

_objc_rootAlloc

_objc_rootAlloc中调用了_objc_rootAllocWithZone,同样进入该函数中仅需调试。 image.png

_objc_rootAllocWithZone

查看该函数的返回值,即x0寄存器,发现是LGPerson。 image.png 查看该函数源码,调用的是_class_createInstanceFromZone,查看该函数,发现该函数会对一个obj开辟内存空间并返回。

至此,alloc方法的底层调用顺序应为:objc_alloc->obj_msgSend->alloc->_objc_rootAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone,最终的作用就是创建一个对象并为其开辟内存空间,最后返回该对象。

callAlloc哪里去了

根据我们刚才对源码、汇编调试的结果,发现源码中有对callAlloc函数的调用,但汇编中没有,这是由于编译器优化的原因。

init方法探索

同样对init方法打个符号断点,进入汇编后读取寄存器,发现init方法只是返回了LGPerson,并没有什么其他的操作。 image.png 查看源码,发现也只是返回obj对象而已。

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

所以init方法只是用来给开发者重写用于做一些开发者自己需要的操作的方法。

字节对齐

对象内存对齐

_class_createInstanceFromZone

static ALWAYS_INLINE id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{

    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    //计算需要的大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;
    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }
    
    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

查看_class_createInstanceFromZone函数的实现,其中size = cls->instanceSize(extraBytes);是计算对象所需要的空间,我们查看其实现。

inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }
        
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

当size<16时size=16,说明对象最小大小是16,推测对象是以16字节对齐。其中alignedInstanceSize函数就是对象的字节对齐算法。

uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
}
    
static inline uint32_t word_align(uint32_t x) {
    // WORD_MASK宏的值在64位下是7,32位下是3
    return (x + WORD_MASK) & ~WORD_MASK;
}

(x + WORD_MASK) & ~WORD_MASK;就是字节对齐的算法,原理是7的二进制位0111,~7为1000,所有数字与1000进行&操作时,低三位都为0,而低三位为0必为8的整数。

由此得出结论,在调用instanceSize创建对象时使用8字节对齐。

fastInstanceSize

size_t fastInstanceSize(size_t extra) const
{
    ASSERT(hasFastInstanceSize(extra));

    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
        size_t size = _flags & FAST_CACHE_ALLOC_MASK;
        // remove the FAST_CACHE_ALLOC_DELTA16 that was added
        // by setFastInstanceSize
        return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
}

instanceSize函数中,创建对象的对齐方式为8字节对齐,但是在取缓存的fastInstanceSize函数中调用的却是align16,16字节对齐的方法。

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

计算对象大小以8字节对齐,那开辟内存空间时调用了calloc,接下来就查看一下calloc函数的实现,是否是以16字节对齐的。

calloc

calloc函数的实现在libmalloc中,并且其最终会到_nano_malloc_check_clear函数中。其中计算对象开辟空间大小的关键代码为

size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);

查看segregated_size_to_fit的实现

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
    *pKey = k - 1;													// Zero-based!

    return slot_bytes;
}

其中NANO_REGIME_QUANTA_SIZE=16,SHIFT_NANO_QUANTUM=4,将size+15,右移4位,再左移4位,将低4位变为0,就是16字节对齐。

由此,在为对象开辟内存空间时,是以16字节对齐的方式。

为什么是16

其原因应该是在以“空间换时间”的操作下,权衡空间和时间因素选择的值。

结构体内存对齐

规则

  1. 数据成员对⻬规则:结构(struct)的第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储)。
  2. 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储)。
  3. 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。

实例

struct LGStruct1 {
    double a;   // 8字节 [0-7]
    char b;     // 1字节 [8]
    int c;      // 4字节 [9,10,11,12,13,14,15] 根据规则1,int占4字节,c要在4的倍数的位置存储,所以9,10,11都为空,从12开始存储
    short d;    // 2字节 [16 17],根据规则3,以最大的double8字节取整,结构体占24个字节
}struct1;

struct LGStruct2 {
    double a;   // 8字节 [0-7]
    int b;      // 4字节 [8,9,10,11]
    char c;     // 1字节 [12]
    short d;    // 2字节 [13,14,15] 根据规则1,short占2字节,d要在2的倍数的位置存储,所以13都为空,从14开始存储,根据规则3,以最大的double8字节取整,结构体占16个字节
}struct2;

struct LGStruct3 {
    double a;   // 8字节 [0-7]
    int b;      // 4字节 [8,9,10,11]
    char c;     // 1字节 [12]
    short d;    // 2字节 [13,14,15]
    int e;      // 4字节 [16,17,18,19]
    struct LGStruct1 str; // 根据规则2,从LGStruct1最大元素大小的整数倍开始存储,最大是double,所以要存在8的倍数的位置
    //  LGStruct1中double a: 8字节 [24-31]
    //  LGStruct1中char b:   1字节 [32]
    //  LGStruct1中int c:    4字节 [33-35为空,36,37,38,39]
    //  LGStruct1中short d:  2字节 [40 41]
    // 根据规则3,以8取整,结构体占48字节
}struct3;

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        // 执行结果为:24 16 48
        NSLog(@"%lu %lu %lu",sizeof(struct1),sizeof(struct2),sizeof(struct3));
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

对象的本质

输出main.cpp

使用clang将main.m输出成main.cpp。

clang -rewrite-objc main.m

查看main.cpp

typedef struct objc_object LGPerson;

 struct NSObject_IMPL {
     Class isa;
 };

struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	NSString *_name;
};

搜索LGPerson,发现LGPerson是objc_object类型的结构体,除了成员变量外,结构体中有一个NSObject_IMPL结构体,其中存储的是isa指针。可推测对象中存储有isa和成员变量的值。