类和元类

295 阅读4分钟

类对象 类 元类

举个栗子🌰

    Person *p = [Person alloc];
    
    Class p1 = [Person class];
    Class p2 = [p class];
    Class p3 = object_getClass(p);
    
    NSLog(@"p1=====%p", p1);
    NSLog(@"p2=====%p", p2);
    NSLog(@"p3=====%p", p3);

同样从一段简单的代码切入,可以先想想这里答案是如何。没错,三个打印地址是一样的,那么为什么呢 -w739 因为这三个方法取到的都是Person类对象,只有一份,所以地址是一样的,那么还是进入源码来看看到底是怎样

类方法class

+ (Class)class {
    return self;
}

很明显就是返回类对象本身

对象方法class

- (Class)class {
    return object_getClass(self);
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

可以看到上面例子使用的第三个方法就是类方法class的实际调用函数,同样返回的是类对象,所以它们结果是一样的。 那么此时如果再对类对象进行一次会得到什么

Class p4 = object_getClass(p1);

-w735 这次很明显就变得跟之前不一样了,那这次得到的是什么?? 答案是元类

元类

元类这个概念并不像我们之前看到的实例对象那样清晰可见,元类是由系统所创建的,实例对象所属的类是类对象,同样类比可以知道元类类对象所属的类。 在源码里可以看到其中有一段

inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {

///省略很多

clsbits &= ISA_MASK;

return (Class)clsbits;
}

这里看到所谓isa其实是共用体,所以可以使用与运算得到一些值,这里重点看到ISA_MASK

#   define ISA_MASK        0x00007ffffffffff8ULL

这里可以进行一段测试 -w663 可以对比其地址 -w664

这里由于截图截不下,第一个对象0x6000003302b0其实就是Person的实例对象,我们根据源码里的操作,每次都能得到一个新的地址,并取其isa指针,重复进行操作,最后发现一直指向同一地址,这时发现这个地址就是NSObject的元类即根源类

类方法&对象方法

首先来看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
    ...
}
  • ISA属性是继承于objc_objectisa占8字节
  • superclass:由objc_object定义的Class类型的指针,占有8字节 那么如果要取得bits内容显然要偏移量是前三个属性的地址后才能取得,现在来看cache_t,这个稍有复杂

cache_t

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;//8字节
    explicit_atomic<mask_t> _mask;//实际为int类型,4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;//指针,占8字节
    mask_t _mask_unused;//4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#if __LP64__
    uint16_t _flags;//2字节
#endif
    uint16_t _occupied;//2字节
}

首先要明确的是想要知道cache_t类型占有的内存大小不需要记录里面static修饰的属性,因为static修饰的属性并不计入结构体内存中 上方代码内已经注明各自的属性占有内存,总共三种情况,最后得到的结果均为8+4+2+216字节,此处不再赘述。 那么很轻松的可以得到只要偏移isa+superclass+cache8+8+16字节的偏移后可以得到bits,下面立刻验证 -w698 可以看到这里成功取得了class_rw_t类型的data -w861 查看里面的方法立刻看到我们很熟悉的三个方法,分别对应方法列表,属性列表,协议列表

方法列表

-w787 通过$8.methods()获得list_array_tt类型的一个二维数组,取得list后即可得到想要的方法列表,这里看到是得到的都是实例方法列表,如果想得到是和之前同样的方法即可 -w822

属性列表

-w944 这里是和方法列表是同样的方法来探究,因为他们的结构是一样的,都是list_array_tt这个结构,那么这里有个问题,很明显属性列表里只有属性,那么成员变量存放到哪里了呢 在class_rw_t里看到有个class_ro_t的结构,里面有ivars属性可以试试看 -w651 -w558

通过地址偏移来验证

得到ivars的地址可以通过基于class_ro_t首地址的偏移来得到,下面进行验证 -w435 首先得到基于class_ro_t首地址的偏移48字节,从上图可知class_ro_t的首地址是0x00000001000080b0,那么偏移48字节后地址则为0x00000001000080e0,来验证一下 -w595 最后得证,我们上面的猜想均得到了验证 那么方法列表、协议列表、属性列表是如何被添加进去的呢,直接定位到attachLists函数

attachLists

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

整体分为三种情况,多对多,零列表,一对多,最主要的一点看到这里 -w460 这就表明了类似于后来加入的排在前面的原则。