iOS底层原理探究04-OC对象的本质&联合体位域&isa分析

通过C++源码分析OC对象、类的本质

1. OC编译生成C++代码的方法的两种方法

这里我们有在main里写一些代码声明一个LGPerson类看一下编译之后的结果

image.png

1.1 clang

clang -rewrite-objc main.m -o main.cpp //把⽬标⽂件编译成c++⽂件

这种方式比较简单直接但是如果引用到OC的系统库像UIKit之类的会报错,解决报错需要加一坨参数
UIKit报错问题
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

通过clang编译之后会生成一个.cpp文件
![image.png](https://upload-images.jianshu.io/upload_images/3910976-6d896d9ea365e18e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1.2 xcrun

xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进⾏了
⼀些封装,要更好⽤⼀些

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o 
main-arm64.cpp //(模拟器) 
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main�arm64.cpp //(⼿机) 

xcrun编译之后也是生成.cpp产物

xcrun编译

下面我们来看下这个cpp文件

2.对象的本质

image.png

搜索一下LGPerson可以看到一下几点

  • OC中的LGPerson类底层是struct LGPerson_IMPL结构体
  • OC@interface LGPerson : NSObject LGPerson继承NSObject 底层是 typedef struct objc_object LGPerson;这样体现的

到这个对象的本质就显而易见了 对象的本质是结构体

struct NSObject_IMPL NSObject_IVARS;LGPerson_IMPL中的这个成员变量是isa

struct NSObject_IMPL {
    Class isa;
};
typedef struct objc_class *Class;


struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

typedef struct objc_object *id;

typedef struct objc_selector *SEL;

typedef void (*IMP)(void );
  • Class 底层是struct objc_class *类型,NSObject底层是struct objc_object结构体,id底层是struct objc_object *类型
  • struct objc_object的实现是struct objc_object { Class _Nonnull isa __attribute__((deprecated)); };也就是说NSObject底层实现的结构体里只要一个成员变量isa
  • id底层实现是struct objc_object *类型,怪不得声明id类型变量时 后面不用再加"*"了
  • SELstruct objc_selector *类型
  • IMPvoid (*)(void )函数指针类型

接下来看下KCName属性的gettersetter方法的底层实现

static NSString * _I_LGPerson_KCName(LGPerson * self, SEL _cmd) { 
return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)); 
}
static void _I_LGPerson_setKCName_(LGPerson * self, SEL _cmd, NSString *KCName) { 
(*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)) = KCName;
 }
  • 首先数入参LGPerson * self, SEL _cmd,所有OC方法都有这两个隐藏参数,所以在OC的方法中我们可以使用self
  • 然后可以看到gettersetter里是通过首地址+偏移量的方式取和存的

2. 联合体位域

联合体跟结构体对比着来会好理解一点

//结构体
struct LGCar1 {
    BOOL front; // 0 1
    BOOL back;
    BOOL left;
    BOOL right;
};

先来看这个结构体,它包含4个BOOL型的成员变量,占4个字节的内存,BOOL类型用0和1就能标识也就是说用一个二进制位就足够了,4个BOOL类型用4位就可以表示也就是半个字节就够了(一个字节包含8个二进制位),由此该位域出场了

// 位域
struct LGCar2 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};

这个结构体中的每个变量都只占用一个二进制位

image.png

输出car1car2占用的内存,car1占用了4字节,car2只占用1个字节,虽然car2只需要4位(半个字节)就够了,但是内存是以字节为单位的,所以分配了1个字节,这样用了位域之后一下子就节省了3/4的内存,car2中会使用一个二进制位标识一个属性,了解了位域再来了解一下联合体(也叫共用体)
image.png

teacher1teacher2包含相同的变量,但是前者开辟24字节的内存,后者只开辟了8字节内存,这是啥情况呢

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

也就是说结构体会给所有的成员变量都分配内存,所以分配了24字节的内存,联合体里的变量内存是公用的所以只需要开辟一个最大变量需要的内存就够了,最大的是double类型的所以分配了8个字节。这时候你可能还是有点蒙蔽,接下来我们来看下联合体的内存情况

image.png

刚创建的就是内存都初始化为了0x0000000000000000这是正常的
image.png

