iOS开发 -- Runtime 的几个小例子

作者: 啊左 | 来源:发表于2016-05-20 17:15 被阅读6279次

    一、什么是 Runtime(也就是所谓的“运行时”,因为是在运行时实现的。)

    • 1.Runtime 是一套底层的c语言API(包括很多强大实用的c语言类型,c语言函数); [runtime运行系统]
    • 2.实际上,平时我们编写的oc代码,底层都是基于 Runtime 实现的; [OC语言的动态性]

    运行时系统 (runtime system),对于C语言,使用“静态绑定”,函数的调用在编译的时候就会决定运行时调用哪个函数。对于OC的函数,属于“动态调用”过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用,甚至可以在运行时改变方法的调用。Runtime就是OC辛苦的幕后工作人员。(编译器会自动帮助我们编译成 Runtime 代码。
    动态特性:使得它在语言层面上支持程序的可扩展性。只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法。利用runtime机制让我们可以在程序运行时动态修改类的具体实现、包括类中的所有私有属性、方法。这也是本文runtime例子的出发点。
    我们所敲入的代码转化为运行时的runtime函数代码,最终在程序运行时转成了底层的runtime的c语言代码;
    举例
    当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:

    id objc_msgSend ( id self, SEL op, ... ); 
    

    也就是说,我们平时编写的oc代码,方法调用的本质,就是在编译阶段,编译器转化为向对象发送消息。


    【本次开发环境: Xcode:7.2 iOS Simulator:iphone6 By: 啊左
    本文Demo下载链接:runtime-Demo

    二、runtime的几种使用方法

    我们通过继承于NSObject的person类,来对runtime进行学习。
    本文共有6个关于runtime机制方法的小例子,分别是:

    • 1.获取person类的所有变量;
    • 2.获取person类的所有方法;
    • 3.改变person类的私有变量name的值;
    • 4.为person的category类增加一个新属性;
    • 5.为person类添加一个方法;
    • 6.交换person类的2个方法的功能;

    (个人习惯,喜欢为6个例子添加按钮各自的行为方法,并分别执行相应的行为,以此看清各个runtime函数的具体功能所带来的效果。)

    首先,创建新的项目,并在项目中新建一个普通的OC类:person类(继承于NSObject),为了避免后面与其他方法函数搞混,我们把完整的person类编写齐全,用于后面使用runtime的几种方法:
    person.h如下:

    #import <Foundation/Foundation.h>
    @interface person : NSObject
    @property (nonatomic,assign)int age;  //属性变量
    -(void)func1;
    -(void)func2;
    @end
    

    person.m如下:

    #import "person.h"
    @implementation person{ 
      NSString *name; //实例变量
    }
    //初始化person属性
    -(instancetype)init{ 
    self = [super init]; 
    if(self) { 
      name = @"Tom"; 
      self.age = 12; 
    } 
    return self;
    }
    //person的2个普通方法
    -(void)func1{ 
      NSLog(@"执行func1方法。");
    }-(void)func2{ 
      NSLog(@"执行func2方法。");
    }
    //输出person对象时的方法:
    -(NSString *)description{ 
    return [NSString stringWithFormat:@"name:%@ age:%d",name,self.age];
    }
    @end
    

    从person类的描述中,我们可以看到person类含有一个可供外类使用的共有属性age,以及一个外界不可以访问私有属性name,但是,有木有想过,其实在外类,name也是可以访问的。OC里面,通过runtime系统,苹果允许不受这些私有属性的限制,对私有属性私有方法等进行访问、添加、修改、甚至替换系统的方法。
    那么,为项目的故事板添加6个按钮;


    在使用runtime的地方,我们都需要包含头文件:
    #import <objc/runtime.h>  //(在需要使用runtime的实现文件.m中包含即可.)
    

    1.获取person类的所有变量

    将第一个按钮关联到ViewController.h,添加行为并命名其方法为:“getAllVariable”:

    - (IBAction)getAllVariable:(UIButton *)sender; //获取所有变量
    

    在ViewController.m中的实现如下:

    /*1.获取person所有的成员变量*/
    - (IBAction)getAllVariable:(UIButton *)sender {
    unsigned int count = 0; 
    //获取类的一个包含所有变量的列表,IVar是runtime声明的一个宏,是实例变量的意思. 
    Ivar *allVariables = class_copyIvarList([person class], &count); 
    for(int i = 0;i<count;i++) 
    { 
    //遍历每一个变量,包括名称和类型(此处没有星号"*") 
    Ivar ivar = allVariables[i]; 
    const char *Variablename = ivar_getName(ivar); //获取成员变量名称 
    const char *VariableType = ivar_getTypeEncoding(ivar); //获取成员变量类型 
    NSLog(@"(Name: %s) ----- (Type:%s)",Variablename,VariableType); 
    }
    }
    

    点击按钮后,得到的输出如下:(i表示类型为int)

    2016-05-18 17:17:10.502 runtime运行时[10164:452725] (Name: name) ----- (Type:@"NSString")
    2016-05-18 17:17:10.503 runtime运行时[10164:452725] (Name: _age) ----- (Type:i) 
    

    分析Ivar,一个指向objc_ivar结构体指针,包含了变量名、变量类型等信息。
    可以看到,私有属性name能够访问到了。 在有些项目中,为了对某些私有属性进行隐藏,某些.h文件中没有出现相应的显式创建,而是如上面的person类中,在.m中进行私有创建,但是我们可以通过runtime这个有效的方法,访问到所有包括这些隐藏的私有变量。
    拓展
    class_copyIvarList能够获取一个含有类中所有成员变量的列表,列表中包括属性变量和实例变量。需要注意的是,如果如本例中,age返回的是"_age",但是如果在person.m中加入:@synthesize age;
    那么控制台第二行返回的是"(Name: age) ----- (Type:i) ;"
    (因为@property是生成了"_age",而@synthesize是执行了"@synthesize age = _age;",关于OC属性变量与实例变量的区别、@property、@synthesize的作用等具体的知识,有兴趣的童鞋可以自行了解。)

    如果单单需要获取属性列表的话,可以使用函数:class_copyPropertyList();只是返回的属性变量仅仅是“age”,做为实例变量的name是不被获取的。
    class_copyIvarList()函数则能够返回实例变量和属性变量的所有成员变量。

    2.获取person类的所有方法

    将第二个按钮关联到ViewController.h,添加行为并命名其方法为:“getAllMethod”:

    - (IBAction)getAllMethod:(UIButton *)sender;  //获取所有方法
    

    在ViewController.m中的实现如下:

    /*2.获取person所有方法*/
    - (IBAction)getAllMethod:(UIButton *)sender {
    unsigned int count;
    //获取方法列表,所有在.m文件显式实现的方法都会被找到,包括setter+getter方法; 
    Method *allMethods = class_copyMethodList([person class], &count); 
    for(int i =0;i<count;i++)
    { 
    //Method,为runtime声明的一个宏,表示对一个方法的描述 
    Method md = allMethods[i]; 
    //获取SEL:SEL类型,即获取方法选择器@selector() 
    SEL sel = method_getName(md); 
    //得到sel的方法名:以字符串格式获取sel的name,也即@selector()中的方法名称 
    const char *methodname = sel_getName(sel); NSLog(@"(Method:%s)",methodname); 
    }
    }
    

    点击按钮后,控制台输出:

    2016-05-19 17:05:19.880 runtime运行时[14054:678124] (Method:func1)
    2016-05-19 17:05:19.881 runtime运行时[14054:678124] (Method:func2)
    2016-05-19 17:05:19.881 runtime运行时[14054:678124] (Method:setAge:)
    2016-05-19 17:05:19.881 runtime运行时[14054:678124] (Method:age)
    2016-05-19 17:05:19.881 runtime运行时[14054:678124] (Method:.cxx_destruct) 
    2016-05-19 17:05:19.882 runtime运行时[14054:678124] (Method:description)
    2016-05-19 17:05:19.882 runtime运行时[14054:678124] (Method:init)
    

    控制台输出了包括setget等方法名称。【备注:.cxx_destruct方法是关于系统自动内存释放工作的一个隐藏的函数,当ARC下,且本类拥有实例变量时,才会出现;】
    分析Method是一个指向objc_method结构体指针,表示对类中的某个方法的描述。在API中的定义:typedef struct objc_method Method;
    而objc_method结构体如下:

    truct objc_method { 
    SEL method_name OBJC2_UNAVAILABLE; 
    char *method_types OBJC2_UNAVAILABLE; 
    IMP method_imp OBJC2_UNAVAILABLE;
    } 
    
    • method_name :方法选择器@selector(),类型为SEL。 相同名字的方法下,即使在不同类中定义,它们的方法选择器也相同。
    • method_types:方法类型,是个char指针,存储着方法的参数类型和返回值类型。
    • method_imp:指向方法的具体实现的指针,数据类型为IMP,本质上是一个函数指针。 在第五个按钮行为“增加一个方法”部分会提到。

    SEL:数据类型,表示方法选择器,可以理解为对方法的一种包装。在每个方法都有一个与之对应的SEL类型的数据,根据一个SEL数据“@selector(方法名)”就可以找到对应的方法地址,进而调用方法。
    因此可以通过:获取 Method结构体->得到SEL选择器名称->得到对应的方法名 ,这样的方式,便于认识OC中关于方法的定义。

    3.改变person对象的私有变量name的值.

    将第三个按钮关联到ViewController.h,添加行为并命名其方法为:“changeVariable”:

    - (IBAction)changeVariable:(UIButton *)sender;//改变其中name变量
    

    在ViewController.m中创建一个person对象,记得初始化

    @implementation ViewController{ 
      person *per; //创建一个person实例
    }
    - (void)viewDidLoad { 
      [super viewDidLoad]; 
      per = [[person alloc]init]; //记得要初始化...不然后果自己尝试下
    }
    

    在ViewController.m中的实现如下:

    /*3.改变person的name变量属性*/
    - (IBAction)changeVariable:(UIButton *)sender {
    
    NSLog(@"改变前的person:%@",per); 
    
    unsigned int count = 0;
    Ivar *allList = class_copyIvarList([person class], &count); 
    Ivar ivv = allList[0]; //从第一个例子getAllVariable中输出的控制台信息,我们可以看到name为第一个实例属性。 
    object_setIvar(per, ivv, @"Mike"); //name属性Tom被强制改为Mike。 
    
    NSLog(@"改变之后的person:%@",per);
    }
    

    点击按钮后,控制台输出:

    2016-05-19 22:45:05.125 runtime运行时[1957:34730] 改变前的person:name:Tom age:12
    2016-05-19 22:45:05.126 runtime运行时[1957:34730] 改变之后的person:name:Mike age:12
    

    4.为person的category类增加一个新属性:

    如何在不改动某个类的前提下,添加一个新的属性呢?
    答:可以利用runtime为分类添加新属性
    在iOS中,category,也就是分类,是不可以为本类添加新的属性的,但是在runtime中我们可以使用对象关联,为person类进行分类的新属性创建:
    ①新建一个新的OC类:

    命名为:PersonCategory ,点击next


    在出现的新类“person+PersonCategory.h”中,添加“height”:
    #import "person.h"
    @interface person (PersonCategory)
    @property (nonatomic,assign)float height; //新属性@end
    

    person+PersonCategory.m”类的代码如下:

    #import "person+PersonCategory.h"
    #import <objc/runtime.h> //runtime API的使用需要包含此头文件
    
    const char * str = "myKey"; //做为key,字符常量 必须是C语言字符串;
    
    @implementation person (PersonCategory)
    
    -(void)setHeight:(float)height{ 
    NSNumber *num = [NSNumber numberWithFloat:height]; 
    /* 
    第一个参数是需要添加属性的对象; 
    第二个参数是属性的key; 
    第三个参数是属性的值,类型必须为id,所以此处height先转为NSNumber类型; 
    第四个参数是使用策略,是一个枚举值,类似@property属性创建时设置的关键字,可从命名看出各枚举的意义; 
    objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    */ 
    objc_setAssociatedObject(self, str, num, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    //提取属性的值: 
    -(float)height{ 
    NSNumber *number = objc_getAssociatedObject(self, str);
    return [number floatValue];
    }
    @end
    

    接下来,我们可以在ViewController.m中对person的一个对象进行height的访问了,
    将第四个按钮关联到ViewController.h添加行为并命名其方法为:“addVariable:”(记得:#import "person+PersonCategory.h"

    - (IBAction)addVariable:(UIButton *)sender;
    

    在ViewController.m中的实现如下:

    /* 4.添加新的属性*/
    - (IBAction)addVariable:(UIButton *)sender { 
    per.height = 12;           //给新属性height赋值 
    NSLog(@"%f",[per height]); //访问新属性值
    }
    

    点击按钮、再点击按钮获取类的属性、方法。

    2016-05-20 15:39:54.432 runtime运行时[4605:178974] 12.000000
    2016-05-20 15:39:56.295 runtime运行时[4605:178974] (Name: name) ----- (Type:@"NSString")
    2016-05-20 15:39:56.296 runtime运行时[4605:178974] (Name: _age) ----- (Type:i)
    2016-05-20 15:39:57.195 runtime运行时[4605:178974] (Method:func1)
    2016-05-20 15:39:57.196 runtime运行时[4605:178974] (Method:func2)
    2016-05-20 15:39:57.196 runtime运行时[4605:178974] (Method:setAge:)
    2016-05-20 15:39:57.196 runtime运行时[4605:178974] (Method:age)
    2016-05-20 15:39:57.196 runtime运行时[4605:178974] (Method:.cxx_destruct)
    2016-05-20 15:39:57.197 runtime运行时[4605:178974] (Method:description)
    2016-05-20 15:39:57.197 runtime运行时[4605:178974] (Method:init)
    2016-05-20 15:39:57.197 runtime运行时[4605:178974] (Method:height)
    2016-05-20 15:39:57.197 runtime运行时[4605:178974] (Method:setHeight:)
    

    分析:可以看到分类的新属性可以在per对象中对新属性height进行访问赋值。
    获取到person类属性时,依然没有height的存在,但是却有height和setHeight这两个方法;因为在分类中,即使使用@property定义了,也只是生成set+get方法,而不会生成_变量名,分类中是不允许定义变量的。
    使用runtime中objc_setAssociatedObject()objc_getAssociatedObject()方法,本质上只是为对象per添加了对height的属性关联,但是达到了新属性的作用;
    使用场景:假设imageCategory是UIImage类的分类,在实际开发中,我们使用UIImage下载图片或者操作过程需要增加一个URL保存一段地址,以备后期使用。这时可以尝试在分类中动态添加新属性MyURL进行存储。

    5.为person类添加一个新方法;

    将第五个按钮关联到ViewController.h,添加行为并命名其方法为:“addMethod”:

    - (IBAction)addMethod:(UIButton *)sender;
    

    在ViewController.m中的实现如下:

    /*5.添加新的方法试试(这种方法等价于对Father类添加Category对方法进行扩展):*/
    - (IBAction)addMethod:(UIButton *)sender { 
    /* 动态添加方法: 
      第一个参数表示Class cls 类型; 
      第二个参数表示待调用的方法名称; 
      第三个参数(IMP)myAddingFunction,IMP一个函数指针,这里表示指定具体实现方法myAddingFunction; 
      第四个参数表方法的参数,0代表没有参数; 
    */ 
      class_addMethod([per class], @selector(NewMethod), (IMP)myAddingFunction, 0); 
      //调用方法 【如果使用[per NewMethod]调用方法,在ARC下会报“no visible @interface"错误】 
      [per performSelector:@selector(NewMethod)];
    }
    
    //具体的实现(方法的内部都默认包含两个参数Class类和SEL方法,被称为隐式参数。)
    int myAddingFunction(id self, SEL _cmd){ 
      NSLog(@"已新增方法:NewMethod"); 
      return 1;
    }
    

    点击按钮后,控制台输出:

    2016-05-20 14:08:55.822 runtime运行时[1957:34730] 已新增方法:NewMethod
    

    6.交换person类的2个方法的功能:

    将第六个按钮关联到ViewController.h,添加行为并命名其方法为:“replaceMethod”:

    - (IBAction)replaceMethod:(UIButton *)sender;
    

    在ViewController.m中的实现如下:

    /* 6.交换两种方法之后(功能对调),可以试试让苹果乱套... */
    - (IBAction)replaceMethod:(UIButton *)sender { 
    Method method1 = class_getInstanceMethod([person class], @selector(func1)); 
    Method method2 = class_getInstanceMethod([person class], @selector(func2)); 
    
    //交换方法 
    method_exchangeImplementations(method1, method2); 
    [per func1]; //输出交换后的效果,需要对比的可以尝试下交换前运行func1;
    }
    

    点击按钮后,控制台输出:

    2016-05-20 14:11:57.381 runtime运行时[1957:34730] 执行func2方法。
    

    交换方法的使用场景:项目中的某个功能,在项目中需要多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,且要求不改变旧的项目(也就是不改变原来方法实现的前提下)。那么,我们可以在分类中,再写一个新的方法(符合新的需求的方法),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码的情况下,就完成了项目的改进,很好地体现了该项目的封装性与利用率。
    :交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次。


    (转载请标明原文出处,谢谢支持 ~ - ~)
     by:啊左~

    相关文章

      网友评论

      • david51:object_setIvar(per, ivv, @"Mike"); 如果要改变那个age应该怎么办呢? 动态改变int类型?
        啊左:@david51 可以呀,我记得只是发出,但是有修改。如果改基本类型,应该还要加上转型之类的步骤。
        david51:@啊左 不行啊, 只能是id类型的, 基本类型的改不了 直接33不行
        啊左:@david51 同理,你可以试着这样写:
        NSLog(@"改变前的age:%d",per.age);
        Ivar ivv2 = allList[1];
        object_setIvar(per, ivv2,33);
        NSLog(@"改变之后的age:%d",per.age);
      • 知行合一认知升级:业界良心好文
        啊左:夸张了老铁😂
      • 红枫叶HM:这个教程很适合初学 runtime 的学生,一目了然,给楼主点个赞
        啊左:@红枫叶007 笔芯~💗
      • Maj_sunshine:终于找到实用的例子了
        啊左:@学污直径 好的,👌。
      • 零零零零凌:初学者 看了你好几篇文章 感觉都不错...谢啦..还有就是你这个demo申请的变量 比如 Ivar 需要手动free .
        /**
        * Describes the instance variables declared by a class.
        *
        * @param cls The class to inspect.
        * @param outCount On return, contains the length of the returned array.
        * If outCount is NULL, the length is not returned.
        *
        * @Return An array of pointers of type Ivar describing the instance variables declared by the class.
        * Any instance variables declared by superclasses are not included. The array contains *outCount
        * pointers followed by a NULL terminator. You must free the array with free().
        *
        * If the class declares no instance variables, or cls is Nil, NULL is returned and *outCount is 0.
        */
        啊左::smile:
        貌似C语言在ARC中通过copy产生,所以需要手动释放allVariables;
      • GOOGxu:马克
      • percy杨:棒棒棒
      • ylyadai:获取的实例变量方法中,name没有下划线是因为楼主写的不规范,实例变量name没有加下划线,所以打印出来就没有,而系统自动生成的age实例变量就有下划线
        啊左:@阿呆的乐乐园 😅,翻了资料共享一下。
        ylyadai:@啊左 写的好多,好详细:stuck_out_tongue::stuck_out_tongue::stuck_out_tongue:
        啊左:@阿呆的乐乐园
        其实这里一直重点强调的是,age会因为属性变量机制中默认的@synthesize使得它自动补全为_age,所以很明显把name命名为没有下划线是为了区分出来:属性变量age在@property后会补全下划线,在这里没有强调name的下划线,是为了避免混淆;
        你可以直接说这种命名不规范,但不是“因为不规范”所以才有下划线😂,那是因为:在语法正确情况下不管怎么命名实例变量,它都就只输出原来的名字,即使我加了2个下划线,它也照样输出2个下划线。
        另外,实例变量这种语法属于比较老派的用法,以前为了防止内存管理泄露,与属性变量调用访问方法区分开来,”_变量名”往往意味着实例变量,包括我公司旧的项目也是实例变量加_、属性变量加“@synthesize name = _name;”但是在现在ARC后,不存在上述问题,一年多前我身边的挺多同行也是常常加下划线,包括后来看《精通iOS开发》这些书介绍的时候,也是发现里面的实例变量也没有加下划线,所以我想现在更多的已经是习惯问题而不是规范,当然也会有相关资料建议下划线做为编码规范。这就看开发者怎么去处理了。
      • 鼻毛长长:什么是编译,什么是运行?
        啊左:@鼻毛长长
        编译:编译器帮你把源代码翻译成机器能识别的代码,或者说识别语法等代码错误并产生能够识别的指令以便让运行时能够进行相应的内存分配。
        运行时:代码跑(run)起来了,进行内存的分配与操作。
        以上可能不太准确,纯属个人见解。具体的可以网上查找这两个的意义与区别。
      • 酷爱西西的伪球迷:这里获取所有对象的所有方法得到的是其所有的实例方法
      • butterflyer:给楼主点个赞。
        啊左:@butterflyer 木有看到你的赞哦~ :dizzy_face:
      • 0ef0376dc7d1:楼主,额能请教你些问题吗?
        啊左:@0ef0376dc7d1 你可以直接问呀。
      • 夜3033:写的很好,重点是有代码能自己试一下,学习了。
      • 简单也好:写了一下,在添加新方法那里,
        class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
        selector()里面的方法名好像没什么用?,只会执行IMP类型的C语言函数?
        啊左:@简单也好 NewMetho只是一个方法名,可随意命名,主要是给对象用来调用的。但是实际上具体方法实现是在(IMP)函数指针指向的内容里面,那是“实现这个方法的函数”。
        虽然一般都说class_addMethod是添加方法,但个人觉得说是添加函数更贴切些。
        简单也好:@啊左 [per performSelector:@selector(NewMethod)]; 却没有执行NewMethod方法,执行的是(IMP)myAddingFunction里面的内容
        啊左:@简单也好 第二个参数是让对象调用的方法名称。
        [per performSelector:@selector(NewMethod)];
      • 简单也好:说的明白,还有例子,👍
      • a3c619581c4f:不错,好好学习一下

      本文标题:iOS开发 -- Runtime 的几个小例子

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