关于ios:iOS-Runtime常用示例总结

36次阅读

共计 10027 个字符,预计需要花费 26 分钟才能阅读完成。

前言

Runtime 是 iOS 外面十分重要的基础知识,首次与它见面时,甚是糊涂,但没有关系,万事万物都是要由生疏到相熟。学就完了。

注释

常常有小伙伴私下在 Q 上问一些对于 Runtime 的货色,问我有没有 Runtime 的相干博客,之前还真没正儿八经的总结过。之前只是在解析第三方框架源码时,聊过一些用法,也就是这些第三方框架中用到的 Runtime。比方属性关联,动静获取属性等等。本篇博客就针对 Runtime 这个主题来总结一些其罕用的一些办法.

本篇博客所聊的 Runtime 的内容大略有:动静获取类名、动静获取类的成员变量、动静获取类的属性列表、动静获取类的办法列表、动静获取类所遵循的协定列表、动静增加新的办法、类的实例办法实现的替换、动静属性关联、音讯发送与音讯转发机制等。当然,本篇博客总结的是运行时罕用的性能,并不是所有 Runtime 的内容。

一、构建 Runtime 测试用例

本篇博客的内容是依靠于实例的,所以咱们在本篇博客中先构建咱们的测试类,Runtime 将会对该类进行相干的操作。下方就是本篇博客所波及 Demo 的目录,下面的 RuntimeKit 类是讲 Runtime 罕用的性能进行了简略的封装,而下方的 TestClass 以及相干的类目就是咱们 Runtime 要操作的对象了。下方会对 TestClass 以及类目中的内容进行具体介绍。

下方的代码就是咱们的测试类 TestClass 的次要局部,因为 TestClass 是专门用来测试的类,所以其波及的内容要尽量的全面。TestClass 遵循了 NSCoding, NSCopying 这两个协定,并且为其增加了私有属性、公有属性、公有成员变量、私有实例办法、公有实例办法、类办法等。这些增加的内容,都将是咱们 Runtime 的操作对象。下方那几个 TestClass 的类目稍后在应用 Runtime 时再进行介绍。

TestClass.h@interface TestClass : NSObject <NSCoding, NSCopying> @property (nonatomic, strong) NSArray *publicProperty1;@property (nonatomic, strong) NSString *publicProperty2; + (void)classMethod:(NSString *)value;- (void)publicTestMethod1:(NSString *)value1 Second:(NSString *)value2;- (void)publicTestMethod2; - (void)method1; @end TestClass.m@interface TestClass() {    NSInteger _var1;    int _var2;    BOOL _var3;    double _var4;    float _var5;} @property (nonatomic, strong) NSMutableArray *privateProperty1;@property (nonatomic, strong) NSNumber *privateProperty2;@property (nonatomic, strong) NSDictionary *privateProperty3; @end @implementation TestClass + (void)classMethod: (NSString *)value {NSLog(@"publicTestMethod1");} - (void)privateTestMethod1 {NSLog(@"privateTestMethod1");} - (void)privateTestMethod2 {NSLog(@"privateTestMethod2");} #pragma mark - 办法替换时应用 - (void)method1 {NSLog(@"我是 Method1 的实现");} @end

二、RuntimeKit 的封装

接下来咱们就来看看 RuntimeKit 中的内容,其中对 Runtime 罕用的办法进行了简略的封装。次要是动静的获取类的一些属性和办法的,以及动静办法增加和办法替换的。本局部的干货还是不少的。

1、获取类名

动静的获取类名是比较简单的,应用 class_getName(Class) 就能够在运行时来获取类的名称。class_getName() 函数返回的是一个 char 类型的指针,也就是 C 语言的字符串类型,所以咱们要将其转换成 NSString 类型,而后再返回进来。下方的 +fetchClassName: 办法就是咱们封装的获取类名的办法,如下所示:

/** 获取类名  @param class 响应类 @return NSString 类名 */+ (NSString *)fetchClassName:(Class)class {const char *className = class_getName(class);    return [NSString stringWithUTF8String:className];}

2、获取成员变量

下方这个 +fetchIvarList: 这个办法就是咱们封装的获取类的成员变量的办法。当然咱们在获取成员变量时,能够用 ivar_getTypeEncoding() 来获取相应成员变量的类型。应用 ivar_getName() 来获取相应成员变量的名称。下方就是对获取成员变量的性能的封装。返回的是一个数组,数组的元素是一个字典,而字典中存储的就是相应成员变量的名称和类型。

