本人github上的项目RuntimeHelper
什么是Runtime
因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
Runtime基本是用C和汇编写的API函数。Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。
许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。
Runtime常见作用
1. 获取类名、类成员变量、类的属性列表,(包括私有和公有属性,即定义在延展中的属性)
2. 获取对象方法列表:getter, setter, 对象方法等。但不能获取类方法
3. 获取协议列表
4. 交换类、实例方法
5. 动态添加方法
Runtime术语
下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。
SEL
id objc_msgSend ( id self, SEL op, ... );
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。
id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档
Class
之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;
而objc_class就是我们摸到的那个瓜,里面的东西多着呢:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; // //也有一个 isa 指针,指向其所属的元类(meta)
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; //指向其超类
const char *name OBJC2_UNAVAILABLE; //类名
long version OBJC2_UNAVAILABLE; //类的版本信息
long info OBJC2_UNAVAILABLE; //类的详情
long instance_size OBJC2_UNAVAILABLE; //该类的实例对象的大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; //指向该类的成员变量列表,它将方法选择器和方法实现地址联系起来。methodLists 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; //指向该类的实例方法列表
struct objc_cache *cache OBJC2_UNAVAILABLE; //Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; //指向该类的协议列表
#endif
} OBJC2_ UNAVAILABLE;
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
PS:OBJC2_UNAVAILABLE
之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。
Objective-C 2.0 的头文件虽然没暴露出objc_class
结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义小窥端倪:在objc_class结构体中:ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也Category实现的原理,同样解释了Category不能添加属性的原因。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category
PS:任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)其中objc_ivar_list和objc_method_list分别是成员变量列表和方法列表:
struct objc_ivar_list {
int ivar_count OBJC2_ UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_ UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
如果你C语言不是特别好,可以直接理解为objc_ivar_list结构存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method
数组列表,而objc_method结构体存储了类的某个方法的信息。
最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。
不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当[NSObject alloc]这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
上图实线是super_class指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。
Method
Method是一种代表类中的某个方法的类型。
typedef struct objc_method *Method;
而objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;} OBJC2_UNAVAILABLE;
方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
Ivar
Ivar是一种代表类中实例变量的类型。
typedef struct objc_ivar *Ivar;
而objc_ivar在上面的成员变量列表中也提到过:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以根据实例查找其在类中的名字,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance
{
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) { continue; }
if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
key = [NSString strin gWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
class_copyIvarList函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。
IMP
IMP在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的方法实现地址;反之亦然。
Cache
在runtime.h中Cache的定义如下:
typedef struct objc_cache *Cache
还记得之前objc_class结构体中有一个struct objc_cache *cache
吧,它到底是缓存啥的呢,先看看objc_cache的实现:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache的道理挺像,而我猜苹果为提Cache命中率应该也做了努力吧。
Property
@property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList和protocol_copyPropertyList方法来获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回类型为指向指针的指针,哈哈,因为属性列表是个数组,每个元素内容都是一个objc_property_t指针,而这两个函数返回的值是指向这个数组的指针。举个栗子,先声明一个类:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以用下面的代码获取属性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以用property_getName函数来查找属性名称:
const char *property_getName(objc_property_t property)
你可以用class_getProperty和protocol_getProperty通过给出的名称来在类和协议中获取属性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes函数来发掘属性的名称和@encode类型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代码放一起,你就能从一个类中获取它的属性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
对比下class_copyIvarList函数,使用class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
方法调用
让我们看一下方法调用在运行时的过程(参照前文类在runtime中的表示)如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行
2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
3.如果没找到,去父类指针所指向的对象中执行1,2.
4.以此类推,如果一直到根类还没找到,转向拦截调用。
5.如果没有重写拦截调用的方法,程序报错。
拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。那么什么是拦截调用呢。拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
第二个方法和第一个方法相似,只不过处理的是实例方法。
第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。
多说无用,代码为证
为了方便起见,首先我们必须先导入两个头文件:
#import <objc/runtime.h>
#import <objc/message.h>
获取类名
/**
获取类名
@param class 相应类
@return NSString:类名
*/
+ (NSString *)fetchClassName:(Class)class
{
const char *className = class_getName(class);
return [NSString stringWithUTF8String:className];
}
获取成员变量
/**
获取成员变量
@param class Class
@return NSArray
*/
+ (NSArray *)fetchIvarList:(Class)class
{
unsigned int outCount = 0;
/**
* __unsafe_unretained Class cls 哪个类
* unsigned int *outCount 放一个接收值的地址,用来存放属性的个数
*/
// Ivar是一种代表类中实例变量的类型
Ivar *ivars = class_copyIvarList(class, &outCount);
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:outCount];
// 遍历所有成员变量
for (unsigned int i = 0; i < outCount; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
const char *ivarName = ivar_getName(ivar);
const char *ivarType = ivar_getTypeEncoding(ivar);
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity:2];
dic[@"ivarType"] = [NSString stringWithUTF8String: ivarType];
dic[@"ivarName"] = [NSString stringWithUTF8String: ivarName];
[mutableArray addObject:dic];
}
// 注意释放内存!
free(ivars);
return mutableArray;
}
获取类的属性列表
/**
获取类的属性列表, 包括私有和公有属性,以及定义在延展中的属性
@param class Class
@return 属性列表数组
*/
+ (NSArray *)fetchPropertyList:(Class)class {
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList(class, &count);
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
const char *propertyName = property_getName(propertyList[i]);
[mutableArray addObject:[NSString stringWithUTF8String: propertyName]];
}
free(propertyList);
return mutableArray;
}
获取实例方法列表
/**
获取实例方法列表:getter, setter, 对象方法等。但不能获取类方法
@param class 类名
@return 类的实例方法列表数组
*/
+ (NSArray *)fetchMethodList:(Class)class {
unsigned int count = 0;
Method *methodList = class_copyMethodList(class, &count);
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
Method method = methodList[i];
SEL methodName = method_getName(method);
[mutableArray addObject:NSStringFromSelector(methodName)];
}
free(methodList);
return mutableArray;
}
获取协议列表
/**
获取协议列表
@param class 类名
@return 协议列表数组
*/
+ (NSArray *)fetchProtocolList:(Class)class {
unsigned int count = 0;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList(class, &count);
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
Protocol *protocol = protocolList[i];
const char *protocolName = protocol_getName(protocol);
[mutableArray addObject:[NSString stringWithUTF8String: protocolName]];
}
return mutableArray;
}
交换实例方法
/**
交换实例方法,可拦截系统方法
@param class 交换方法所在的类
@param method1 方法1
@param method2 方法2
*/
+ (void)swapInstanceMethod:(Class)class firstMethod:(SEL)method1 secondMethod:(SEL)method2 {
Method firstMethod = class_getInstanceMethod(class, method1);
Method secondMethod = class_getInstanceMethod(class, method2);
method_exchangeImplementations(firstMethod, secondMethod);
}
交换类方法
/**
交换类方法,可拦截系统方法
@param class 交换方法所在的类
@param method1 方法1
@param method2 方法2
*/
+ (void)swapClassMethod:(Class)class firstMethod:(SEL)method1 secondMethod:(SEL)method2 {
Method firstMethod = class_getClassMethod(class, method1);
Method secondMethod = class_getClassMethod(class, method2);
method_exchangeImplementations(firstMethod, secondMethod);
}
动态添加方法
/**
往类上添加新的方法与其实现
@param class 相应的类
@param methodSel 方法的名
@param methodSelImpl 对应方法实现的方法名
*/
+ (void)addMethod:(Class)class method:(SEL)methodSel method:(SEL)methodSelImpl {
Method method = class_getInstanceMethod(class, methodSelImpl);
IMP methodIMP = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(class, methodSel, methodIMP, types);
}
@end
关联对象
在你的某个类扩展.m文件中加入:
/** 要动态添加的属性 */
@property (nonatomic, copy) NSString *dynamicAddedProperty;
在其.m文件中加入:
#pragma mark - 动态属性关联
char nameKey;
/**
setter方法
@param dynamicAddedProperty 设置关联属性的值
*/
- (void)setDynamicAddedProperty:(NSString *)dynamicAddedProperty
{
/** objc_setAssociatedObject
* 参数 object:给哪个对象设置属性
* 参数 key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
* 参数 value:给属性设置的值
* 参数policy:存储策略 (assign 、copy 、 retain就是strong)
*/
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, &nameKey, dynamicAddedProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/**
getter方法
@return 返回关联属性的值
*/
- (NSString *)dynamicAddedProperty {
return objc_getAssociatedObject(self, &nameKey);
}
假设你要扩展的类名为 TestClass,在你的 Controller 中加入:
TestClass *instance = [TestClass new];
instance.dynamicAddedProperty = @"我是动态添加的属性";
NSLog(@"动态关联属性内容:%@", instance.dynamicAddedProperty);
敝人文采有限,讲到这里,文章也该结束了,我只是根据自己的理解把下面参考的资料进行了汇总,相信任何一个人的资料并非十全十美,本文章也仅供参考,如果有什么疑问,可及时在下方回复沟通。
如果你感觉理解起来有困难或者理解不全,建议去看下我的项目RuntimeHelper,如果还不行,那我建议你看完下面的参考再重新回到我的文章中来吧。
我们不单单是代码的搬运工,我们更有我们自己的思想。天道酬勤,代码之路,你我共行。
道虽迩,不行不至;事虽小,不为不成!
参考来源:
玉令天下的博客:Objective-C Runtime(强烈推荐仔细研究下)
青玉伏案的博客:iOS开发之Runtime常用示例总结(项目基于此写的例子,并进行了简单优化和扩展)
滕先红的简书:OC最实用的runtime总结,面试、工作你看我就足够了!(项目简单参考了下,代码已注释)
戴尼玛的简书:Runtime全方位装逼指南(讲的简而精,确实适合装逼)
网友评论