本文主要讨论两个方面 意识OC对象的本质 ,二是
isa
的结构及与类的关联
对象的本质
结构体,位域,联合体
首先我们来了解下 结构体,位域,联合体概念
struct LMDirection{
BOOL up;
BOOL down;
BOOL left;
BOOL right;
};
struct LMDirection lmDirection;
NSLog(@"LMDirection--------%lu",sizeof(lmDirection));
//LMDirection--------4
struct LMDirection1{
BOOL up :1;
BOOL down :1;
BOOL left :1;
BOOL right :1;
};
struct LMDirection1 lmDirection1;
NSLog(@"LMDirection1--------%lu",sizeof(lmDirection1));
//LMDirection1--------1
union LMdirection2{
BOOL up;
BOOL down;
BOOL left;
BOOL right;
};
union LMdirection2 lmDirection2;
NSLog(@"LMDirection2--------%lu",sizeof(lmDirection2));
//LMDirection2--------1
- lmDirection 是一个简单的结构体对象,占4个字节空间
- lmDirection1是一个使用位域的结构体对象,占用1个字节空间
- lmDirection2是一个联合体对象,占用1个字节空间
下面我们通过对结构体和联合体成员变量赋值,看下,赋值过程中,成员变量的变化
struct LMPerson{
NSString *name;
NSInteger age;
CGFloat height;
};
struct LMPerson person;
person.name = @"MC";
person.age = 18;
person.height = 190.0;
union LMUnionPerson{
char *name;
int age;
float height;
};
union LMUnionPerson person1;
person1.name = "lilei";
person1.age = 16;
person1.height = 180.0;
我们从创建person对象开始打断点,一步一个输出,查看成员变量变化:
结论:
- 结构体占用空间大小为其内部变量占用大小的总和
- lmDirection1 中每个变量占
1位
,总共占用4位
即一个字节
- 联合体占用空间大小为其内部成员变量占用空间最大的那个大小
- 结构体内成员变量的存储是互不影响的,联合体内是互斥的
Clang
我们知道Objective-c语言底层的实现是C语言和C++,因此我们可以通过clang轻量级编译器的代码还原,还原OC代码的底层实现,将.m文件编译成.cpp文件。
-
Clang
是一个C++
编写、基于LLVM
、发布于LLVM BSD许可证
下的C/C++/Objective-C/Objective-C++
编译器 -
Clang
主要功能是把.m
文件编译为C++
的.mm
文件等,便于我们探究底层代码逻辑
Clang 终端编译命令
clang -rewrite-objc main.m -o main.cpp
//UIKit报错 ---main.m:8:9: fatal error: 'UIKit/UIKit.h' file not found
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
类的本质
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <objc/runtime.h>
@interface LMPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation LMPerson
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
LMPerson *person = [LMPerson alloc];
person.name = @"MC";
person.age = 18;
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
首先将main.m
转化为main.cpp
,打开main.cpp
分析源码.
全局搜索LMPerson
,找到如下代码:
typedef struct objc_object LMPerson;
typedef struct {} _objc_exc_LMPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LMPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LMPerson$_age;
struct LMPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
NSInteger _age;
};
这里我们可以看到
LMPerson
类本质是一个objc_object
结构体
在LMPerson_IMPL
中包含一个结构体成员NSObject_IVARS
,它是NSObject_IMPL
类型,查看NSObject_IMPL
的代码以下:
struct NSObject_IMPL {
Class isa;
};
OC
中的对象其实就是经过结构体来实现的。在NSObject_IMPL
包含了一个Class
类型的成员isa
,这也说明上面代码中的NSObject_IVARS
就是isa
我们接着找一下objc_object
的具体实现,全局搜索objc_object {
:
typedef struct objc_class *Class;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
我们发现
objc_object
是一个结构体,内部有一个Class
类型的isa
,无意中我们又发现,Class
是一个objc_class
的结构体指针 ,id
是一个objc_object
的结构体指针 ,SEL
是一个objc_selector
的结构体指针
Set&&Get
static NSString * _I_LMPerson_name(LMPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LMPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LMPerson_setName_(LMPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LMPerson, _name), (id)name, 0, 1); }
static NSInteger _I_LMPerson_age(LMPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_LMPerson$_age)); }
static void _I_LMPerson_setAge_(LMPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_LMPerson$_age)) = age; }
这里是属性的
set
和get
方法,我们注意到LMPerson * self, SEL _cmd
是隐藏参数
get
方法是通过内存平移的方式获取对应的属性值
set
方法也是通过内存平移的方式对属性进行赋值
这里我们注意到name
属性赋值的时候有一个objc_setProperty
,什么时候会有这个方法呢?我们来做个实验:
main.m
修改为如下代码
@interface LMPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *name1;
@property (nonatomic, strong) NSString *name2;
@property (atomic, strong) NSString *name3;
@property (nonatomic, assign) NSInteger age;
@property (atomic, assign) NSInteger age1;
@end
@implementation LMPerson
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
LMPerson *person = [LMPerson alloc];
person.name = @"MC";
person.name1 = @"1";
person.name2 = @"2";
person.name3 = @"3";
person.age = 18;
person.age1 = 16;
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
然后通过Clang
转化为main.cpp
文件,打开main.cpp
找到set
和get
方法代码:
结论:
objc_setProperty
与copy
修饰有关系,与atomic
,nonatomic
,assign
,strong
无关
objc4源码查找objc_setProperty
分析后续补
isa 结构 及与类的关联
我们上面查看main.cpp
文件发现isa
是Class
类型的,但是我们在objc4源码
中查看objc_object
结构发现:
struct objc_object {
private:
isa_t isa;
public:
...
这是怎么回事呢? 下面一起来探究下
我们在探究alloc
流程的时候OC对象原理探索(上)-alloc流程,最后一步_class_createInstanceFromZone
中obj->initInstanceIsa(cls, hasCxxDtor)
创建isa关联类,下面我们进入源码看下具体实现
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); //如果纯指针 直接赋值
} 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; //macOS 0x000001a000000001ULL
// 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;
}
// 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) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
分析源码:isa_t
是一个联合体, 有一个Class
类型的cls
变量,有一个uintptr_t
类型的bits
,如果定义了ISA_BITFIELD
,还包含一个结构体,结构体内部是ISA_BITFIELD
;
这些变量是互斥的,一个被赋值以后,其他的变量值为空或被覆盖
我们来看下ISA_BITFIELD
具体是什么
在macOS中的图示:
各个变量的含义
-
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
那么为什么初始化initIsa
的时候isa
的类型是isa_t
,而在NSObject
定义中isa
的类型是Class
? 来看下objc_object
中的源码函数ISA()
:
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
我们发现在得到isa
的时候做了一个类型强转
,这是因为isa
反应的都是类的信息,方便开发者的理解
isa与类的关联
cls
与 isa
关联原理就是isa
指针中的shiftcls位域
中存储了类信息,下面我们通过几种方式来验证一下
-
通过
initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3
打开可编译的源码objc4-818.2
,创建一个LGPerson
类,在main.m
里创建类对象
LGPerson *p = [LGPerson alloc] ;
并打上断点
执行进入源码objc_object::initIsa
,跳过纯指针的赋值后,在newisa.bits = ISA_INDEX_MAGIC_VALUE;
前打印newisa
(lldb) p newisa
(isa_t) $4 = {
bits = 0
cls = nil
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 0
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
newisa.bits
赋值之后打印
(lldb) p newisa
(isa_t) $5 = {
bits = 8303511812964353
cls = 0x001d800000000001
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 59
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
其中magic是59是由于将isa指针地址转换为二进制,从47(因为前面有4个位域,共占用47位,地址是从0开始)位开始读取6位,再转换为十进制
继续往下执行,走到
newisa.setClass(cls, this);
,进入isa_t::setClass
方法:我们看下关键代码:
shiftcls = (uintptr_t)newCls >> 3;
继续执行走到
initIsa
的isa = newisa
我们输出一下这个
newisa
与
bits赋值
结果的对比,bits
的位域中有两处变化
-
cls
由默认值,变成了LGPerson
,将isa
与cls
关联 -
shiftcls
由0
变成了536875124
-
通过
isa & MASK
我们在alloc
之后打上断点,在lldb
里进行调试
LGPerson *p = [LGPerson alloc];
arm64
中,ISA_MASK
宏定义的值为0x0000000ffffffff8ULL
x86_64
中,ISA_MASK
宏定义的值为0x00007ffffffffff8ULL
(lldb) x/4gx p
0x10153a300: 0x011d8001000083a1 0x0000000000000000
0x10153a310: 0x0000000000000000 0x0000000000000000
(lldb) po 0x011d8001000083a1 & 0x00007ffffffffff8ULL
LGPerson
我们看到isa的地址&ISA_MASK
最后得到的就是类LGPerson
-
通过
isa
位运算验证
类的信息存储在isa
的shiftcls
中,在macOS
环境下,shiftcls
占44
位,前面有3
位,后面有17
位
-
isa
初始地址0x011d8001000083a1
- 右移
3
位得到十进制地址--10045138768236660
--转16进制为--0x0023b00020001074
- 左移
20
位得到十进制地址--562954370023424
--转16进制为--0x0002000107400000
- 右移
17
位得到十进制地址--4295000992
--转16进制为0x00000001000083a0
- 输出
po
--$3
得到LGPerson
- 输出
LGPerson.class
的地址0x00000001000083a0
与$3
吻合,得到验证.