/** 获取成员变量  @param class 响应类 @return NSArray 成员变量列表 */+ (NSArray *)fetchIvarList:(Class)class {unsigned int count = 0;    Ivar *ivarList = class_copyIvarList(class, &count);        NSMutableArray *mutableList = [[NSMutableArray alloc] initWithCapacity:count];    for (unsigned int i = 0; i < count; i++) {NSMutableDictionary *dic = [[NSMutableDictionary alloc] initWithCapacity:2];        const char *ivarName = ivar_getName(ivarList[i]);        const char *ivarType = ivar_getTypeEncoding(ivarList[i]);        dic[@"type"] = [NSString stringWithUTF8String:ivarType];        dic[@"ivarName"] = [NSString stringWithUTF8String:ivarName];        [mutableList addObject:dic];    }     free(ivarList);    return [NSArray arrayWithArray:mutableList];}

下方就是调用上述办法获取的 TestClass 类的成员变量。当然在运行时就没有什么公有和私有之分了,只有是成员变量就能够获取到。在 OC 中的给类增加成员属性其实就是增加了一个成员变量和 getter 以及 setter 办法。所以获取的成员列表中必定带有成员属性,不过成员属性的名称后方增加了下划线来与成员属性进行辨别。咱们也能够获取成员变量的类型,下方的_var1 是 NSInteger 类型,动静获取到的是 q 字母,其实是 NSInteger 的符号。而 i 就示意 int 类型,c 示意 Bool 类型,d 示意 double 类型,f 则就示意 float 类型。当然这些根本类型都是由一个字母代替的,如果是援用类型的话,则间接就是一个字符串了,比方 NSArray 类型就是 ”@NSArray”。

3. 获取成员属性

下面获取的是类的成员变量,那么下方这个 +fetchPropertyList: 获取的就是成员属性。当然此刻获取的只包含成员属性,也就是那些有 setter 或者 getter 办法的成员变量。下方次要是应用了 class_copyPropertyList(Class,&count) 来获取的属性列表,而后通过 for 循环通过 property_getName() 来获取每个属性的名字。当然应用 property_getName() 获取到的名字仍然是 C 语言的 char 类型的指针,所以咱们还须要将其转换成 NSString 类型,而后放到数组中一并返回。如下所示:

/** 获取类的属性列表,包含公有和私有属性,以及定义在延展(extention) 中的属性  @param class 响应类 @return NSArray 属性列表数组 */+ (NSArray *)fetchPropertyList:(Class)class {unsigned int count = 0;    objc_property_t *propertyList = class_copyPropertyList(class, &count);        NSMutableArray *mutableList = [[NSMutableArray alloc] initWithCapacity:count];    for (unsigned int i = 0; i < count; i++) {const char *propertyName = property_getName(propertyList[i]);        [mutableList addObject:[NSString stringWithUTF8String:propertyName]];    }        free(propertyList);    return [NSArray arrayWithArray:mutableList];}

下方这个截图就是调用上述办法获取的 TestClass 的所有的属性,当然 dynamicAddProperty 是咱们应用 Runtime 动静给 TestClass 增加的,所以也是能够获取到的。当然咱们获取到的属性的名称为了与其对应的成员变量进行辨别,成员属性的名字前边是没有下划线的。

4、获取类的实例办法

接下来咱们就来封装一下获取类的实例办法列表的性能,下方这个 +fetchMethodList: 就是咱们封装的获取类的实例办法列表的函数。在下方函数中,通过 class_copyMethodList() 办法获取类的实例办法列表,而后通过 for 循环应用 method_getName() 来获取每个办法的名称,而后将办法的名称转换成 NSString 类型,存储到数组中一并返回。具体代码如下所示:

/** 获取类的实例办法列表:getter,setter,对象办法等,但不能获取类办法  @param class 响应类 @return NSArray 实例办法列表 */+ (NSArray *)fetchInstanceMethodList:(Class)class {unsigned int count = 0;    Method *instanceMethodList = class_copyMethodList(class, &count);        NSMutableArray *mutableList = [[NSMutableArray alloc] initWithCapacity:count];    for (unsigned int i = 0; i < count; i++) {Method method = instanceMethodList[i];        SEL methodName = method_getName(method);        [mutableList addObject:NSStringFromSelector(methodName)];    }        free(instanceMethodList);    return [NSArray arrayWithArray:mutableList];}

下方这个截图就是上述办法在 TestClass 上运行的后果,其中打印了 TestClass 类的所有实例办法,当然其中也必须得蕴含成员属性的 getter 和 setter 办法。当然 TestClass 类目中的办法也是必须能获取到的。后果如下所示:

5、获取协定列表

下方是获取咱们类所遵循协定列表的办法,次要应用了 class_copyProtocolList() 来获取列表,而后通过 for 循序应用 protocol_getName() 来获取协定的名称,最初将其转换成 NSString 类型放入数组中返回即可。

/** 获取协定列表  @param class 响应类 @return NSArray 协定列表 */ + (NSArray *)fetchProtocolList:(Class)class {unsigned int count  = 0;    __unsafe_unretained Protocol **protocolList = class_copyProtocolList(class, &count);        NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:count];    for (unsigned int i = 0; i < count; i++) {Protocol *protocol = protocolList[i];        const char *protocolName = protocol_getName(protocol);        [mutableArray addObject:[NSString stringWithUTF8String:protocolName]];    }    free(protocolList);    return [NSArray arrayWithArray:mutableArray];}

下方就是咱们获取到的 TestClass 类所遵循的协定列表:

6、动静增加办法实现

下方就是动静的往相应类上增加办法以及实现。下方的 +addMethod 办法有三个参数,第一个参数是要增加办法的类,第二个参数是办法的 SEL,第三个参数则是提供办法实现的 SEL。稍后在音讯发送和音讯转发时会用到下方的办法。下方次要是应用 class_getInstanceMethod() 和 method_getImplementation() 这两个办法相结合获取相应 SEL 的办法实现。下方的 IMP 其实就是 Implementation 的办法缩写,获取到相应的办法实现后,而后再调用 class_addMethod() 办法将 IMP 与 SEL 进行绑定即可。具体做法如下所示。

/** 往类上增加新的办法与实现  @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);}

7、办法实现替换

下方就是讲类的两个办法的实现进行替换。如果将 MethodA 与 MethodB 的办法实现进行替换的话,调用 MethodA 时就会执行 MethodB 的内容,反之亦然。

/** 办法替换  @param class 响应类 @param oldMethod 被替换办法 @param newMethod 替换办法 */+ (void)exchangeMethod:(Class)class method:(SEL)oldMethod method:(SEL)newMethod {Method method1 = class_getInstanceMethod(class, oldMethod);    Method method2 = class_getInstanceMethod(class, newMethod);    method_exchangeImplementations(method1, method2);}

下方这段代码就是对上述办法的测试。下方是 TestClass 的一个类目,在该类目中将类目中的办法与 TestClass 中的办法进行了替换。也就是将 method1 与 method2 进行了替换,替换后在 method2 中调用的 method2 其实就是调用的 method1。在第三方库中,常常会应用该个性,已达到 AOP 编程的目标。

#import "TestClass+ExchangeMethod.h"#import "RuntimeKit.h" @implementation TestClass (ExchangeMethod) - (void)exchangeMethod {[RuntimeKit exchangeMethod:[self class] method:@selector(method1) method:@selector(method2)];} - (void)method2 {[self method2];    NSLog(@"能够在 Method1 根底上增加任何货色了");} @end

三、属性关联

属性关联说白了就是在类目中动静的为咱们的类增加相应的属性,如果看过之前公布的对 Masonry 框架源码解析的博客的话,对下方的属性关联并不生疏。在 Masonry 框架中就利用 Runtime 的属性关联在 UIView 的类目中给 UIView 增加了一个束缚数组,用来记录增加在以后 View 上的所有束缚。下方就是在 TestClass 的类目中通过 objc_getAssociatedObject() 和 objc_setAssociatedObject() 两个办法为 TestClass 类增加了一个 dynamicAddProperty 属性。下面咱们获取到的属性列表中就含有该动静增加的成员属性。

下方就是属性关联的具体代码,如下所示。

#import "TestClass+AssociatedObject.h"#import "RuntimeKit.h" @implementation TestClass (AssociatedObject) #pragma mark - 动静属性关联 static char kDynamicAddProperty; /** getter 办法  @return NSString 关联属性的值 */- (NSString *)dynamicAddProperty {return objc_getAssociatedObject(self, &kDynamicAddProperty);} /** setter 办法  @param dynamicAddProperty 动静增加属性 */ - (void)setDynamicAddProperty:(NSString *)dynamicAddProperty {objc_setAssociatedObject(self, &kDynamicAddProperty, dynamicAddProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);} @end

