一、对象的本质
探究方式:编译还原
在底层探索的过程中,由于Objective-c
语言是C
语言和C++
的超集,那么可以通过clang
轻量级编译器的代码还原,还原OC
代码的基层实现,或者说用来查看OC代码的底层结构以及过程结构。clang
可以将.m
文件编译成.cpp
文件。
Clang
与xcrun
Clang是什么
Clang
是C
、C++
、Objective-c
语言的轻量编译器。源代码发布于BSD
协议下。Clang
将支持lambad
表达式,返回类型的简单处理以及更好的处理constexpr
关键字。Clang
由Apple
主导编写,基于LLVM
的C/C++/Objective-c
编译器。
xcrun
是什么
xcrun
是Xcode
基本的命令行工具,在clang
的基础上进行了封装,使用更加方便。
准备阶段:
- 直接将
main.m
编译成main.cpp
文件,要将依赖的平台系统要求加入。
clang
指令编译:
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
xcrun
指令编译:
- 模拟器:
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o
main-arm64.cpp
- 真机:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o
main-arm64.cpp
分析阶段:
main.m
文件源码,声明了一个LGPerson
的class
类用于编译成main.cpp
objc部分源码:
#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;
}
编译的c++部分源码:
关注点一、struct (LGPerson_IMPL
):
在main.cpp
文件看到LGPerson
对应的地方:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_KCName;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *__strong _KCName;
};
结论一
:针对上面声明与.cpp
文件源码的对比,可以得出结论对象在底层的本质就是结构体
关注点二、isa结构体 (NSObject_IMPL
):
在LGPerson_IMPL
结构体中看到了struct NSObject_IMPL NSObject_IVARS
;在.cpp
文件中查找NSObject_IMPL
struct NSObject_IMPL {
__unsafe_unretained Class isa;
};
结论二
:NSObject_IVARS
为成员变量isa
关注点三、objc_object
:
LGPerson
继承的是NSObject
,但是在.cpp
文件中确是objc_object
类型、搜索其结构体组成
typedef struct objc_object NSObject;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
结论三
:NSObject
对象在底层的对象是结构体objc_object
,而objc_object
的主要成员是Class isa
,而Class
是objc_class
的结构体指针!在过程中发现id
的类型是objc_object *
,所以可以定义任何类型的变量。
关注点四、getter
:
setter
隐藏参数:在.cpp
文件中查找LGPerson
的get
、set
方法
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_KCName;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *__strong _KCName;
};
// @property (nonatomic, strong) NSString *KCName;
/* @end */
// @implementation LGPerson
//方法:隐藏参数
static NSString * _I_LGPerson_KCName(LGPerson * self, SEL _cmd) {
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_LGPerson$_KCName));
}
static void _I_LGPerson_setKCName_(LGPerson * self, SEL _cmd, NSString *KCName) {
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)) = KCName;
}
结论四
:在Get
与Set
方法中看到了两组参数LGPserson * self
、SEL _cmd
这是隐藏参数,我们通常创建的方法默认携带这两个参数,这也就是为什么我们在每一个方法里面都可以使用self
的原因。set
方法是就是获取到对象的地址然后移位查找变量内存。
二、结构体、联合体、位域
案例一、布尔变量的struct
直接打印正常的结构体,不做位域处理
#import <Foundation/Foundation.h>
// 4 * 8 = 32 0000 0000 0000 0000 0000 0000 0000 1111
// 4 位
// 1 字节 3倍浪费
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGCar1 p1;
NSLog(@"---%lu---",sizeof(p1));
}
return 0;
}
- 打印
sizeof
结果为:p1占用4字节空间
2021-06-12 19:46:51.463279+0800 001-联合体位域[98512:2394618] ---4---
案例二、位域处理的struct
对结构体做位域处理,让每个布尔值只占用一个1bit
(1byte = 8bit):
// 位域
// 互斥
// 0000 1111
struct LGCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGCar2 p2;
NSLog(@"---%lu---",sizeof(p2));
}
return 0;
}
- 打印sizeof结果为:p2占用1字节空间
2021-06-20 17:30:16.330232+0800 001-联合体位域[4793:5619332] 1
案例三、修改位域值的struct
改变位域值,改变结构体占用字节数
// 位域
// 互斥:只能单方向:先前占位,不可以同时向后占位
// 后面代表占用位数
struct LGCar2 {
BOOL front: 1; // 0000 0000 0001
BOOL back : 2; // 0000 0000 0110
BOOL left : 6; // 0001 1111 1000
BOOL right: 1; // 0010 0000 0000
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGCar2 p2;
NSLog(@"---%lu---",sizeof(p2));
}
return 0;
}
- 打印sizeof结果为:p2占用
2字节
空间,由于结构体成员变量
本只占10bits
,根据内存字节对齐原则可得到2byte
.
2021-06-20 17:33:58.893876+0800 001-联合体位域[4906:5623242] 2
案例四、不同类型变量的struct
通过断点来分析对结构体的赋值时的内存变化
// 共存
struct LGTeacher1 {
char *name;
int age;
double height ;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGTeacher1 teacher1;
teacher1.name = "Cooci";// 断点1
teacher1.age = 18; // 断点2
NSLog(@"Hello, World!");// 断点3
}
return 0;
}
- 对
teacher1
的赋值过程teacher1.name = @"BBLv"
、teacher1.age
设置了断点,分别打印了在当下状态下的teacher1
的信息
(lldb) p teacher1
(LGTeacher1) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p teacher1
(LGTeacher1) $1 = (name = "Cooci", age = 0, height = 0)
(lldb) p teacher1
(LGTeacher1) $2 = (name = "Cooci", age = 18, height = 0)
(lldb)
- 图解补充:
案例五、不同类型变量的union
通过断点来分析对联合体的赋值时的内存变化
// 联合体 : 互斥
union LGTeacher2 {
char *name;
int age;
double height ;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
union LGTeacher2 teacher2;
teacher2.name = "Cooci"; // 断点1
teacher2.age = 18; // 断点2
NSLog(@"%ld",sizeof(teacher2)); // 断点3
}
return 0;
}
- 对联合体
teacher2
对象的赋值过程teacher2.name
、teacher2.age
设置了断点,分别打印了在当下状态下的teacher2
的信息。
(lldb) p teacher2
(LGTeacher2) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p teacher2
(LGTeacher2) $1 = (name = "Cooci", age = 15966, height = 2.1220036792173738E-314)
(lldb) p teacher2
(LGTeacher2) $2 = (name = "", age = 18, height = 2.1219957998584539E-314)
2021-06-20 17:49:48.308431+0800 001-联合体位域[5287:5637154] 8
- 图解补充:
案例六、包含不同变量的struct的union
// 共存
struct LGTeacher1 {
char *name; // 8 [0 1 2 3 4 5 6 7]
int age; // 4 [8 9 10 11 12]
double height ;//8 (13 14 15 [16 17 18 19 ... 23])在以最大变量字节倍数为基,刚好8*3=24
};
// 联合体 : 互斥
union LGTeacher2 {
char *name;
int age;
double height ;
struct LGTeacher1 teacher;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct LGTeacher1 teacher1;
teacher1.name = "Cooci";
teacher1.age = 18;
union LGTeacher2 teacher2;
teacher2.name = "Cooci"; // 断点1
teacher2.age = 18; // 断点2
teacher2.teacher = teacher1; // 断点3
NSLog(@"%ld",sizeof(teacher2)); //断点4
NSLog(@"Hello, World!");
}
return 0;
}
- 对联合体
teacher2
对象的赋值过程teacher2.name
、teacher2.age
、teacher2.teacher
设置了断点,分别打印了在当下状态下的teacher2
的信息。发现即使有结构体变量,还是最大成员变量作为内存字节。
(lldb) p teacher2
(LGTeacher2) $0 = {
name = 0x0000000100000000 "\317\372\355\376"
age = 0
height = 2.1219957909652723E-314
teacher = (name = "\317\372\355\376", age = 0, height = 0)
}
(lldb) p teacher2
(LGTeacher2) $1 = {
name = 0x0000000100003e5e "Cooci"
age = 15966
height = 2.1220036792173738E-314
teacher = (name = "Cooci", age = 0, height = 0)
}
(lldb) p teacher2
(LGTeacher2) $2 = {
name = 0x0000000100000012 ""
age = 18
height = 2.1219957998584539E-314
teacher = (name = "", age = 0, height = 0)
}
(lldb) p teacher2
(LGTeacher2) $3 = {
name = 0x0000000100003e5e "Cooci"
age = 15966
height = 2.1220036792173738E-314
teacher = (name = "Cooci", age = 18, height = 0)
}
2021-06-20 17:54:59.714195+0800 001-联合体位域[5403:5641254] 24
结论:
- 通过案例一结构体内声明了
4
个bool
类型,占用4字节内存 - 通过案例二对结构体内声明的
4
个bool
类型进行指定位域,指定每一个bool类型的成员变量使用1bit
的内存空间,那么4个bool类型最终占用4bit
空间,即0.5字节的空间,最终占用了1字节内存,为对象指定位域是内存优化的方式 - 通过案例三与案例四,也就是
struct
与union
的区别,struct
内成员变量的存储互不影响,union
内的对象存储是互斥的 - 结构体(
struct
)中所有的变量是共存的,优点是可以存储所有的对象的值,比较全面。缺点是struct
内存空间分配是粗放的,不管是否被使用,全部分配 - 联合体(
union
)中所有的变量是互斥的,优点是内存使用更加精细灵活,也节省了内存空间,缺点也很明显,就是不够包容 - 即使有结构体(
struct
)变量的联合体(union
),内存还是以最大占用字节的变量为主
三、nonpointerIsa
初探
如何找到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)
// nonPinterIsa 无指向的指针:(地址指针|其他功能)
// 类 指真
// 8 * 8 = 64
// 其他功能:是否释放、引用计数、weak、关联对象、析构函数
// 要看:位域
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);
};
- 找到
isa
指针内存了什么
- 通过宏定义
ISA_BITFIELD
找到了isa
指针内都存放了什么,分为两种模式
arm64:
define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# 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; //C++的析构函数 \
uintptr_t shiftcls : 33; / *MACH_VM_MAX_ADDRESS 0x1000000000*/ \ //类的指针地址
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; //弱引用 \
uintptr_t unused : 1; //是否引用 \
uintptr_t has_sidetable_rc : 1; //散列表 \
uintptr_t extra_rc : 19 //引用计数
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
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; //C++的析构函数 \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \ //类的指着地址
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)
图形分析:
四、isa的位运算,还原类信息
案例代码:
对象通过掩码->class
#import <Foundation/Foundation.h>
#import "LGPerson.h"
//1:关于对象的本质
//2:nonPointerIsa
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
NSLog(@"%@",p);
}
return 0;
}
- 位运算测试结果:
(lldb) x/4gx p
0x10060eee0: 0x011d800100008275 0x0000000000000000
0x10060eef0: 0x0000000000000000 0x0000000000000000
0x10060eee0: 0x011d800100008275 0x0000000000000000
0x10060eef0: 0x0000000000000000 0x0000000000000000
(lldb) p/x LGPerson.class
(Class) $2 = 0x0000000100008270 LGPerson
(lldb) p/x 0x0000000100008270 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008270
(lldb) p/x 0x011d800100008275 & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x0000000100008270
(lldb) p 0x011d800100008275 >> 3
(long) $5 = 10045138768236622
(lldb) p/x 0x011d800100008275 >> 3
(long) $6 = 0x0023b0002000104e
(lldb) p/x 0x0023b0002000104e << 20
(long) $7 = 0x0002000104e00000
(lldb) p/x 0x0002000104e00000 >> 17
(long) $8 = 0x0000000100008270
(lldb) p/x LGPerson.class
(Class) $9 = 0x0000000100008270 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