前言:
OC对象的本质到底是什么?里面到底是什么结构呢?我们常说的isa是什么?我们在iOS底层原理 01 : alloc&init中探索的obj->initInstanceIsa(cls, hasCxxDtor)
是将isa与类信息关联
的,那么isa具体是如何与类信息关联的呢?下面我们带着这些问题一起来学习。
OC对象的本质
接下来我们一起探索OC对象的本质:
- 首先我们在新建好的工程的main.c文件里面自定义一个LGHPerson类
@interface LGHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGHPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGHPerson *person = [LGHPerson alloc];
NSLog(@"Hello, World!");
}
return 0;
}
然后打开终端,将mian.c文件通过clang命令编译成我们需要的main.cpp, 即:在终端输入:
clang -rewrite-objc main.c -o main.cpp
打开mian.cpp,快速查询LGHPerson,我们会发现如下code,我们发现clang编译器会把
LGHPerson
编译成struct LGHPerson_IMPL{}
结构体,所以LGHPerson类
的本质其实就是LGHPerson_IMPL{}的结构体
struct LGHPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //isa
NSString *_name;
};
总结:
OC对象的本质是结构体
,每一个结构体都有struct NSObject_IMPL NSObject_IVARS
的成员变量(即isa
),OC类的属性会被编译成对应结构体的成员变量。
探索obj->initInstanceIsa()具体实现?
我们从objc4源码(传送:可编译调试的objc4源码),找到initInstanceIsa()这个函数
-
initInstanceIsa()
里面会执行initIsa()
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
- 在
initIsa()
函数里面,如果是!nonpointer
, 通过初始化函数isa_t((uintptr_t)cls)
并赋值给isa
,否则新建isa_t
,并对isa_t的位域里面的变量进行赋初始值
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
isa的本质
接下来我们来探索一下isa,我们在initIsa()
函数里面,我们看到isa
是一个isa_t的联合体
。
1. 最终我们在isa.h文件中,看到isa_t的结构
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD;
};
#endif
};
2. ISA_BITFIELD
宏定义,我们看到定义的是位域, 在__arm64__
和__x86_64__
下里面定义的变量是相同的,只是所占的字节数略微有些不同。
# if __arm64__
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# elif __x86_64__
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
endif
3. 所以其实isa_t
的真正结构是联合体位域
,__x86_64__
下,我们可以看到位域里面所有变量所占的字节总数是64位,恰好是8字节
union isa_t {
Class cls;
uintptr_t bits;
struct {
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 deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
};
4. 那么每个的作用是什么呢?这里总结一下:
nonpointer
:表示是否对 isa 指针开启指针优化, 0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 (自定义的类nonpointer为1)has_assoc
:关联对象标志位,0没有,1存在has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象shiftcls
: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针,x86架构用44位来存储magic
:⽤于调试器判断当前对象是真的对象还是没有初始化的空间weakly_referenced:志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
deallocating
:标志对象是否正在释放内存has_sidetable_rc
:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位extra_rc
:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
总结:
1.isa_t
是一个联合体,里面的成员变量共用一段内存
,成员变量之间setter方法互斥
(不能同时set),所以对cls赋值的时候就不能对bits赋值,对bits赋值的时候就不能对cls赋值。
2.shiftcls
是用来存储类信息的。
使用lldb去体验isa_t的结构
接下来我们去验证到底LGHPerson类信息是否存储到shiftcls里面?
-
首先断点到initIsa()函数里面
- 我们使用lldb打印一些信息
newisa.bits =ISA_MAGIC_VALUE
; 是给nonpointer
和magic
附上初始值。
(lldb) p newisa
(isa_t) $12 = {
cls = 0x001d800000000001
bits = 8303511812964353
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
从下面打印的信息来看,newisa.cls
和newisa.bits
是共用同一段内存
,0x001d800000000001的10进制就是8303511812964353。
(lldb) p newisa.cls
(Class) $9 = 0x001d800000000001
(lldb) p newisa.bits
(uintptr_t) $10 = 8303511812964353
(lldb) p/d 0x001d800000000001
(long) $11 = 8303511812964353
我们接着断点往下走
打印
newisa
,我们看到shiftcls
已经有值了,因为newisa.shiftcls = (uintptr_t)cls >> 3;
将类存到shiftcls
p newisa
(isa_t) $13 = {
cls = LGPerson
bits = 8303516107940081
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 536871966
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
我们看到(uintptr_t)cls >> 3
是536871966,newisa.shiftcls
的值536871966,说明类信息确实是存放在shiftcls。
(lldb) p (uintptr_t)cls >> 3
(uintptr_t) $17 = 536871966
(lldb) p newisa.shiftcls
(uintptr_t) $18 = 536871966
验证shiftcls确实是存放的是类信息(在__x86__
架构为例)
-
先在main函数打个断点
- 使用lldb打印一些信息
通过lldb查看objc的内存,得到isa
是0x001d8001000020e9
(lldb) po objc
<LGHPerson: 0x100681f90>
(lldb) x/4gx 0x100681f90
0x100681f90: 0x001d8001000020e9 0x0000000000000000
0x100681fa0: 0x756e654d534e5b2d 0x776569566d657449
我们通过移位操作,获取isa
在[3-46]位置上的值
,通过与LGHPerson.class
比对,发现是一致的,都是0x00000001000020e8
。
(lldb) p/x 0x001d8001000020e9>>3
(long) $4 = 0x0003b0002000041d
(lldb) p/x 0x0003b0002000041d<<20
(long) $5 = 0x0002000041d00000
(lldb) p/x 0x0002000041d00000>>17
(long) $6 = 0x00000001000020e8
(lldb) p/x LGHPerson.class
(Class) $7 = 0x00000001000020e8 LGHPerson
我们也可以通过算法 & ISA_MASK (0x00007ffffffffff8ULL)
,来得到shiftcls
的值,
我们看的也能得到0x00000001000020e8
(lldb) p/x 0x001d8001000020e9 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x00000001000020e8
补充
lldb相关的调试命令
p/x //以16进制输出
p/d //以10进制输出
p/o // 以8进制输出
p/t // 以2进制输出
p/c // 打印字符
x // 读取内存 (等价于 memory read)
memory read // 读取内存
x/4gx // 读取4段内存(每段8字节,即32字节)
x/6gx // 读取6段内存(每段8字节,即48字节)
x/8gx // 读取8段内存(每段8字节,即64字节)
po <expr> // 打印对象的 description 方法的结果
help // 列出所有的命令
help <command> // 列出某个命令更多的细节,例如 help print
移位运算
比如有一段8位的内存,我们如何从取出【2-6】位上的值呢?请看下面示意图:
先
>>2
,然后<<3
,最后>>1
,最后得到0111 1000
。
LLVM与Clang
-
LLVM
是构架编译器(compiler)的框架系统
,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。 -
Clang
是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器
。比起GCC,Clang
编译速度快
、占用内存小
、非常方便进行二次开发
。
下图是LLVM和Clang的关系图
总结 :
1. Clang相当于编译过程的前端,而LLVM相当于编译过程的后端。
2. Clang侧重于语法语义分析
和生成中间代码
,而LLVM侧重于代码优化
,生成目标程序
。
3.clang命令编译mian.c的指令:clang -rewrite-objc main.m -o main.cpp