56559d115392d1bbcc8a857ce2d34822
iOS 高级之美(五)—— 类结构分析上篇

一、这里补充关于前面内容的一些细节

1.1 alloc 的一个小细节

我们在探索 alloc 流程的时候,有一个分支流程是这样定义的:

if (fastpath(cls->canAllocFast())) {
    // No ctors, raw isa, etc. Go straight to the metal.
    bool dtor = cls->hasCxxDtor();
    id obj = (id)calloc(1, cls->bits.fastInstanceSize());
    if (slowpath(!obj)) return callBadAllocHandler(cls);
    obj->initInstanceIsa(cls, dtor);
    return obj;                
}
  • fastpath(!cls->ISA()->hasCustomAWZ()) 的决定条件就是你是否有重写allocWithZone的方法

  • 第二个判断fastpath(cls->canAllocFast()) 就是关于宏定义的设置

    bool canAllocFast() {
        assert(!isFuture());
        return bits.canAllocFast();
    }

  // 一般流程都会走这个 false 的返回
    bool canAllocFast() {
        return false;
    }

    bool canAllocFast() {
        return bits & FAST_ALLOC;
    }
  • 那么为什么是这个呢?我们都会走上面返回 false。这时候我们就需要去查看条件控制

  • #if FAST_ALLOC 这个宏定义决定走向

  • #define FAST_ALLOC (1UL<<2) 而这个宏定义在外层还加了一层判断

#if !__LP64__
....
#elif 1
......
#else

#define FAST_ALLOC              (1UL<<2)
....
#endif
  • 我们进入 canAllocFast 底层之后会发现有一个宏 FAST_ALLOC ,因为这个宏永远没有define,所以就会有以下结果:

  • 也就是说 callAlloc 方法会进入到红色圈中区域:

1.2 联合体互斥

在分析 isa 的时候,我们知道 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 的时候,一个分支是赋值 cls 属性,一个分支是赋值 bis 属性了。

1.3 类和元类创建的时机

我们在探索类和元类的时候,对于其创建时机还不是很清楚,这里我们先抛出结论:

  • 类和元类是在编译期创建的,即在进行 alloc 操作之前,类和元类就已经被编译器创建出来了。

那么如何来证明呢,我们有两种方式可以来证明:

  • LLDB 打印类和元类的指针

  • Command + B 编译项目,然后使用 MachoView 打开程序二进制可执行文件查看

二、指针内存偏移

2.1 值拷贝

int a = 10; 
int b = 10; 
LGNSLog(@"%d -- %p",a,&a);
LGNSLog(@"%d -- %p",b,&b);   

输出:
KC打印: 10 -- 0x7ffeefbff4fc
KC打印: 10 -- 0x7ffeefbff4f8

我们观察上面的代码,虽然整型变量 ab 都是被赋值为 10,但是 ab 内存地址是不一样的,这种方式被称为 值拷贝

2.2 引用拷贝

LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [LGPerson alloc];
LGNSLog(@"%@ -- %p",p1,&p1);
LGNSLog(@"%@ -- %p",p2,&p2);    
输出:
KC打印: <LGPerson: 0x100544c30> -- 0x7ffeefbff4f8
KC打印: <LGPerson: 0x100545a20> -- 0x7ffeefbff4f0

显然,这里 p1p2 对象不光自身内存地址不一样,连指向的对象的内存地址也不一样了,这种方式被称为 引用拷贝

用一幅图可以总结上面两个例子:

三、类的结构

OC 中的类其实也是一种对象,怎么来证明呢,很简单,我们只需要用 clang 命令重写我们的 OC 代码即可:

typedef struct objc_class *Class;

我们再在 libObjc 的源码中找到 objc_class 的详细定义:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;    // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    ...省略后面的代码...    
}

通过 objc_class 的定义,我们不难看出,objc_class 是继承于 objc_object 的,那么本质上就说明类是一种对象,并且第一个属性是从 objc_object 上继承而来的 isa。除了 isa,类还包含了

  • superclass 父类:表达继承关系
  • cache : 方法缓存重要结构体
  • bits : 存储数据的结构体

总结:类是一种对象,帮我们定义了一些属性和方法。同时,由于 OC 在底层是封装 C的实现的,也就有下面的对应关系

C OC
objc_object NSObject
objc_class NSObject(Class)

四、类的属性存储

OC 中的类都会有属性以及成员变量,那么它们究竟是如何存在于类里面的呢?

这里我们需要先对类的内存结构有一个清晰的认识:

类的内存结构 大小(字节)
isa 8
superclass 8
cache 16

前两个大小很好理解,因为 isasuperclass 都是结构体指针,而在 arm64 环境下,一个结构体指针的内存占用大小为 8 字节。而第三个属性 cache 则需要我们进行抽丝剥茧了。

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4
}

从上面的代码我们可以看出,cache 属性其实是 cache_t 类型的结构体,其内部有一个 8 字节的结构体指针,有 2 个各为 4 字节的 mask_t。所以加起来就是 16 个字节。也就是说前三个属性总共的内存偏移量为 8 + 8 + 16 = 32 个字节,32 是 10 进制的表示,在 16 进制下就是 20。

我们可以用 LLDB 命令来探索下类结构的第四个属性 bits

@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *nickName;

@end

LGPerson *person = [LGPerson alloc];
Class pClass     = object_getClass(person);

我们先拿到类对象 pClass,然后在这一行打上断点,然后在 LLDB 上下文环境中输入:

(lldb) x/4gx pClass

然后会有输出:

我们为了得到 bits 的指针地址,需要进行指针偏移,这里进行一下 16 进制的地址偏移计算:

0x1000023b0 + 0x20 = 0x1000023d0

所以我们就再次输入

(lldb) po 0x1000023d0

会有输出如下:

显然 bits 并不是一个对象,而是一个结构体,这里我们需要强转一下:

然后由 objc_class 内置的 data() 方法可知:

```c

top Created with Sketch.