美文网首页runtime相关iOS开发干货
Runtime攻略秘籍(初级篇)

Runtime攻略秘籍(初级篇)

作者: 徐不同 | 来源:发表于2016-12-06 18:10 被阅读801次

    大家好久不见,今天我带来的是Runtime的一些讲解,希望大家喜欢。

    注:Demo在最下方!感谢

    Runtime(运行时机制,是iOS开发人员迈向高阶的必学课程),今天我就在这篇文章中,简单介绍一些Runtime的使用方法,希望可以帮助大家更快速度的掌握Runtime。

    一、简介

    Runtime呢,简称也就是运行时(翻译过来也是运行时),是一套底层的C语言的API,在iOS开发中的核心组成部分。

    说到Runtime是iOS开发的核心组成部分是有原因滴。

    大家想想,Objective-C语言,本身就是一种动态语言,它把很多静态语言在编译时候干的事,都推迟到运行的时候才干。简单举个例子来说,在开发的时候,我们在.h中声明一个方法,我们并不在.m文件中实现这个方法,在编译阶段,这是没有问题的。但是如果程序运行起来,发现没有实现该方法,则会报异常,崩溃。这就是所谓的运行时。相比较C语言,因为C语言是一个静态语言,在编译的时候,如果某个方法只声明而不实现,往往在编译的时候直接报错了。

    简单说呢,就是OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

    这种动态语言的优势,就是写代码更加的灵活,比如我们可以把消息重新定向到别的对象,我们还可以动态添加一个方法,一个属性,替换方法等等。

    接下来我就说一些Runtime的作用以及使用方法。

    二、作用

    • 1、消息转发

    在OC语言的开发中,我们常常调用最多的便是方法。那么方法是怎么调用的呢?其实,方法的调用就是让对象发送消息。

    怎么理解方法的调用就是让对象发送消息呢,这里我们就应该想到了OC的运行时机制。
    比如我们调用一个 [p eat],在编译的阶段,编译器并不知道eat需要执行哪段代码,这个时候[p eat]--->会转换成objc_msgSend(p,"eat"),这个字面意思,就是p这个对象,发一个eat的消息,以Selector的形式。
    在运行阶段,执行到objc_msgSend这个函数的时候,函数内部会到p的对应的内存地址,寻找eat方法的参数,并执行。如果找不到,就报异常咯。

    使用攻略:
    在工程中,我们导入#import <objc/message.h>,然后在工程中输入objc_msgSend,如下图


    图片.png

    默认的情况是不会出现后方参数提示的,比如id self,SEL op。因为苹果并不喜欢我们使用它的底层,我们首先要做的是,开启提示。如下图:

    图片.png

    我们选择NO就会有提示了。

    将对象方法的”消息机制“

    //    执行run方法
    //    [p run];
    //    [p performSelector:@selector(run)]; 
    //    objc_msgSend(p, @selector(run));
    

    类方法的”消息机制“

    //    执行run方法
    //    [Person run];
    //    [[Person class] performSelector:@selector(run)]; 
    //    objc_msgSend([Person class], @selector(run));
    

    总结,类名调用类方法,本质是类名转换成类对象,去发送消息。

    • 2、交换方法
      利用底层的一些API,我们可以实现一些方法的交换,一般呢,我们都是跟系统的方法做交换的。比如交换数组的objectAtIndex:方法,来时实现即使数组越界,程序也不崩溃。比如交换UIImage的imageWithName方法,可以在图片为空的时候,打印出不存在的图片名称,等等。
      下面我就实现下替换系统的UIImage的imageNamed的方法。
      首先我们新建一个工程,创建一个UIImage的分类,在分类中我们扩冲如下代码:
    + (UIImage *)xz_imageName:(NSString *)imageName
    {
        // 1.加载图片
        UIImage *image = [UIImage xz_imageName:imageName];
        
        // 2.判断功能
        if (image == nil) {
            NSLog(@"%@图片为空",imageName);
        }
        
        return image;
    }
    
    

    之后我们在分类加载的时候,替换系统的方法。代码如下:

    
    + (void)load
    {
        NSLog(@"%s",__func__);
        
        // 1.表示获取方法的实现
        //class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>)
        // 2.获取对象方法
        //  class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
        // 3.获取类方法
        //  class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
    
        /**
         class:获取哪个类的方法
         sel:获取方法编号,根据sel去找相应的类方法
         */
        Method imageNameMethod =  class_getClassMethod([UIImage class], @selector(imageNamed:));
        
        Method xz_imageNameMethod = class_getClassMethod([UIImage class], @selector(xz_imageName:));
        
        // 交换方法实现
        method_exchangeImplementations(imageNameMethod, xz_imageNameMethod);
    }
    

    经过上面方法的实现,我们在调用imageNamed的时候,便会替换成我们的方法了。

    • 3、动态添加方法
      动态添加方法是一个很有意义的事情,因为程序在编译的时候,会把所有的方法加到一个方法列表中,但是我们并不是所有的方法都会使用到,耗时耗力。我们应该多利用懒加载的方式,用到的方法,在添加,不用的方法,用到的时候在添加。
      我们先创建一个Person类,.h文件中声明一个eat的方法,.m中我们并不实现这个方法,往常一执行这个eat:的方法(随便传个参数),程序一定是报异常,这里我们利用runtime,在程序发现没有实现这个方法的时候拦截它,让他执行相应的操作。

    • 3.1、动态添加对象方法

    我们需要在Person的.m文件中,导入#import <objc/message.h>,然后写下下面2个方法:

    // 当某个对象方法没有只声明,没有实现的时候,会执行下面的方法。
    + (BOOL)resolveInstanceMethod:(SEL)sel ;
    

    我们先创建一个函数:

    // 我们还需要定义一个函数
    void eat(id self, SEL _cmd,id param1){  
        NSLog(@"调用%@---%@---%@",self,NSStringFromSelector(_cmd),param1);
    }
    

    讲解一下,每一个函数,都有2个默认的隐式参数,一个是谁调用了自己,一个是SEL,SEL就是对方法的一种包装。包装的SEL类型数据它对应相应的方法地址,找到方法地址就可以调用方法。后面的id类型的param1是我写的一个参数,因为是C语言的函数,我们无法创建NS之类的类型,这里我就用id类型来接参数。

    接下来,根据官方文档我们可以添加下面的代码做判断,使得在找不到eat方法的时候,可以执行我们动态添加的eat方法,注意,上面的函数名可以随意写,只需要在下面添加方法的时候做好关联就好:

    // 外界调用一个没有实现的对象方法-
    // resolveInstanceMethod中sel是没有实现的参数
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        
        NSLog(@"%@",NSStringFromSelector(sel));
    //    if (sel == @selector(eat)) {}这句话等同下面的判断
        
        if ([NSStringFromSelector(sel) isEqualToString:@"eat:"] ) {
            // 这里添加方法
            // 给哪个类
            //SEL:方法名
            //IMP:方法的实现(函数的入口-函数的指针-函数的名)
            //type :方法类型
            class_addMethod(self, sel, (IMP)eat, "v@:@");  
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    

    这里我着重说下OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)这个函数的参数意义。

    Class cls:就是你给哪个类添加的这个方法
    SEL name:就是方法名字是啥,默认进入方法的时候,肯定是方法上带的参数sel没有,所以我们这里传入的是sel。
    (IMP)eat:这里需要我们传入一个IMP,啥实IMP,IMP就是方法的实现(函数的入口-函数的指针-函数的名)大家这里意会下
     const char *types,这里我们可以好好说一说。我先说下意思,*v@:@*的意思就是,返回类型是void,参数是id,SEL,id。具体大家参考上面我写的函数以及函数说明。
    
    如果想查到这些代表啥意思,可以打开苹果文档,输入Type Encodings,选择我箭头所指。查询。
    

    !!!!注意,是选择我箭头所指啊

    图片.png
    • 3.2、动态添加类方法
    • 4、动态添加属性
      通过运行时添加属性,作用面还是比较广的。比如想给button绑定一个模型,大家肯定会继承button来操作,其实通过运行时添加属性,我们就可以实现给系统button添加一个模型的需求。
      这里我们不讲这个,我们讲讲分类中如果添加属性。
      默认我们在创建分类的时候,添加一个成员属性,大家往往会发现,直接调用这个类的点语法,我们获取不了属性,为什么呢?
      因为默认分类创建的属性,不会执行set和get方法。如果我们一定要获取到这个属性,我们应该怎么做呢?这里有2种方法,一种就是添加一个静态变量,重写它的set和get方法.
      另外一种就是通过运行时,添加这个属性。代码如下:
    // 声明一个char型的key
    static char nameKey;
    
    - (void)setName:(NSString *)name
    {
        // 属性跟对象有关联-就是添加属性
        
        // object:对象
        // key:属性名,根据key去获取到值
        // value:值
        // policy:策略
        objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    

    这里我们讲解一下OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)这个方法。
    这个方法的字面意思就是把一个值,通过一个key绑定到一个类中,最后设置一下保存的策略。代码的意思是,把name的值,通过nameKey绑定到当前类,保存的是nonatomic的copy类型。

    补充一下最后一个参数(策略):

    // assign类型
       OBJC_ASSOCIATION_ASSIGN = 0,       
    // 非原子性Retain-->相当于Strong
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    // 非原子性 copy-
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   
    // 原子性Retain                                          
        OBJC_ASSOCIATION_RETAIN = 01401,     
    // 原子性copy                                                                          
        OBJC_ASSOCIATION_COPY
    

    取得动态绑定属性的方法如下:

    - (NSString *)name
    {
        return objc_getAssociatedObject(self, &nameKey);
    }
    

    简单的解释就是通过self这个类的,nameKey这个key,我们就可以取到nameKey相对应的值了。
    通过上面的做法,我们就实现了分类中扩充尚需经的功能了。

    在这里我给大家留下一个思考,如果实现了下面的样式,在我点击按钮的时候,希望能够判断,每一个cell的每一个textField的值,是否填写,希望可以打印出,第几行的Cell中,姓名还是身份证的值没有填写。比如,第一行的cell中,身份证没有填写。

    大家有没有好的思路呢?后期我会把我的思路放到github,大家抽空可以想一想的哟~

    图片.png

    如果你喜欢我的文章,不要忘记关注我,谢谢大家了~
    另外如果你要转载,希望可以注明出处,我会写出更多更好的文章,来回馈大家~

    本期Runtime的所有Demo链接
    消息转发Demo地址
    交换方法Demo地址
    动态添加方法Demo地址
    动态添加属性Demo地址

    相关文章

      网友评论

      • qazws:xmg?
        徐不同:@qazws 淘宝买的视频。
      • aydwb:+ (UIImage *)xz_imageName:(NSString *)imageName
        {
        // 1.加载图片
        UIImage *image = [UIImage xz_imageName:imageName];

        这个有点看不懂啊,不会循环出不来么
        徐不同:@菩尘一物 可以试一下,我测试过了哟。
        lattr:@徐不同 我感觉也会一直递归下去,走不到return吧:joy:
        徐不同:不会滴~return出去了。。。
      • 资料库:每一个cell代表的是一个model,然后model里面有两个属性,写一个方法循环遍历,然后拿到model,运行时拿到模型的属性,判断一下有没有赋值,然后返回。我的大致思路就是这样。
        5150250fc088:@徐不同 你的数组放在哪个类里做记录?
        徐不同:@资料库 我这边是给textfield的,增加一个分类,增加个indexPath的属性,然后一个数组搞定。哈哈
      • 我唔知啊:讲得很详细,深入浅出,谢谢分享。
        徐不同:@我唔知啊 类方法是+号,声明的时候要注意,这点代码是没有问题的,方法名是classEat吗?注意是类方法。。 :scream:
        我唔知啊:@徐不同 问个问题,动态添加方法时,实例方法有效,类方法却报错,帮我看下是什么问题。代码在下面:
        + (BOOL)resolveClassMethod:(SEL)sel {

        NSLog(@"类方法:%@",NSStringFromSelector(sel));

        // 如果调用了类方法
        if ([NSStringFromSelector(sel) isEqualToString:@"classEat"]) {
        class_addMethod(self, sel, (IMP)eat, "v@:@");
        return YES;
        }
        return [super resolveClassMethod:sel];
        }
        徐不同: @我唔知啊 谢谢😊

      本文标题:Runtime攻略秘籍(初级篇)

      本文链接:https://www.haomeiwen.com/subject/desrmttx.html