深入Objective-C Runtime机制(一):类和对象的实现

1.概要

          对于Runtime系统,相信大部分iOS开发工程师都有着或多或少的了解。对于Objective-C,Runtime系统是至关重要的,可以说是Runtime系统让Objective-C成为了区分于C语言,C++之外的一门独立开发语言,让OC在拥有了自己的面向对象的特性以及消息发送机制。并且因为其强大的消息发送机制,也让很多人认为Objective-C是一门动态语言(实际上每种语言都具有一定的动态性,只是OC的Runtime更加强大,但它仍比不上Python,Lua等动态语言)。

          而Runtime系统的核心就是一个用C,C++,以及在最核心的消息发送部分甚至使用汇编语言而编写的一套底层API库。它是OC面向对象和动态发送消息的基石,它把很多编译时做的决定推迟到运行时。而且研究Runtime源码能知道很多底层知识,比如类是什么,分类是怎么实现的,方法是什么等。所以准备写一系列文章,详细分析一下Runtime的源码以及设计机制。

2.面向对象特性 —— 类与对象的实现

(一)类的实现

          在C++中,类和结构体就已经非常相似了。只是属性的默认访问权限有些区别。而OC中的Class究竟是什么呢?很幸运,苹果已经把Runtime库开源,可以去苹果的openSource上下载。打开Runtime工程,OC中的Class定义即可在Object.mm源码中初见端倪:

typedef struct objc_class *Class;

          我们使用的Class其实就是一个指向objc_class结构体的指针,那么探寻类的构成其实就是弄清楚objc_class结构体的组成。在objc-runtime-new.h中,可以找到objc_class的定义,源码过长,我截取了关键部分,代码如下:

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

class_rw_t *data() {

return bits.data();

}

}

在分析结构体内的属性之前,还能发现objc_class继承于objc_object结构体。从名字上就能看出来,objc_object是对象的结构体。这也说明了类本身其实也是一个对象。关于objc_object的问题留到后面再谈,回到类结构体。

类结构体有三个属性,superclass,cache,以及bit属性。

(1)superclass,从名字上就能看出来,它保存了自己的父类。如果本身已经是根类NSObject,则为空。

(2) cache,从名字上也能看出来,它跟缓存相关。但是它究竟缓存了什么东西,还需要进入cache_t结构体一探究竟,代码如下:

struct cache_t {

struct bucket_t *_buckets;

mask_t _mask; //在find方法中可知

mask_t _occupied; //occupied:一个整数,指定实际占用的缓存bucket的总数。

public:

struct bucket_t *buckets();

mask_t mask();

mask_t occupied();

void incrementOccupied();

void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);

void initializeToEmpty();

mask_t capacity();

bool isConstantEmptyCache();

bool canBeFreed();

static size_t bytesForCapacity(uint32_t cap);

static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

void expand();

void reallocate(mask_t oldCapacity, mask_t newCapacity);

struct bucket_t * find(cache_key_t key, id receiver);

static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));

};

有几个需要重点关注的点:bucket_t结构体的数组*_buckets,mask_t结构体的_mask和_occupied属性,以及返回类型为bucket_t类型的find(cache_key_t,id receiver)方法。

看起来有好几处都指向了bucket_t结构体,那我们先来看看这个结构体的组成内容:

struct bucket_t {

private:

cache_key_t _key;

IMP _imp;

public:

inline cache_key_t key() const { return _key; }

inline IMP imp() const { return (IMP)_imp; }

inline void setKey(cache_key_t newKey) { _key = newKey; }

inline void setImp(IMP newImp) { _imp = newImp; }

void set(cache_key_t newKey, IMP newImp);

};

bucket_t结构体有两个属性,cache_key_t(unsighed long)型的key,以及方法指针IMP。大概知道了这个结构体存了一个key与方法指针的对应关系,再结合cache_t结构体里的find()方法(在消息发送的章节中会重点介绍),不难推测出cache_t缓存的是一个bucket链表,即近期调用过的方法的缓存区,目的是加快方法调用的速度。不过究竟是如何加快,查找的规则又是如何,将在消息发送的章节中进行详解。

(3)class_data_bits_t结构体的bits,这是类结构中最重要的一环,它存储了类最基本的信息,如方法,成员变量,遵循的protocal列表等等。而我们要的数据都存在class_rw_t结构体中,这点在objc_class中的注释也能看出来:

