zoukankan      html  css  js  c++  java
  • 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代码,方法调用的本质,就是在编译阶段,编译器转化为向对象发送消息。

    Demo地址:https://github.com/mengzhihun6

     

    二、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的几种方法:

    ①JGPerson.h如下:

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface JGPerson : NSObject
    
    @property (nonatomic, copy) NSString *name; //属性变量
    
    @property (nonatomic, assign) int age;
    
    -(void)func1;
    -(void)func2;
    
    @end
    
    NS_ASSUME_NONNULL_END

    ②JGPerson.m如下:

    #import "JGPerson.h"
    
    @implementation JGPerson {
        double _score; //实例变量
    }
    
    - (instancetype)init {
        if (self = [super init]) {
            
            self.name = @"小明";
            self.age = 22;
            _score = 66.5;
        }
        return self;
    }
    
    //person的2个普通方法
    -(void)func1
    {
        NSLog(@"执行func1方法。");
    }
    -(void)func2
    {
        NSLog(@"执行func2方法。");
    }
    
    
    - (NSString *)description {
        return [NSString stringWithFormat:@" %d岁的 %@考了%.1f分",self.age,  self.name, _score];
    }
    
    
    @end

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

    那么,为项目的故事板添加6个按钮;

    在使用runtime的地方,我们都需要包含头文件:    (本文几个例子中,都只需要在ViewController.m中包含.)

    #import <objc/runtime.h>

    1.获取person类的所有变量

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

    /*1.获取person所有的成员变量*/
    - (IBAction)Func1:(id)sender {}

    在ViewController.m中的实现如下:

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

    得到的输出如下:(i表示类型为int d表示类型为double)

    2019-04-03 15:59:17.415756+0800 RuntimeDemo[20661:192149] 名称:_score => 类型:d
    2019-04-03 15:59:17.415868+0800 RuntimeDemo[20661:192149] 名称:_age => 类型:i
    2019-04-03 15:59:17.415939+0800 RuntimeDemo[20661:192149] 名称:_name => 类型:@"NSString"

    分析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”:

    /*2.获取person所有方法*/
    - (IBAction)Func2:(id)sender {}

    在ViewController.m中的实现如下:

    /*2.获取person所有方法*/
    - (IBAction)Func2:(id)sender {
        
        unsigned int count = 0;
        //获取方法列表,所有在.m文件显式实现的方法都会被找到,包括setter+getter方法;
        Method *allMethods = class_copyMethodList([JGPerson 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%d:%s",i+1, methodName);
        }
        
    }

     控制台输出:

        
    2019-04-03 16:09:18.052985+0800 RuntimeDemo[20818:197062] Method1:eat
    2019-04-03 16:09:18.053088+0800 RuntimeDemo[20818:197062] Method2:.cxx_destruct
    2019-04-03 16:09:18.053154+0800 RuntimeDemo[20818:197062] Method3:description
    2019-04-03 16:09:18.053212+0800 RuntimeDemo[20818:197062] Method4:name
    2019-04-03 16:09:18.053273+0800 RuntimeDemo[20818:197062] Method5:setName:
    2019-04-03 16:09:18.053333+0800 RuntimeDemo[20818:197062] Method6:init
    2019-04-03 16:09:18.053389+0800 RuntimeDemo[20818:197062] Method7:run
    2019-04-03 16:09:18.053443+0800 RuntimeDemo[20818:197062] Method8:setAge:
    2019-04-03 16:09:18.053502+0800 RuntimeDemo[20818:197062] Method9:age

    控制台输出了包括set和get等方法名称。【备注:.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”:

    /*3.改变person的name变量属性*/
    - (IBAction)Func3:(id)sender {}

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

    @implementation ViewController {
        JGPerson *_person;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        _person = [[JGPerson alloc] init];
     
    }

    在ViewController.m中的实现如下:

    /*3.改变person的name变量属性*/
    - (IBAction)Func3:(id)sender {
        NSLog(@"改变之前的Penson:%@",_person);
        
        unsigned int count = 0;
        
        Ivar *allList = class_copyIvarList([JGPerson class], &count);
        
        Ivar ivar = allList[2];
        //从第一个例子getAllVariable中输出的控制台信息,我们可以看到name为第2个实例属性;
        // object_setIvar(<#id  _Nullable obj#>, <#Ivar  _Nonnull ivar#>, <#id  _Nullable value#>)
        object_setIvar(_person, ivar, @"小刚"); //name属性小明被强制改为小刚。
        
        NSLog(@"改变之后的Penson:%@",_person);
    
    }

         控制台输出:

        
    2019-04-03 16:20:51.312155+0800 RuntimeDemo[20975:202727] 改变之前的Penson: 22岁的 小明考了66.5分
    2019-04-03 16:20:51.312235+0800 RuntimeDemo[20975:202727] 改变之后的Penson: 22岁的 小刚考了66.5分

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

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

    答:可以利用runtime为分类添加新属性。

    iOS中,category也就是分类,是不可以为本类添加新的属性的,但是在runtime中我们可以使用对象关联,为person类进行分类的新属性创建:

    分类.h文件

    #import "JGPerson.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface JGPerson (JGCategory)
    
    @property (nonatomic, assign) float height; //新属性
    
    @end
    
    NS_ASSUME_NONNULL_END

    分类.m文件

    #import "JGPerson+JGCategory.h"
    #import <objc/runtime.h>
    
    const char *str = "personKey"; //做为key,字符常量 必须是C语言字符串;
    
    @implementation JGPerson (JGCategory)
    
    
    
    - (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”:

    /* 4.添加新的属性*/
    - (IBAction)Func4:(id)sender {
        
        /*
         如何在不改动某个类的前提下,添加一个新的属性呢?
         
         答:可以利用runtime为分类添加新属性。
         */
        //给新属性height赋值
        _person.height= 168; //给新属性height赋值
        //访问新属性值
        NSLog(@"%f",[_person height]);
    
    }

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

    2019-04-03 17:27:02.265178+0800 RuntimeDemo[21710:227208] 168.000000
    2019-04-03 17:27:09.185091+0800 RuntimeDemo[21710:227208] 名称:_score => 类型:d
    2019-04-03 17:27:09.185190+0800 RuntimeDemo[21710:227208] 名称:_age => 类型:i
    2019-04-03 17:27:09.185265+0800 RuntimeDemo[21710:227208] 名称:_name => 类型:@"NSString"
    2019-04-03 17:27:11.165884+0800 RuntimeDemo[21710:227208] Method1:func1
    2019-04-03 17:27:11.165984+0800 RuntimeDemo[21710:227208] Method2:func2
    2019-04-03 17:27:11.166057+0800 RuntimeDemo[21710:227208] Method3:.cxx_destruct
    2019-04-03 17:27:11.166123+0800 RuntimeDemo[21710:227208] Method4:description
    2019-04-03 17:27:11.166176+0800 RuntimeDemo[21710:227208] Method5:name
    2019-04-03 17:27:11.166228+0800 RuntimeDemo[21710:227208] Method6:setName:
    2019-04-03 17:27:11.166293+0800 RuntimeDemo[21710:227208] Method7:init
    2019-04-03 17:27:11.166460+0800 RuntimeDemo[21710:227208] Method8:height
    2019-04-03 17:27:11.166551+0800 RuntimeDemo[21710:227208] Method9:setHeight:
    2019-04-03 17:27:11.166661+0800 RuntimeDemo[21710:227208] Method10:setAge:
    2019-04-03 17:27:11.166771+0800 RuntimeDemo[21710:227208] Method11:age

    分析:可以看到分类的新属性可以在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”:

    /*5.添加新的方法试试(这种方法等价于对Father类添加Category对方法进行扩展):*/
    - (IBAction)Func5:(id)sender {}

    在ViewController.m中的实现如下:

    /*5.添加新的方法试试(这种方法等价于对Father类添加Category对方法进行扩展):*/
    - (IBAction)Func5:(id)sender {
        
        class_addMethod([_person class], @selector(NewMethod),(IMP)myAddingFunction, 0);
        
        
        [_person performSelector:@selector(NewMethod)];
    }
    
    //具体的实现(方法的内部都默认包含两个参数Class类和SEL方法,被称为隐式参数。)
    int myAddingFunction(id self, SEL _cmd)
    {
        NSLog(@"已新增方法:NewMethod");
        return 1;
    }

    控制台输出:

    2019-04-03 16:42:14.522734+0800 RuntimeDemo[21232:211364] 已新增方法:NewMethod

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

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

    /* 6.交换两种方法之后(功能对调*/
    - (IBAction)Func6:(id)sender {}

    在ViewController.m中的实现如下:

    /* 6.交换两种方法之后(功能对调*/
    - (IBAction)Func6:(id)sender {
        
        Method method1 = class_getInstanceMethod([JGPerson class], @selector(func1));
        Method method2 = class_getInstanceMethod([JGPerson class], @selector(func2));
        
        method_exchangeImplementations(method1, method2);
        
        [_person func1];
        
    }

    控制台输出:

    2019-04-03 16:48:14.902453+0800 RuntimeDemo[21328:214212] 执行func2方法。

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

    注:交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次。

    参考:https://www.cnblogs.com/azuo/p/5505782.html

  • 相关阅读:
    Lighting maps_练习二
    Lighting maps_练习一
    Materials_练习
    Basic Lighting_练习二
    *201809-3
    程序设计思维与实践 Week14 作业 (3/4/数据班)
    程序设计思维与实践 Week14 限时大模拟 (3/4/数据班)
    程序设计思维与实践 Week15 作业 (3/4/数据班)
    程序设计思维与实践CSP-M4补题
    CCF201609-3
  • 原文地址:https://www.cnblogs.com/GJ-ios/p/10650377.html
Copyright © 2011-2022 走看看