四、音讯解决与音讯转发

在 Runtime 中不得不提的就是 OC 的音讯解决和音讯转发机制。当然网上也有不少相干材料,本篇博客为了完整性,还是要聊一下音讯解决与音讯转发的。当你调用一个类的办法时,先在本类中的办法缓存列表中进行查问,如果在缓存列表中找到了该办法的实现,就执行,如果找不到就在本类中的方列表中进行查找。在本类方列表中查找到相应的办法实现后就进行调用,如果没找到,就去父类中进行查找。如果在父类中的办法列表中找到了相应办法的实现,那么就执行,否则就执行下方的几步。

当调用一个办法在缓存列表,本类中的办法列表以及父类的办法列表找不到相应的实现时,到程序解体阶段两头还会有几步让你来解救。接下来就来看看这几步该怎么走。

1. 音讯解决(Resolve Method)

当在相应的类以及父类中找不到类办法实现时会执行 +resolveInstanceMethod: 这个类办法。该办法如果在类中不被重写的话,默认返回 NO。如果返回 NO 就表明不做任何解决,走下一步。如果返回 YES 的话,就阐明在该办法中对这个找不到实现的办法进行了解决。在该办法中,咱们能够为找不到实现的 SEL 动静的增加一个办法实现,增加结束后,就会执行咱们增加的办法实现。这样,当一个类调用不存在的办法时,就不会解体了。具体做法如下所示:

// 运行时办法拦挡 - (void)dynamicAddMethod:(NSString *)value {NSLog(@"OC 替换的办法:%@",value);} #pragma mark - 音讯解决 /** 没有找到 SEL 的 IMP 执行以下办法  @param sel 以后对象调用并且找不到 IMP 的 SEL @return BOOL 找到其余的执行办法时,返回 YES */+ (BOOL)resolveInstanceMethod:(SEL)sel {//    return NO; // 默认返回 NO,当返回 NO 的时候,解开正文,会接着执行????forwardingTargetForSelector: 办法    [RuntimeKit addMethod:[self class] method:sel method:@selector(dynamicAddMethod:)];    return YES;}

2、音讯疾速转发

如果不对上述音讯进行解决的话,也就是 +resolveInstanceMethod: 返回 NO 时,会走下一步音讯转发,即 -forwardingTargetForSelector:。该办法会返回一个类的对象,这个类的对象有 SEL 对应的实现,当调用这个找不到的办法时,就会被转发到 SecondClass 中去进行解决。这也就是所谓的音讯转发。当该办法返回 self 或者 nil, 阐明不对相应的办法进行转发,那么就该走下一步了。

#pragma mark - 音讯转发 /** 当以后对象不存在的 SEL 传给其它存在该 SEL 的对象  @param aSelector 以后对象不存在的 SEL @return id 其它存在该 SEL 的对象 */- (id)forwardingTargetForSelector:(SEL)aSelector {//    return self; // 解开正文走上面???? 俩办法    return [SecondClass new]; // 让 SecondClass 执行 }

3. 音讯惯例转发

如果不将音讯转发给其余类的对象,那么就只能本人进行解决了。如果上述办法返回 self 的话,会执行 -methodSignatureForSelector: 办法来获取办法的参数以及返回数据类型,也就是说该办法获取的是办法的签名并返回。如果上述办法返回 nil 的话,那么音讯转发就完结,程序解体,报出找不到相应的办法实现的解体信息。

在 +resolveInstanceMethod: 返回 NO 时就会执行下方的办法,下方也是讲该办法转发给 SecondClass,如下所示:

#pragma mark - 音讯惯例转发 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {// 查找父类的办法签名    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];    if (!signature) {signature = [NSMethodSignature signatureWithObjCTypes:"@@:"];    }    return signature;} - (void)forwardInvocation:(NSInvocation *)anInvocation {SecondClass *forwardClass = [SecondClass new];    SEL sel = anInvocation.selector;    if ([forwardClass respondsToSelector:sel]) {[anInvocation invokeWithTarget:forwardClass];    } else {[self doesNotRecognizeSelector:sel];    }}

作为一个开发者,有一个学习的气氛跟一个交换圈子特地重要,这是一个我的 iOS 交换群:642363427 不论你是小白还是大牛欢送入驻,分享 BAT, 阿里面试题、面试教训,探讨技术,大家一起交流学习成长!

正文完
 0