前言
对象
我们几乎每天都在说的词,不管是生活中,还是工作中。生活中你如果没有对象,那么兄弟你得加油了,实在不行我给你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
{
NSString * height;
}
@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;
double height;
NSString *_LWname;
NSInteger _age;
};
// @property(nonatomic, copy)NSString *LWname;
// @property(nonatomic,assign)NSInteger age;
/* @end */
// @implementation LWPerson
源码分析:LWPerson
底层是结构体
,LWPerson_IMPL
结构体中有4
个变量,其中height
,_LWname
,_age
是自定义的属性,NSObject_IVARS
就是 NSObject
中isa
。LWPerson
继承于NSObject
,意味着LWPerson
也有NSObject
所有的成员变量。
NSObject
的底层是NSObject_IMPL
里面只有一个成员变量是Class isa
。通常叫isa
叫做isa
指针,那么这里的Class
应该是个指针类型,在main.cpp
文件中全局搜索*Class
。代码如下
typedef struct objc_class *Class;
//对象的底层实现
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
//NSObject的底层实现
struct NSObject_IMPL {
Class isa;
};
源码分析:
Class
类型实际是一个objc_class
类型的结构体指针,objc_class
是所有类的底层实现。由此我们猜测isa
可能跟类信息存在着重要的关联,具体的关联下面会进行探究。NSObject
的底层实现和对象
的底层实现有什么区别。两者成员变量的结构体都是Class isa
,那么就必然存在继承关系。所有对象的底层都是继承objc_object
,在OC
中基本上所有的对象都是继承NSObject
,但是真正的底层实现是objc_object
的结构体类型。
对象的本质拓展
在main.cpp
文件中全局搜索*Class
时,发现有这样几行代码如下
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
源码分析:熟悉的id
和SEL
。常用的id
原来是一个objc_object
结构体指针,这就解释了id
修饰变量和作为返回值的时候为什么不加*
,意料之外的是SEL
也是结构体指针。
在main.cpp
中,发现了方法的底层实现代码如下
图中显示:
- 属性的
get
和set
方法中都有获取当前变量的位置代码,怎么去获取当前的变量呢?底层实现就是当前对象的首地址
+变量的偏移值
。 - 代码中我自定义了一个局部变量
height
,但是底层代码仅添加了一个变量
。而定义的属性,底层自动添加了带_变量
以及get
和set
方法的实现。哎呀,这不是我们常见的面试题嘛。.cpp
文件还是很不错的啊。
对象本质总结
- 对象的本质是
结构体
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); // isa初始化
if (!nonpointer) {
newisa.setClass(cls, this);//如果是纯指针 isa被直接cls赋值
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#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;
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
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
为什么需要右移3
位
MACH_VM_MAX_ADDRESS
表示虚拟内存最大寻址
空间,在__arm64__
中MACH_VM_MAX_ADDRESS
=0x1000000000
虚拟内存最大寻址
空间是36
位。在__x86_64__
中MACH_VM_MAX_ADDRESS
=0x7fffffe00000
虚拟内存最大寻址
空间是47
位。- 字节对齐是
8字节
对齐,也就是说指针的地址只能是8
的倍数,那么指针地址的后3位
只能是0
,比如0x8
,0x18
,0x30
转换成二进制后3位
都是0
。
基于以上两条,为了节省内存空间,把后3位
是0
抹去。在__arm64__
中shiftcls
占33
位,在__x86_64__
中shiftcls
占44
位。因此需要将类地址
右移3
位,即(uintptr_t)newCls >> 3
。可以说isa
的优化算是做到了极致。
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位
是1
,用来显示isa
中的shiftcls
。ISA_MASK
就像一个面具把露出来的显示,其它的全部抹掉。
init
和new
探究
init
和new
的探究已经更新到 IOS 底层原理之 alloc 探究 博客中
今天的文章IOS 底层原理之对象的本质&isa关联类分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23340.html