美文网首页iOS底层基础知识
深入理解Runtime中的isa

深入理解Runtime中的isa

作者: 小凉介 | 来源:发表于2018-12-01 20:31 被阅读151次

    objc_object

    Objective-C 所有对象都是 C 语言结构体objc_object,这个结构体中包含一个isa成员变量,不是一个普通的指针,是一个isa_t结构体。

    struct objc_object {
    private:
        isa_t isa;
    
    public:
    
        // ISA() assumes this is NOT a tagged pointer object
        Class ISA();
    
        // getIsa() allows this to be a tagged pointer object
        Class getIsa();
    
        // initIsa() should be used to init the isa of new objects only.
        // If this object already has an isa, use changeIsa() for correctness.
        // initInstanceIsa(): objects with no custom RR/AWZ
        // initClassIsa(): class objects
        // initProtocolIsa(): protocol objects
        // initIsa(): other objects
        void initIsa(Class cls /*nonpointer=false*/);
        void initClassIsa(Class cls /*nonpointer=maybe*/);
        void initProtocolIsa(Class cls /*nonpointer=maybe*/);
        void initInstanceIsa(Class cls, bool hasCxxDtor);
    }
    

    isa_t

    因为iPhone5s之后的设备是在arm64架构下,所以截取arm64架构下的代码,这个结构体是苹果经过优化的

    armv64: iPhoneX, iPhone 5s-8, iPad Air — iPad Pro
    armv7 : iPhone3Gs-5c, iPad WIFI(4th gen)
    armv6 : iPhone — iPhone3G
    the above if for real devices
    i386 : 32-bit simulator
    x86_64 : 64-bit simulator

    i386是针对intel通用微处理器32位处理器
    x86_64是针对x86架构的64位处理器

    模拟器32位处理器测试需要i386架构
    模拟器64位处理器测试需要x86_64架构
    真机32位处理器需要armv7,或者armv7s架构
    真机64位处理器需要arm64架构

    为何要进行优化呢,进行了怎样的优化,看看Friday Q&A 2013-09-27: ARM64 and You怎么说的

    尽管指针为64位,但在实际使用中,这些位数并不是都用上了。例如X86-64的Mac OS X系统仅使用了其中的47位。而ARM64上占用得更少,目前只有33位。只要未被系统全部占用,这些指针就能用于存储数据。这是Objective-C Runtime演进史上最重要的变化之一。

    Objective-C对象是连续的内存块,这个内存块中第一个指针大小的部分称为ISA。一般来说,ISA是一个指向该对象所属类的指针。不过这么大的空间仅作为指针有点儿浪费,尤其是在64位CPU上。运行iOS的ARM64目前仅使用了一个指针的33位,而其余31位则另作他用。另外,类 指针还需要对齐,这就释放了另外3位,于是ISA指针中共有34位可另作他用。苹果的ARM64 Runtime正是利用了这一点使性能有了大幅提升。

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            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)
        };
    }
    
    

    Hamster Emporium: [objc explain]: Non-pointer isa - Sealie Software一文对这些bits做了如下解释

    bits 作用
    nonpointer 0表示普通isa,1表示non-pointe isa
    has_assoc 对象具有或曾经具有关联的引用。没有关联引用的对象可以更快地析构。
    has_cxx_dtor 对象有一个C ++或ARC析构函数。 没有析构函数的对象可以更快地解除分配。
    shiftcls 类指针的非零位
    magic 等于0xd2,调试器使用它来判断对象是否完成了初始化
    weakly_referenced 对象有或者曾经有过ARC的weak对象,如果没有,析构更快
    deallocating 对象正在析构
    has_sidetable_rc 对象的应用计数太大,无法内敛存储
    extra_rc 这个数值加1为对象的引用计数.(例如,如果extra_rc为5,则对象的实际引用计数为6.)

    initIsa

    objc_object的代码中我们可以看到有好几初始化方法,最后都会走到下面这个方法,我只是截取了部分我们需要的

    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
    { 
        assert(!isTaggedPointer()); 
        
        if (!nonpointer) {
            isa.cls = cls;
        } else {
            assert(!DisableNonpointerIsa);
            assert(!cls->instancesRequireRawIsa());
    
            isa_t newisa(0);
    
            newisa.bits = ISA_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.shiftcls = (uintptr_t)cls >> 3;
            isa = newisa;
        }
    }
    
    

    有一个地方不好理解

    newisa.shiftcls = (uintptr_t)cls >> 3
    

    这里我们可以看到是将类地址右移三位得到有效的33位shiftcls位,Friday Q&A 2013-09-27: ARM64 and You从 NSObject 的初始化了解 isa 解释:

    使用整个指针大小的内存来存储 isa 指针有些浪费,尤其在 64 位的 CPU 上。在 ARM64 运行的 iOS 只使用了 33 位作为指针(与结构体中的 33 位无关,Mac OS 上为 47 位),而剩下的 31 位用于其它目的。类的指针也同样根据字节对齐了,每一个类指针的地址都能够被 8 整除,也就是使最后 3 bits 为 0,为 isa 留下 34 位用于性能的优化。

    绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址

    objc_object::ISA()

    objc-object.h文件中

    inline Class 
    objc_object::ISA() 
    {
        return (Class)(isa.bits & ISA_MASK);
    }
    
    define ISA_MASK        0x0000000ffffffff8ULL
    

    ffffffff8 = 1111 1111 1111 1111 1111 1111 1111 1111 1000共36位,isa.bits和&运算后就拿到33位shiftcls,也就是类指针。

    objc_class

    Objective-C 中类都是 C 语言的类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,说明在Objective-C 中类也是一个对象。我们先来看看objc_class的结构:

    1. isa:因为objc_class继承自objc_object,所以它包含一个isa成员变量,指向(MetaClass)元类
    2. superclass:指向当前类的父类
    3. cache:用来缓存指针和vtable(virtual table虚函数表)。Runtime运行时系统库实现了一种自定义的虚函数表分派机制。这个表是专门用来提高性能和灵活性的。用来存储IMP类型的数组
    4. bits:class_rw_t 指针加上 rr/alloc 标志,用来存储类的属性,方法,协议等信息

    class_rw_t

    class_data_bits_t的data是class_rw_t结构体

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    }
    
    
    1. methods:方法列表
    2. properties:属性列表
    3. protocols:协议列表

    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;
        }
    };
    
    
    1. instanceSize:instance对象占用的内存空间
    2. name:类名
    3. baseMethodList:类的原始方法列表
    4. baseProtocols:类的原始属性列表
    5. ivars:成员变量列表

    我们看到class_rw_tclass_ro_t都有方法列表等,深入解析 ObjC 中方法的结构对此作出的解释:

    在分析方法在内存中的位置时,笔者最开始一直在尝试寻找只读结构体 class_ro_t 中的 baseMethods 第一次设置的位置(了解类的方法是如何被加载的)。尝试从 methodizeClass 方法一直向上找,直到 _obj_init 方法也没有找到设置只读区域的 baseMethods 的方法。

    而且在 runtime 初始化之后,realizeClass 之前,从 class_data_bits_t 结构体中获取的 class_rw_t 一直都是错误的,这个问题在最开始非常让我困惑,直到后来在 realizeClass 中发现原来在这时并不是 class_rw_t 结构体,而是class_ro_t,才明白错误的原因。

    后来突然想到类的一些方法、属性和协议实在编译期决定的(baseMethods 等成员以及类在内存中的位置都是编译期决定的),才感觉到豁然开朗。

    类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。

    类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。

    在 class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods,对于方法的添加会在之后的文章中分析。

    class and metaclass

    在前面的源码中可以看到,对象通过isa找到类,同时类也是一种对象,类隐式的拥有isa,那这个isa指向哪里呢,元类。

    static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
    {
        runtimeLock.assertWriting();
    
        class_ro_t *cls_ro_w, *meta_ro_w;
        
        cls->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
        meta->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
        cls_ro_w   = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
        meta_ro_w  = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
        cls->data()->ro = cls_ro_w;
        meta->data()->ro = meta_ro_w;
    
        // Set basic info
    
        cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
        meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
        cls->data()->version = 0;
        meta->data()->version = 7;
    
        cls_ro_w->flags = 0;
        meta_ro_w->flags = RO_META;
        if (!superclass) {
            cls_ro_w->flags |= RO_ROOT;
            meta_ro_w->flags |= RO_ROOT;
        }
        if (superclass) {
            cls_ro_w->instanceStart = superclass->unalignedInstanceSize();
            meta_ro_w->instanceStart = superclass->ISA()->unalignedInstanceSize();
            cls->setInstanceSize(cls_ro_w->instanceStart);
            meta->setInstanceSize(meta_ro_w->instanceStart);
        } else {
            cls_ro_w->instanceStart = 0;
            meta_ro_w->instanceStart = (uint32_t)sizeof(objc_class);
            cls->setInstanceSize((uint32_t)sizeof(id));  // just an isa
            meta->setInstanceSize(meta_ro_w->instanceStart);
        }
    
        cls_ro_w->name = strdupIfMutable(name);
        meta_ro_w->name = strdupIfMutable(name);
    
        cls_ro_w->ivarLayout = &UnsetLayout;
        cls_ro_w->weakIvarLayout = &UnsetLayout;
    
        meta->chooseClassArrayIndex();
        cls->chooseClassArrayIndex();
    
        // Connect to superclasses and metaclasses
        cls->initClassIsa(meta);
        if (superclass) {
            meta->initClassIsa(superclass->ISA()->ISA());
            cls->superclass = superclass;
            meta->superclass = superclass->ISA();
            addSubclass(superclass, cls);
            addSubclass(superclass->ISA(), meta);
        } else {
            meta->initClassIsa(meta);
            cls->superclass = Nil;
            meta->superclass = cls;
            addRootClass(cls);
            addSubclass(cls, meta);
        }
    
        cls->cache.initializeToEmpty();
        meta->cache.initializeToEmpty();
    }
    
    objc-isa-class-diagram.png

    上图实线是 superclass 指针,虚线是isa指针

    1. 对象调用实例方法时,是在对应类对象及其继承链上找方法。类对象调用类方法时,是在其元类及继承链上找方法
    2. 所以元类的isa指针都指向根元类(NSObject),根元类的isa指针指向自己
    3. 所有的类方法都储存在元类当中
    4. NSObject 的超类为 nil

    关于元类Classes and metaclasses What is a meta-class in Objective-C?解释的很详细

    下面是What is a meta-class in Objective-C?的测试代码

    Class newClass =
        objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
    class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
    objc_registerClassPair(newClass);
    
    
    void ReportFunction(id self, SEL _cmd)
    {
        NSLog(@"This object is %p.", self);
        NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
     
        Class currentClass = [self class];
        for (int i = 1; i < 5; i++)
        {
            NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
            currentClass = object_getClass(currentClass);
        }
     
        NSLog(@"NSObject's class is %p", [NSObject class]);
        NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
    }
    
    id instanceOfNewClass =
        [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
    [instanceOfNewClass performSelector:@selector(report)];
    [instanceOfNewClass release];
    
    

    打印结果:

    This object is 0x10010c810.
    Class is RuntimeErrorSubclass, and super is NSError.
    Following the isa pointer 1 times gives 0x10010c600
    Following the isa pointer 2 times gives 0x10010c630
    Following the isa pointer 3 times gives 0x7fff71038480
    Following the isa pointer 4 times gives 0x7fff71038480
    NSObject's class is 0x7fff710384a8
    NSObject's meta class is 0x7fff71038480
    
    

    观察isa到达过的地址的值:

    • 对象的地址是 0x10010c810
    • 类的地址是 0x10010c600
    • 元类的地址是 0x10010c630
    • 根元类(NSObject的元类)的地址是 0x7fff71038480
    • NSObject元类的类是它本身

    元类是 Class 对象的类。每个类(Class)都有自己独一无二的元类(每个类都有自己第一无二的方法列表)。这意味着所有的类对象都不同。

    元类总是会确保类对象和基类的所有实例和类方法。对于从NSObject继承下来的类,这意味着所有的NSObject实例和protocol方法在所有的类(和meta-class)中都可以使用。

    所有的meta-class使用基类的meta-class作为自己的基类,对于顶层基类的meta-class也是一样,只是它指向自己而已。

    从 NSObject 的初始化了解 isa
    Hamster Emporium: [objc explain]: Non-pointer isa - Sealie Software
    Testing if an arbitrary pointer is a valid Objective-C object
    从 NSObject 的初始化了解 isa
    Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand ...
    iOS framework file was built for x86_64 which is not the architecture being linked (arm64), linker command failed with exit code 1
    Friday Q&A 2013-09-27: ARM64 and You
    深入解析 ObjC 中方法的结构
    What is a meta-class in Objective-C?
    Classes and metaclasses
    用 isa 承载对象的类信息

    相关文章

      网友评论

        本文标题:深入理解Runtime中的isa

        本文链接:https://www.haomeiwen.com/subject/qogpqqtx.html