OC对象底层的探索

2021-09-20  本文已影响0人  iOS之文一

OC对象的底层分析

OC底层原理探索文档汇总

从对象的创建过程、对象的底层结构两方面来分析对象

一、对象的创建

下面代码就是在对对象进行创建,可以看出包括自定义类的alloc 、init、new三个方法的底层实现。以及NSObject的创建。接下来分别对此进行讨论。

NSPerson *person1 = [[NSperson alloc] init];
NSPerson *person2 = [NSperson new];
NSObject *object = [[NSObject alloc] init];

1.1 alloc 的分析

1.1.1 案例分析

源码:

源码.png

执行结果:

结果.png

分析:

1.1.2 分析过程:

通过查看源码了解到alloc在底层是如何创建空间,并返回一个对象的。

0、查找源码

在objc源码中查找底层源码。

【第一步】 跳转到alloc方法

//alloc第一进入的就是这个方法,注意这里还是OC方法,而不是C函数
+ (id)alloc {
    return _objc_rootAlloc(self);
}

tips:此时仍然是OC方法,而不是C函数,所以搜索时可以通过"alloc {"来查找

【第二步】 跳转到_objc_rootAlloc方法

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

【第三步】 跳转到callAlloc方法

// 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;
    //优化
    //是否存在alloc/allocWithZone,类或父类默认会有(实现存储在元类中)
    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);
    }
    //NSObject的创建会进入这里,它是由系统自己调用的。
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

tips:

  1. 遇到分叉口,需要通过调试断点来判断从哪条分支执行
  2. 经过调试发现该方法会执行两次,第一次是执行alloc语句,也就是调用NSObject的alloc方法,第二次会执行objc_rootAllocWithZone(),具体的两次执行过程请看下面的NSObject与自定义
  3. slowpath和fastpath的具体使用过程请查看slowpath和fastpath的认识

【第四步】 跳转到_objc_rootAllocWithZone方法

NEVER_INLINE
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);
}

【第五步】 跳转到_class_createInstanceFromZone方法

到此就找到了alloc实现的底层源码,该方法就是真正创建空间的方法。共有三步,1)计算内存空间大小,2)申请内存空间,3)关联isa和类,下面分别进行了解。

代码:

/*
 开辟空间
 关联类
 返回对象
 */
//当前zone是废弃掉了
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;
    // 1:计算要开辟多少内存
    /*
     extraBytes是0,后续看下这里什么时候不是0
     就算什么都没有最起码也会有isa,这是8个字节,64位
     */
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 2;怎么去申请内存
        //通过calloc来申请空间
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    // 3: 申请之后将空间赋给对象变量,也就是得到isa
    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);
    }
    //obj先是创建了个内存空间,是内存空间的地址值,其isa与类进行关联后,就变为了对象,所以此处返回的是一个对象指针
    //4、返回obj
    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

1、计算内存空间

调用代码

size = cls->instanceSize(extraBytes);

说明:

点进去查看具体实现

【第1步】 拿到字节对齐后的对象大小

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;
    }

通过断点调试,会执行到cache.fastInstanceSize(extraBytes);用来快速计算内存大小

【第二步】 执行cache.fastInstanceSize(extraBytes);

#define FAST_CACHE_ALLOC_DELTA16      0x0008

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
            //删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节
            //进行16字节对齐
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

核心代码就是进行16字节对齐,size是实例大小

这个size是通过_flags & FAST_CACHE_ALLOC_MASK计算得到的。
该内容需要通过了解类结构后才能明白,类的结构底层学习请看
此时可以先简单认为size为对象的成员变量大小即可。

【第三步】 字节对齐

代码:

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

此处简单解释:
1、该函数的作用是16字节对齐,也就是对一个数以16倍数向上取整
2、x + size_t(15)的作用是向上取整,而且是16的整数倍。试想,任何一个大于1的数再加上15,肯定会大于16而向前进一位,所以这里+15就是以16的整数倍向上取整。
3、&size_t(15)的作用是抹掉后四位,试想,上一步的操作已经得到了进位,也就是取整完成了,那么此时剩下的不够进位的数据就可以抹掉了。 size_t(15)就是对后四位取0,将源数据与其相与,就可以抹掉后四位了。

也可以通过代入几个数,真实的计算一下,这个比较简单,不再赘述了。
这里仅粗略概括性的进行解释,关于字节对齐的详细内容请查看苹果的内存对齐原理

总结: 传入的extraBytes+对象属性大小进行16字节对齐后得到的数据就是需要开辟的内存大小。

2、申请内存空间,返回内存空间地址

代码

// 2;怎么去申请内存
//通过calloc来申请空间
obj = (id)calloc(1, size);

通过calloc来申请内存,size是内存大小,此处返回的是内存的地址空间,并不是真正的对象指针

3、关联类,返回对象地址

代码:

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
        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
        //arm64位进入这里,具体的后面学习,这里只要知道是在对isa结构体的成员变量进行初始化
        //掩码
        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;
        //把类信息存储到isa中,也就是类与isa的绑定
        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的认识,但也不妨碍该函数的解读。具体的认识可以在第三大节对象与类的关系中详细了解。

在此处只要知道newisa.shiftcls = (uintptr_t)cls >> 3;这条语句就将类信息赋值到isa中,通过这种方式将类与该对象的isa关联起来。

流程图:
alloc源码查找流程图


alloc源码流程图.png

1.1.3 总结:

1、alloc开辟的空间大小是16的整数倍,因为采用的是16字节对齐算法,最小不小于16个字节
2、影响内存大小的因素是属性
3、alloc生成了一个对象,并且返回的是一个对象地址,而不是内存空间地址
4、alloc通过三步创建:1)计算空间大小;2)开辟空间;3)将isa与类绑定

