美文网首页iOS高手
Runtime:逐个分析OC类结构体的成员变量

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

作者: 意一ineyee | 来源:发表于2019-10-02 13:29 被阅读0次
一、isa指针和superclass指针的指向
二、OC方法的本质和methods成员变量
三、OC属性、协议、成员变量的本质和它们对应的成员变量
四、OC方法缓存的本质——cache成员变量
五、Runtime关于方法、属性、协议、成员变量的常用API

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


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

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

具体地说:

  • 实例对象的isa指针指向它所属的类,类的isa指针指向元类,元类的isa指针指向根元类,根元类的isa指针指向它自己。isa指针体系中根元类是终结)
  • 子类的superclass指针指向它的父类,这样一层一层往上直到根类,根类的superclass指针为nil;子元类的superclass指针指向父元类,这样一层一层往上直到根元类,根元类的superclass指针指向根类。superclass指针体系中nil是终结)

二、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类型的结构体,该结构体内部有三个成员变量:

  • SEL:选择器指针,指向该方法的选择器。所谓选择器其实就是一个objc_selector类型的结构体,它是在编译时系统根据方法的方法名生成的,所以选择器和方法名是一一对应的。因为类的每个方法名不同,所以每个选择器也不同,所以选择器可以用来作为方法的唯一标识,而不同的选择器又存储在不同的内存中,所以它们的地址——选择器指针SEL也可以用来作为方法的唯一标识。那为什么不拿选择器,而要拿选择器的地址——选择器指针SEL作为方法的唯一标识呢?因为将来查找方法,对比地址肯定比对比结构体效率高啊。那为什么不拿方法名的地址,而要拿选择器的地址——选择器指针SEL作为方法的唯一标识呢?首先因为系统是没有开辟内存来专门存储方法名的,所以要想拿方法名作为方法的唯一标识,就得先把它们存储在内存中,其次之所以不直接存储方法名,而是根据方法名额外生成了一套选择器存储,目的是对这套选择器使用一套算法,来使得它们的地址比较特别,从而提高查找方法的效率。我们可以通过@selector(方法名)NSSelectorFromString(@"方法名的OC字符串")sel_registerName("方法名的C字符串")来获取一个方法的SEL
  • types:类型编码字符串,包含了该方法的参数和返回值信息。这里是类型编码对照表,类型编码中第一个字母代表该方法的返回值类型,后面的字母依次代表该方法的各个参数类型;第一个数字代表该方法所有参数占用内存的总大小,后面的数字依次代表该方法各个参数的内存地址距离内存首地址的偏移量。
  • IMP:函数指针,指向该方法的具体实现——即某个函数。

举个例子来验证下,假设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

  • (struct objc_selector *)"addA:andB:"SEL,根据该方法的方法名生成。
  • "i24@0:8i16i20":类型编码,根据该方法的参数和返回值信息生成。i代表该方法的返回值类型为int@代表该方法第一个参数的类型为id——即self的类型,:代表该方法第二个参数的类型为SEL——即_cmd的类型,i代表该方法第三个参数的类型为int——即a的类型,i代表该方法第四个参数的类型为int——即b的类型;24代表该方法所有参数占用内存的总大小为24个字节 = 8 + 8 + 4 + 4,0代表该方法第一个参数的内存地址距离内存首地址的偏移量为0,8代表该方法第二个参数的内存地址距离内存首地址的偏移量为8,16代表该方法第三个参数的内存地址距离内存首地址的偏移量为16,20代表该方法第三个参数的内存地址距离内存首地址的偏移量为20。
  • (void *)_I_INEPerson_addA_andB_IMP,指向该方法的具体实现——即根据OC方法生成的一个C函数。

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类型的结构体,该结构体内部有三个成员变量:

  • _buckets:方法缓存散列表。
  • _mask:(散列表的长度 - 1)。
  • _occupied:已缓存方法的数量。

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

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

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

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

  • cache散列表的散列算法:
unsigned int cache_hash(SEL sel, mask_t mask)
{
    return (unsigned int)(unsigned long)sel & mask;
}

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

  • cache散列表处理冲突

散列表都会存在的一个问题是:不同的唯一标识经过散列算法后可能得到相同的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,然后遍历散列表,直到找到空闲的内存来存储方法,或者直到找到我们真正想读取的方法。

  • cache散列表存取数据

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

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

  • cache散列表扩容
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;
}

相关文章

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

    一、isa指针和superclass指针的指向二、OC方法的本质和methods成员变量三、OC属性、协议、成员变...

  • Objective-C基础

    OC OC知识点 OC的字符串 - 1.类:是结构体的升级,用于定义变量 - - 与结构体的区别 关键字 成员变量...

  • Non Fragile ivars

    ivar结构体 从runtime的源码中,可以看到类结构体中有成员变量的列表.(class_ro_t也是属于类结构...

  • Note 7 类和结构体

    结构体 类 定义 类的属性 成员变量 静态变量 成员方法 类方法 与结构体的区别 用let定义的结构体变量,成员不...

  • Runtime小结

    在runtime中一个对象就是用结构体来表示的 runtime中的表示 获取类的属性列表 获取类的成员变量 获取类...

  • OC底层cache_t小结

    众所周知oc的类的底层是一个结构体,如下。 该结构体包含了4个成员变量:isa、superclass、cache、...

  • iOS 底层探索-类

    一、类的结构体 1.类的本质 在OC中,类是一个指向objc_class结构体的指针。 包含isa指针、成员变量列...

  • isa理解

    由类生成对象。对象的结构体实例通过isa这个成员变量来保持类的结构体实例指针,建立类与对象间的关系。oc运行时为每...

  • 10、【Swift】属性

    存储属性 - Stored Properties 相当于 OC 的下划线成员变量 适用于:结构体 、 类 类型:常...

  • objc_class结构体中cache_t分析

    前面的文章分析了OC类的结构构体实现,了解了objc_class结构体中有几个主要成员分别是isa、supercl...

网友评论

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

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