通过这篇文章可以获得什么:
- 什么是对象,对象的本质是什么?
- 什么是clang,什么是xcrun,基础定义
- 如何将.m文件编译为.cpp文件
- .cpp文件分析过程
- 结构体与联合体有什么区别
- 如何指定成员变量的位域
- nonpointerIsa指针
- isa的64个bit位都存了什么?
- getter和setter是如何读取或写入值的?
什么是对象?
探究方式:
在底层探索的过程中,由于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
Xcode安装的时候一起安装了
xcrun
命令,xcrun
在clang
的基础上进行了封装,使用更加方便
准备阶段:
将main.m编译成main.cpp文件
clang编译:
clang -rewrite-objc main.m -o main.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
复制代码
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
mainarm64.cpp
复制代码
分析阶段:
main.m
文件源码,声明了一个FFPerson
的class:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface FFPerson : NSObject
@property (nonatomic, strong) NSString *bblvName;
@end
@implementation FFPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
main.cpp
文件:main.cpp
关注点一struct
:在main.cpp
文件看到FFPerson
对应的地方:
struct FFPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_bblvName;
};
复制代码
结论一:针对上面声明与
.cpp
文件源码的对比,可以得出结论对象在底层的本质就是结构体
关注点二isa
:在FFPerson_IMPL结构体中看到了struct NSObject_IMPL NSObject_IVARS;
在.cpp文件中查找NSObject_IMPL
struct NSObject_IMPL {
Class isa;
};
复制代码
结论二:
NSObject_IVARS
为成员变量isa
关注点三objc_object
:FFPerson继承的是NSObject,但是在.cpp文件中确是objc_object类型
typedef struct objc_object NSObject;
复制代码
结论三:NSObject对象在底层的对象是
objc_object
关注点四Class
:通过类比关注点三,在cpp文件中搜索objc_class
typedef struct objc_class *Class;
复制代码
结论四:我们通常声明一个类时使用的
Class
的类型是objc_class *
,是一个结构体指针
关注点五:id
类型,通常代表任何类型的id是什么
typedef struct objc_object *id;
复制代码
结论五:
id
的类型是objc_object *
,所以可以定义任何类型的变量
关注点六getter
setter
隐藏参数
:在.cpp
文件中查找FFPerson
的get
set
方法
//getter
static NSString * _I_FFPerson_bblvName(FFPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_FFPerson$_bblvName)); }
//setter
static void _I_FFPerson_setBblvName_(FFPerson * self, SEL _cmd, NSString *bblvName) { (*(NSString **)((char *)self + OBJC_IVAR_$_FFPerson$_bblvName)) = bblvName; }
复制代码
结论六:在Get与Set方法中看到了两组参数
FFPserson * self
、SEL _cmd
这是隐藏参数,我们通常创建的方法默认携带
这两个参数,这也就是问什么我们在每一个方法里面都可以使用self
的原因
那么关于get、set方法是如何在内存中找到对一个的值的呢?
结构体、联合体、位域
案例一(struct):
#import <Foundation/Foundation.h>
struct FFPerson {
bool left;
bool right;
bool front;
bool back;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct FFPerson ffPerson;
NSLog(@"---%lu---",sizeof(ffPerson));
}
return 0;
}
复制代码
打印sizeof结果为:ffPerson占用4字节空间
2021-06-12 19:46:51.463279+0800 001-联合体位域[98512:2394618] ---4---
复制代码
案例二(struct):
struct FFPerson {
bool left: 1;
bool right: 1;
bool front: 1;
bool back: 1;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct FFPerson ffPerson;
NSLog(@"---%lu---",sizeof(ffPerson));
}
return 0;
}
复制代码
打印sizeof结果为:ffPerson占用1字节空间
2021-06-12 19:51:51.061432+0800 001-联合体位域[98541:2398442] ---1---
复制代码
案例三(struct):
struct FFPerson {
NSString * name;
int age;
float height;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct FFPerson ffPerson;
ffPerson.name = @"BBlv";
ffPerson.age = 27;
ffPerson.height = 180.0;
NSLog(@"---%lu---",sizeof(ffPerson));
}
return 0;
}
复制代码
对ffPerson的赋值过程ffPerson.name = @"BBLv"
、ffPerson.age
设置了断点,分别打印了在当下状态下的ffPerson的信息
(lldb) p ffPerson
(FFPerson) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p ffPerson
(FFPerson) $1 = (name = @"BBlv", age = 0, height = 0)
(lldb) p ffPerson
(FFPerson) $2 = (name = @"BBlv", age = 27, height = 0)
2021-06-12 20:14:28.321207+0800 001-联合体位域[98575:2402057] ---16---
复制代码
图解补充:
案例四(union):
union FFPerson {
char * name;
int age;
float height;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
union FFPerson ffPerson;
ffPerson.name = "BBlv";
ffPerson.age = 27;
ffPerson.height = 180.0;
NSLog(@"---%lu---",sizeof(ffPerson));
}
return 0;
}
复制代码
对联合体ffPerson对象的赋值过程ffPerson.name
、ffPerson.age
设置了断点,分别打印了在当下状态下的ffPerson的信息
(lldb) p ffPerson
(FFPerson) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p ffPerson
(FFPerson) $1 = (name = "BBlv", age = 16284, height = 2.28187442E-41)
(lldb) p ffPerson
(FFPerson) $2 = (name = "", age = 27, height = 3.78350585E-44)
(lldb)
复制代码
图解补充:
结论:
- 通过
案例一
结构体内声明了4个bool
类型,占用4字节
内存- 通过
案例二
对结构体内声明的4个bool
类型进行指定位域,指定每一个bool
类型的成员变量使用1bit
的内存空间,那么4个bool
类型最终占用4bit
空间,即0.5字节
的空间,最终占用了1字节
内存,为对象指定位域是内存优化的方式- 通过案例三与案例四,也就是
struct
与union
的区别,struct
内成员变量的存储互不影响,union
内的对象存储是互斥的- 结构体(struct)中所有的变量是
共存
的,优点是可以存储所有的对象的值,比较全面。缺点是struct
内存空间分配是粗放的
,不管是否被使用,全部分配- 联合体(union)中所有的变量是互斥的,优点是内存使用更加精细灵活,也节省了内存空间,缺点也很明显,就是
不够包容
nonpointerIsa初探
如何找到isa?
找到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的位运算,还原类信息
案例代码:
#import <Foundation/Foundation.h>
#import "FFPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
FFPerson * person = [FFPerson 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) po 0x00000001000080e8
FFPerson
(lldb)
复制代码
图形分析:
测试所用架构为x86_64的架构,初始化的对象的isa的bit位信息第3号标志位至47号标志位为FFPerson的信息,还原方式为:
- 通过
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
内代表FFPerson
信息的33
个bit位还原,将0x0002000101d00000
向右移17
个bit位 - 最后输出的结果为
FFPerson
补充:
getter方法图解
setter方法图解
如何找到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);
};
复制代码
流程图: