建议先看下我之前的
Objc4-818底层探索(一):alloc探索(一)
Objc4-818底层探索(二):内存对齐
开始探索ISA前, 先看个例子,
// 声明
@interface TestObj : NSObject
@property (nonatomic, copy) NSString *SAName;
@end
// 实现
@implementation TestObj
@end
创建一个只有main的项目, 里面添加一个对象继承NSObject (创建main项目可以参考我这一篇章: IOS创建个只有main.m工程) 。完成之后我们Clang
转一下 。
接下来介绍一个重要的前端编辑器Clang
Clang
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Clang是一个由Apple主导编写,基于
LLVM的C/C++/Objective-C编译器
Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更 好的处理constexpr关键字。Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。
iOS架构
对于初学者来说光看Clang
定义, 可能有些迷茫, 因为涉及一些ios架构内容, 这里简单介绍下。
这块简单介绍一些架构概念:
前端编译器(Frontend)
: 编译器的前端任务是解析源代码
, 会进行词法分析
、语法分析
、语义分析
。优化器(Optimizer)
: 负责各种优化, 改善代码的运行时间,如消除冗余计算等后端编译器(Backkend)/ 代码生成器(CodeGenerator)
: 将代码映射到目标指令集,生成机器语言,并进行机器相关的代码优化(目标指不同操作系统)。
Objective C / C / C++ 使用的编译器前端是Clang
,Swift
是swift
,后端都是LLVM
。LLVM
可参考上一篇 Objc4-818底层探索(二):内存对齐 llvm
部分
总结
iOS编译器架构流程顺序依次是 OC→Clang→LLVM→机器代码
, Clang
为前端编译器, llvm
为后端编译器
我们回到Clang
命令转一下main
, 通过命令将main.m
转成C++文件main.cpp
方便我们更好的探究底层
- 打开终端
cd 进入对应项目文件夹
clang -rewrite-objc main.m -o main.cpp //将 main.m 编译成 main.cpp
- 打开 main.cpp
里面代码很多, 我们看想要找的就行, 因为我们要查看自定义类TestObj
, 全篇搜索TestObj
TestObj_IMPL
这句代码可看出:对象在底层本质就是结构体
, 即OC对象的本质其实就是结构体
typedef struct objc_object TestObj;
可看出: 虽然OC层面TestObj
是继承于NSObject
的, 但实际在底层我们可看出,TestObj
是继承于objc_object
这里补充几个知识点:
补充知识点1
typedef struct objc_class *Class;
typedef struct objc_object *id;
①: 可看出 Class
类实际上是 struct objc_class *
结构体指针类型, Class
其实就是个别名
②: 可看出 id
实际上是 struct objc_object *
结构体指针类型, 这就是为什么我们平常定义id类型
不带*
原因
补充知识点2
struct NSObject_IMPL {
Class isa;
};
- 上面代码可看出:
struct NSObject_IMPL NSObject_IVARS;
其实就是ISA
。
补充知识点3
结构体是可以继承的, 在C++是可以继承的
, 在C可以伪继承
。这种继承的方式是直接将NSObject结构体定义为 TestObj 中的第一个属性, 意味着 TestObj 拥有 NSObject中的所有成员变量。所以 NSObject_IVARS
等效于 NSObject中的isa
补充知识点4
NSString *_SAName;
就是我们定义的属性, 可看到属性在底层是以成员变量形式存放的_I_TestObj_SAName
,_I_TestObj_setSAName_
就是属性的get
,set
方法
补充知识点5
static NSString * _I_TestObj_SAName(TestObj * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_TestObj$_SAName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_TestObj_setSAName_(TestObj * self, SEL _cmd, NSString *SAName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct TestObj, _SAName), (id)SAName, 0, 1); }
// 偏移量定义
extern "C" unsigned long int OBJC_IVAR_$_TestObj$_SAName __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct TestObj, _SAName);
里面可看到实例法里面有
TestObj *
,SEL
等, 这些属于隐藏参数
, 只存在底层, 上层OC不需要get方法取值
return (*(NSString **)((char *)self + OBJC_IVAR_$_TestObj$_SAName));
, 实际上也是一种平移取值方法,self
(首地址) +OBJC_IVAR_$_TestObj$_SAName
(偏移量) = 想要获取值的地址
, 然后通过地址, 获得想查找的值。set方法赋值也是同上, 关键点:
首地址 + 偏移量
联合体与联合体位域
我们先看下联合体相关知识点, 方便我们接下来探索ISA
。
1.联合体与结构体
联合体
与结构体
都是基础的构造数据类型
结构体(struct)
结构体
: 把不同的数据整合成一个整体
, 其变量是共存的
, 变量是否使用都会为其开辟内存空间。
优点:
储存容量大
,包容性强
, 并且成员与成员之间不会互相影响
缺点: 因为结构体里面成员都会给分配内存(不管用没用到), 没有用到的会造成
内存浪费
联合体(union)
联合体
: 也叫共用体
, 把不同的数据整合成一个整体
,其变量是互斥的
, 所有成员同一片内存。联合体
采用内存覆盖技术, 同一时刻只能保存一个成员值
, 即:
优点: 所有成员共用一段内存
节省内存空间
缺点:
包容性弱
,成员互斥
, 同一时刻只能保存一个成员的值
两者的区别
-
内存占用
-
结构体
: 各个成员占用不同
内存, 相互没有影响 -
联合体
: 各个内存占用同一
内存, 修改一个成员影响其他所有成员
-
-
内存分配
-
结构体
: 开辟的内存大于等于
所有成员总和(对齐可能导致有空余) -
联合体
: 内存占用为最大成员
占用的内存
-
例子
例子1
例子2
针对于上面例子也可看出, 结构体
开辟的内存大于等于
所有成员总和, 而 联合体
: 内存占用为最大成员
占用的内存
位域
还是先看个例子
我们发现, 针对结构体1我们给每一项赋值得到结构体2, 当打印内存占用时候发现, 结构体占用内存大小变为1了, 这就是位域
(:
后面定义位数
)
结构体1存放情况
0000 0000 0000 0000 0000 0000 0000 1111
可看到其实浪费了很多内存
结构体2存放情况
0000 1111
→
0000 dcba 存放情况(从后往前排)
结构体2只占4位
, 留意是位
, 占0.5字节, 系统会自动分配1字节
联合体位域
有了联合体
还有位域
概念, 我们看下联合体位域, 还是先看例子
针对于上面例子, 我们每赋值一次, p
一下, 可看到
- 结构体成员是
共存
的, 而联合体成员是互斥
的, 同一阶段只能有一个被使用, 这就是联合体位域
(age = 16122, height = 2.1220037562916146E-314, 这种是脏内存脏数据, 不被使用的)
isa
alloc核心三个方法的最后一个: 指针关联 obj->initInstanceIsa(cls, hasCxxDtor);
其中isa
很关键, 我们先看下其定义
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
// 对位域的赋值
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
isa = newisa;
}
在initIsa
可看到isa
是一个isa_t
类型
union isa_t {
// isa构造方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
// 定义bits 位域
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_t
是union联合体类型
, 而联合我们重点要看位域, 看下里面存储那些东西, 而isa_t
的位域为ISA_BITFIELD; // defined in isa.h
, 点进去看下有:
其中ISA_BITFIELD
, 其中我们重点看下arm手机端
的和x86_64模拟器
的就行
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# 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; \
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)
# endif
# elif __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; /*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)
-
nonpoint
: 占1
位, 表示是否对isa
开启了指针优化, 默认创建都是nonpointisa
(不纯), 纯isa
需配置- 0: 纯
isa
指针 - 1: 不仅仅是类对象指针, isa中还包含
类信息
, 引用计数, 关联对象, 析构函数等
- 0: 纯
-
has_assoc
: 占1
位, 是否关联了对象- 0: 没有关联了对象
- 1: 关联了对象
-
has_cxx_dtor
: 占1
位, 是否有C++或者Objc的析构器, 占1位。类似dealloc
, 因为底层的C++释放, 才是真正的释放。可以理解成底层C++没有释放, OC层其实不是释放。-
0
: 没有, 则可以更快的释放对象 -
1
: 有, 有析构函数,则需要做析构逻辑
-
-
shiftcls
: 类相关信息, 其实存的是类的指针地址
-
arm64
: 占33
位 -
x86_64
: 占44
位
-
magic
: 占6
位, 用于调试器判断当前对象是真的对象 还是 没有初始化的空间weakly_refrenced
: 占1
位, 指对象是否被指向 或者 曾经指向一个ARC的弱变量
, 没有弱引用的对象可以更快释放unused
: 占1
位has_sidetable_rc
: 占1
位, 散列表, 通常对象引用计数大于10时,需用到它, 借用该变量存储进位-
extra_rc
:对象的引用计数
表示该对象的引用计数值,实际上是引用计数值减1
例如: 引用计数为10,那么extra_rc为9。对象引用计数大于10, 会用到has_sidetable_rc
-
arm64
: 占19
位 -
x86_64
: 占8
位
-
arm64下ISA
位域:
x86_64下ISA
位域:
当然我们也可以断点读一下
isa
信息, 也可以看位域情况
isa获取类
之前提到过isa
中存的就是类信息, 那么已知isa怎么获取类呢?
方法1: isa
通过掩码ISA_MASK
得到类地址
OC对象中的isa
并不是直接存放所指向对象的地址值,而是需要通过和ISA_MASK
进行一次&
运算才能得出真正的地址
看下例子
-
x/4gx test
: 以4片16进制读取test内存段, 其中0x011d8001000081ad
为isa -
p/x 0x011d8001000081ad & 0x00007ffffffffff8ULL
: 取isa
&ISA_MASK掩码
(留意下模拟器取x86_64掩码)得到0x00000001000081a8
-
p或p/x test.class
: 直接读取下test
的类信息, 发现0x00000001000081a8
, 两者相等
方法2: 字节平移得到类地址
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
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
由于类信息存在shiftcls
, 即中间44位(X86_64下), 固定前面3位, 后面17位。那么我们可以通过字节平移得到类信息。
看下例子
-
isa
先向右平移3
位, 低3位抹零 -
isa
再向左平移20
位, 高位抹17位零(20位原因是, 因为先向右移动了3位) -
isa
再向右平移17
位, 即得到类信息
注意: arm64下shiftcls
为33
, 固要按33
进行相应平移得到类信息
总结
根据我们之前的探索可得出
struct objc_class : objc_object
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
类(Class), 对象(NSObject)底层都是继承于objc_object
, 其本质为结构体
, 结构体里面有个nonpointer
的isa
其本质是联合体
, 同时isa
里面按位域储存相应信息。