iOS高手

Runtime:逐个分析OC类结构体的成员变量

2019-10-02  本文已影响0人  意一ineyee
一、isa指针和superclass指针的指向
二、OC方法的本质和methods成员变量
三、OC属性、协议、成员变量的本质和它们对应的成员变量
四、OC方法缓存的本质——cache成员变量
五、Runtime关于方法、属性、协议、成员变量的常用API

一、isa指针和superclass指针的指向


isa指针和superclass指针是两个非常重要的指针,弄清它俩的指向有助于我们理解很多东西。

概括地说:isa指针指向它所属的类,superclass指针指向它的父类。

具体地说:

二、OC方法的本质和methods成员变量


1、OC方法的本质

通过查看Runtime的源码(objc-runtime-new.h文件),我们得到OC方法的定义如下(伪代码):

typedef struct method_t *Method; // Method类型的本质就是一个method_t类型的结构体指针,所以它可以指向任意一个OC方法

struct method_t {
    SEL name; // 选择器指针,用来作为方法的唯一标识
    const char *types; // 类型编码字符串,包含了该方法的参数和返回值信息
    IMP imp; // 函数指针,指向该方法的具体实现——即某个函数
};


// SEL和IMP
typedef struct objc_selector *SEL;
typedef id (*IMP)(id _Nonnull, SEL _Nonnull, ...);

可见OC方法的本质就是一个method_t类型的结构体,该结构体内部有三个成员变量:

举个例子来验证下,假设Person类有一个OC方法:

- (int)addA:(int)a andB:(int)b {
    
    return a + b;
}

那么编译后,这个OC方法就被编译成了下面这样一个method_t结构体连带一个函数,存储在内存中:

method_t ocMethod = {
    (struct objc_selector *)"addA:andB:", // SEL
    "i24@0:8i16i20", // types
    (void *)_I_INEPerson_addA_andB_ // IMP
};

// 该方法的具体实现——即某个函数
int _I_INEPerson_addA_andB_(INEPerson * self, SEL _cmd, int a, int b) {

    return a + b;
}

可见所有的OC方法,默认都有两个参数:id类型的selfSEL类型的_cmd

2、OC方法存储在哪里——methods成员变量

我们知道类的methods成员变量存储着该类所有的实例方法信息,那方法到底存储在哪里呢?类的methods成员变量其实是一个数组指针,它里面存储着一个地址,指向一个数组。而这个数组又是一个指针数组,里面存储着一堆地址,分别指向真正的实例方法列表,这些实例方法列表包括类本身的实例方法列表(数组),分类1的实例方法列表(数组),分类2的实例方法列表(数组)等等。

我们知道元类的methods成员变量存储着该类所有的类方法信息,类方法也是这么存储的。

三、OC属性、协议、成员变量的本质和它们对应的成员变量


这三者跟OC方法同理。不过要注意方法、属性和协议都存储在class_rw_t里面,可读可写,运行时还是可以修改的,而成员变量则存储在class_ro_t里面,只读,编译后就不能再修改类的成员变量了。

四、OC方法缓存的本质——cache成员变量


我们知道一个对象接收到消息,会根据它的isa指针找到它所属的类,然后根据类的methods成员变量找到所有的方法列表,然后依次遍历这些方法列表来查找要执行的方法。但实际情况中,一个对象只有一部分方法是常用的,其它方法很少用到或根本用不到,那如果对象每接收一个消息就要遍历一次所有的方法列表,这性能肯定很差。类的cache成员变量就是用来解决这个问题的,对象每调用一个方法,系统就会把这个方法存储到cache中,下一次对象再调用方法时就会优先去cache中查找,如果找到方法则直接调用,如果找不到才去methods那里找,这就大大提高了方法查找的效率,而且cache还不是简单地存取方法,它用了散列表,这就使得方法查找的效率更高。

通过查看Runtime的源码(objc-runtime-new.h文件),我们得到cache的定义如下(伪代码):

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

struct bucket_t {
    SEL _sel;
    IMP _imp;
}

可见cache的本质就是一个cache_t类型的结构体,该结构体内部有三个成员变量:

散列表中的元素也不直接是方法——method_t,而是一个叫bucket_t的东西,它是用方法的SELIMP组成的结构体。(为方便叙述,下文中“方法”即指bucket_t

关键词:散列表、表中元素、表中元素唯一标识、散列算法和散列函数、index

散列表(Hash Table,也叫哈希表),就是把表中元素的唯一标识通过某种算法得到一个index,然后通过这个index直接访问表中元素的一种数据结构,这样就不用遍历了,因此可以大大提高数据查找的效率。实现这个算法的函数叫作散列函数,存储数据的数组叫作散列表(但这个数组不是普通的数组,它的元素可以不连续存储,因此散列表就有可能造成内存的空闲,它是一个典型的“以空间换时间”的例子)。散列表的核心就在于散列算法。

接下来我们就看看苹果是如何实现cache散列表的。

unsigned int cache_hash(SEL sel, mask_t mask)
{
    return (unsigned int)(unsigned long)sel & mask;
}

可见苹果关于cache散列表的散列算法其实很简单,就是:用方法的SEL & (散列表的长度 - 1),这样就能得到一个index了,我们知道方法的SEL确实是表中元素的唯一标识。

散列表都会存在的一个问题是:不同的唯一标识经过散列算法后可能得到相同的index那这样数据存取就可能出现冲突,怎么处理呢?

// 这里只是读取方法的源码,存储方法也是一样的道理

bucket_t * cache_t::find()
{
    // 先通过散列算法得到某个元素的index
    mask_t begin = cache_hash(sel, _mask);

    mask_t i = begin;
    do {
        if (_buckets[i].sel() == sel) { // 然后去读取该index处的元素,如果发现该元素的唯一标识SEL和我们想要读取元素的SEL一样,就表明读对了,直接返回该元素
            return &_buckets[i];
        }
    } while ((i = cache_next(i, _mask)) != begin);
}

mask_t cache_next(mask_t i, mask_t mask) {
    // 否则(index-1),遍历散列表,直到读取到想要的元素
    return i ? i-1 : mask;
}

可见cache散列表处理冲突的方式为:index-1,然后遍历散列表,直到找到空闲的内存来存储方法,或者直到找到我们真正想读取的方法。

通过散列算法得到index之后,系统就会把这个方法直接存储到散列表相应的index处,因此这就可能造成内存的空闲。

而读取方法的时候也是先通过散列算法得到index,直接从相应的index处拿出方法,因此就不用遍历了,大大提高了方法查找的效率。

void cache_t::expand()
{
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity * 2; // 两倍扩容
    // 开辟新的散列表
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    
    // 释放旧的散列表,清空所有的方法缓存
    bucket_t *oldBuckets = buckets();
    cache_collect_free(oldBuckets);
}

随着散列表缓存的方法越来越多,它的内存可能就不够用了,此时系统会对散列表进行两倍扩容,创建一个新的散列表,释放旧的散列表并清空所有的方法缓存。

四、Runtime关于方法、属性、协议、成员变量的常用API


// 获取方法列表(最后需要用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount);
// 获取一个实例方法
Method class_getInstanceMethod(Class cls, SEL name);
// 获取一个类方法
Method class_getClassMethod(Class cls, SEL name);


// 获取方法的SEL
SEL method_getName(Method m);
// 获取方法的IMP
IMP method_getImplementation(Method m);
// 获取方法的类型编码
const char *method_getTypeEncoding(Method m);


// 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
// 交换两个方法的实现
void method_exchangeImplementations(Method m1, Method m2);

例如获取一个类所有的实例方法:

- (NSArray *)methodsOfClass:(Class)cls {
    
    NSMutableArray *methods = [@[] mutableCopy];
    
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    for (NSInteger i = 0; i < count; i ++) {
        
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [methods addObject:methodName];
    }
    free(methodList);
    
    return methods;
}
// 获取属性列表(最后需要用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
// 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name);


// 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount);

例如获取一个类所有的属性:

- (NSArray *)propsOfClass:(Class)cls {
    
    NSMutableArray *props = [@[] mutableCopy];
    
    unsigned int count;
    objc_property_t *propList = class_copyPropertyList(cls, &count);
    for (NSInteger i = 0; i < count; i ++) {
        
        objc_property_t property = propList[i];
        NSString *propName = [NSString stringWithUTF8String:property_getName(property)];
        
        [props addObject:propName];
    }
    free(propList);
    
    return props;
}
// 获取协议列表(最后需要用free释放)
Protocol **objc_copyProtocolList(unsigned int *outCount);
// 获取一个协议
Protocol *objc_getProtocol(const char *name);
// 获取成员变量列表(最后需要用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount);
// 获取一个成员变量
Ivar class_getInstanceVariable(Class cls, const char *name);


// 返回成员变量的值
id object_getIvar(id obj, Ivar ivar);
// 设置成员变量的值
void object_setIvar(id obj, Ivar ivar, id value);

// 动态添加成员变量(因为类的成员变量是只读的,所以类在注册后就无法动态添加成员变量了,一定要在注册前添加)
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types);

例如获取一个类所有的成员变量:

- (NSArray *)ivarsOfClass:(Class)cls {
    
    NSMutableArray *ivars = [@[] mutableCopy];
    
    unsigned int count;
    Ivar *ivarList = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i ++) {
        
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        [ivars addObject:ivarName];
    }
    free(ivarList);
    
    return ivars;
}
上一篇下一篇

猜你喜欢

热点阅读