当给name属性复制之后,ageheight变成了0x0000000100003e68(age是int型的四个字节所以只展示了0x00003e68)p (char *)0x0000000100003e68当我们以字符串的类型输出0x0000000100003e68 发现也是"Cooci",所有的属性公用了相同的内存,到这里对联合体公用内存的意思,应该有个清晰的认识了吧,最后再来看下联合体位域

#import 

NS_ASSUME_NONNULL_BEGIN

@interface LGCar : NSObject

@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;

@end

NS_ASSUME_NONNULL_END

定义一个LGCar类,有向前,向后,向左,向右四个属性

#import "LGCar.h"

#define LGDirectionFrontMask    (1 << 0)
#define LGDirectionBackMask     (1 << 1)
#define LGDirectionLeftMask     (1 << 2)
#define LGDirectionRightMask    (1 << 3)

@interface LGCar(){
    // 联合体
    union {
        char bits;
        // 位域
        struct {
            char front  : 1;
            char back   : 1;
            char left   : 1;
            char right  : 1;
        };
    } _direction;
}
@end

.m里加了个_direction的联合体实现前后左右属性

- (void)setFront:(BOOL)isFront {
        
    if (isFront) {
        _direction.bits |= LGDirectionFrontMask;
    } else {
        _direction.bits &= ~LGDirectionFrontMask;
    }
    NSLog(@"%s",__func__);
}

- (BOOL)isFront{
    return _direction.front;
}

- (void)setBack:(BOOL)isBack {
    _direction.back = isBack;

    NSLog(@"%s",__func__);
}

- (BOOL)isBack{
    return _direction.back;
}

分别通过操作_direction.bits_direction.back的方式改变值

image.png

我们知道对象类型的起始位置存的是isa 后面存属性(也就是0x100705760 + 8)就是_direction的位置为了避免干扰只看_direction这1个字节的内存情况
image.png

执行完setFront后的结果
image.png

setFront:是直接操作bits
image.png

setBack:同样改变的是这片内存,这样有一个好处用联合体位域存放数据_direction.front由于联合体公用内存其实就是取bits的第一位(从右向左),_direction.back就是去的bits的第二位 ,赋值的时候不用分别赋值,可以直接给bits赋值,取值的时候通过.front.back等由可以很方便的单独取

3.isa分析

前面讲了那么多就是为了引出isa_t,那么这个isa_t又是从哪儿冒出来的呢,还记的我们在iOS底层原理探究01-alloc底层原理里我们说过OC对象在创建的过程中是通过initIsaisa进行初始化的,当时也说了对isa的初始化其实就是对isa_t结构体的初始化过程,下面就来看看这个isa_t的定义

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

这么大一坨看着是不是有点懵,来简化一下

union isa_t {
    uintptr_t bits;
    Class cls;

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

这样看着简单多了吧,接下来通过位域看下isa里存了些啥

define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t has_cxx_dtor      : 1;                                       \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19

这个是arm64下isa_t的位域

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

nonpointerisa 需要跟ISA_MASK做&运算才能得到class

image.png

下面放上isa的走位图


isa流程图.png

最后补充一个知识点
我们前面那么多内容都是对alloc的探究,那init呢,init方法做了什么

image.png

alloc之后已经生成了对象,所以对调用- (id)init这个对象方法,而不是调用类方法,init方法里调用了_objc_rootInit,进去看一下
image.png

什么都没做直接返回了传进来的obj,这不是多此一举吗?你是不是会有这个疑问,其实不是的,苹果的工程师又不傻,这样做肯定是有他的道理,这是工厂设计模式的体现,目的是定义工厂方法让子类重写这个方法,根据不同子类的重写做不同的操作。
我们有时候会使用new而不是allocinit,来看下new的实现
image.png

new方法是直接调用callAlloc,然后再调用init方法,其实跟allocinit是一样的,但是前面alloc流程我们知道LLVMhookalloc方法,那它会不会也hooknew呢,咱们运行起来看下
image.png

我们分别来个allocnew看下运行时的汇编
image.png

alloc被替换成了objc_alloc这个我们之前已经知道了
new被替换成了objc_opt_new接下来看下这个方法
image.png

如果是OBJC2的情况下还是[callAlloc(cls, false/*checkNil*/) init];这样调用,因为我们现在都是用的OBJC2所以流程还是没有变的,最终的结论是newalloc+init是一样的

你可能感兴趣的:(iOS底层原理探究04-OC对象的本质&联合体位域&isa分析)