美文网首页
iOS Runtime 常⽤示例 总结

iOS Runtime 常⽤示例 总结

作者: 红色海_ | 来源:发表于2019-12-16 22:38 被阅读0次

https://www.cnblogs.com/ludashi/p/6294112.html
Demo:https://github.com/lizelu/ObjCRuntimeDemo

转自网络有所修改,留给自己 温故而知新,记录在此!!

Runtime的内容大概有:动态获取类名、动态获取类的成员变量、动态获取类的属性列表、动态获取类的方法列表、动态获取类所遵循的协议列表、动态添加新的方法、类的实例方法实现的交换、动态属性关联、消息发送与消息转发机制等。当然,本篇博客总结的是运行时常用的功能,并不是所有Runtime的内容。

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

#import <Foundation/Foundation.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
@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)publicTestMethod1: (NSString *)value1 Second: (NSString *)value2 {
    NSLog(@"publicTestMethod1");
}

- (void)publicTestMethod2 {
    NSLog(@"publicTestMethod2");
}

- (void)privateTestMethod1 {
    NSLog(@"privateTestMethod1");
}

- (void)privateTestMethod2 {
    NSLog(@"privateTestMethod2");
}

#pragma mark - 方法交换时使用
- (void)method1 {
    NSLog(@"我是Method1的实现");
}

二、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 Class
 @return NSArray
 */
