`runtime`的实际应用

runtime的实际应用

你在项目中用过 runtime 吗?举个例子

runtime的概念

runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h>), 编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件).

runtime的实际应用

动态交换两个方法的实现 – method swizzling

我们想要在一款 iOS app 中追踪每一个视图控制器被用户呈现了几次: 这可以通过在每个视图控制器的 viewDidAppear: 方法中添加追踪代码来实现,但这样会大量重复的样板代码。继承是另一种可行的方式,但是这要求所有被继承的视图控制器如 UIViewController, UITableViewController, UINavigationController 都在 viewDidAppear:实现追踪代码,这同样会造成很多重复代码。 幸运的是,这里有另外一种可行的方式:从 category 实现 method swizzling.

核心API

// 获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name);

// 获取某个类的实例方法
Method class_getInstanceMethod(Class cls , SEL name);

// 交换两个方法的实现
void method_exchangeImplementations(Method m1, Method m2);

实例代码

#import &lt;objc/runtime.h&gt;

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@&quot;viewWillAppear: %@&quot;, self);
}

@end

为分类添加属性

众所周知,分类中是无法设置属性的,如果在分类的声明中写@property 只能为其生成get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash.

核心API

// 设置动态关联对象
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy);
// 获取动态关联对象
id objc_getAssociatedObject(id object , const void *key);

实例代码

@interface UIView (SQIAdditions)

@property(nonatomic, strong) NSArray &lt;void (^)(void)&gt;*sqi_handleBlockArr;

@end

@implementation UIView (SQIAdditions)

static char *HandleBlockArrKey = &quot;HandleBlockArrKey&quot;;


- (void)setSqi_handleBlockArr:(NSArray&lt;void (^)(void)&gt; *)handleBlockArr {
    objc_setAssociatedObject(self, HandleBlockArrKey, handleBlockArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSArray&lt;void (^)(void)&gt; *)sqi_handleBlockArr {
    return objc_getAssociatedObject(self, HandleBlockArrKey);
}

@end

自定义对象的归档和解档

自定义对象如果想实现归档和解档, 必须遵守NSCoding协议, 必须实现encodeWithCoder,initWithCoder的两个方法告诉系统,你的对象里面到底有那些数据.

核心API

// 获取某个类的所有成员变量
Ivar  _Nonnull * class_copyIvarList(Class cls, unsigned int *outCount);
// 获取成员变量的名字
const char *ivar_getName(Ivar v);
// 获取成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v);

实例代码

  • 获取Person类中所有成员变量的名字和类型
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &amp;outCount);

// 遍历所有成员变量
for (int i = 0; i &lt; outCount; i++) {
    // 取出i位置对应的成员变量
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@&quot;成员变量名:%s 成员变量类型:%s&quot;,name,type);
}
// 注意释放内存!
free(ivars);
  • 利用runtime 获取所有属性来重写归档解档方法
// 设置不需要归解档的属性
- (NSArray *)ignoredNames {
    return @[@&quot;_aaa&quot;,@&quot;_bbb&quot;,@&quot;_ccc&quot;];
}

// 解档方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        // 获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &amp;outCount);

        for (int i = 0; i &lt; outCount; i++) {
            Ivar ivar = ivars[i];
            // 将每个成员变量名转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }

            // 根据变量名解档取值,无论是什么类型
            id value = [aDecoder decodeObjectForKey:key];
            // 取出的值再设置给属性
            [self setValue:value forKey:key];
            // 这两步就相当于以前的 self.age = [aDecoder decodeObjectForKey:@&quot;_age&quot;];
        }
        free(ivars);
    }
    return self;
}

// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
     // 获取所有成员变量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &amp;outCount);
    for (int i = 0; i &lt; outCount; i++) {
        Ivar ivar = ivars[i];
        // 将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }

        // 通过成员变量名,取出成员变量的值
        id value = [self valueForKeyPath:key];
        // 再将值归档
        [aCoder encodeObject:value forKey:key];
        // 这两步就相当于 [aCoder encodeObject:@(self.age) forKey:@&quot;_age&quot;];
    }
    free(ivars);
}

依据上面的原理我们就可以给NSObject做一个分类,让我们不需要每次都写这么一长串代码,只要实现一小段代码就可以让一个对象具有归解档的能力。

完整的分类代码

NSObject+SQIExtension.h

#import &lt;Foundation/Foundation.h&gt;

@interface NSObject (SQIExtension)

- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder;
- (void)decode:(NSCoder *)aDecoder;

@end

NSObject+SQIExtension.m

#import &quot;NSObject+SQIExtension.h&quot;
#import &lt;objc/runtime.h&gt;

@implementation NSObject (Extension)

- (void)decode:(NSCoder *)aDecoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &amp;&amp;c != [NSObject class]) {

        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &amp;outCount);
        for (int i = 0; i &lt; outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }

            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
        c = ;
    }

}

- (void)encode:(NSCoder *)aCoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &amp;&amp;c != [NSObject class]) {

        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &amp;outCount);
        for (int i = 0; i &lt; outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }

            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = ;
    }
}
@end

分类的使用方法

在需要归解档的对象中实现下面方法即可:

// 设置需要忽略的属性
- (NSArray *)ignoredNames {
    return @[@&quot;bone&quot;];
}

// 在系统方法内来调用我们的方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self decode:aDecoder];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [self encode:aCoder];
}

这样看来,我们每次又要写同样的代码,我们可以将归解档两个方法封装为宏,在需要的地方一句宏搞定,如果有不需要归解档的属性就实现ignoredNames 方法,具体可以看我的demo,这个也是MJExtension中那个一句宏就可以解决归解档的实现原理。

《`runtime`的实际应用》有一个想法

发表评论

邮箱地址不会被公开。 必填项已用*标注