介绍正文前,我们思考一个问题,什么是对象
?或者说OC对象的本质是什么
?
对象本质以及拓展
在探索oc对象本质前,先了解一个编译器:clang
- Clang是C语言、C++、Objective-c语言的
轻量编译器
。源代码发布于BSD协议下。 - Clang将支持
lambad表达式
,返回类型的简单处理以及更好的处理constexpr
关键字。 - Clang是一个由
Apple主导编写
,基于LLVM的C/C++/Objective-c编译器
什么是xcrun
- Xcode安装的时候一起安装了
xcrun命令
,xcrun在clang的基础上进行了封装,使用更加方便
探索对象本质
- 在main中自定义一个类
LGPerson
,有一个属性name
<!-- main.m文件 -->
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
// 对象在底层的本质就是结构体
@interface LGPerson : NSObject
@property (nonatomic, strong) NSString *KCName;
@end
@implementation LGPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
- 通过终端,利用
clang
将main.m
编译成main.cpp
,有以下几种编译命令,这里使用的是第一种
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
如果出现问题:
In file included from ViewController.m:9:
./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~~~~
1 error generated.
解决方案
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
- 打开编译好的
main.cpp
,找到LGPerson的定义,发现LGPerson在底层会被编译成 struct 结构体
<!-- main.cpp文件 -->
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_KCName;
};
// 查看结构体NSObject_IMPL
struct NSObject_IMPL {
Class isa;
};
// LGPerson的本质类型是objc_object
typedef struct objc_object LGPerson;
// 通过类比LGPerson本质类型objc_object,在cpp文件中搜索objc_class
typedef struct objc_class *Class;
// 通常代表任何类型的id是什么
typedef struct objc_object *id;
// 在main.cpp文件中查找LGPerson的get set方法
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; }
通过上述探索过程可以得出:
- OC对象的本质其实就是
结构体
- LGPerson中的isa是继承自
NSObject中的isa
- NSObject对象在底层的对象是
objc_object
- 我们通常声明一个类时使用的Class的类型是
objc_class *
,是一个结构体指针
- id的类型是
objc_object *
,所以可以定义任何类型的变量 - 在Get与Set方法中看到了两组参数
LGPserson * self、SEL _cmd
这是隐藏参数,我们通常创建的方法默认携带这两个参数,这也就是为什么我们在每一个方法里面都可以使用self
的原因
结构体 联合体 位域拓展
结构体一
#import <Foundation/Foundation.h>
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGCar1 car1;
NSLog(@"---%lu---",sizeof(car1));
}
return 0;
}
// 打印结果是4
2021-07-11 21:35:43.363366+0800 001-联合体位域[18592:2045823] 4
- 结构体内声明了4个BOOL类型,占用
4字节内存
结构体二
// 位域
struct LGCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGCar2 car2;
NSLog(@"---%lu---",sizeof(car2));
}
return 0;
}
// 打印结果是1
2021-07-11 21:43:40.195428+0800 001-联合体位域[18637:2051439] 1
// 如果LGCar2定义如下,就会占用2字节内存,2表示2bit
struct LGCar2 {
BOOL front: 1;
BOOL back : 2;
BOOL left : 6;
BOOL right: 1;
};
- 对结构体内声明的4个BOOL类型进行指定位域,指定每一个bool类型的成员变量使用1bit的内存空间,那么4个bool类型最终占用4bit空间,即
0.5字节
的空间,最终占用了1字节内存
,为对象指定位域是内存优化的方式
结构体三
struct LGTeacher1 {
char *name;
int age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGTeacher1 teacher1;
teacher1.name = "kevin";
teacher1.age = 18;
}
return 0;
}
对teacher1的赋值过程teacher1.name = "kevin";、teacher1.age = 18;
设置了断点,分别打印了在当前状态下的teacher1的信息
联合体一
// 联合体 : 互斥
union LGTeacher2 {
char *name;
int age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
union LGTeacher2 teacher2;
teacher2.name = "kevin";
teacher2.age = 18;
}
return 0;
}
对联合体teacher2对象的赋值过程teacher2.name、teacher2.age
设置了断点,分别打印了在当前状态下的teacher2的信息
- 通过结构体三与联合体一,也就是
struct与union的区别
,struct内成员变量的存储互不影响
,union内的对象存储是互斥的
- 结构体(struct)中所有的变量是共存的,
优点是可以存储所有的对象的值,比较全面
。缺点是struct内存空间分配是粗放的,不管是否被使用,全部分配
- 联合体(union)中所有的变量是互斥的,
优点是内存使用更加精细灵活,也节省了内存空间
,缺点也很明显,就是不够包容
nonPointerisa分析
isa的类型 isa_t
以下是isa指针的类型isa_t的定义,从定义中可以看出是通过联合体
(union)定义的。
union isa_t { //联合体
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls 和 bits ,两者是互斥关系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t类型使用联合体的原因也是基于内存优化
的考虑,这里的内存优化是指在isa指针中通过char + 位域
(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节即64位
,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能
从isa_t
的定义中可以看出:提供了一个结构体定义的位域,用于存储类信息及其他信息。结构体的成员ISA_BITFIELD
,这是一个宏定义,有两个版本 __arm64__
(对应ios 移动端) 和__x86_64__
(对应macOS),以下是它们的一些宏定义,如下图所示
-
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。
isa的位运算,还原类信息
- main.m文件编写如下代码,并在
LGPerson * person = [LGPerson alloc];
添加断点,运行工程
<!-- main.m文件 -->
#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson * person = [LGPerson alloc];
NSLog(@"%@",person); }
return 0;
}
(lldb) x/4gx person
0x10064d420: 0x011d8001000080e9 0x0000000000000000
0x10064d430: 0x566265574b575b2d 0x74696e6920776569
(lldb) p 0x011d8001000080e9 >> 3
(long) $1 = 10045138768236573
(lldb) p/x 0x011d8001000080e9 >> 3
(long) $2 = 0x0023b0002000101d
(lldb) p/x 0x0023b0002000101d << 20
(long) $3 = 0x0002000101d00000
(lldb) p/x 0x0002000101d00000 >> 17
(long) $4 = 0x00000001000080e8
(lldb) p/x LGPerson.class
(class) $5 = 0x00000001000080e8 LGPerson
(lldb)
测试所用架构为x86_64的架构,初始化的对象的isa的bit位信息第3号标志位至47号标志位为LGPerson的信息,还原方式为:
- 通过
x/4gx person
,格式化输出person
对象的内存地址,首地址为isa
指针0x011d8001000080e9
- 将
isa
的前三个bit位移除,即0x011d8001000080e9
向右移3
位,通过p/X
得到新的isa
指针0x0023b0002000101d
- 将
isa
指针的后17
个bit位移除,由于刚刚向右移了3
个bit位,那么现在需要向左移20
个bit位,即0x0002000101d00000
向左移17+3
位,通过p/x
得到新的isa
指针0x0002000101d00000
- 最后将
isa
内代表LGPerson
信息的33
个bit位还原,将0x0002000101d00000
向右移17
个bit位 - 最后输出的结果为
LGPerson
如何找到Isa
- 在对象alloc过程中执行
_class_createInstanceFromZone
方法中,会执行initIsa方法将obj与class进行绑定,这里删除了跟isa部分无关的代码,只保留了isa相关的代码
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
//移除跟isa部分无关代码......
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
//移除跟isa部分无关代码......
}
- 然后可能进入
initInstanceIsa
函数或者initIsa函数,但是initInstanceIsa函数执行后依然会进入到initIsa
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
-
initIsa函数
删除了与此次探索无关的代码
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
//删除了部分无关代码
isa = newisa;
}
- 可以看到源码内声明isa的类型是isa_t,而isa_t的类型是
联合体
(union)
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);
};
init与new的补充
<!-- LGPerson.h -->
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end
NS_ASSUME_NONNULL_END
<!-- LGPerson.m -->
#import "LGPerson.h"
@implementation LGPerson
- (instancetype)init {
if (self = [super init]) {
self.name = @"kevin";
}
return self;
}
@end
<!-- main.m -->
#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p1 = [[LGPerson alloc] init];
LGPerson *p2 = [LGPerson new];
}
return 0;
}
// 添加断点打印
(lldb) p p1.name
(__NSCFConstantString *) $0 = 0x0000000100004050 "kevin"
(lldb) p p2.name
(__NSCFConstantString *) $0 = 0x0000000100004050 "kevin"
得出结论
-
new
等价于alloc init
网友评论