class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

class_data_bits_t其实就是class_rw_t加上了自定义的rr/alloc标志位。而最核心的数据都在class_rw_t中。所以这个结构体的源码是我们重点关注的,做了一些精简之后如下:

struct class_rw_t {

uint32_t flags;

uint32_t version;

const class_ro_t *ro;

method_array_t methods;

property_array_t properties;

protocol_array_t protocols;

}

首先映入眼帘,几个令人兴奋的关键字:method!property!protocals!

看来终于找到重点了,从名字就能看出来它保存了Method,Property,protocol列表。不过需要注意的是,因为在新版的Xcode提供了property自动合成成员变量的功能,很多人对property和Ivar的认知出现了混淆,需知道property本身不包括成员变量。而另外的methods和protocols,一目了然,就是我们要找的方法和遵循的协议列表。而flags与version标志位则是标志了该类是否是metaClass(下文会讲解),是否被实现等等。

但是新的问题随之产生,这些array是怎么被生成的,又是按照什么规则生成的,category里的方法是什么时候添进去的呢?而且还有一个class_ro_t常量指针,它有什么作用,又指向了什么内容呢?让我们刨根问底吧!首先先解答第二个问题,class_ro_t结构体的内容如下:

struct class_ro_t {

uint32_t flags;

uint32_t instanceStart;

uint32_t instanceSize;

#ifdef __LP64__

uint32_t reserved;

#endif

const uint8_t * ivarLayout;

const char * name;

method_list_t * baseMethodList;

protocol_list_t * baseProtocols;

const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;

property_list_t *baseProperties;

method_list_t *baseMethods() const {

return baseMethodList;

}

};

事实上,class_ro_t保存了类在编译期就确定的method list,Ivar list等。关于这点我们可以在_read_images方法中的readClass方法中求证,Runtime系统从image文件中拿到类的定义,然后将这个类的data()数据,赋给了新生成的类的rw数据中的ro,这里说的比较晦涩,因为它更底层,以后会专门用一篇文章来讲这部分的内容。

最后把这一个个根据image文件中类定义生成出来的新类进行实现,即realizeClass方法。我们现在就来看看类的方法,协议和分类的方法,协议是如何串起来的。

进入realizeClass()方法,会发现它会先realize自己的superClass,metaClass,以及设置标志位。在方法快结束的时候,有一句代码:

// Attach categories

methodizeClass(cls);

看注释就能明白,在这个地方会把类和分类串起来,生成最终的类。那跳进去看看具体做了什么事情。首先它将ro中保存的baseMethod,baseProperty,baseProtocols等添加进class_rw_t中的methods,propertys,protocols。然后再开始加载category,关键代码:

// Attach categories.

category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);

attachCategories(cls, cats, false /*don't flush caches*/);

首先拿到尚未attach到class的category列表,然后进入attachCategories方法,这里面就做了真正添加分类属性的工作。这部分代码不是很难,就是把方法数组等的第一个元素地址添加进Array。唯一需要注意的是,添加category的顺序是按照category的load顺序,最先被load的category首先被加载。

(二)对象的实现

          相比于类的实现,对象的实现要简单的多。在上文中,我们能看到类结构体是继承于对象结构体objc_object的。可知类其实也是个对象,那我们顺着追进去,看看对象结构体里究竟存了些啥。

struct objc_object {

private:

isa_t isa;

public:

........

}

截取了部分代码,发现objc_object有一个唯一的私有变量:isa。相信很多有研究过Runtime的同学都知道,isa是一个指向自己类的指针。而实际上,在ISA()方法中,我们可以知道在64位CPU上,isa已经不再是一个指针,而是non-pointer isa。

那什么是non-pointer isa?我们都知道在64位的机器上,一个指针会占8个字节,即64位。但是我们的地址空间并不需要那么多位数来表示,如果把这64位的一部分用来存储实际地址,而另外一部分存一些标志位,如这个对象是不是有弱引用的对象,它有没有关联对象,这个对象是否正在被销毁等等。那么我们就可以更好的利用起来这64位空间。那么基于这个思想,isa就步入了non-pointer isa时代,它提升了内存的使用效率,降低了64位系统上的内存消耗。

