[OC底层]对象的本质

1,677 阅读4分钟

1:Objective-C的本质

Objective的底层代码是通过C/C++来实现,所以Objective-C 面向对象是基于C/C++数据结构来实现. Screenshot 2021-10-04 at 11.01.35.png

OC的文件编译成C++文件一共有两种方式:

一种是clang,另一种是xcrun.

  • clang: clang -rewrite-objc main.m -o main.cpp
  • xcrun: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp xcode 安装的时候顺带安装了xcrun命令,在clang的基础上进行了一些封装,更好用一些

2:Objective-C的对象的本质

可以新建一个工程,然后在main 函数里面新建一个叫person的对象,并让他具有name的属性,如下图所示.

@interface Person : NSObject
@property (nonatomic, strong) NSString *Name;
@end

@implementation Person

@end

int main(int argc, const char* argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

然后用指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cppmain.m 转换成main.cpp. 接下来,我们在main.cpp里面提取关键信息.

1:对象在底层的本质是结构体

main.cpp搜索我们刚才创建出来的对象名Person, 会发现如下代码 Screenshot 2021-10-04 at 11.29.07.png

所以我们推测对象在底层的本质是结构体. 那么NSObject_IMPL又是什么呢?我们继续在main.cpp搜索,发现NSObject_IMPL是装有isa指针的结构体.

Screenshot 2021-10-04 at 11.34.35.png

2:对象在OC层面里继承NSObject, 在底层实则继承objc_object

我们首先搜索NSObject, 发现如下代码

typedef struct objc_object NSObject;

Screenshot 2021-10-04 at 11.37.21.png

说明Person在底层继承的是objc_object.

  • NSObject对应的, 我们又想到了id类型在底层是什么样子的呢?

接着我们发现如下代码 typedef struct objc_object *id;

所以我们还能得出结论底层里id的本质是一个指针,而NSObject的本质是一个结构体. 这也解释了为什么用NSObject创建对象需要加*,而id类型不需要.

3:Class在底层的本质类型是结构体指针

搜索Class,我们发现如下底层代码

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa __attribute__ ((deprecated));
} __attribute__ ((unavailable));

说明Class在底层是objc_class *类型,是一个结构体指针.

4:getset方法在底层的实现.

首先我们找到gettersetter参数的实现.

// Get
static NSString * _I_Person_Name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_Name)); }
// Set
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *Name) { (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_Name)) = Name; }

我们可以注意到:

  • 1:在GetSet方法中看到了两组参数FFPserson * selfSEL _cmd这是隐藏参数,我们通常创建的方法默认携带这两个参数,这也就是问什么我们在每一个方法里面都可以使用self的原因
  • 2:在Get方法里,可以看出,我们如果要读一个成员变量,首先要拿到对象的首地址, 然后平移OBJC_IVAR_个位置,就能拿到对应的成员变量的地址. 拿到地址再获取值. set方法同理.

3: OC变量的大小和成员变量的关系.

首先我们创建一个继承自NSObject的对象, 再加几个成员变量:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@end

然后用xcrun命令将Person.m 转换成C++文件: 上面的代码就可以用C++表示为:

struct Person_IMPL {

 struct NSObject_IMPL NSObject_IVARS; // isa [0,7]
 int _age;                            // [8, 9, 10, 11]
 NSString * _Nonnull _name;           // (12, 13, 14, 15, [16, 17...23])
 NSString * _Nonnull _nickName;       // [24....32]
 long _height;                        // (33, 34, 35, [36...40])

};

通过上面代码的的注释可以分析出结构体Person_IMPL的大小是40+8(16字节对齐)= 48字节. 然后我们在main函数里面也验证OC对象Person所占空间大小.

Person *person = [Person alloc];
person.name      = @"Cooci";
person.nickName  = @"KC";

NSLog(@"class_getInstanceSize([LGPerson class]): %lu", class_getInstanceSize([Person class]));

NSLog(@"malloc_size((__bridge const void *)(person)): %lu", malloc_size(( __bridge const void *)(person)));

打印的结果是:

class_getInstanceSize([LGPerson class]): 40

malloc_size((__bridge const void *)(person)): 48

可见,和我们分析的结果一致. 从上,我们可以得出两个结论:

  • 1、Person对象与 Person_IMP结构体大小与占用内存空间大小一致。
  • 2、对象内的成员变量的大小,决定了对象实际占用的内存空间。

4:对象大小和方法的关系

我们给上面的Person对象添加一些对象方法和方法:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

- (void)test:(NSString *)str;
+ (NSString *)test1:(NSString *)str;
@end

然后继续打印一下

class_getInstanceSize([LGPerson class]): 40

malloc_size((__bridge const void *)(person)): 48
  • 得出结论: 方法的添加对Person的大小占用内存大小,没有任何影响,说明添加方法的数量对OC对象的大小没有影响。 那么为什么方法的添加不影响对象的大小呢,我们分析一下添加完方法之后的main.cpp里的结构体. Screenshot 2021-10-04 at 17.59.02.png
  • 可以看到编译后的方法,都被注释掉,实际参与计算大小的部分,还是成员变量部分,所以类中添加实例化方法类方法,都不会影响对象大小和对象占用的内存大小

Reference:

juejin.cn/post/694962…