触摸iOS底层:Object-C 的类和isa (一)

类和isa(一)

OC中的类是什么,长什么样?

这是一个很好的问题,若是说 runtime 是灵魂,  就是他的使者。了解类,是咱们继续探索的基石。macos

请记住一句话, 万物皆对象! 哪怕是类,也终究逃不出来自苹果的这个魔咒markdown

类结构

**对于iOS编译,**iOS的底层代码是由C++实现,但系统库在.h中以C的形式向咱们提供API,因此OC会在编译时由 Clang 编译器转成C++继续编译。 想要了解底层源码的同窗,苹果也开源了源码,这里是苹果开源代码Source Browser ,其中就有咱们须要常常用到的 objc4libmalloc ,这俩货分别是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 是否是很像 image.png 看到这个C++的LYPerson,咱们会注意到:ide

  1. LYPerson做为一个类,自己也能够实例化一个对象,在这里,居然是结构体 objc_object 的实例,那么类自己有没有多是一个对象?结构体 objc_object 是什么?
  2. LYPerson 内定义了成员:name,同时还定义一个 NSObject_IMP 结构体类型的成员:NSObject_IVARS。
  3. 咱们自定义的属性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。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类型那他指向的又是什么类?

扩展:自定义类中属性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开始,咱们来验证一下 image.png image.png 能够看到self是当前对象,_cmd是当前方法也就是setName,newValue是咱们赋的值,offset是8!copy是true,atomic是false,由于咱们定义的是(copy, nonatomic)。

isa

初始化isa

咱们从一个对象的alloc方法开始看,何时产生的isa,依然是在obj4源码里,咱们断点调试 alloc方法 image.png 进入_objc_rootAlloc image.png 再进入callAlloc image.png 再往里面走,进入_objc_rootAllocWithZone image.png 继续往里走,进入_class_createInstanceFromZone, 这个方法就是在建立实例化的对象,有几个核心的步骤 image.png 其中须要注意的是:

  • instanceSize 是计算须要开辟多少个字节
  • calloc 是正儿八经的开辟空间,建立对象obj,calloc的源码能够查看苹果开源的libmalloc源码。
  • initInstanceIsa 初始化isa,或者说是给这个对象设置isa
  • object_cxxConstructFromClass 这一步就是完善obj,好比说,设置obj的super_class。

在_class_createInstanceFromZone,咱们一步一步进入到了initInstanceIsa image.png initInstanceIsa看这个方法名,就知道太符合咱们的要求了,初始化一个isa,好,继续进入这个方法,来到了initIsa image.png initIsa image.png 咱们最初的断点式[LYPerson alloc] 这句代码是建立一个LYPerson的对象,咱们看到,这里的isa是一个isa_t, isa 的shiftcls竟然存的是当前的类,而不是父类。咱们是否是能够这么说,对象的isa里他的类,他的类又是一个对象,类对象的isa存了类对象的类。哇,是否是和咱们常规的类的继承:子类-父类-父父类-。。。。-NSObject有冲突啊。不冲突,这是另外一条线:isa指向这个对象(也包括类对象)的类。 咱们在建立LYPerson的时候,里买就有了isa,并且是NSObject的isa,这个上面刚讲过。 image.png 咱们是否是能够得出一个信息:obj的isa->Person,Person的isa->NSObject. 接下来咱们获取obj、person、NSObject的isa来验证是否是

获取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

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 image.png

对象的isa

咱们使用object_getClass,获取对象的isa image.png 最终进入到下面这个方法 将ISA_MASK:0x0000000ffffffff8ULL 转成二进制: image.png 这就是一个简单的C语言的位预算了:过滤为1的位。$2(ISA_MASK)64位,为1的刚好是shiftcls的范围,因此,isa.bits & ISA_MASK 这个位运算,目的就是为了得到shiftcls范围的内容,也就是咱们当初initIsa时放入shiftcls里的值——对象的类指针! 咱们打印看一下 image.png LYPerson的实例化对象obj 调用class方法,得到的是obj的类“LYPerson”,

咱们这里用的是LYPerson的实例化对象,获取的是实例对象obj的isa,那咱们若是要获取LYPerson这个类的isa,咱们又会获得什么?

类的isa

image.png 咱们来看一下LYPerson类的isa指向的是谁 image.png 第一个是对象obj的isa,指向了LYPerson,第二个是类LYPerson的isa也指向了LYPerson,可是内存地址不同,说明这两个LYPerson不是一个。 若是咱们在这样继续下去:不断的查看isa指向 image.png image.png isa指向关系: obj -> LYPerson -> LYPerson -> NSObject(指向本身)

是否是一个很是经典的图就出来了: isa流程图.png

虚线 就是 isa 指向,从类的开始,后面都是前一个的 元类 ,直到NSObject,由于它是OC里的根元类。 实线 是咱们熟知的类继承,也就是 objc_class 里的那个 super_class  存储的类指针。

到了这里,isa是干吗的,想必你们也已经清楚: isa说白了,就是实例某个对象(在runtime源码里类也是对象)的类  isa的顺序也比较固定: 对象 -> 类 -> 元类 -> 根元类(NSObject) -> 根根元类(NSObject)

相关文章
相关标签/搜索