Runtime的几个小例子


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

          1.runtime是一套底层的c语言API(包括很多强大实用的c语言类型,c语言函数);  [runtime运行系统]  

        2.实际上,平时我们编写的oc代码,底层都是基于runtime实现的;                             [OC语言的动态性

 运行时系统 (runtime system),对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。 runtime就是OC辛苦的幕后工作人员。(编译器会自动帮助我们编译成runtime代码。

动态特性,使得它在语言层面上支持程序的可扩展性。只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法。利用runtime机制让我们可以在程序运行时动态修改类的具体实现、包括类中的所有私有属性、方法。这也是本文runtime例子的出发点。

我们所敲入的代码转化为运行时的runtime函数代码,最终在程序运行时转成了底层的runtimec语言代码 ;

举例:

当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:

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

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

二、runtime的几种使用方法

我们通过继承于NSObjectperson类,来对runtime进行学习。

本文共有6个关于runtime机制方法的小例子,分别是:

  1. 获取person类的所有变量;
  2. 获取person类的所有方法;
  3. 改变person类的私有变量name的值;
  4. personcategory类增加一个新属性;
  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;

}


//person2个普通方法

-(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的地方,我们都需要包含头文件:    (本文几个例子中,都只需要在ViewController.m中包含.

#import <objc/runtime.h>

 

1.获取person类的所有变量

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

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

ViewController.m中的实现如下:


/*1.获取person所有的成员变量*/

- (IBAction)getAllVariable:(UIButton *)sender {

    unsigned int count = 0;

    //获取类的一个包含所有变量的列表IVarruntime声明的一个宏,是实例变量的意思.

    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];

        //获取SELSEL类型,即获取方法选择器@selector()

        SEL sel = method_getName(md);

        //得到sel的方法名:以字符串格式获取selname,也即@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.改变personname变量属性*/

- (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改变前的personname:Tom age:12

2016-05-19 22:45:05.126 runtime运行时[1957:34730改变之后的personname:Mike age:12

 

4.personcategory类增加一个新属性:

如何在不改动某个类的前提下,添加一个新的属性呢?

答:可以利用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(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:17897412.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的存在,但是却有heightsetHeight这两个方法;因为在分类中,即使使用@property定义了,也只是生成set+get方法,而不会生成_变量名,分类中是不允许定义变量的。

使用runtimeobjc_setAssociatedObject()objc_getAssociatedObject()方法,本质上只是为对象per添加了对height的属性关联,但是达到了新属性的作用;

使用场景:假设imageCategoryUIImage类的分类,在实际开发中,我们使用UIImage下载图片或者操作过程需要增加一个URL保存一段地址,以备后期使用。这时可以尝试在分类中动态添加新属性MyURL进行存储。

 

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

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

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

ViewController.m中的实现如下:


/*5.添加新的方法试试(这种方法等价于对Father类添加Category对方法进行扩展)*/

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

    

    /* 动态添加方法:

       第一个参数表示Class cls 类型;

       第二个参数表示待调用的方法名称;

       第三个参数(IMP)myAddingFunctionIMP一个函数指针,这里表示指定具体实现方法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方法会在程序运行前加载一次。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值