使用Runtime的API

runtime 的运行时机制

runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的。Objective-C语言是一门动态语言。

runtime 的作用

  • 获取某个类的所有成员变量、属性、方法、协议
  • 添加一个成员变量、属性、方法
  • 过滤
  • 重定向
  • 交换方法
  • 转发
  • 字典转模型(重难点)

demo演示

1.runtime获取变量、属性、方法、协议

- (void)testRunTimeGetValue
{
    unsigned int count;
    
    //获取成员变量列表
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        Ivar myivar = ivarList[i];
        const char *ivarname = ivar_getName(myivar);
        const char *ivarType = ivar_getTypeEncoding(myivar); // 获取变量编码类型
        
        NSString *ivarNameStr = [NSString stringWithUTF8String:ivarname];
        NSLog(@"ivar----="">%@  %@", ivarNameStr, [NSString stringWithUTF8String:ivarType]);
        if ([ivarNameStr isEqualToString:@"_property2"]) {
            [self setValue:@"这是属性值" forKey:@"property2"];
            NSLog(@"self.property2:%@", self.property2);
        }
    }
    
    //获取属性列表
    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=0; i<count; i++) {
        Method method = methodList[i];
        NSLog(@"method----=>%@", NSStringFromSelector(method_getName(method)));
    }
    
    //获取协议列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        Protocol *myprotocal = protocolList[i];
        const char *protocolname = protocol_getName(myprotocal);         NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolname]);
    }
}

2.runtime添加属性,还可以添加任何对象

- (void)testRunTimeAddValue
{
    //添加属性
    objc_setAssociatedObject(self, &associatedObjectKey, @"addProperty", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    //添加对象
    UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 300, 60)];
    label.numberOfLines = 2;
    label.textColor = [UIColor blueColor];
    label.backgroundColor = [UIColor redColor];
    objc_setAssociatedObject(self, &associatedObjectKey2,label, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    [self getValueOfRunTime];
}

- (void)getValueOfRunTime
{
    //获取关联对象string与label

    NSString *string = objc_getAssociatedObject(self, &associatedObjectKey);
    
    UILabel *label = objc_getAssociatedObject(self, &associatedObjectKey2);
    label.text = [NSString stringWithFormat:@"动态添加的属性名称为:%@\\\\n此label也是动态添加的对象", string];
    [self.view addSubview:label];
}

3. runtime过滤

调用一个不存在的实例方法的时候,会调用resolveInstanceMethod:方法;调用一个不存在的类方法的时候,会调用resolveClassMethod:方法;
如果resolveInstanceMethod:返回NO,则会再掉用forwardingTargetForSelector方法实现转发。如果返回Yes,反之不掉用。从而实现过滤的作用。

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
#if 1
    return NO;
#else
    if (sel == @selector(testCategory)) {
        class_addMethod([self class], sel, (IMP)defualtFunc, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
#endif
}

4. runtime重定向

将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要返回一个有这个方法的target。

- (id)forwardingTargetForSelector:(SEL)sel
{
    if(sel == @selector(testCategory)) {
        return appDelegate;
    }
    return [super forwardingTargetForSelector:sel];
}

5. runtime交换方法

交换实例方法使用class_getInstanceMethod。交换类方法使用class_getClassMethod。

- (void)testRunTimeChangeFunction
{
    // 获取 testFunction1方法
    Method testFunction1 = class_getInstanceMethod([self class], @selector(testFunction1));
    
    // 获取 testFunction2方法
    Method testFunction2 = class_getInstanceMethod([self class], @selector(testFunction2));
    
    // 交换方法地址, 相当于交换实现
    method_exchangeImplementations(testFunction1, testFunction2);
    
    [self testFunction1];
    //实际上会掉用testFunction2方法
}

- (void)testFunction1
{
    NSLog(@"runtime交换方法-testFunction1");
}

- (void)testFunction2
{
    NSLog(@"runtime交换方法-testFunction2");
}

6. runtime转发

是将调用不存在的方法打包成了NSInvocation传递来。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([appDelegate respondsToSelector:
         [anInvocation selector]]) {
        [anInvocation invokeWithTarget:appDelegate];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

7. runtime字典转模型(第三方MJExtension库也是按照如下方法实现的字典转模型)

  • 用法:通过方法modelWithDictionary:传入了一个字典,返回一个对应的model,用法简单。
- (void)testRunTimeDictionaryToModel
{
    //获取字典数据
    NSDictionary *dictionary = [self getMainBundleResource:@"jsonData.json"];
    
    LXResultModel *model = [LXResultModel modelWithDictionary:dictionary];
    NSLog(@"model.foods[0].food: %@", model.foods[0].food);
}
  • 必要条件:根据字典结构(在jsonData.json文件中)创建对应的model层级结构(在RuntimeDicToModel文件夹中),并将字典的key声明为model的属性名称。字典结构如下,因此对应的model可以这样取到值,正如上面的NSLog输出为:model.foods[0].food: rice。
{
    "address": {
        "city": "遵义市",
        "likePlaces": {
            "place": "HongKong"
        },
        "province": "贵州省"
    },
    "age": 25,
    "foods" :[
    {
        "food": "rice",
        "fruit": "apple",
    },
    {
        "food": "noodle",
        "fruit": "watermelon",
    }
    ],
    "name": "lixu"
}
  • 核心代码:使用到runtime的逻辑处理,可以将字典里的arr子元素数据转化为model,也可以将字典里包含的字典转化为model(用递归算法)。
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary
{
    id objc = [[self alloc] init];
    unsigned int count;

    // 获取类中的所有成员属性
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员属性
        Ivar ivar = ivarList[i];
        
        // 获取成员属性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 从第一个角标开始截取,因为属性变量的第一个字符为“_”,
        if (![[name substringToIndex:1] isEqualToString:@"_"]) {
            continue;
        }
        NSString *key = [name substringFromIndex:1];
        // 根据成员属性名去字典中查找对应的value
        id value = dictionary[key];
        
        // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
        // 判断下value是否是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            // 获取成员属性类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            // 根据字符串类名生成类对象
            Class modelClass = NSClassFromString([self cutClassString:type]);
            
            if (modelClass) { // 有对应的模型才需要转
                // 把字典转模型,采用递归
                value = [modelClass modelWithDictionary:value];
            }
        }
        
        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 获取数组中字典对应的模型
                Class classModel = [self arrayContainModelClass][key];
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                    id model =  [classModel modelWithDictionary:dict];
                    [arrM addObject:model];
                }

                // 把模型数组赋值给value
                value = arrM;
            }
        }
        if (value) { // 有值,才需要给模型的属性赋值
            // 利用KVC给模型中的属性赋值
            [objc setValue:value forKey:key];
        }
    }
    
    return objc;
}

讨论

为了瘦身ViewController或者AppDelegate代码,常常用到继承、封装、分类等方案,这里为了结合runtime只讨论分类。

现在的需求是将AppDelegate里的代码部分隔离到AppDelegate+RunTime中,其中需要引用AppDelegate的一个属性testStr。

好,重点来了,下面我采用了三种方案可以在AppDelegate+RunTime中引用属性testStr。分别用testStr1、testStr2 、testStr3表示。

方案一:在本类AppDelegate的.h声明属性,即可在分类中导入本类头文件,引用其属性了

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (nonatomic, copy)   NSString *testStr1;

- (void)testRedirect;

@end

将testStr2与testStr3都声明在了分类中

@interface AppDelegate (RunTime)

@property (nonatomic, copy)   NSString *testStr2;
@property (nonatomic, copy)   NSString *testStr3;

- (void)testCategory;

@end

方案二:在AppDelegate+RunTime.m中重写testStr2 的set和get方法,使用runtime实现给分类添加属性。

-(NSString *)testStr2
{
    return objc_getAssociatedObject(self, _cmd);//_cmd当前方法的一个SEL指针,与@selector(str)对应,这样可以避免定义一个静态全局变量
}

-(void)setTestStr2:(NSString *)testStr2
{
    objc_setAssociatedObject(self, @selector(testStr2), testStr2, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

方案三:在AppDelegate+RunTime.m中重写testStr3 的set和get方法,使用全局静态变量switchStr去引用testStr3的值实现给分类添加属性。

static NSString *switchStr;

-(NSString *)testStr3
{
    return switchStr;
}

-(void)setTestStr3:(NSString *)testStr3
{
    switchStr = [testStr3 copy];
}

通过调用testCategory方法可以看到三种方案设置的属性都可以正常获取值,那么平时你使用的是哪种方案呢?为什么呢?



源码请点击github地址下载。


QQ:2239344645 我的github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容