一、对象的本质
探究方式:编译还原
在底层探索的过程中,由于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字节空间,由于结构体成员变量本只占10bit,根据内存字节对齐原则可得到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";
teacher1.age = 18;
NSLog(@"Hello, World!");
}
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";
teacher2.age = 18;
NSLog(@"%ld",sizeof(teacher2));
}
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";
teacher2.age = 18;
teacher2.teacher = teacher1;
NSLog(@"%ld",sizeof(teacher2));
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内代表LFPerson信息的33个bit位还原,将0x0002000101d00000向右移17个bit位
最后输出的结果为LGPerson