前言
对象
我们几乎每天都在说的词,不管是生活中,还是工作中。生活中你如果没有对象,那么兄弟你得加油了,实在不行我给你new
一个。在这愉快的玩笑中我们走入工作中的对象,说到对象不得不提到isa
,因为isa
告诉我们这个对象是属于谁的。下面就探究下对象的本质
以及isa
。
准备工作
联合体(union)
联合体和结构体类型也是由不同类型的数据组成,下面通过代码探究下联合体
union LWPerson {
int a; //4
short b; //2
char c; //1
};
int main(int argc, char * argv[]) {
@autoreleasepool {
union LWPerson person;
person.a = 8;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.b = 2;
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
person.c = 'd';
NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
NSLog(@"%lu---%lu",sizeof(person),sizeof(union LWPerson));
}
return 0;
}
复制代码
2021-06-10 15:50:45.789591+0800 isa探究[78582:10551210] a=8---b=8---c=
2021-06-10 15:50:45.790087+0800 isa探究[78582:10551210] a=2---b=2---c=
2021-06-10 15:50:45.790107+0800 isa探究[78582:10551210] a=100---b=100---c=d
2021-06-10 15:50:45.790125+0800 isa探究[78582:10551210] 4---4
复制代码
总结:
- 联合体可以定义多个不同类型的成员,联合体的
内存大小
由其中最大的成员的大小
决定。 - 联合体中修改其中的某个变量会
覆盖
其他变量的值。 - 联合体所有的变量
公用一块内存
,变量之间互斥
。
联合体优缺点
- 优点:内存使用更为灵活,节省内存。
- 缺点:不够包容。
位域(Bit field)
有些信息在存储时,并不需要占用一个
完整的字节, 而只需占几个或一个二进制位
。例如在存放一个只有0
和1
两种状态成员, 用一位二进位即可,目的是节省存储空间,处理简便。下面通过代码探究位域
struct OldCar {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct NewCar {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
int main(int argc, char * argv[]) {
@autoreleasepool {
struct OldCar oldCar;
struct NewCar newCar;
NSLog(@"----%lu----%lu",sizeof(oldCar),sizeof(newCar));
}
return 0;
}
复制代码
2021-06-10 16:25:04.266985+0800 isa探究[78608:10560097] ----4----1
复制代码
总结:oldCar
的内存大小是4字节
,newCar
的内存大小是1字节
。1字节
包含8位(bit)
,newCar
中所有变量都是按 位(bit)
存在这1字节
中,具体的存放格式是从右往左往 0000 1111
依次存放 front
、back
、left
、right
没有45°仰望天空
哈。
对象的本质
探究对象本质之前,先了解编辑器Clang
Clang
Clang
是一个C语言
、C++
、Objective-C
语言的轻量级编译器,是由Apple
主导编写的Clang
主要用于把源文件编译成底层文件,比如把main.m
文件编译成main.cpp
、main.o
或者可执行文件
。便于观察底层的逻辑结构,便于我们探究底层。
Clang
终端编译命令
clang -rewrite-objc main.m -o main.cpp
//UIKit报错
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//3、模拟器编译
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
复制代码
对象的本质
下面探究对象的本质,通过实例进行探究代码如下
@interface LWPerson : NSObject
@property(nonatomic, copy)NSString *LWname;
@property(nonatomic,assign)NSInteger age;
@end
@implementation LWPerson
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
}
return 0;
}
复制代码
通过clang
命令把 main.m
文件编译成 main.cpp
部分代码如下
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
struct NSObject_IMPL {
Class isa;
};
extern "C" unsigned long OBJC_IVAR_$_LWPerson$_LWname;
extern "C" unsigned long OBJC_IVAR_$_LWPerson$_age;
struct LWPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_LWname;
NSInteger _age;
};
// @property(nonatomic, copy)NSString *LWname;
// @property(nonatomic,assign)NSInteger age;
/* @end */
// @implementation LWPerson
复制代码
总结:LWPerson
底层是结构体
,LWPerson_IMPL
结构体中有3
个变量,其中_LWname
_age
是自定义的属性,NSObject_IVARS
就是 NSObject
中isa
。LWPerson
继承于NSObject
,意味着LWPerson
也有NSObject
所有的成员变量。
对象本质总结
- 对象的本质是
结构体
LWPerson
的isa
是继承NSObject
中的isa
NSObject
中只有一个成员变量那就是isa
isa
关联类
对象本质探究让我们知道对象的第一个变量就是isa
,在探究 IOS 底层原理之 alloc 探究 中我们发现alloc
一个对象最核心的三个方法cls->instanceSize
计算内存大小 , (id)calloc(1, size)
开辟内存返回地址指针, obj->initInstanceIsa
初始化isa
关联类。既然这么多地方都指向isa
,那么就来探究下isa
的结构以及isa
是如何关联类的。
isa
结构
通过alloc
流程 alloc
–> _objc_rootAlloc
–> callAlloc
–> _objc_rootAllocWithZone
–> _class_createInstanceFromZone
,断点在 obj->initInstanceIsa
,进入obj->initInstanceIsa
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);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
...
newisa.bits = ISA_MAGIC_VALUE;
...
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
复制代码
我们发现了isa
的结构类型是isa_t
,进入isa_t
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_t
是联合体
。isa_t
有两个变量 一个是bits
,一个是cls
。通过上面分析联合体
是互斥
的,那就意味着初始化isa
有两种方式:
bits
被赋值,cls
没有值或者被覆盖cls
被赋值,bits
没有值或者被覆盖
isa_t
中还有一个结构体成员变量ISA_BITFIELD
。这是一个宏
对应有两个端,一个是__arm64__
(iOS),一个是__x86_64__
(macOS)。ISA_BITFIELD
通过位域存储信息,那就具体看下有哪些信息
图中的省略了iOS
的模拟器数据只留下了真机
和macOS
的宏
bits
的64位
存储分布图
各存储变量的含义:
nonpointer
:表示是否对isa
指针进行优化,0
表示纯指针,1
表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等has_assoc
:关联对象标志位,0
表示未关联,1
表示关联has_cxx_dtor
:该对象是否C ++
或者Objc
的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象shiftcls
:储存类指针的值,开启指针优化的情况下,在arm64
架构中有33
位用来存储类指针,x86_64
架构中占44
位magic
:用于调试器判断当前对象是真的对象
还是没有初始化
的空间weakly_referenced
:指对象是否被指向或者曾经指向一个ARC
的弱变量,没有弱引用的对象可以更快释放deallocating
:标志对象是否正在释放has_sidetable_rc
:当对象引用计数大于10
时,则需要借用该变量存储进位hextra_rc
:表示该对象的引用计数值,实际上引用计数值减1
,例如,如果对象的引用计数为10
,那么extra_rc
为9
,如果大于10
,就需要用到上面的has_sidetable_rc
isa
结构分析总结
isa
分为nonpointer
类型和非nonpointer
。非nonpointer
类型只是一个纯指针,nonpointer
还包含了类的信息isa
是联合体
+位域
的方式存储信息的。采用这种方式的有点就是节省大量内存
。万物皆对象,只要是对象就有isa
指针,大量的isa
就占用了很多内存,联合体
公用一块内存节省了部分内存,而位域
更是在节省内存的基础上存储了信息,可以说isa
指针的内存得到了充分的利用。
isa
关联类探究
首先定义一个LWPerson
类,初始化[LWPerson alloc]
,流程如下:alloc
–> _objc_rootAlloc
–> callAlloc
–> _objc_rootAllocWithZone
–> _class_createInstanceFromZone
–> obj->initInstanceIsa
–>initIsa
,现在探究isa
到底怎么和类关联起来
图中显示 newisa(0)
对bits
进行赋值,bits
里面所有的变量都是0
图中显示 newisa.bits
= ISA_MAGIC_VALUE
, ISA_MAGIC_VALUE
是一个宏 ISA_MAGIC_VALUE
= 0x001d800000000001
,变量值改变的有bits
= 8303511812964353
,cls
= 0x001d800000000001
,nonpointer
= 1
,margic
= 59
。我们恢复下赋值的过程。如下图
0x001d800000000001
转换成10进制
等于8303511812964353
cls
=0x001d800000000001
是因为给bits
赋值的时候覆盖了cls
,isa_t
是联合体
- 第
0
位的1
等于nonpointer
=1
margic
的值怎么算呢,按位计算从47位
开始,长度是6位
结果就是111011
,从第0
个位置开始算111011
的10进制
就是59
.(对比图中的第一张计算图和 第三张计算图)
断点进入setClass
图中显示shiftcls
= 536873007
,下面来验证下结果是否正确
图中显示
shiftcls
=536873007
与上面的算法shiftcls
=(uintptr_t)newCls >> 3
得到的结果是一样的。LWPerson
的类地址>>3
进行10进制
转换赋值给shiftcls
。此时isa
已经关联LWPerson
类 ,cls
变量被覆盖
,cls
=LWPerson
。(uintptr_t)newCls >> 3
原因,isa
指针的变量是按位域
存储的,shiftcls
从第3
位开始存储,为了不影响前3位
的数据 ,只有把newCls
的前3位
右移抹零
isa
关联类总结
cls
与 isa
关联类的原理就是isa
指针中的shiftcls
存储了类信息。
总结
通过对isa
和对象的本质
的探究,认识到对层次探究的必要性和重要性,虽然探究的过程是复杂的繁琐的,但是结果却令人兴奋,这就是底层探究的魅力。
补充
isa
关联类的几种方式
shiftcls
=(uintptr_t)newCls >> 3
isa
位运算isa
&ISA_MASK
shiftcls
=(uintptr_t)newCls >> 3
上面已经验证过了
isa
位运算
类信息是存储在isa
指针中,shiftcls
在isa
是按位
存储,macOS
从第3
位开始存储,大小是44
位。位运算
是目的是只保留shiftcls
信息,其它位的信息抹零
。位运算
过程shiftcls
的相对位置要保持不变。如下图
isa
的值是0x011d8001000041a1
,0x011d8001000041a1 >> 3
结果等于0x0023b00020000834
0x0023b00020000834 << 20
结果等于0x0002000083400000
0x0002000083400000 >> 17
结果等于0x00000001000041a0
po 0x00000001000041a0
的结果是LWPerson
,说明isa
已经关联了类
isa
位运算过程图
isa
&ISA_MASK
ISA_MASK
是一个宏
。__x86_64__
的值等于 0x00007ffffffffff8ULL
,__arm64__
的值等于0x0000000ffffffff8ULL
。isa
的值是0x011d8001000041a1
,验证下0x011d8001000041a1
&0x00007ffffffffff8ULL
结果。如下图
图中显示 po 0x011d8001000041a1 & 0x00007ffffffffff8ULL
的结果是LWPerson
。说明isa
已经关联了类。二进制方式打印ISA_MASK
p/t 0x00007ffffffffff8ULL
,结果显示高17位
是0
,低3位
是0
,中间的44位
就是shiftcls
是1
。ISA_MASK
就像一个面具把露出来的显示,其它的全部抹掉。
init
和new
探究
init
和new
的探究已经更新到 IOS 底层原理之 alloc 探究 博客中