Objective-C runtime - 应用和示例

前言

系列第五篇:将展示几个runtime的例子以及runtime的实际应用。这些例子包括:

  1. 动态添加属性以及添加用来存储属性对应的实例变量的关联对象
  2. 方法决议:父类实现resolveInstanceMethod,然后在子类中添加方法
  3. 消息转发示例:
    1. forwardingTargetForSelector
    2. forwardInvocation

本文的demo代码可以在我的github上找到

几个有趣的示例

动态添加任意属性

class_addProperty runtime添加属性API

class_addMethod runtime添加方法API

仅仅添加属性是没什么用的,因为还需要添加属性对应的实例变量。

虽然runtime提供了class_addIvar方法来给类添加实例变量,但是注意,该方法只能在创建新的类的时候才能使用;对于已经存在的类,是不允许添加实例变量的

鉴于上述原因,所以可以采用动态添加关联对象来存储属性对应的实例变量。

添加属性

BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)

  1. cls : 要添加属性的类
  2. name : 添加属性的名字
  3. attributes : 属性的特性
  4. attributeCount : 属性特性数量
// runtime add property
objc_property_attribute_t attribute1 = {"T", @encode(NSString *)};
objc_property_attribute_t attribute2 = {"N", ""};
objc_property_attribute_t attribute3 = {"&", ""};
objc_property_attribute_t attribute[] = {attribute1, attribute2, attribute3};
class_addProperty([HCBase class], "sb", attribute, 3);

如上代码,添加了一个属性sb,该属性有3个特性:1. NSString类型 2. nonatomic 3. strong

添加关联对象

为了向正常属性那样访问,给该类添加sb属性的存取方法sbsetSb:

// add property getter and setter
//class_addMethod([HCBase class], @selector(sb), (IMP) sb, "@@:");
class_addMethod([HCBase class], NSSelectorFromString(@"sb"), (IMP) customGetter, "@@:");
//class_addMethod([HCBase class], @selector(setSb:), (IMP) setSb, "v@:@");
class_addMethod([HCBase class], NSSelectorFromString(@"setSb:"), (IMP) customSetter, "v@:@");

注意该方法的最后一个参数是添加的方法的签名:返回类型 + self + selector + 参数 的字符串:

  1. sb方法,返回类型为id(@),self为id(@),选择子(:),参数为空 => @@:
  2. setSb:方法,返回类型为void(v),self为id(@),选择子(:),参数为id(@) => v@:@

现在来看看setter和getter的通用实现方法

// custom getter && setter

void customSetter(id self, SEL _cmd, id value) {
    NSString *propertyStr = NSStringFromSelector(_cmd);
    // divide set
    NSString *realProperty = [propertyStr substringFromIndex:3];
    realProperty = [realProperty substringToIndex:realProperty.length - 1];
    realProperty = [realProperty lowercaseString];
    //const NSString *key = [realProperty copy];
    objc_setAssociatedObject(self, NSSelectorFromString(realProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //objc_setAssociatedObject(self, "sb", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

id customGetter(id self, SEL _cmd) {
    id result = objc_getAssociatedObject(self, _cmd);
    //id result = objc_getAssociatedObject(self, "sb");
    return result;
}

这两个方法通过判断selector来判断要添加的是哪一个属性的关联对象,也就是说这个方法是对任何属性通用的(之后新添加其他的属性,也可以使用该实现)

注意:objc_setAssociationObjectobjc_getAssociationObject方法中的第二个参数,也就是对象的key是一个 const void * 类型:常量指针;正好@selector也是常量指针。

访问属性

// new HCBase and set && printf sb
HCBase *baseOBJ = [[HCBase alloc] init];
//[baseOBJ performSelector:@selector(setSb:) withObject:@"sb"];
[baseOBJ performSelector:NSSelectorFromString(@"setSb:") withObject:@"sb"];
//NSLog(@"vanney code log : new property is %@", [baseOBJ performSelector:@selector(sb)]);
NSLog(@"vanney code log : new property is %@", [baseOBJ performSelector:NSSelectorFromString(@"sb")]);

已经添加了属性,并添加了存取方法。现在可以使用performSelector方法来执行存取方法

方法决议

+ (BOOL)resolveInstanceMethod:(SEL)sel;

当没有找到方法的实现时,会执行该方法,可以在该方法里面动态添加缺失的方法的实现。

来看看两个类的声明:

/*--------- HCBase -----------*/
#import "runtime.h"
#import <Foundation/Foundation.h>

@interface HCBase : NSObject

//@property (nonatomic, strong) NSString *curry;
//@property (nonatomic, copy) NSString *kd;
//@property (nonatomic, assign) int kt;

- (void)needResolve;

- (void)needForwardTarget;

- (void)needFinalForward;

@end

/*----------- HCSon -----------*/
#import "HCBase.h"

@interface HCSon : HCBase

@end

HCSon 继承自 HCBase 类,另外HCBase中声明了一个方法needResolve,但是没有提供实现。

那么当一个HCSon对象调用needResolve方法时,无法找到实现;接着会去寻找resolveInstanceMethod:,并执行该方法。但是该resolveInstanceMethod方法定义在父类HCBase里面

void resolveImp(id self, SEL _cmd) {
    NSLog(@"vanney code log : resolved function");
}

@implementation HCBase
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"needResolve"]) {
        class_addMethod(self, @selector(needResolve), (IMP) resolveImp, "v@:");
        return YES;
    } else {
        return [[self superclass] resolveInstanceMethod:sel];
    }
}
@end

