OC对象底层的探索

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

分析:

  • 通过%@打印出来的是相同的地址空间
  • 通过%p打印出来的是不一样的对象变量地址
  • 因此可以看出来通过alloc其实也已经创建了对象
  • alloc创建了一个内存空间,返回了一个对象地址

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

说明:

  • 断点查看此处extraBytes是0
  • 这里虽然传入的是额外的字节数是0,但是还需要加上创建的内存空间大小,因为会有类的信息本身,比如肯定会存在的isa就占有8个字节。

点进去查看具体实现

【第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关联起来。

  • 1、给一个对象初始化一个isa指针
  • 2、指针与cls类进行 关联
  • 3、关联之后objc就是一个对象了,而不仅仅是一个内存空间

流程图:
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的所有属性
    • 这种写法是伪继承,是结构体的继承方式
    • 伪继承的方式是直接将父类的结构体定义为LGPerson_IMPL中的一个成员
    • 意味着LGPerson拥有NSObject的所有成员
  • 第二个第三个是LGPerson自身的属性

查看NSObject_IMPL结构体

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

说明:

  • 这个NSObject_IMPL是NSObject的底层编译
  • 说明自定义类会拥有NSObject的所有成员变量
  • NSObject中肯定包含有isa,因此可以说明自定义类的isa是继承自NSObject

Class的结构体

typedef struct objc_class *Class;

  • 在ojbc源码中搜索发现Class结构体是以objc_class为模板创建的

总结:

  • 从上往下推,这就说明了所有的类结构体都是基于objc_class为模板创建的
  • 只不过NSObject_IMPL结构体是直接继承自Class结构体,也就是包含了所有Class结构体中的所有信息
  • 而我们自定义类比如NSPerson_IMPL结构体也是伪继承自NSObject_IMPL的
  • 这样就做到了所有的类都是都是以objc_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;
};

说明:

  • 可以看到类的结构体中只有一个isa,其他的都没有
  • 对象的所有属性方法成员都是保存在类或元类中的,并且通过isa来进行获取

2.2.3 总结

  • OC对象的本质是一个结构体,通过objc_object结构体为模板创建的
  • 对象结构体内仅有isa,其他的属性、方法列表这些都存储在类或元类中,而不是对象中
  • 继承关系存在于类结构体中,通过伪继承将父类的结构体作为子类结构体的成员,以此在底层达到继承的作用

2.3 属性的理解

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

属性底层.png

开始查找

objc_setProperty.png
reallySetProperty().png
  • 可以看到原理就是新值retain,旧值release
  • 属性进行设置时,其实进入的是objc_setProperty这个方法,
  • 因为所有属性进行的设置操作都是一样的,每个属性都有一个setXXX方法会占用更多的内存空间
  • 所以就用objc_setProperty作为中间函数把共有的操作统一执行,每个属性只执行各自特殊的内容
  • 苹果采用适配器设计模式,对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的
  • 以后源码查找时就用objc_setProperty来查找的

2.4 总结

  • OC对象的本质 其实就是 结构体,通过objc_object作为模板来创建的
  • objc_object中只有一个isa成员,这个对象的其他成员都是在类的底层结构体objc_class中的,所以我们看属性方法这些就要去看类的信息即可
  • 上层类的继承关系是通过底层结构体的继承来实现的,将结构体的第一个成员定义为父类的结构体,通过这种伪继承方式来实现
  • 自定义类是伪继承自NSObject_IMPL结构体的,NSObject_IMPL又是伪继承自Class的,而Class结构体是以objc_class结构体为模板创建的
  • 因为获取isa对外反馈的其实只是Class,因此getIsa()得到的其实就是Class信息,因此可以直接通过Class强转
  • objc_setProperty()函数作为中间函数将所有setter方法的共有操作统一执行,只将不同属性的差异点作为参数传入。达到更加高效率的操作

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

说明:

  • isa_t有两个成员,cls和bits,共同占用了这一段内存空间,因此他们是互斥的
    • 说明isa_t有两种存储形式,一个是直接存储类信息,一个是存储bits,bits中会带有Class信息
    • 直接存储类信息一般来说是系统创建的类,比如元类就只有Class信息
    • 存储bits是自定义的类,除了Class信息还会带有其他的信息
    • 通过cls初始化得到的isa_t,bits没有默认值,通过bits初始化得到的isa_t,cls会有默认值,因为bits中本来就带有class信息
    • 因此我们知道在初始化isa指针时,有两种初始化方式
  • ISA_BITFIELD的内容解释了这段内存的每位存储的是什么信息
  • isa_t使用联合体位域,可以优化内存空间
  • 一个isa有8个字节,也就是64位,是可以存储很多信息了

3.2 查看位域ISA_BITFIELD

位域.png
  • nonpointer,表示是否是自定义的类,占1位
    • 0:纯isa指针
    • 1:不只是类对象地址,isa中包含了类信息、对象的引用计数等
    • 在初始化isa的方法中可以看到如果是nonpointer=0,说明是纯isa指针,只包含Class信息
  • has_assoc表示关联对象标志位,占1位
    • 0:没有关联对象
    • 1:存在关联对象
  • has_cxx_dtor 表示该对象是否有C++/OC的析构器(类似于dealloc),占1位
    • 1:有析构函数,需要做析构逻辑
    • 0:没有析构函数,可以更快的释放对象
  • shiftcls表示存储类的指针的值(类的地址), 即类信息
    • arm64中占 33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针
    • x86_64中占 44位
  • magic 用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6位
  • weakly_refrenced是 指对象是否被指向 或者 曾经指向一个ARC的弱变量
    • 没有弱引用的对象可以更快释放
  • deallocating 标志对象是是否正在释放内存
  • has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位
  • extra_rc(额外的引用计数) ,用来存储引用计数的值
    • 如果对象的引用计数为10,那么extra_rc为9(这个仅为举例说明),实际上iPhone 真机上的 extra_rc 是使用 19位来存储引用计数的
    • 如果太大了,就将其折一半存储到散列表中
存储格式.png

3.3 总结

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

你可能感兴趣的:(OC对象底层的探索)