一、
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
类型的self
和SEL
类型的_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
的东西,它是用方法的SEL
和IMP
组成的结构体。(为方便叙述,下文中“方法”即指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;
}
网友评论