OC对象原理

2023-01-06  本文已影响0人  星星杨

1、alloc底层原理

1.1 三种方式调试底层源码:

1: 符号断点 libobjc.A.dylib`objc_alloc:

2: 汇编 跟流程 - 符号断点: objc_alloc



3: 符号断点 确定未知 : libobjc.A.dylib`+[NSObject alloc]:



以上,都定位到libobjc这个库下面,所以可以通过苹果源码开源地址下载objc这个库,来一探alloc底层实现原理;
苹果源码下载地址:https://opensource.apple.com/releases/


通过上图可以分析得出,alloc具备开辟内存空间的能力,因为p1、p2、p3都指向同一片内存地址;&p1、&p2、&p3分别代表他们的指针地址;

接下来就通过源码看一下其内部具体实现,以下代码都摘抄自objc->NSObject.mm文件中

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

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

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

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

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_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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);
}

执行calloc之后就会分配一个内存地址




执行完initInstanceIsa之后会发现,此时obj已经成功绑定上Person对象了;


obj绑定上Person类
_class_createInstanceFromZone这一步是我们的核心方法,我们会发现,它起到的作用是开辟内存+绑定我们的对象类名;
alloc方法调用,汇编查看

通过查看汇编调用流程,因为在编译阶段,链接完objc之后再经过llvm的编译处理,我们会发现alloc会通过llvm重定向到objc_alloc;

修改alloc的imp指向
sel跟imp的关系,其实就是一本书的目录中的一个章节,sel代表章节名称,imp代表页码,imp指向实际的内容,修改了imp,就会导致我们调用的实际方法进行改变; llvm判断是alloc执行重定向 llvm指定调用objc_alloc
llvm动态生成函数

之所以这么处理,也可以看出苹果对特殊函数(类似alloc开辟内存这种)处理的看重程度,防止被人编译源码,导致的一些问题,就把特殊函数的处理放在llvm中,防止别人随意篡改;


llvm尝试特殊函数的消息转发
处理完特殊函数之后再走一遍正常流程

这里也有解释为什么callAlloc会执行两次的原因,所以alloc的基本流程如下:
objc_alloc->callAlloc->alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone

2、内存分配

在_class_createInstanceFromZone中有这么一行代码
size = cls->instanceSize(extraBytes);
获取当前要开辟的内存大小,一步步点进去会发现,它其实是以16进制对齐的方式,x+15 & ~15,相当于加上15,再把后四位抹0;

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

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

alloc核心方法_class_createInstanceFromZone
cls->instanceSize --》先计算出需要的内存空间大小
calloc --》 向系统申请开辟内存,返回地址指针
obj->initInstanceIsa --》关联到相应的类

影响对象开辟内存大小的因素

分析可能因素:属性、方法、实例变量、协议、分类、扩展

啥都没有时需要的内存大小 一个属性时需要的内存大小
定义一个方法时需要的内存大小
定义一个成员变量时需要的内存大小
因为 属性-方法(set/get)=变量,所以真正影响内存大小的因素是变量
其实根据我们的常识,方法是定义在方法区,而我们的对象是开辟在堆区,而指针的开辟是在栈区,所以我们的方法定义根本不影响我们类对象的大小,其他协议、分类、扩展其实也一样,不会影响对象内存大小;

用x/4gx打印p1的地址,会发现第一个内存地址,其实就是isa地址,通过Class强转或者与上0x00007ffffffffff8ULL掩码,都可以得到真实的class name;

3、内存对齐

先看下面两张图



同样的成员变量,只是因为书写顺序不一样,导致需要开辟的内存空间大小就不一样,why?



主要是因为类的本质其实也就是结构体,像下面俩结构体,LGStruct1就需要开辟24个字节的内存,而LGStruct2只需要16个字节的内存就可以;

因为在内存对齐的原则中:

struct LGStruct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]
    int c;          // 4    (9 10 11 [12 13 14 15]
    short d;        // 2    [16 17] 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] 16
}struct2;

struct LGStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct LGStruct1 str;
}struct3;

前文有提到过,对象在开辟内存的时候是以16字节对齐的方式,那为什么这里的属性又说以8字节对齐呢?



上图可以看到成员变量是8字节对齐,对象和对象之间是16字节对齐;
那是因为一个对象至少都有一个属性isa,而在我们实际开发中,有属性的概率是90%以上,苹果为了保证读取的效率,也同样遵循空间换时间的原则,在新的版本中都以16字节方式对齐,这也可以说明当前的iPhone设备内存越来越大,之前16GB、32GB内存的手机越来越卡的原因;
第二个上文也有提到,对象其实是通过calloc进行内存开辟的,通过阅读calloc底层源码会发现,它其实也是16字节对齐,对当前的size + 15 之后先右移4位再左移4位;


calloc底层实现

附上一张牛逼plus的kc画的instanceSize流程图;


4、对象本质

我们可以使用下面命令行把目标文件编译成c++文件

clang -rewrite-objc main.m -o main.cpp

对象的本质其实就是个结构体,NSObject_IMPL表示继承NSObject的所有属性


4.1 结构体和联合体区别

// 4 * 8 = 32  0000 0000 0000 0000 0000 0000 0000 1111
// 4 位
// 1 字节 3倍浪费
struct LGCar1 {
    BOOL front; // 0 1
    BOOL back;
    BOOL left;
    BOOL right;
};

// 位域 使用1位内存就可以
// 互斥
union LGCar2 {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};

结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间;
isa使用union,为了兼容不同的版本;

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
}

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD
: 1;
 uintptr_t nonpointer
uintptr_t has_assoc
uintptr_t has_cxx_dtor
uintptr_t shiftcls : 33; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)

isa里面包含的信息:
nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc:关联对象标志位,0没有,1存在
has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls:
存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针,用isa地址与上 is_mask掩码就可以查看当前isa指向。
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间 weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放。 deallocating:标志对象是否正在释放内存
has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

4.2 isa走位

元类就是meta,它也是有继承链的;继承链这一块就不多做缀述了,层层继承,层层关联;

继承链
isa走位就比较牛逼了,
对象的isa指向类;
类的isa的指向元类;
元类的isa指向根类的元类;
根类的元类的isa指向本身;

即LGPerson对象->LGPerson.class->LGPerson.meta->NSObject.meta
探索isa走位 isa走位图

最后附上一张完整alloc的流程图,同样也是摘抄自666归零666;


666归零666
上一篇下一篇

猜你喜欢

热点阅读