Runtime—实战篇

目录:

  • Runtime简介
  • runtime实战应用
    • 代码一:OC代码对象调用代码 -> 消息发送机制代码 的转换
    • 代码二:验证OC底层实现
    • 代码三:Runtime项目中的实用
    • 代码四:归档解档
    • 代码五:用runtime解耦取消依赖
    • 代码六:利用 runtime 一键改变字体
    • 代码七:用runtime-关联对象,使用Category添加属性
    • 代码八:利用Runtime减少应用崩溃-例如数组越界
    • 代码九:iOS-UIButton同时点击&&重复点击规避
    • 代码十:iOS利用Runtime自定义控制器POP手势动画
    • 代码十一:利用runtime对一些常用并且容易导致崩溃的方法进行处理

零、demo地址

https://github.com/lionsom/LXRunTimeAll

一、Runtime简介

  • OC运用的是消息发送机制
  • OC代码底层变成了C语言运行时代码
  • OC编译的时候没有加载进内存

Runtime:就是苹果提供的一个API
1、利用Runtime,在程序运行过程中,动态的创建一个类
2、利用Runtime,在程序运行过程中,动态的修改一个类的属性、方法
3、遍历一个类所有的成员变量

头文件:
<obj/message.h> 包含了runtime的API
<obj/runtime.h>

两个概念
1、Method:成员方法
2、Ivar:成员变量

二、runtime实战应用

代码一:OC代码对象调用代码 -> 消息发送机制代码 的转换

第一步:先将项目对消息发送机制的检测给关了

图一:Projects -> Target -> Build Setting -> search msg -> NO

第二步:导入“消息发送机制”头文件

#import <objc/message.h>

第三步:代码
由一个简单的对象调用函数,逐步进行“消息发送机制”代码的替换

- (void)viewDidLoad {
    [super viewDidLoad];

//方式一:OC标准写法
//    Person * p = [[Person alloc]init];
    
//方式二:进行拆分
//    Person * p =[Person alloc];
//    p = [p init];

//方式三:拆分后,逐步采用消息发送机制进行替换   此步骤之后可完全不用导入头文件也可以进行Person的调用
    //NSClassFromString(@"Person") == [Person class] == objc_getRequiredClass("Person")
    NSObject * p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));
    objc_msgSend(p, @selector(init));

//方式四: 最后组合
//    NSObject * p = objc_msgSend(objc_msgSend(NSClassFromString(@"Person"), @selector(alloc)),@selector(init));

//方式一:标准的OC对象调用函数
//    [p eat];

//方式二:
//    [p performSelector:@selector(eat)];    

//方式三:调用消息发送机制
    //消息发送机制
    objc_msgSend(p, @selector(eat));
}

代码二:验证OC底层实现

第一步:创建命令行项目


创建

第二步:创建一个Person类


Person类

第三步:打开main.m函数,将Person添加到里面
main.m

第四步:进入终端,进入该目录下,将mian.m转成C语言文件
clang -rewrite-objc的作用是把oc代码转写成c/c++代码

clang -rewrite-objc main.m

执行之后,目录下多出一个main.cpp文件
第五步:打开main.cpp到最后,进行OC代码与C语言代码的比较


比较

代码三:Runtime项目中的实用

案例:因为NSURL出现汉字后,url为空,但是又不会报错

//目的:给系统NSURL这个类的URLWithString 方法添加一个功能,创建URL又能判断是否为空
    NSURL * url = [NSURL URLWithString:@"www.baidu.com/你好"];
    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    NSLog(@"AAA == %@",url);
  • 解决方案一:使用Category换一个
+(instancetype)LX_URLWithString:(NSString *)URLString {
    NSURL * url = [NSURL LX_URLWithString:URLString];
    if (url == nil) {
        NSLog(@"url为空");
    }
    return url;
}

然后调用新的方法,新方法内部加上判断

    NSURL * url = [NSURL LX_URLWithString:@"www.baidu.com/你好"];
    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    NSLog(@"AAA == %@",url);
弊端:项目较大的时候,要讲全局都替换,很麻烦,并且代码不好维护,其他人也不知道这个函数。
  • 解决方案二:利用runtime动态进行函数实现交换

第一步:为什么使用runtime??
优点:不需要进行太大的修改,而且继续使用原生方法名,只不过方法实现改变了!
弊端:要进行及时的注释,不然很容易忘记,出错
第二步:原理

runtime:可以交换方法的实现!!

原理图

第三步:代码实现
在Category刚刚加载的时候就需要对函数方法进行交换
1、导入头文件#import <objc/runtime.h>
2、在load函数中进行函数实现的交换
3、拿到这两个方法

  • class_getClassMethod 获取类方法
  • class_getInstanceMethod 获取对象方法

4、交换方法

#import "NSURL+url.h"
#import <objc/runtime.h>

@implementation NSURL (url)

+(void)load {
    NSLog(@"%s",__func__);

    //交换我们的URLWithString和LX_URLWithString方法
    //第一步:拿到这两个方法
        //class_getClassMethod     获取类方法
        //class_getInstanceMethod  获取对象方法
    Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
    Method LXURLWithStr = class_getClassMethod(self, @selector(LX_URLWithString:));
    //
第二步:交换方法
    method_exchangeImplementations(URLWithStr, LXURLWithStr);
}

//重新创建一个
+(instancetype)LX_URLWithString:(NSString *)URLString {
    NSURL * url = [NSURL LX_URLWithString:URLString];
    if (url == nil) {
        NSLog(@"url为空");
    }
    return url;
}
@end

代码四:归档解档

其他的参考文档:http://www.jianshu.com/p/fed1dcb1ac9f

问题引入:在iOS中一个自定义对象是无法直接存入到文件中的,必须先转化成二进制流才行。从对象到二进制数据的过程我们一般称为对象的序列化(Serialization),也称为归档(Archive)。同理,从二进制数据到对象的过程一般称为反序列化或者反归档。在序列化实现中不可避免的需要实现NSCoding以及NSCopying(非必须)协议的以下方法:

- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;
- (id)copyWithZone:(NSZone *)zone;

假设我们现在需要对直接继承自NSObject的Person类进行序列化,代码一般长这样子://对变量编码

- (void)encodeWithCoder:(NSCoder *)coder
{
    [coder encodeObject:self.name forKey:@"name"];
    [coder encodeint:self.age forKey:@"age"];
    [coder encodeObject:_father forKey:@"_father"];
    //... ... other instance variables
}
//对变量解码
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super init];
    if (self) {
       self.name = [coder decodeObjectForKey:@"name"];
       self.age = [coder decodeintForKey:@"age"];
       _father = [coder decodeObjectForKey:@"_father"];
       //... ... other instance variables
    }
    return self;
}
弊端:
  • 工程代码中冗余代码很多
  • 父类层级复杂容易导致遗漏点一些父类中的属性变量

解决方案:Runtime
第一步:使用Runtime获取变量以及属性
runtime中获取某类的所有变量(属性变量以及实例变量)API:

Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

获取某类的所有属性变量API:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

Ivar是runtime对于变量的定义,本质是一个结构体:

struct objc_ivar {
    char *ivar_name;                                   
    char *ivar_type;                                    
    int ivar_offset;
#ifdef __LP64__
    int space;
#endif
} 
typedef struct objc_ivar *Ivar;
  • ivar_name:变量名,对于一个给定的Ivar,可以通过*const char ivar_getName(Ivar v)函数获得 char * 类型的变量名;
  • ivar_type: 变量类型,在runtime中变量类型用字符串表示,例如用@表示id类型,用i表示int类型...。这不在本文讨论之列。类似地,可以通过*const char ivar_getTypeEncoding(Ivar v)函数获得变量类型;
  • ivar_offset: 基地址偏移字节数,可以不用理会

获取所有变量的代码一般长这样子:

  unsigned int numIvars; //成员变量个数
  Ivar *vars = class_copyIvarList(NSClassFromString(@"UIView"), &numIvars);
  NSString *key=nil;
  for(int i = 0; i < numIvars; i++) {
      Ivar thisIvar = vars[i];
      key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];  //获取成员变量的名字
      NSLog(@"variable name :%@", key);
      key = [NSString stringWithUTF8String:ivar_getTypeEncoding(thisIvar)]; //获取成员变量的数据类型
      NSLog(@"variable type :%@", key);
  }
  free(vars);//记得释放掉

objc_property_t是runtime对于属性变量的定义,本质上也是一个结构体(事实上OC是对C的封装,大多数类型的本质都是C结构体)。在runtime.h头文件中只有*typedef struct objc_property objc_property_t,并没有更详细的结构体介绍。虽然runtime的源码是开源的,但这里并不打算深入介绍,这并不影响我们今天的主题。与Ivar的应用同理,获取类的属性变量的代码一般长这样子:

  unsigned int outCount, I;   
  objc_property_t *properties = class_copyPropertyList([self class], &outCount);   
  for (i = 0; i < outCount; i++) {   
      objc_property_t property = properties[i];   
      NSString *propertyName = [[[NSString alloc] initWithCString:property_getName(property)] ;   
      NSLog(@"property name:%@", propertyName); 
  }   
  free(properties);

最后代码:

#import "Person.h"
//第一步:导入runtime头文件
#import <objc/runtime.h>

@implementation Person

//解档
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super init];
    if (self) {
    /*
        unsigned int count1 = 0;
        objc_property_t * objArr = class_copyPropertyList([Person class], &count1);
        for (int j = 0; j<count1; j++) {
            const char * name1 = property_getName(objArr[j]);
            NSString * key1 = [NSString stringWithUTF8String:name1];
            NSLog(@"AAAAAA == %@",key1);
        }
     */

        //如果属性过多,这样写就比较麻烦
        unsigned int count = 0;
        Ivar * ivars = class_copyIvarList([Person class], &count);
        //for 搞定
        for (int i = 0; i < count; i++) {
            //拿个每一个ivar
            Ivar ivar = ivars[I];
            //ivar对应的名称
            const char * name = ivar_getName(ivar);
            //转成 OC
            NSString * key = [NSString stringWithUTF8String:name];
            //解档
            id value = [coder decodeObjectForKey:key];
            //设置到属性 -- KVC
            [self setValue:value forKey:key];
            NSLog(@"BBBBB == %@",key);
        }
        //这个变量不归OC管理,ARC处理不了,所以需要手动释放
        free(ivars);
    }
    return self;
}

//告诉系统需要归档的属性
- (void)encodeWithCoder:(NSCoder *)coder
{
    //如果属性过多,这样写就比较麻烦
    unsigned int count = 0;
    Ivar * ivars = class_copyIvarList([Person class], &count);
    //for 搞定
    for (int i = 0; i < count; i++) {
        //拿个每一个ivar
        Ivar ivar = ivars[I];
        //ivar对应的名称
        const char * name = ivar_getName(ivar);
        //转成 OC
        NSString * key = [NSString stringWithUTF8String:name];
        //获取属性值 -- KVC
        id value = [self valueForKey:key];
        //归档
        [coder encodeObject:value forKey:key];
    }
    //这个变量不归OC管理,ARC处理不了,所以需要手动释放
    free(ivars);
}
@end



更新20180104

代码五:用runtime解耦取消依赖

Bang - iOS 组件化方案探索

+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
   Class cls = NSClassFromString(@"BookDetailComponent");
   return [cls performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"bookId":bookId}];
}



更新20180123

代码六:利用 runtime 一键改变字体

iOS中利用 runtime 一键改变字体

思路 :写一个UILabel+FontChange的扩展,在load函数中进行系统函数自定义函数的交换,从而达到目的。
如果只是针对个别UILabel做特殊处理的话,可以使用Label的Tag来进行区分!!!

+ (void)load {
    //方法交换应该被保证,在程序中只会执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //获得viewController的生命周期方法的selector
        SEL systemSel = @selector(willMoveToSuperview:);
        //自己实现的将要被交换的方法的selector
        SEL swizzSel = @selector(myWillMoveToSuperview:);
        //两个方法的Method
        Method systemMethod = class_getInstanceMethod([self class], systemSel);
        Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
        
        //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
        BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
        if (isAdd) {
            //如果成功,说明类中不存在这个方法的实现
            //将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
        } else {
            //否则,交换两个方法的实现
            method_exchangeImplementations(systemMethod, swizzMethod);
        }
    });
}

- (void)myWillMoveToSuperview:(UIView *)newSuperview {
    
    [self myWillMoveToSuperview:newSuperview];
    if ([self isKindOfClass:NSClassFromString(@"UIButtonLabel")]) {
        return;
    }
    if (self) {
        if (self.tag == 10086) {
            self.font = [UIFont systemFontOfSize:self.font.pointSize];
        } else {
            if ([UIFont fontNamesForFamilyName:CustomFontName])
                self.font  = [UIFont fontWithName:CustomFontName size:self.font.pointSize];
        }
    }
}



更新20180126

代码七:用runtime-关联对象,使用Category添加属性

了解OC的都应该知道,在一般情况下,我们是不能向Category中添加属性的,只能添加方法,但有些情况向,我们确实需要向Category中添加属性,而且很多系统的API也有一些在Category添加属性的情况,例如我们属性的UITableViewsectionrow属性,就是定义在一个名为NSIndexPath的分类里的,如下

NSIndexPath

那这到底是怎么实现的呢?




更新20180201

代码八:利用Runtime减少应用崩溃-例如数组越界

我们首先把__NSArrayIobjectAtIndex方法换成我们的ls_objectAtIndex,然后方法里面判断但是否越界,是的话直接返回nil

[NSClassFromString(@"__NSArrayI") swapMethod:@selector(objectAtIndex:) currentMethod:@selector(ls_objectAtIndex:)];

- (id)ls_objectAtIndex:(NSUInteger)index
{
    if (index >= [self count])
    {
        return nil;
    }
    return [self ls_objectAtIndex:index];
}

然后当我们想下面这样写的时候就不会崩溃了:

NSArray *array = @[@"aa",@"ddd"];
array[5];



更新20180211

代码九:iOS-UIButton同时点击&&重复点击规避




更新20180212

代码十:iOS利用Runtime自定义控制器POP手势动画




更新20180312

代码十一:利用runtime对一些常用并且容易导致崩溃的方法进行处理

完!!别忘记点波关注和喜欢

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

推荐阅读更多精彩内容

  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,697评论 7 64
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,129评论 0 9
  • 昨天鼓起勇气又去听梁老师的课程——扪心自问。去之前给自己立规矩,一定要少说多听。似乎整个听课过程比较和谐了,我也听...
    冠世墨玉yanzi阅读 276评论 0 1
  • 学习太极,年轻是个好的开始,今天学习也是比明天开始来得早些! 本周六上午基础太极拳课程正常进行。 时间:9:30-...
    起合太极孙康阅读 248评论 0 0