该方法给self类添加一个needResolve方法,之后会重新执行needResolve方法。

现在来看看一个HCSon对象调用needResolve的过程

// resolve

// print method
int methodCount;
Method *methodList = class_copyMethodList([HCBase class], &methodCount);
for (int j = 0; j < methodCount; ++j) {
Method curMethod = methodList[j];
printf("before HCBase method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

int methodCountS;
Method *methodListS = class_copyMethodList([HCSon class], &methodCountS);
for (int j = 0; j < methodCountS; ++j) {
Method curMethod = methodListS[j];
printf("before HCSon method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

HCSon *son = [[HCSon alloc] init];
//[baseOBJ needResolve];
[son needResolve];

Method *newMethodList = class_copyMethodList([HCBase class], &methodCount);
for (int j = 0; j < methodCount; ++j) {
Method curMethod = newMethodList[j];
printf("new HCBase method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

Method *newMethodListS = class_copyMethodList([HCSon class], &methodCountS);
for (int j = 0; j < methodCountS; ++j) {
Method curMethod = newMethodListS[j];
printf("new HCSon method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

在执行[son needResolve]之前,先打印父类和子类的方法,可以发现都没有needResolve方法;执行之后再次打印,发现HCSon里面增加了needResolve方法。

因为执行resolveInstanceMethod方法时,传入的selfHCSon,所以方法就添加在HCSon里面

resolve

注意图片的最后一行

消息转发

备援对象

- (id)forwardingTargetForSelector:(SEL)selector;

HCBase声明了needForwardTarget方法,但是没有实现;但是HCBeitai类有该方法。可以使用forwardingTargetForSelector方法,返回一个HCBeitai对象,让该对象来执行needForwardTarget方法。

/*------------ HCBeiTai -----------*/
#import <Foundation/Foundation.h>

@interface HCBeiTai : NSObject

- (void)needForwardTarget;

- (void)beitaiForwarding:(int)index;

@end

@implementation HCBeiTai

- (void)needForwardTarget {
    NSLog(@"bei tai 作用了");
}

- (void)beitaiForwarding:(int)index {
    NSLog(@"vanney code log : bei tai 终极forwarding起作用了 : index is %d", index);
}

@end

/*-------------- HCBase ------------*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(needForwardTarget)) {
        NSLog(@"vanney code log : forwarding Target start");
        HCBeiTai *beitai = [[HCBeiTai alloc] init];
        return beitai;
    } else {
        return [super forwardingTargetForSelector:aSelector];
    }
}

这么一来,就好像HCBase继承了HCBeitai一样,可以用来模拟多重继承。来看看运行的结果:

// forwardingTarget
[baseOBJ needForwardTarget];

forwarding1

完整的消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)invocation;

当所有的手段都无法处理该方法时,就开启了完整的消息转发。先调用methodSignatureForSelector方法,给该未知方法签名— 类似@@: 。当签名不为空时,调用forwardInvocation,将位置方法的所有信息存放在NSInvocation对象中;这时可以更改方法的信息,比如selector,signature等等,然后交由其他对象来执行更改之后的方法。

来看看一个示例:HCBase声明了needFinalForward方法,但是没有提供实现;HCBeitai实现了beitaiForwarding:方法。消息转发,将给HCBase对象的needFinalForward方法 转发给了 HCBeitai对象的beitaiForwarding:方法。注意:这两个方法的selector和signature都不一样

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"vanney code log : selector is %@", NSStringFromSelector(aSelector));
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        NSLog(@"vanney code log : inside create method signature");
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:i"];
    }

    return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"vanney code log : forwarding invocation selector is %@", NSStringFromSelector(anInvocation.selector));
    NSLog(@"vanney code log : invocation is %@, and method signature is %@", anInvocation, anInvocation.methodSignature);

    if (anInvocation.selector == @selector(needFinalForward)) {
        NSLog(@"vanney code log : inside forwarding");
        anInvocation.selector = NSSelectorFromString(@"beitaiForwarding:");
        int *a;
        *a = 99;
        [anInvocation setArgument:a atIndex:2];
        HCBeiTai *beiTai = [[HCBeiTai alloc] init];
        [anInvocation invokeWithTarget:beiTai];
    } else {
        NSLog(@"vanney code log : else forwarding");
        [super forwardInvocation:anInvocation];
    }

}

转发的同时,还可以设置参数。把99传递给新的方法。来看看运行结果:

// final forwarding
[baseOBJ needFinalForward];

forwarding2

runtime应用

Method Swizzling

可以在类的load方法中,通过class_addMethod class_replaceMethod来改变方法的默认实现方式。因为load方法执行时,所有类的方法都有了,但都还没有执行过一次

JSON -> Model

- (instancetype)initWithDict:(NSDictionary *)dict {

    if (self = [self init]) {
        //(1)获取类的属性及属性对应的类型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */
        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通过property_getName函数获得属性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即释放properties指向的内存
        free(properties);

        //(2)根据类型给属性赋值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;

}

该方法参考自[iOS] runtime 的使用场景–实战篇

KVO, KVC

海带学习

JSPatch

JSPatch充分的应用了runtime的性质,详细内容可以参考我的这篇文章JSPatch源码分析

参考

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器