一、对象的本质
1、大师重器:Clang
Clang是一个C语言、C++、Objective-C、C++语言的轻量级编译器。源代码发布于BSD协议下。也是Xcode 第一的编译器
1)输出可执行文件
cd 到文件目录,输入 以下指令输出可执行文件
clang -fobjc-arc -framework Foundation main.m -o hello
2)生成 cpp 文件
clang -rewrite-objc main.m
桌面上会出现.cpp文件,即是c++的实现文件,如下图所示
3)UIkit报错 使用复杂的clang,指定SDK路径
eg:如果文件中引入了<UIKit/UIkit.h>库,报如下错误
main.m:12:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~
此时 需要使用
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.4 -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.4.sdk/ main.m
具体SDK版本根据实际版本来书写:
否则会报错:使用的Foundation库找不到
main.m:9:9: fatal error: 'Foundation/Foundation.h' file not found
#import <Foundation/Foundation.h>
^~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
4)xcrun命令
Xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了封装,更为好用
模拟器的xcrun命令
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
真机的xcrun命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64-iphoneos.cpp
2、Clang探索对象的本质
@interface Person : NSObject
@property (nonatomic,copy) NSString *qzUserName;
@property (nonatomic,copy) NSString *qzSex;
@end
使用clang生成.cpp, 查看.cpp源码,搜索qzUserName,可以看到Person的结构体
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif
extern "C" unsigned long OBJC_IVAR_$_Person$_qzUserName;
extern "C" unsigned long OBJC_IVAR_$_Person$_qzSex;
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_qzUserName;
NSString *_qzSex;
};
// @property (nonatomic,copy) NSString *qzUserName;
// @property (nonatomic,copy) NSString *qzSex;
/* @end */
// @implementation Person
// qzUserName的getter方法
static NSString * _I_Person_qzUserName(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_qzUserName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// qzUserName的setter方法
static void _I_Person_setQzUserName_(Person * self, SEL _cmd, NSString *qzUserName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _qzUserName), (id)qzUserName, 0, 1); }
static NSString * _I_Person_qzSex(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_qzSex)); }
static void _I_Person_setQzSex_(Person * self, SEL _cmd, NSString *qzSex) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _qzSex), (id)qzSex, 0, 1); }
// @end
NSObject_IMPL
struct NSObject_IMPL {
Class isa;
};
objc_object
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
可以发现
1、Person对象实际上是一个结构体类型Person_IMPL
2、当前结构体Person_IMPL又嵌套了一个结构体,相当于一个伪继承(不是真正的继承,但是在c++里面是可以的),它继承了NSObject_IMPL,这里的NSObject_IVARS就是成员变量的isa。
假如再有一个继承自Person的Student的对象
小结:
对象的底层是一个结构体
- 扩展留意 objc_object 和 objc_class 和 id
NSObject 底层是 objc_object 结构体
typedef struct objc_object NSObject;
Class 底层是 objc_class 结构体指针
typedef struct objc_class *Class;
id底层是 objc_object 结构体指针
typedef struct objc_object *id;
OC中的任意对象id,都是以objc_object为模板创建的;所以实例对象,类对象,元类对象都会有isa指针;
OC中的class类对象,都是以objc_class为模板创建,而objc_class继承自objc_object,所以也会有isa指针。
二、ISA分析
1、基础知识
1.1、联合体 (共用体)
union PersonUnion {
int a; //4
short b; //2
char c; //1
};
union PersonUnion person;
person.a = 8;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.b = 2;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.c = 'd';
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
打印结果:
a=8---b=8---c=
a=2---b=2---c=
a=100---b=100---c=d
1、联合体可以定义多个不同类型的成员,联合体的内存大小由其中最大的成员的大小决定。
2、联合体中修改其中的某个变量会覆盖其他变量的值。
3、联合体所有的变量公用一块内存,变量之间互斥。
联合体优缺点
优点:内存使用更为精细灵活,节省内存。
缺点:不够包容。
结构体优缺点
优点:‘有容乃大’,全面。
缺点:内存空间分配是粗放的,不管用不用,全分配。
结构体和 联合体对比
| 成员是否共存 | 占用内存 | |
|---|---|---|
| 结构体 | 共存 | 大于等于所有成员占用内存的总和(内存对齐) |
| 联合体 | 互斥 | 等于最大的成员占用的内存,同一时刻只能保存一个成员的值 |
1.2、位域
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
定义:在定义结构体或联合体时,成员变量后面加 : 数字,用来限定成员变量占用的位数,这就是位域;
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
} car1;
// 位域
struct LGCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
} car2;
NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2));
结果:
4-1
从结果可以看出没有位域的限制,此结构的大小为4字节,加了位域之后,变成1字节。
2、ISA
之前文章alloc探索中,我们了解到
_class_createInstanceFromZone 方法中有核心三个方法需要实现
cls->instanceSize : 计算内存大小 8字节对齐
(id)calloc(1, size) : 开辟内存,返回地址指针
obj->initInstanceIsa :初始化指针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)
{
...
if (!zone && fast) {
// 初始化 isa指针 和cls 关联起来
obj->initInstanceIsa(cls, hasCxxDtor);// 关联内存空间到相应的类
} else {
obj->initIsa(cls);
}
...
}
initIsa
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
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_t 是一个联合体
union isa_t {
// 构造方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
// 成员变量
uintptr_t bits;
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);
};
从上面源码可以看出
- isa_t是一个联合体,联合体的成员变量存储是互斥的,成员变量bits和结构体使用同一块内存;
- 最上面是两个构造函数,第一个构造函数没有做任何处理,第二个构造函数使用传入的参数value对成员变量bits进行赋值;
- 下面结构体中的ISA_BITFIELD为宏定义,实际上这里使用了位域,此结构体和成员变量bits共用同一块内存空间;
结构体成员变量 ISA_BITFIELD
isa指针占用的内存大小是8字节,即64位,除了存储地址信息,结合位域还可以存储一些其他的信息了。
例如:是否为纯指针,是否关联对象,是否有析构函数,是否弱引用,是否还没有初始化空间,是否正在释放,以及引用计数,存储类指针的值等。。。
这样可以极大的节省内存,以提高性能。
分为两种模式:arm64: 和 x86_64
以x86_64为例:
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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 : 44; //类的指针地址
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; //是否弱引用 \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; //散列表 \
uintptr_t extra_rc : 8 //该对象的引用计数值
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
各变量的含义:
- nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等
- has_assoc:关联对象标志位,0表示未关联,1表示关联
- has_cxx_dtor:该对象是否C ++ 或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象
- shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位
- magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放
- deallocating:标志对象是否正在释放
- has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
- hextra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc
ISA_MASK面具
我们更多关注的是isa的shiftcls,我们可以通过isa的地址 & ISA_MASK,去除其他信息,直接获得类的指针地址cls。
真机 arm64的 ISA_MASK
# define ISA_MASK 0x0000000ffffffff8ULL
模拟器 x86_64的 ISA_MASK
# define ISA_MASK 0x00007ffffffffff8ULL
&MASK的效果相当于 如下位移操作得到shiftcls:
x86环境下:
操作验证 得到的cls
(lldb) x/4gx p
0x1005bf8e0: 0x001d800100008225 0x0000000000000012
0x1005bf8f0: 0x0000000100004038 0x0000000000000000
(lldb) p/x p.class
(Class) $22 = 0x0000000100008220 LGPerson
1、通过位移isa得到cls
(lldb) p/x 0x001d800100008225 >> 3
(long) $23 = 0x0003b00020001044
(lldb) p/x 0x0003b00020001044 << 20
(long) $24 = 0x0002000104400000
(lldb) p/x 0x0002000104400000 >> 17
(long) $25 = 0x0000000100008220 // 2、得到cls 和 上面得到的结果相同
2、使用面具直接获得cls
(lldb) p/x 0x001d800100008225 & 0x00007ffffffffff8ULL
(unsigned long long) $26 = 0x0000000100008220
(lldb)