类和isa(一)
OC中的类是什么,长什么样?
这是一个很好的问题,如果说 runtime 是灵魂, 类 就是他的使者。了解类,是我们继续探索的基石。
请记住一句话, 万物皆对象! 哪怕是类,也终究逃不出来自苹果的这个魔咒
类结构
**对于iOS编译,**iOS的底层代码是由C++实现,但系统库在.h中以C的形式向我们提供API,所以OC会在编译时由 Clang 编译器转成C++继续编译。 想要了解底层源码的同学,苹果也开源了源码,这里是苹果开源代码Source Browser ,其中就有我们需要经常用到的 objc4 和 libmalloc ,这俩货分别是runtime和alloc的不同版本的源码,非常值得大家去选择一个版本,下载和编译,推荐选择最新的编号最大的版本。 什么是Clang? Clang是一个C语言、C++、Objective-C、C++语言的轻量级编译器,是LLVM的一个重要组成部分。如果有时间,非常愿意和小伙伴们探讨iOS的编译和OC的动态化,这两个话题在实际的开发生活中是非常有必要的。
我们准备一段很普通的OC代码:在 main.m
中定义了一个简单的类 LYPerson
,
// 定义一个Person类
@interface LYPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)say;
@end
@implementation LYPerson
- (void)say {}
@end
//
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
打开终端,定位到 main.m
目录, 通过下面Clang的命令,将main.m 生成main.cpp 的C++ 代码。
clang -rewrite-objc main.m -o main.cpp
看一看main.cpp 也就是main的C++实现。我们只找LYPerson, cmd+f
搜索“LYPerson”, 很快,我们看到一段眼熟的代码,和原始的OC的 main.m
是不是很像
看到这个C++的LYPerson,我们会注意到:
- LYPerson作为一个类,本身也可以实例化一个对象,在这里,竟然是结构体
objc_object
的实例,那么类本身有没有可能是一个对象?结构体objc_object
是什么? - LYPerson 内定义了成员:name,同时还定义一个
NSObject_IMP
结构体类型的成员:NSObject_IVARS。 - 我们自定义的属性name,在C++代码里,通过格式拼接成
_I_LYPerson_setName_(LYPerson * self, SEL _cmd, NSString *name)
NSObject_IMPL、objc_class、objc_object
在main.cpp 中我们可以找到LYPerson类型objc_object 和 成员类型NSObject_IMP的定义 IMPL 顾名思义,是implementation的意思。
// 这个是NSObject的定义
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
// 这个是NSObject的实现
struct NSObject_IMPL {
Class isa;
};
// 这个是LYPerson的实现
struct LYPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
typedef struct objc_class *Class; // 注意:Class 是一个指针类型,意味着所有的Class类型都是一个指针,而指针指向的是一个objc_class类型的数据
上面就是cpp中的NSObject和LYPerson的IMPL实现,我们发现, LYPerson 的NSObject_IVARS的isa就是 NSObject 的isa。而且每一个类都有isa!
LYPerson 为什么要有这么一个 struct NSObject_IMPL NSObject_IVARS;
?因为类有继承关系。怎么继承?就是通过内部定义这个 struct NSObject_IMPL NSObject_IVARS;
实现伪继承NSOBject的isa。
亲爱的伙伴们,你们一定还注意到一点,我们的 LYPerson 是一个 obj_object 的结构体类型
typedef struct objc_object LYPerson;
我们又知道, obj_object 在苹果的解释中,他是一个对象,而我们的LYPerson是一个自定义的类,请回忆我最开始讲的那句话: 万物皆对象! LYPerson 也是一个对象,而且它是一个 类对象。作为一个对象,他也有属于自己的类,那么这个类对象(就是我们OC里的类)的类是什么呢,他叫 元类 ,元类上面还有一个类,他叫 根类 ,根类上面是不是还有类呢?类对象是不是只有一份?这些都不在我们这篇文章讨论的范畴,太大了,总得分开阐述。 ** 我们只要知道:类也是一个obj_object对象,而obj_object是一个结构体,换句话说,类的本质是一个结构体,对象也是一个结构体。
又有小伙伴疑问?为什么 万物皆对象 ?我们来看一看obj_object 和 obj_class , 进入我们从obj4的源码:
// 这个是objc_class, 继承于objc_object
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 指向父类
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
......// 太多省略
}
// 这个是objc_object
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
我的天呐,有没有注意到什么。objc_class继承于objc_object,类也是对象,那么每一个obj_class都会有一个isa,在objc_class里,除了isa,还有superclass,superclass就是我们所说的指向的父类。这也是为什么,从NSObject开始,每一个类都有isa,原来是obj_object这个娘胎里就带了isa,那么,isa到底是什么,而是还是个Class类型那他指向的又是什么类?
扩展:自定义类中属性set方法的工作流程
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
static void _I_LYPerson_setName_(LYPerson * self, SEL _cmd, NSString *name)
{
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LYPerson, _name), (id)name, 0, 1);
}
在set方法里,通过固定的API:objc_setProperty实现setName功能,查看objc_setProperty方法,需要到我们前面提到的objc4 的源码工程,下面就是objc4中objc_setProperty的源码和内部的调用:
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
// 核心的一步:reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
读不懂没关系,通过上面这一段,我们大致可以知道:自定义属性的set实现是通过 底层 **objc_setProperty -> reallySetProperty **来完成。在reallySetProperty中,最终新的属性值,存在了slot指针指向的位置,而slot指向的位置就是方法中提到的 :
(id*) ((char*)self + offset);
表示便宜地址,对象的指针占了8个字节,所以当我们给name赋值时,offset会从8开始,我们来验证一下
可以看到self是当前对象,_cmd是当前方法也就是setName,newValue是我们赋的值,offset是8!copy是true,atomic是false,因为我们定义的是(copy, nonatomic)。
isa
初始化isa
我们从一个对象的alloc方法开始看,什么时候产生的isa,依然是在obj4源码里,我们断点调试
alloc方法
进入_objc_rootAlloc
再进入callAlloc
再往里面走,进入_objc_rootAllocWithZone
继续往里走,进入_class_createInstanceFromZone, 这个方法就是在创建实例化的对象,有几个核心的步骤
其中需要注意的是:
- instanceSize 是计算需要开辟多少个字节
- calloc 是正儿八经的开辟空间,创建对象obj,calloc的源码可以查看苹果开源的libmalloc源码。
- initInstanceIsa 初始化isa,或者说是给这个对象设置isa
- object_cxxConstructFromClass 这一步就是完善obj,比如说,设置obj的super_class。
在_class_createInstanceFromZone,我们一步一步进入到了initInstanceIsa
initInstanceIsa看这个方法名,就知道太符合我们的要求了,初始化一个isa,好,继续进入这个方法,来到了initIsa
initIsa
我们最初的断点式[LYPerson alloc] 这句代码是创建一个LYPerson的对象,我们看到,这里的isa是一个isa_t, isa
的shiftcls居然存的是当前的类,而不是父类。我们是不是可以这么说,对象的isa里他的类,他的类又是一个对象,类对象的isa存了类对象的类。哇,是不是和我们常规的类的继承:子类-父类-父父类-。。。。-NSObject有冲突啊。不冲突,这是另一条线:isa指向这个对象(也包括类对象)的类。 我们在创建LYPerson的时候,里买就有了isa,而且是NSObject的isa,这个上面刚讲过。
我们是不是可以得出一个信息:obj的isa->Person,Person的isa->NSObject.
接下来我们获取obj、person、NSObject的isa来验证是不是
获取isa
我们平时想要获取某一个对象的类,是怎么获取? 1、通过runtime的 objc_getClass 函数API 2、直接调用class方法:[obj class]; class方法的实现还是调用了objc_getClass
查看runtime的源码(objc4)
// 这个就是我们熟悉的id类型,和Class一样,也是一个objc_object结构体指针
typedef struct objc_object *id;
- (Class)class
{
// 还是调用了object_getClass
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
很有意思的是,object_getClass的参数是一个id,id是一个objc_object,也在告诉我们,无论是对象还是类,其实都是obj_object。都要再去调用obj_object的getIsa()
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
在getIsa()里,我们关注的是第一行,ISA();
inline Class
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_t类型。我们在初始化isa到最后的时候,将对象的 Class 类型的类放到了一个 isa_t 类型的 shiftcls 成员里,在这,我们通过 isa.bits & ISA_MASK 返回了一了同样 Class 类型的东西出去,我们当初存入的 shiftcls 和这个**isa.bits & ISA_MASK **有什么关系?
如果isa.bits & ISA_MASK 就是shiftcls,我们就完美的串出了一个信息:创建对象时,将对象类绑定到shiftcls,从而让isa指向这个类;通过class方法和objc_getClass函数获取对象的类,实际上是获取shiftcls里绑定的类信息。 ** 要证明上面的假设,我们得先了解一下,isa_t
isa_t 和 isa内存储的信息
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls; // 直接指向Class
uintptr_t bits; // 通过bit位运算,完成绑定Class
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
为什么这里用了一个联合体,我们的开发过程中,大部分的iOS开发者很少会用到这种数据结构,这种结构使用了bits位域,他可以有效的节省我们的内存空间。
扩展:联合体和结构体
结构体
结构体
是指把不同的数据组合成一个整体
,其变量
是共存
的,变量不管是否使用,都会分配内存。
- 缺点:所有属性都分配内存,比较
浪费内存
,假设有4个int成员,一共分配了16
字节的内存,但是在使用时,你只使用了4
字节,剩余的12
字节就是属于内存的浪费 - 优点:存储
容量较大
,包容性强
,且成员之间不会相互影响
联合体
联合体也是由不同的数据类型组成
,但其变量是互斥
的,所有的成员共占一段内存
。而且共用体
采用了内存覆盖技术
,同一时刻只能保存一个成员的值
,如果对新的成员赋值
,就会将原来成员的值覆盖掉
- 缺点:,包容性弱
- 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
内存占用情况
结构体的各个成员会占用不同的内存,互相之间没有影响 共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员 内存分配大小
结构体内存 >= 所有成员占用的内存总和(成员之间可能会有缝隙) 共用体占用的内存等于最大的成员占用的内存
isa_t的成员:bits、cls、ISA_BITFIELD
如果有bits没有值,就通过cls完成初始化;如果有bits有值,则通过bits和ISA_BITFIELD位域完成初始化 这里使用bits 结合 位域ISA_BITFIELD 的目的就是为了节约内存。我们知道,一个指针占8个字节,就是64个二进制位,64位能存储多少信息呢?答案是2的64次方,而我们要存储那些信息呢?我们要存储的信息都在位域ISA_BITFIELD里:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# 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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
这里有两个架构版本:arm64和X86_64。iOS使用的是arm64,macos使用的是X86_64,所以我们只看arm64。 我来解释一下,isa_t中,存储的几个字段的意思
nonpointer
占 1 bit位。表示是否对 isa 指针开启指针优化。- 0:纯isa指针,
- 1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
has_assoc
占 1 bit位。关联对象标志位。- 0没有
- 1存在
has_cxx_dtor
占 1 bit位。该对象是否有 C++ 或者 Objc 的析构器。- 如果有析构函数,则需要做析构逻辑,
- 如果没有,则可以更快的释放对象
shiftcls
占 33 bit位。 存储类指针的值。(重点)magic
占 6 bit位。⽤于调试器判断当前对象是真的对象还是没有初始化的空间weakly_referenced
占 1 bit位。对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放deallocating
占 1 bit位。标志对象是否正在释放内存has_sidetable_rc
占 1 bit位。当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位extra_rc
占 19 bit位。当表示该对象的引⽤计数值,实际上是引⽤计数值减1。- 例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。
- 如果引⽤计数⼤于 10,则需要使⽤到下⾯的has_sidetable_rc。
如果这些不用位域,最保守每个字段用short int,那也不得了,一个short int就占了2个byte,也就是16bit,现在有10个字段,这就是需要160bit位!。而现在,我们只需要64bit。
前面的分析,我们知道在初始化isa的时候,我们将对象的类指针放到了isa_t的shiftcls里,我们在获取class时使用的isa.bits & ISA_MASK。
**
在arm64下的isa.bits
对象的isa
我们使用object_getClass,获取对象的isa
最终进入到下面这个方法
将ISA_MASK:0x0000000ffffffff8ULL 转成二进制:
这就是一个简单的C语言的位预算了:过滤为1的位。$2(ISA_MASK)64位,为1的恰好是shiftcls的范围,所以,isa.bits & ISA_MASK 这个位运算,目的就是为了获得shiftcls范围的内容,也就是我们当初initIsa时放入shiftcls里的值——对象的类指针!
我们打印看一下
LYPerson的实例化对象obj 调用class方法,获得的是obj的类“LYPerson”,
我们这里用的是LYPerson的实例化对象,获取的是实例对象obj的isa,那我们如果要获取LYPerson这个类的isa,我们又会得到什么?
类的isa
我们来看一下LYPerson类的isa指向的是谁
第一个是对象obj的isa,指向了LYPerson,第二个是类LYPerson的isa也指向了LYPerson,但是内存地址不一样,说明这两个LYPerson不是一个。
如果我们在这样继续下去:不断的查看isa指向
isa指向关系:
obj -> LYPerson -> LYPerson -> NSObject(指向自己)
是不是一个非常经典的图就出来了:
虚线 就是 isa 指向,从类的开始,后面都是前一个的 元类 ,直到NSObject,因为它是OC里的根元类。 实线 是我们熟知的类继承,也就是 objc_class 里的那个 super_class 存储的类指针。
到了这里,isa是干嘛的,想必大家也已经清楚: isa说白了,就是实例某个对象(在runtime源码里类也是对象)的类 isa的顺序也比较固定: 对象 -> 类 -> 元类 -> 根元类(NSObject) -> 根根元类(NSObject)