runtime是什么?
是一套比较底层的纯C语言API, 属于1个C语言库,runtime算是OC的幕后工作者
runtime可以做什么?
- 动态的创建类(KVO的实现原理)
- 动态的创建属性/方法
- 遍历所有成员变量和属性/方法(比如字典–>模型)
runtime相关头文件和函数
<objc/runtime.h>
<objc/message.h>
举例:
objc_msgSend : 给对象发送消息
class_copyMethodList : 遍历某个类所有的方法
class_copyIvarList : 遍历某个类所有的成员变量...
Ivar : 成员变量
Method : 成员方法
复制代码
OC程序与运行时系统交互的三个不同场景:
- 通过OC源代码(编译器创建数据结构和函数调用,实现语言的动态特性)
- 通过定义在Foudation框架中NSObject中的方法(NSObject方法有一些简单的查询的运行时系统信息,这些方法允许对象自省-自我查找 举例:isKindOfClass、isMemberOfClass、respondsToSelector、conformsToProtocol、methodForSelector)
- 通过直接调用运行时的函数
runtime相关术语的数据结构
SEL
它是selector在 OC 中的表示。selector 是方法选择器,在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:
typedef struct objc_selector *SEL;
复制代码
你可以通过 OC 编译器器命令@selector() 或者 Runtime 系统的 sel_registerName函数来获取一个 SEL 类型的方法选择器。
注意:不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。
id
id 是一个参数类型,它是指向某个类的实例的指针。 定义如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
复制代码
以上定义,看到 objc_object 结构体包含一个isa指针,根据 isa 指针就可以找到对象所属的类。
注意: isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 - isKindOfClass等方法。
KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类
Class
typedef struct objc_class *Class;
复制代码
Class 其实是指向 objc_class 结构体的指针。objc_class 的数据结构如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#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;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
复制代码
// 成员变量列表
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;
}
复制代码
我们可以动态修改 *methodList 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因
objc_class 中也有一个 isa 指针,这说明 Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做 Meta Class(元类) ,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。
Method
Method 代表类中某个方法的类型
typedef struct objc_method *Method;
复制代码
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
复制代码
objc_method 存储了方法名,方法类型和方法实现: 方法名类型为 SEL 方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型 method_imp 指向了方法的实现,本质是一个函数指针
Ivar
Ivar 是表示成员变量的类型。
typedef struct objc_ivar *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
}
复制代码
其中 ivar_offset 是基地址偏移字节
IMP
IMP在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
复制代码
它就是一个函数指针,这是由编译器生成的。当你发起一个 OC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。IMP指向的方法与 objc_msgSend 函数类型相同,参数都包含 id和SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。
Cache
Cache 定义如下:
typedef struct objc_cache *Cache
复制代码
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
复制代码
Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时优先在 Cache 中查找。 Runtime 系统会把被调用的方法存到 Cache中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。
利用runtime获取信息:
unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
复制代码
RunTime使用场景举例
1. 消息发送
#import "HTMITest.h"
#import <objc/message.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 使用 Runtime 创建一个对象
// 根据类名获取到类
Class testClass = objc_getClass("HTMITest");
// 通过类创建实例对象
// 如果这里报错,请将 Build Setting -> Enable Strict Checking of objc_msgSend Calls 改为 NO
HTMITest *test = objc_msgSend(testClass, @selector(alloc));
test = objc_msgSend(test, @selector(init));
objc_msgSend(test, @selector(test));
HTMITest *otherTest = objc_msgSend(objc_msgSend(objc_getClass("HTMITest"), sel_registerName("alloc")), sel_registerName("init"));
objc_msgSend(otherTest, @selector(testInteger:), 10);
}
@end
复制代码
2. MethodSwizzling
#import "UIImage+HTMITest.h"
#import <objc/message.h>
@implementation UIImage (Success)
+ (void)load {
// 获取到两个方法
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method tuc_imageNamedMethod = class_getClassMethod(self, @selector(htmi_imageNamed:));
// 交换方法
method_exchangeImplementations(imageNamedMethod, htmi_imageNamedMethod);
}
+ (UIImage *)htmi_imageNamed:(NSString *)name {
// 因为来到这里的时候方法实际上已经被交换过了
// 这里要调用 imageNamed: 就需要调换被交换过的 htmi_imageNamed
UIImage *image = [UIImage tuc_imageNamed:name];
return image;
}
@end
复制代码
3. 动态加载方法
#import <objc/message.h>
@implementation HTMITest
void test(id self, SEL _cmd, NSNumber *test) {
NSLog(@"测试%@",test);
}
// 当调用了一个未实现的方法会来到这里
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"test:")) {
// 动态添加方法
class_addMethod(self, @selector(test:), test, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
复制代码
4. 动态关联属性
.h
@interface NSObject (Property)
// @property 在分类中只会生成 getter/setter 方法
// 不会生成成员属性
@property (nonatomic, copy) NSString *name;
@end
.m
#import <objc/message.h>
@implementation NSObject (Property)
- (void)setName:(NSString *)name {
// 属性关联给对象
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
// 得到属性
return objc_getAssociatedObject(self, "name");
}
@end
复制代码
5. 字典转模型
#import <objc/message.h>
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict updateDict:(NSDictionary *)updateDict {
id model = [[self alloc] init];
// 遍历模型中属性
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(self, &count);
for (int i = 0 ; i < count; i++) {
Ivar ivar = ivars[i];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
ivarName = [ivarName substringFromIndex:1];
id value = dict[ivarName];
// 模型中属性名对应字典中的key
if (value == nil) {
if (updateDict) {
NSString *keyName = updateDict[ivarName];
value = dict[keyName];
}
}
[model setValue:value forKeyPath:ivarName];
}
return model;
}
@end
复制代码
6. 消息转发
#import "HTMIOtherTest.h"
#import <objc/message.h>
@implementation HTMITest
#pragma mark - 实例方法
void test(id self,SEL _cmd) {
NSLog(@"测试");
}
//
// 1、在没有找到方法时,会先调用此方法,可用于动态添加方法
// 返回 YES 表示相应 selector 的实现已经被找到并添加到了类中,否则返回 NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// if (sel == NSSelectorFromString(@"test")) {
// // 动态添加方法
// class_addMethod(self, @selector(test), test, "v@:");//添加的方法会立即执行
//
// return NO;//一旦添加方法了,返回什么都没有影响
// }
//
// return [super resolveInstanceMethod:sel];
return NO;
}
// 2、如果第一步的返回 NO 或者直接返回了 YES 而没有添加方法,该方法被调用
// 在这个方法中,我们可以指定一个可以返回一个可以响应该方法的对象
// 如果返回 self 就会死循环
- (id)forwardingTargetForSelector:(SEL)aSelector {
// HTMIOtherTest *other = [[HTMIOtherTest alloc] init];
// return other;
return nil;
}
// 3、如果 `forwardingTargetForSelector:` 返回了 nil,则该方法会被调用,系统会询问我们要一个合法的方法签名
// 若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
// 当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 在这里进行完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 我们还可以改变方法选择器
[anInvocation setSelector:@selector(myTest)];
// 改变方法选择器后,还需要指定是哪个对象的方法
[anInvocation invokeWithTarget:self];
}
//如果不实现forwardInvocation:方法,则会进入此方法,程序不会崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"无法处理消息:%@", NSStringFromSelector(aSelector));
}
- (void)myTest {
NSLog(@"HTMITest 没有实现 -test 方法,并且成功的转成了 -myTest 方法");
}
#pragma mark - 类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setSelector:@selector(myTest)];
[anInvocation invokeWithTarget:self];
}
+ (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"无法处理消息:%@", NSStringFromSelector(aSelector));
}
+ (void)myTest {
NSLog(@"HTMITest 没有实现 +test 方法,并且成功的转成了 +myTest 方法");
}
@end
复制代码
7. 对象归档、解档
#import <objc/runtime.h>
@implementation NSObject (Archive)
- (NSArray *)ignoredProperty {
return @[];
}
- (void)htmi_initWithCoder:(NSCoder *)aDecoder {
Class selfClass = self.class;
while (selfClass &&selfClass != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(selfClass, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([self respondsToSelector:@selector(ignoredProperty)]) {
if ([[self ignoredProperty] containsObject:key]) continue;
}
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
selfClass = [selfClass superclass];
}
}
- (void)htmi_encodeWithCoder:(NSCoder *)aCoder {
Class selfClass = self.class;
while (selfClass &&selfClass != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([self respondsToSelector:@selector(ignoredProperty)]) {
if ([[self ignoredProperty] containsObject:key]) continue;
}
id value = [self valueForKeyPath:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
selfClass = [selfClass superclass];
}
}
@end
复制代码