+ (NSArray *)fetchIvarList:(Class)class {
    unsigned int count = 0;

   // 描述类声明的实例变量。variables 美 [ˈvɛriəbəlz] : 变量
   // instance -> I  ,              variables->var
   // Ivar
   // Describes the instance variables declared by a class.
    Ivar *ivarList = class_copyIvarList(class, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++ ) {
        NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity: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"。

获取TestClass的成员变量列表:(
        {
        ivarName = "_var1";
        type = q;
    },
        {
        ivarName = "_var2";
        type = i;
    },
        {
        ivarName = "_var3";
        type = c;
    },
        {
        ivarName = "_var4";
        type = d;
    },
        {
        ivarName = "_var5";
        type = f;
    },
        {
        ivarName = "_publicProperty1";
        type = "@\"NSArray\"";
    },
        {
        ivarName = "_publicProperty2";
        type = "@\"NSString\"";
    },
        {
        ivarName = "_privateProperty1";
        type = "@\"NSMutableArray\"";
    },
        {
        ivarName = "_privateProperty2";
        type = "@\"NSNumber\"";
    },
        {
        ivarName = "_privateProperty3";
        type = "@\"NSDictionary\"";
    }
)

3.获取成员属性

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

/**
 获取类的属性列表, 包括私有和公有属性,以及定义在延展中的属性
 
 @param class Class
 @return 属性列表数组
 */
+ (NSArray *)fetchPropertyList:(Class)class {
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList(class, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity: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添加的,所以也是可以获取到的。

****当然我们获取到的属性的名称为了与其对应的成员变量进行区分,成员属性的名字前边是没有下划线的。******
获取TestClass的属性列表:(
    dynamicAddProperty,
    privateProperty1,
    privateProperty2,
    privateProperty3,
    publicProperty1,
    publicProperty2
)

4、获取类的实例方法

在下方函数中,通过class_copyMethodList()方法获取类的实例方法列表,然后通过for循环使用method_getName()来获取每个方法的名称,然后将方法的名称转换成NSString类型,存储到数组中一并返回。具体如下所示:

/**
 获取类的实例方法列表:getter, setter, 对象方法等。但不能获取类方法

 @param class <#class description#>
 @return <#return value description#>
 */
+ (NSArray *)fetchMethodList:(Class)class {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(class, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++ ) {
        Method method = methodList[i];
        SEL methodName = method_getName(method);
        [mutableList addObject:NSStringFromSelector(methodName)];
    }
    free(methodList);
    return [NSArray arrayWithArray:mutableList];
}

运行结果如下,打印了TestClass类的所有实例方法,当然其中也必须得包含成员属性的getter和setter方法。当然Category的方法也必须获取到。

获取TestClass的方法列表:(
    "publicTestMethod1:Second:",
    publicTestMethod2,
    privateTestMethod1,
    privateTestMethod2,
    method1,
    "dynamicAddMethod:",
    publicProperty1,
    "setPublicProperty1:",
    publicProperty2,
    "setPublicProperty2:",
    privateProperty1,
    "setPrivateProperty1:",
    privateProperty2,
    "setPrivateProperty2:",
    privateProperty3,
    "setPrivateProperty3:",
    dynamicAddProperty,
    "setDynamicAddProperty:",
    categoryMethod,
    swapMethod,
    method2,
    ".cxx_destruct",
    "methodSignatureForSelector:",
    "forwardInvocation:",
    "forwardingTargetForSelector:"
)

5、获取协议列表

使用了class_copyProtocolList()来获取类所遵循协议列表,然后通过for循序使用protocol_getName()来获取协议的名称,最后将其转换成NSString类型放入数组中返回即可。

/**
 获取协议列表
 
 @param class <#class description#>
 @return <#return value description#>
 */
+ (NSArray *)fetchProtocolList:(Class)class {
    unsigned int count = 0;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList(class, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++ ) {
        Protocol *protocol = protocolList[i];
        const char *protocolName = protocol_getName(protocol);
        [mutableList addObject:[NSString stringWithUTF8String: protocolName]];
    }
    
    return [NSArray arrayWithArray:mutableList];
    return nil;
}

下方就是我们获取到的TestClass类所遵循的协议列表:
获取TestClass的协议列表:(
    NSCoding,
    NSCopying
)

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、方法实现交换

下方是类的两个方法的实现进行交换。

method_exchangeImplementations

如果将MethodA与MethodB的方法实现进行交换的话,
调用MethodA时就会执行MethodB的内容,反之亦然。

/**
 方法交换
 
 @param class 交换方法所在的类
 @param method1 方法1
 @param method2 方法2
 */
+ (void)methodSwap:(Class)class firstMethod:(SEL)method1 secondMethod:(SEL)method2 {
    Method firstMethod = class_getInstanceMethod(class, method1);
    Method secondMethod = class_getInstanceMethod(class, method2);
    method_exchangeImplementations(firstMethod, secondMethod);
}
@end

下方是TestClass的一个类目,在该类目中将类目中的方法与TestClass中的方法进行了替换。也就是将method1与method2进行了替换,替换后在method2中调用method2,其实就是method1。

**应用场景:在第三方库中,经常会使用该特性达到AOP编程的目的。

AOP编程:在软件业,AOP为Aspect Oriented Programming的缩>写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

iOS中呢??什么是AOP
AOP:Aspect Oriented Programming,译为面向切面编程。

在不修改源代码的情况下,通过运行时给程序添加统一功能的技术。

我觉得其中有两层涵义:
第一:不修改源代码,即尽可能的解耦。
第二:添加统一的功能,即我们能实现的是添加统一的单一的功能,在某处使用AOP,我们只能实现一项单一的功能。如:日志记录。当然你可以添加多个AOP的模块到项目中,每一个实现不同功能,但是每一个功能必须是单一的。
主要功能:日志记录,性能统计等。

想更详细了解AOP请移步--> iOS面向切面编程AOP实践

@implementation TestClass (SwapMethod)

- (void)swapMethod {
    [RuntimeKit methodSwap:[self class]
               firstMethod:@selector(method1)
              secondMethod:@selector(method2)];
}

- (void)method2 {
    // 调用的实际上是 method1的实现
    [self method2];
    NSLog(@"可在Method1的基础上添加 各种东东东了");
}

@end

三、属性关联

在类目中动态的为我们的类添加相应的属性,如果看过之前发布的对Masonry并不陌生。

Masonry就是为屏幕适配而生的三方框架.
Auto Layout是使用频率最高的布局框架,但是其也有弊端。就是在使用UILayoutConstraint的时候,会发现代码量很多,而且大多都是重复性的代码,以至于好多人都不想用这个框架。
后来Github上的出现了基于UILayoutConstraint封装的第三方布局框架Masonry,使用起来非常方便,

想了解Masonry 请移步 ->iOS Masonry详解
# iOS自动布局框架-Masonry详解

在Masonry框架中就利用Runtime的属性关联在UIView的类目中给UIView添加了一个约束数组,用来记录添加在当前View上的所有约束。下方就是在TestClass的类目中通过objc_getAssociatedObject()和objc_setAssociatedObject()两个方法为TestClass类添加了一个dynamicAddProperty属性。上面我们获取到的属性列表中就含有该动态添加的成员属性。

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

#pragma mark - 动态属性关联
static char kDynamicAddProperty;

// Associated  [əˈsoʊsieɪtɪd] 联系;交往
@implementation TestClass (AssociatedObject)
/**
 getter方法
 @return 返回关联属性的值
 */
- (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的消息处理和消息转发机制。
来聊一下消息处理与消息转发的。

当调用一个类的方法时,
●先在本类中的方法缓存列表中进行查询,
如果找到了 该方法的实现,就执行;

●如果找不到 就在本类中的方法列表中进行查找。
在本类方法列表中查找到相应的方法实现后就进行调用;

●如果还是没找到,就去父类中进行查找。
如果在父类中的方法列表中找到了相应方法的实现,那么就执行;

否则就执行下方的几步。

当调用一个方法在缓存列表,//本句存疑

本类中的方法列表以及父类的方法列表找不到相应的实现时,到程序崩溃阶段之前还会有几步让你来挽救。
程序崩款:(找不到方法实现,即 SEL,找不到对应的IMP)

-[TestClass noThisMethod:]: unrecognized selector sent to instance 0x100618880

接下来就来看看这几步该怎么走。

1.消息处理(Resolve Method)

当在相应的类以及父类中找不到类方法实现时会执行+resolveInstanceMethod:这个类方法。该方法如果在类中不被重写的话,默认返回NO。如果返回NO就表明不做任何处理,走下一步。如果返回YES的话,就说明在该方法中对这个找不到实现的方法进行了处理。在该方法中,我们可以为找不到实现的SEL动态的添加一个方法实现,添加完毕后,就会执行我们添加的方法实现。这样,当一个类调用不存在的方法时,就不会崩溃了。

具体做法如下:

//运行时方法拦截
- (void)dynamicAddMethod: (NSString *) value {
    NSLog(@"OC替换的方法:%@", value);
}

/**

 动态方法决议

 Objective C 提供了一种名为动态方法决议的手段,
使得我们可以在运行时动态地为一个 selector 提供实现。
我们只要实现 +resolveInstanceMethod: 或 +resolveClassMethod: 方法,
并在其中为指定的 selector 提供实现即可
(通过调用运行时函数 class_addMethod 来添加)。
这两个方法都是 NSObject 中的类方法,其原型为
+ (BOOL)resolveClassMethod:(SEL)name;  
+ (BOOL)resolveInstanceMethod:(SEL)name;  
参数 name 是需要被动态决议的 selector;
返回值 是表示动态决议成功与否。
但单独在这个方法中(不涉及消息转发的情况下),
如果在该函数内为指定的 selector 提供实现,
无论返回 YES 还是 NO,编译运行都是正确的;
但如果在该函数内并不真正为 selector 提供实现,
无论返回 YES 还是 NO,运行都会 crash,道理很简单,
selector 并没有对应的实现,而又没有实现消息转发。

resolveInstanceMethod 是为对象方法进行决议,
而 resolveClassMethod 是为类方法进行决议。


关联的 
Object-C 声明属性特性(参考 Object-C 编程语言中声明属性) 时包含了 
***@dynamic 指令:***
@dynamic propertyName;
这样就告诉编译器 不用自动生成get set方法,
该属性相对应的方法将被动态提供。

实现 resolveInstanceMethod: 和 resolveClassMethod: 函数分别为实例方法和类方法提供动态方法实现。



 没有找到SEL的IML实现时会执行下方的方法

 @param sel 当前对象调用并且找不到IML的SEL
 @return 
     通常情况下:不实现动态方法决议return NO;
              走消息转发机制forwordingTargetForSelector

     实现动态方法决议return YES;
     当然 return YES之前会为SEL addMethod。
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {

   // return NO;   

   // 追加了方法实现,无论返回 YES NO都会正常执行,
   //不会继续走 消息转发 即forwordingTargetForSelector
    [RuntimeKit addMethod:[self class] method:sel method:@selector(dynamicAddMethod:)];

    return YES;
}

2、消息快速转发

如果不对上述消息进行处理的话,也就是+resolveInstanceMethod:返回NO时(没有为SEL 添发方法实现),会走下一步消息转发即-forwardingTargetForSelector:。

该方法会返回一个类的对象,这个类的对象有SEL对应的实现,就会被转发到SecondClass中去进行处理。这也就是所谓的消息转发。

当该方法返回self或者nil, 说明不对相应的方法进行转发,那么就该走下一步了。

/**
 将当前对象不存在的SEL传给其他存在该SEL的对象

 @param aSelector 当前类中不存在的SEL
 @return 存在该SEL的对象
 */
// forwarding 美 [ˈfɔːrwərdɪŋ] 转发 转寄 转投
- (id)forwardingTargetForSelector:(SEL)aSelector {

     //当该方法返回self或者nil, 说明不对相应的方法进行转发,那么就该走下一步了。
    // return self;

    return [SecondClass new];   //让SecondClass中相应的SEL去执行该方法
}

3.消息常规转发

如果不将消息转发给其他类的对象,那么就只能自己进行处理了。
如果上述方法返回self的话,会执行-methodSignatureForSelector:方法来获取方法的参数以及返回数据类型,也就是说该方法获取的是方法的签名并返回。如果methodSignatureForSelector方法返回nil的话,那么消息转发就结束,程序崩溃,报出找不到相应的方法实现的崩溃信息。
-[TestClass noThisMethod:]: unrecognized selector sent to instance 0x100618880

下方也是讲将方法转发给SecondClass,如下所示:

// Signature 美 [ˈsɪɡnətʃər] 签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    //查找父类的方法签名
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if(signature == nil) {
        signature = [NSMethodSignature signatureWithObjCTypes:"@@:"];

    }
    return signature;
}

//Invocation 美 [ˌɪnvəˈkeɪʃn]  启动
// forwarding 美 [ˈfɔːrwərdɪŋ] 转发 转寄 转投

/*
 消息重定向
*/
- (void)forwardInvocation:(NSInvocation *)invocation {
    SecondClass * forwardClass = [SecondClass new];
    SEL sel = invocation.selector;
    if ([forwardClass respondsToSelector:sel]) {
        [invocation invokeWithTarget:forwardClass];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

github源码分享链接:https://github.com/lizelu/ObjCRuntimeDemo

相关文章

网友评论

      本文标题:iOS Runtime 常⽤示例 总结

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