深入Objective-C Runtime机制(一):类和对象的实现-阿里云开发者社区

开发者社区> 开发与运维> 正文

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

简介: 1.概要      对于Runtime系统,相信大部分iOS开发工程师都有着或多或少的了解。对于Objective-C,Runtime系统是至关重要的,可以说是Runtime系统让Objective-C成为了区分于C语言,C++之外的一门独立开发语言,让OC在拥有了自己的面向对象的特性以及消息发送机制。并且因为其强大的消息发送机制,也让很多人认为Object
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位系统上针对对象指针做的优化细节。下章将会继续从源码的角度去分析消息发送以及转发的流程究竟是怎么实现的,苹果为此又做了什么关键的优化。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章