1.2、init的分析

主要分析init进行初始化做了什么操作?返回的是什么?

1.2.1 查看源码

类方法

代码:

// 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;
}

说明:此处可以看到实例方法也是什么都没做,只是直接返回当前对象

tips:
既然init方法没有进行任何操作,那么它的作用是什么?
1、创建init 方法只是为了更好的复用,便于给其他的初始化方法调用,本身是没有任何操作的。
2、这里的init是一个最简单的构造方法,主要是用于给用户提供构造方法入口

1.2.2 总结

init没有进行任何操作,直接返回当前对象。

1.3 new的分析

主要分析new进行初始化做了什么操作?,返回的是什么?

代码:

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

说明:
new函数中直接调用了callAlloc函数,且调用了init函数,所以可以得出new 就等价于 [alloc init]的结论

总结:

如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

tips:
但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。

1.4 总结:

知识点总结:
1、alloc开辟的空间大小是16的整数倍,因为采用的是16字节对齐算法,最小不小于16个字节
2、计算的内存大小是extraBytes+实例大小,实例大小是类所有属性的实际占用内存大小
3、alloc的目的是开辟内存,并返回的是一个对象地址,而不是内存空间地址
4、alloc创建三步骤:1)计算空间大小;2)开辟空间;3)将类与isa绑定
5、init没有进行任何操作,直接返回当前对象
6、对象的创建开辟的内存采用16字节对齐,对象实际占用的内存大小是8字节对齐
7、苹果自动进行的属性重排会按照属性的内存从大到小到大来排列,这样可以提高性能
8、内存对齐算法有两种,1)拿到最后三位为000,其他为111的值,这样就任何数值与他相与都可以将后三位抹零。2)先右移再左移,右移将后四位抹零,左移恢复其他位数的所在的位置
9、NSObject的alloc调用:NSObject的alloc是系统调用的,并且底层会转化为objc_alloc。基础类的alloc会调用两次,一次是调用的NSObject的alloc,一个是自己的类,原因就是底层会有一个判断,如果不是NSObject类则会执行一次alloc,而这个判断本身就会执行这个NSObject的alloc

一句话总结底层原理:
1、alloc的目的是开辟内存,返回的是一个对象地址,而不是内存空间地址,并且开辟内存大小是16字节对齐,init没有进行任何操作,直接返回当前对象
2、对象的创建开辟的内存采用16字节对齐,真正需要的内存大小是8字节对齐,为了优化性能,苹果内部会进行属性重排

其他知识点总结:
1、slowpath和fastpath这两个宏定义可以告诉编译器最有可能执行的分支,减少指令跳转,可以优化性能
2、位运算,左移是加,右移是减,这里的移可以看做是指1的移动,比如1111>>3就为0001,0111<<3就为0011 1000。
3、new == alloc+init,开发中不建议使用new

2、对象的底层结构

分析对象的底层结构,对象的本质,上层对对象的操作在底层是如何实现的。

2.1 编译cpp文件

clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器,它主要用于底层编译,将.m文件编译成.cpp文件,通过它我们可以更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。

在main中先创建一个类LGPerson,有一个属性name,还有一个变量age

代码:

代码.png

终端命令:

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

编译得到的对象结构

2.2 对象的本质

2.2.1 类结构

打开编译好的main.cpp,找到LGPerson类的定义,发现LGPerson在底层会被编译成 struct 结构体

LGPerson_IMPL结构体

//LGPerson的底层编译
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
    NSString *_name;
    int age;
};

说明:

查看NSObject_IMPL结构体

//NSObject 的底层编译
struct NSObject_IMPL {
    Class isa;
};

说明:

Class的结构体

typedef struct objc_class *Class;

总结:

2.2.2 对象结构

objc_object结构体

//可以看到,objc_object中就是一个isa
struct objc_object {
private:
    isa_t isa;

public://外部可以获取isa
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
}

自定义结构体

#ifndef _REWRITER_typedef_NSStudent
#define _REWRITER_typedef_NSStudent
typedef struct objc_object NSStudent;
typedef struct {} _objc_exc_NSStudent;
#endif

extern "C" unsigned long OBJC_IVAR_$_NSStudent$_age;
struct NSStudent_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
};

说明:

2.2.3 总结

2.3 属性的理解

看一下属性的setter方法是如何调用的
我们会创建很多的属性,每个属性都有自己的方法,这样每次都创建一个方法就会开辟很多的内存,因此苹果其实只用了一个objc_setProperty来实现方法,通过传入不同的值来体现方法的不同之处。

属性底层.png

开始查找

objc_setProperty.png reallySetProperty().png

2.4 总结

3、isa的认识

上文我们看到在objc_object结构体中只有一个isa,因此我们分析一下isa

3.1 isa类型

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
};

说明:

3.2 查看位域ISA_BITFIELD

位域.png 存储格式.png

3.3 总结

  • isa_t采用联合体位域的方式存储信息,有两种存储形式,一个是直接存储Class信息,还有就是存储bits,而bits就包含了Class,二者是互斥关系
  • 一般来说系统的类采用直接存储Class信息,自定义类存储bits信息
  • 我们在获取isa时其实内部会进行处理,其实获取的是Class信息
  • 自定义类的isa_t的类信息存储在bits中的shiftcls位域
  • cls与isa的关联原理就是在isa指针中的shiftcls位域中存储类信息
上一篇 下一篇

猜你喜欢

热点阅读