那么non-pointer isa的每一位究竟表示什么呢?这个跟处理器指令集有关。越靠近底层就关注机器本身的特性,一般在iOS开发中能接触到的指令集有四种:arm架构的v7和64,inter架构上的i386和x86_64,一般在手机上我们会用到前两种架构,而在PC模拟器上会用到后两种。手机型号与CPU架构对应关系如下:

以arm64架构为例,定义如下:

# if __arm64__

#  define ISA_MASK        0x0000000ffffffff8ULL

#  define ISA_MAGIC_MASK  0x000003f000000001ULL

#  define ISA_MAGIC_VALUE 0x000001a000000001ULL

struct {

uintptr_t indexed          : 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)

};

它的non-pointer isa中包括第4位往后的33位是表示真实的isa地址,而其它都是一些相关标志位。有些一目了然就能知道是什么意思,比如has_assoc,weakly_referenced,deallocating,但是其余的标志位相对就比较晦涩,这部分将在以后的文章中进行详解。

有了前面的讲解,那么我们也就知道了,如果是为了拿到一个对象的类,直接访问它的isa是很危险的,因为它并不是一个真实的地址,所以要使用[obj class]或者是objc_getClass的方式,Runtime会帮我们做这一层转换。

3.NSTaggedPointer

在看isa部分的源码时,发现了很奇怪的一点,附源码:

inline Class

objc_object::getIsa()

{

if (isTaggedPointer()) {

uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;

return objc_tag_classes[slot];

}

return ISA();

}

在获得isa的过程中,会先进行isTaggedPointer的判断,若不是TaggedPointer才会返回ISA。而判断是不是TaggedPointer则是很简单的用non-pointer isa与TAG_MASK做一个按位与的操作,事实上上文中的indexed标志位,即isa第一位就标志了该对象是不是一个NSTaggedPointer,若为0则是普通的isa,若为1则表示是支持NSTaggedPointer的isa。

那什么情况下支持NSTaggedPointer,它又有什么作用呢?在objc_config.h中,找到了以下定义:

// Define SUPPORT_NONPOINTER_ISA=1 to enable extra data in the isa field.

#if !__LP64__  ||  TARGET_OS_WIN32  ||  TARGET_IPHONE_SIMULATOR

#  define SUPPORT_NONPOINTER_ISA 0

#else

#  define SUPPORT_NONPOINTER_ISA 1

#endif

可以看出在苹果是在64位平台上开始支持NSTaggedPointer,那我们就可以合理猜测NSTaggedPointer的设计原理以及功效与non-pointer isa是差不多的。

在WWDC2013中,苹果介绍了NSTaggedPointer,在32位时代,一个指针占4个字节,即32位。到了64位时代,一个指针被扩大到了8个字节,即64位。也就是说就算什么都不干,仅仅是把以前的代码放到64位系统上运行,内存占用也会扩大一倍。而在绝大多数情况下,我们并不需要64位去存储指针的地址,我们完全可以像non-pointer isa一样,去存点别的东西,比如说小对象本身的值,即对象的"指针"本身就已经带了值,不仅充分利用了内存空间,而且更美妙的是还不用去二次查找,这也加快了值的访问速度,还减去了开辟内存,销毁的开销,这就是NSTaggedPointer的设计思想。

大家可以去WWDC2013的官方pdf中找到详细的定义,在此就要点做一下简单翻译:

(1)NSTaggedPointer是在64位系统中被加入的,它专门用于存储一些小的对象,如NSNumber,NSDate。

(2)NSTaggedPointer把对象的值本身存在了pointer里面,没有malloc和free的消耗(也不会存在堆中)。

(3)在性能上,它有三倍的内存使用效率,以及106倍的生成和销毁效率。

(附原文地址:http://devstreaming.apple.com/videos/wwdc/2013/404xbx2xvp1eaaqonr8zokm/404/404.pdf)

现在我们也可以理解,为什么在获取isa的时候会先去判断一下是不是NSTaggedPointer,因为它根本不是一个真正的对象,它的pointer本身就已经存储了它的值,当然它也就不会有isa指针了。不过由此也可以得出一个结论,对内存的优化,性能的追求是无止境的!

4.小结

本章讲述了类和对象的实现,以及苹果在64位系统上针对对象指针做的优化细节。下章将会继续从源码的角度去分析消息发送以及转发的流程究竟是怎么实现的,苹果为此又做了什么关键的优化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容