iOS首页投稿(暂停使用,暂停投稿)程序员

Runtime—实战篇

2017-08-28  本文已影响67人  lionsom_lin

目录:

零、demo地址

https://github.com/lionsom/LXRunTimeAll

一、Runtime简介

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);
+(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:可以交换方法的实现!!

原理图

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

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;

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

  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对一些常用并且容易导致崩溃的方法进行处理

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

上一篇 下一篇

猜你喜欢

热点阅读