对象的本质是结构体。这点可以通过编译后的c++代码来加以验证,首先我们先来使用Clang命令来获得编译后的cpp文件。
Clang
clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件
遇到的问题
在编译的过程中,遇到了如下错误:
解决方式
找不到UIKit,对于这种问题,我们可以使用下面的命令来解决:
- clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk main.m
复制代码
或者使用xcrun,更简洁。
xcode
安装的时候顺带安装了xcrun
命令,xcrun
命令在clang
的基础上进行了 一些封装,要更好用一些。
//模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-xx.cpp
复制代码
//真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- xx.cpp
复制代码
如果输出成功,但是找不到对应文件的输出位置,那可以在文件里进行搜索,然后右击选择“在上层文件夹中显示”,就可以找到具体的位置。
验证过程
在main.m中自定义一个类JLObject:
@interface JLObject : NSObject
@property (nonatomic, strong) NSString *name;
@end
//进入NSObject可以看到其成员包含一个isa指针。
@interface NSObject <NSObject> {
...
Class isa OBJC_ISA_AVAILABILITY;
...
}
复制代码
通过clang命令输出main.m的cpp文件,打开文件查看输出内容,发现有几万行代码,所以需要通过搜索找到我们自定义的类,截取代码如下。
...
typedef struct objc_object JLObject;
...
struct JLObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
// @property (nonatomic, strong) NSString *name;
...
typedef struct objc_object NSObject;
struct NSObject_IMPL {
Class isa;
};
...
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
...
复制代码
可以看出,JLObject在被编后是一个结构体,JLObject_IMPL中包含了一个继承于NSObject_IMPL(NSObject)的成员isa,以及一个自定义成员name。
结论:对象的本质是结构体。
id类型
- 问题一、为什么id可以修饰对象?
- 问题二、为什么id修饰对象时不需要*?
JLObject * obj = [[JLObject alloc]init];
id objc1 = [[JLObject alloc]init];
复制代码
在搜索objc_object结构体的同时,发现Class与id都是objc_object的结构体指针。
typedef struct objc_object *id;
typedef struct objc_object JLObject;
复制代码
从编译后的代码可以得到答案,JLObject是objc_object结构体,id是objc_object结构体指针。
对象在内存中的结构
遇到过一个问题,分析的过程中加深了对对象本质的理解,在这里通过这个问题来分析一下。
问题
如图,obj2可不可以调用方法?可不可以访问成员变量?
@interface JLObject : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation JLObject
-(void)sayHello{
NSLog(@"%s - %@",__func__,self.name);
}
@end
复制代码
结果
- 调用方法:obj2可以调用到sayHello方法
- 属性:obj2的name属性并未赋值,却错误的将obj:<JLObject: 0x6~b0>打印了出来。
分析
1、首先我们要知道,属性及成员变量存于对象开辟的空间中,方法存在于类/元类对象中。
2、对象在栈中存储的只是一个结构体指针,指向堆中对象结构体的起始地址,该地址存储的是isa指针,指向类对象的起始地址。
- 当访问属性(成员变量)时,需要通过结构体指针先拿到结构体的起始地址,然后通过内存平移去访问到具体的成员变量。
- 在访问方法时,则需要通过结构体指针先获取到结构体起始地址,这个地址存储的是isa指针,再通过isa指针拿到类在堆中的首地址,然后通过内存平移以及哈希找到对应方法。
3、在iOS alloc 底层原理中我们得知创建对象的三个重要步骤:计算内存大小、开辟空间、赋予isa指针。
- isa是指向类对象首地址的指针,而obj1同样是指向类对象首地址的指针,所以我们可以认为obj2指向的obj1近似于isa。
计算所需内存大小 | 开辟空间 | 赋予isa指针 | |
---|---|---|---|
obj | true | true | true |
obj2 | false | false | true |
结论
根据以上信息可知:
- obj2可以通过obj1访问到类对象,所以可以调用方法。
- obj2没有开辟空间,无法存储/访问属性或成员变量,
- 当obj2访问name时,会先拿到obj2指向的地址obj1,然后会在obj1的地址上平移8位到
obj(0x7~b0 = 0x7~a8 + 8)
的位置。所以会打印出<JLObject: 0x6~b0>
补充
obj在内存中的结构:
obj2在内存中的结构: