Runtime—实战篇

目录:

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

零、demo地址

https://github.com/lionsom/LXRunTimeAll

  • iOS-Runtime-Headers:可以调取苹果的所有私有方法头文件

一、Runtime简介

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

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

头文件:
包含了runtime的API

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

二、runtime实战应用

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

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

Runtime—实战篇_第1张图片
图一:Projects -> Target -> Build Setting -> search msg -> NO

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

#import 

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

- (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底层实现

第一步:创建命令行项目


Runtime—实战篇_第2张图片
创建

第二步:创建一个Person类


Person类

第三步:打开main.m函数,将Person添加到里面
Runtime—实战篇_第3张图片
main.m

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

clang -rewrite-objc main.m

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


Runtime—实战篇_第4张图片
比较

代码三: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:可以交换方法的实现!!

Runtime—实战篇_第5张图片
原理图

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

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

4、交换方法

#import "NSURL+url.h"
#import 

@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 

@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



更新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添加属性

  • 注:结合《Category - Category简介和使用》

  • iOS Runtime之四:关联对象

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

Runtime—实战篇_第6张图片
NSIndexPath

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




更新20180201

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

  • Runtime的运用和减少应用崩溃
  • GitHub代码

我们首先把__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同时点击&&重复点击规避

  • iOS-TD-UIButton同时点击&&重复点击规避



更新20180212

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

  • 轻松学习之二——iOS利用Runtime自定义控制器POP手势动画

  • FDFullscreenPopGesture




更新20180312

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

  • AvoidCrash -- 远离常见的崩溃
  • LongCrash

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

你可能感兴趣的:(Runtime—实战篇)