这是一个很好的问题,若是说 runtime 是灵魂, 类 就是他的使者。了解类,是咱们继续探索的基石。macos
请记住一句话, 万物皆对象! 哪怕是类,也终究逃不出来自苹果的这个魔咒markdown
**对于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++ 代码。app
clang -rewrite-objc main.m -o main.cpp
复制代码
看一看main.cpp 也就是main的C++实现。咱们只找LYPerson, cmd+f
搜索“LYPerson”, 很快,咱们看到一段眼熟的代码,和原始的OC的 main.m
是否是很像 看到这个C++的LYPerson,咱们会注意到:ide
objc_object
的实例,那么类自己有没有多是一个对象?结构体 objc_object
是什么?NSObject_IMP
结构体类型的成员:NSObject_IVARS。_I_LYPerson_setName_(LYPerson * self, SEL _cmd, NSString *name)
在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。oop
亲爱的伙伴们,大家必定还注意到一点,咱们的 LYPerson 是一个 obj_object 的结构体类型优化
typedef struct objc_object LYPerson;
复制代码
咱们又知道, obj_object 在苹果的解释中,他是一个对象,而咱们的LYPerson是一个自定义的类,请回忆我最开始讲的那句话: 万物皆对象! LYPerson 也是一个对象,并且它是一个 类对象。做为一个对象,他也有属于本身的类,那么这个类对象(就是咱们OC里的类)的类是什么呢,他叫 元类 ,元类上面还有一个类,他叫 根类 ,根类上面是否是还有类呢?类对象是否是只有一份?这些都不在咱们这篇文章讨论的范畴,太大了,总得分开阐述。 ** 咱们只要知道:类也是一个obj_object对象,而obj_object是一个结构体,换句话说,类的本质是一个结构体,对象也是一个结构体。 ui
又有小伙伴疑问?为何 万物皆对象 ?咱们来看一看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类型那他指向的又是什么类?
#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)。
咱们从一个对象的alloc方法开始看,何时产生的isa,依然是在obj4源码里,咱们断点调试 alloc方法 进入_objc_rootAlloc
再进入callAlloc
再往里面走,进入_objc_rootAllocWithZone
继续往里走,进入_class_createInstanceFromZone, 这个方法就是在建立实例化的对象,有几个核心的步骤
其中须要注意的是:
在_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来验证是否是
咱们平时想要获取某一个对象的类,是怎么获取? 一、经过runtime的 objc_getClass 函数API 二、直接调用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
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
字节就是属于内存的浪费容量较大
,包容性强
,且成员之间不会相互影响
联合体 联合体也是由不一样的数据类型组成
,但其变量是互斥
的,全部的成员共占一段内存
。并且共用体
采用了内存覆盖技术
,同一时刻只能保存一个成员的值
,若是对新的成员赋值
,就会将原来成员的值覆盖掉
二者的区别
内存占用状况
结构体的各个成员会占用不一样的内存,互相之间没有影响 共用体的全部成员占用同一段内存,修改一个成员会影响其他全部成员 内存分配大小
结构体内存 >= 全部成员占用的内存总和(成员之间可能会有缝隙) 共用体占用的内存等于最大的成员占用的内存
若是有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 指针开启指针优化。
has_assoc
占 1 bit位。关联对象标志位。
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。
若是这些不用位域,最保守每一个字段用short int,那也不得了,一个short int就占了2个byte,也就是16bit,如今有10个字段,这就是须要160bit位!。而如今,咱们只须要64bit。
前面的分析,咱们知道在初始化isa的时候,咱们将对象的类指针放到了isa_t的shiftcls里,咱们在获取class时使用的isa.bits & ISA_MASK。 ** 在arm64下的isa.bits
咱们使用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,咱们又会获得什么?
咱们来看一下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)