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

894 阅读11分钟

类和isa(一)

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

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

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

类结构

**对于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++ 代码。

clang -rewrite-objc main.m -o main.cpp

看一看main.cpp 也就是main的C++实现。我们只找LYPerson, cmd+f 搜索“LYPerson”, 很快,我们看到一段眼熟的代码,和原始的OC的 main.m 是不是很像 image.png 看到这个C++的LYPerson,我们会注意到:

  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。

亲爱的伙伴们,你们一定还注意到一点,我们的 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开始,我们来验证一下 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

我们平时想要获取某一个对象的类,是怎么获取? 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 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)