OC对象的本质
平时我们在工作中所写的OC代码,它的底层实现都是C/C++代码。
所以OC的对象都是基于C/C++的数据结构实现的,所以OC对象本质上就是一个结构体。并且我们也可以通过clang来编译oc里面的文件来验证.
clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
- 编译流程
- 首先在mian.h中定义一个对象
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
- 首先cd到当前要编译文件所在目录
- 例如这里我们编译一下main.h:
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/iPhoneSimulator13.0.sdk main.m
上面的/iPhoneSimulator13.0.sdk要换成自己电脑上相对应的路径.
编译成功之后会得到一个main.cpp的文件,双击打开它,我们可以在里面找到
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
这样一个东西。这个结构体就是LGPerson对象在内存中的本质。
isa指针
根据上面代码,来到mian.h,按住command点击NSObjec然后一层一层去查看,也可以看到这样一个结构:
//点击NSObject
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
//点击class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
通过oc源码中的注释我们可以知道struct objc_object
这个结构体表示一个类、对象。,id
是指向这个结构体的指针,而在这个结构体中只有一个成员变量,一个class类型的isa�,这个isa就是一个结构体指针,它指向这个对象的类。
联合体位域
在探索isa的过程中会发现isa
是将对象内存空间与class
之间联结起来的桥梁,isa在有限的存储空间里,存放了很多的内容,它是用联合体与位域来实现的。
- 联合体
联合体和结构体非常类似,它们两者各有优缺点:
- 结构体:结构体(struct)中所有变量是“共存”的——优点是“有容乃⼤”,
全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。 - 联合体:联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使⽤更为精细灵活,也节省了内存空间。
union Data {
int a;
int b;
short c;
} data;
struct Data1 {
int a;
int b;
short c;
} data1;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
data.a = 4;
NSLog(@"a:%d,b:%d,c:%d",data.a,data.b,data.c);
data.b = 6;
NSLog(@"a:%d,b:%d,c:%d",data.a,data.b,data.c);
data.c = 8;
NSLog(@"a:%d,b:%d,c:%d",data.a,data.b,data.c);
data1.a = 4;
NSLog(@"a1:%d,b1:%d,c1:%d",data1.a,data1.b,data1.c);
data1.b = 6;
NSLog(@"a1:%d,b1:%d,c1:%d",data1.a,data1.b,data1.c);
data1.c = 8;
NSLog(@"a1:%d,b1:%d,c1:%d",data1.a,data1.b,data1.c);
}
return 0;
}
根据上面结果可以知道,结构体
中元素是互不影响的而联合体
中元素是相互影响的,这正验证了结构体共存
和联合体互斥
的特性。
- 位域
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
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)
上面oc源码中ISA_BITFIELD
表示位域
nonpointer
:表示是否对 isa 指针开启指针优化,0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等。
has_assoc
:关联对象标志位,0没有,1存在。
has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。
shiftcls
:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。
magic
:⽤于调试器判断当前对象是真的对象还是没有初始化的空间。
weakly_referenced
:志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
deallocating
:标志对象是否正在释放内存。
has_sidetable_rc
:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。
extra_rc
:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
下面来看一段示例代码,定义一个car类,它有前后左右4个方向:
@interface LDQCar : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end
//在main中赋值
LDQCar *car = [[LDQCar alloc] init];
car.front = YES;
car.back = YES;
car.right = YES;
car.left = YES;
运行,打个断点查看一下内存使用情况
从上图可以看到 01 01 01 01 00 00 00 00 这里至少会使用4个字节存这个4个属性,这是一种很大的浪费。
所以我们可以是有一个字节的char来进行表示,00 00 00 01这里用第一位表示向前,第二位表示向后,第三位表示向左,第四位表示向右。这样总共加起来最多一个字节,大大节省了内存。代码如下:
@interface LDQCar : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
- (void)setFront:(BOOL)isFront;
- (BOOL)isFront;
- (void)setBack:(BOOL)isBack;
- (BOOL)isBack;
- (void)setLeft:(BOOL)isLeft;
- (BOOL)isLeft;
- (void)setRight:(BOOL)isRight;
- (BOOL)isRight;
@end
#import "LDQCar.h"
#define LDQDirectionFrontMask (1 << 0) //左移0位
#define LDQDirectionBackMask (1 << 1) //左移1位
#define LDQDirectionLeftMask (1 << 2) //左移2位
#define LDQDirectionRightMask (1 << 3) //左移3位
@interface LDQCar()
{
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
}
@end
@implementation LDQCar
- (instancetype)init
{
self = [super init];
if (self) {
_direction.bits = 0b0000000000;
}
return self;
}
- (void)setFront:(BOOL)isFront {
if (isFront) {
_direction.bits |= LDQDirectionFrontMask;
} else {
_direction.bits |= ~LDQDirectionFrontMask;
}
_direction.front = isFront;
}
- (BOOL)isFront {
return !!(_direction.front & LDQDirectionFrontMask);
}
- (void)setBack:(BOOL)isBack {
_direction.back = isBack;
}
- (BOOL)isBack {
return !!(_direction.back & LDQDirectionBackMask);
}
- (void)setLeft:(BOOL)isLeft {
_direction.left = isLeft;
}
- (BOOL)isLeft {
return !!(_direction.left & LDQDirectionLeftMask);
}
- (void)setRight:(BOOL)isRight {
_direction.right = isRight;
}
- (BOOL)isRight {
return !!(_direction.right & LDQDirectionRightMask);
}
@end
运行打断点查看:
07 00 00 00 00 00 00 00 这里用一个07就表示了car的4个属性,大大的节省了内存,提升了性能。
isa指针关联类
之前在alloc的流程分析中有提到:
- cls->instanceSize //计算要开辟的内存大小
- calloc //申请内存
- initInstanceIsa //内存与指针的绑定
下面让我们来聊聊在内存与指针绑定时怎么对isa赋值的。
通过alloc流程我们一步步进到 initInstanceIsa方法中
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
然后再来到initIsa,显然从这个方法名就知道这一步是对isa进行init操作,command按住点进去:
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE; ////对bits进行初始赋值
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3; //走到这一步会根据cls的地址更新bit中的数据(因为isa_t是属于联合体(union)类型)那么指向类的地址就保存到了bits中,在以后想要获取类的地址时可以通过ISA()这个方法进行获取,在objc_object结构体ISA()方法中通过return (Class)(isa.bit & ISA_MASK)进行 位与 运算得到指向类结构体的地址进行操作#define ISA_MASK 0x00007ffffffffff8ULL
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
//这里与ISA_MASK这个掩码是将isa指针shiftcls的前面3位以及后面的位置0,这样得到的就是isa指针的类信息,这样就将isa指针和类关联起来了。
#endif
}
结语
学习很苦,写博客很难,坚持住!👊