runtime

为深入学习runtime的原理及使用,找到几篇个人认为写的比较好的文章进行转载分享:

runtime 的原理及使用


简介:

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的OC代码,底层都是基于runtime实现的。


操作对象的类型的定义

/// An opaque type that represents a method in a class definition. 一个类型,代表着类定义中的一个方法

typedef struct objc_method *Method;


/// An opaque type that represents an instance variable.代表实例(对象)的变量

typedef struct objc_ivar *Ivar;


/// An opaque type that represents a category.代表一个分类

typedef struct objc_category *Category;


/// An opaque type that represents an Objective-C declared property.代表OC声明的属性

typedef struct objc_property *objc_property_t;


// Class代表一个类,它在objc.h中这样定义的  typedef struct objc_class *Class;

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;


#if !__OBJC2__

    Class super_class                                        OBJC2_UNAVAILABLE;

    const char *name                                         OBJC2_UNAVAILABLE;

    long version                                             OBJC2_UNAVAILABLE;

    long info                                                OBJC2_UNAVAILABLE;

    long instance_size                                       OBJC2_UNAVAILABLE;

    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;

    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;

    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;

    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

#endif


} OBJC2_UNAVAILABLE;


这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,对操作一个类属性和方法非常方便。OBJC2_UNAVAILABLE标记的属性是Ojective-C 2.0不支持的,但实际上可以用响应的函数获取这些属性,例如:如果想要获取Classname属性,可以按如下方法获取:

Class classPerson = Person.class;

// printf("%s\n", classPerson->name); //用这种方法已经不能获取name 因为OBJC2_UNAVAILABLE

const char *cname  = class_getName(classPerson);

printf("%s", cname); // 输出:Person



函数的定义

  • 对象进行操作的方法一般以object_开头
  • 进行操作的方法一般以class_开头
  • 类或对象的方法进行操作的方法一般以method_开头
  • 成员变量进行操作的方法一般以ivar_开头
  • 属性进行操作的方法一般以property_开头开头
  • 协议进行操作的方法一般以protocol_开头

根据以上的函数的前缀 可以大致了解到层级关系。

对于以objc_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息,类的列表,关联对象和关联属性等操作。



runtime的应用:

方法调用的本质,就是让对象发送消息。

objc_msgSend,只有对象才能发送消息,因此以objc开头.

使用消息机制前提,必须导入#import <objc/message.h>

// 创建person对象

    Person *p = [[Person alloc] init];


    // 调用对象方法

    [p eat];


    // 本质:让对象发送消息

    objc_msgSend(p, @selector(eat));


    // 调用类方法的方式:两种

    // 第一种通过类名调用

    [Person eat];

    // 第二种通过类对象调用

    [[Person class] eat];


    // 用类名调用类方法,底层会自动把类名转换成类对象调用

    // 本质:让类对象发送消息

    objc_msgSend([Person class], @selector(eat));

交换方法

交换方法实现的需求场景:自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)

可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码 的情况下,就完成了项目的改进。

交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。

@implementation ViewController


- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib.

    // 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。

    // 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;

    // 步骤二:交换imageNamedimageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。

    UIImage *image = [UIImage imageNamed:@"123"];

}


@end


@implementation UIImage (Image)

// 加载分类到内存的时候调用

+ (void)load

{

    // 交换方法


    // 获取imageWithName方法地址

    Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));


    // 获取imageWithName方法地址

    Method imageName = class_getClassMethod(self, @selector(imageNamed:));


    // 交换方法地址,相当于交换实现方式

    method_exchangeImplementations(imageWithName, imageName);

}


// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.


// 既能加载图片又能打印

+ (instancetype)imageWithName:(NSString *)name

{

    // 这里调用imageWithName,相当于调用imageName

    UIImage *image = [self imageWithName:name];


    if (image == nil) {

        NSLog(@"加载空的图片");

    }


    return image;

}


@end

\对象的关联对象

关联对象不是为类\对象添加属性或者成员变量(因为在设置关联后也无法通过ivarList或者propertyList取得,而是为类添加一个相关的对象,通常用于存储类信息,例如存储类的属性列表数组,为将来字典转模型的方便。 

使用方式一:给分类添加属性

@implementation ViewController


- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib.


    // 给系统NSObject类动态添加属性name

    NSObject *objc = [[NSObject alloc] init];

    objc.name = @"小码哥";

    NSLog(@"%@",objc.name);

}


@end



// 定义关联的key

static const char *key = "name";


@implementation NSObject (Property)


- (NSString *)name

{

    // 根据关联的key,获取关联的值。

    return objc_getAssociatedObject(self, key);

}


- (void)setName:(NSString *)name

{

    // 第一个参数:给哪个对象添加关联

    // 第二个参数:关联的key,通过这个key获取

    // 第三个参数:关联的value

    // 第四个参数:关联的策略

    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}


@end

使用方式二:给对象添加关联对象。

比如alertView,一般传值,使用的是alertViewtag属性。我们想把更多的参数传给alertView代理:

/**

 *  删除点击

 *  @param recId        购物车ID

 */

- (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId

{

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"确认要删除这个宝贝" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"确定", nil];

    

    // 传递多参数

    objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    

    alert.tag = [recId intValue];

    [alert show];

}


/**

 *  确定删除操作

 */

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {

    if (buttonIndex == 1) {

        

        NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id");

        NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id");

        NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag];

    }

}


objc_setAssociatedObject方法的参数解释:

  1. 第一个参数id object, 当前对象
  2. 第二个参数const void *key, 关联的key,是c字符串 
  3. 第三个参数id value, 被关联的对象的值 
  4. 第四个参数objc_AssociationPolicy policy关联引用的规则

动态添加方法

开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。

@implementation ViewController


- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib.


    Person *p = [[Person alloc] init];


    // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。

    // 动态添加方法就不会报错

    [p performSelector:@selector(eat)];

}


@end



@implementation Person

// void(*)()

// 默认方法都有两个隐式参数,

void eat(id self,SEL sel)

{

    NSLog(@"%@ %@",self,NSStringFromSelector(sel));

}


// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.

// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

    if (sel == @selector(eat)) {

        // 动态添加eat方法


        // 第一个参数:给哪个类添加方法

        // 第二个参数:添加方法的方法编号

        // 第三个参数:添加方法的函数实现(函数地址)

        // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd

        class_addMethod(self, @selector(eat), eat, "v@:");

    }

    return [super resolveInstanceMethod:sel];

}

@end


字典转模型KVC实现

KVC:把字典中所有值给模型的属性赋值。这个是要求字典中的Key,必须要在模型里能找到相应的值,如果找不到就会报错。基本原理如:

 // KVC原理:

    // 1.遍历字典中所有key,去模型中查找有没有对应的属性

    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {

        

        // 2.去模型中查找有没有对应属性 KVC

        // key:source value:来自即刻笔记

        // [item setValue:@"来自即刻笔记" forKey:@"source"]

        [item setValue:value forKey:key];

    }];


但是,在实际开发中,从字典中取值,不一定要全部取出来。因此,我们可以通过重写KVC 中的 forUndefinedKey这个方法,就不会进行报错处理。


// 重写系统方法? 1.想给系统方法添加额外功能 2.不想要系统方法实现

// 系统找不到就会调用这个方法,报错

- (void)setValue:(id)value forUndefinedKey:(NSString *)key

{

    

}

另外,我们可以通过runtime的方式去实现。我们把KVC的原理倒过来,通过遍历模型的值,从字典中取值。

// Ivar:成员变量 以下划线开头

// Property:属性

+ (instancetype)modelWithDict:(NSDictionary *)dict

{

    id objc = [[self alloc] init];

    

    // runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值

    // 1.获取模型中所有成员变量 key

    // 获取哪个类的成员变量

    // count:成员变量个数

    unsigned int count = 0;

    // 获取成员变量数组

    Ivar *ivarList = class_copyIvarList(self, &count);

    

    // 遍历所有成员变量

    for (int i = 0; i < count; i++) {

        // 获取成员变量

        Ivar ivar = ivarList[i];

        

        // 获取成员变量名字

        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 获取成员变量类型

        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

        // @\"User\" -> User

        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];

        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

        // 获取key

        NSString *key = [ivarName substringFromIndex:1];

        

        // 去字典中查找对应value

        // key:user  value:NSDictionary

        

        id value = dict[key];

        

        // 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型

        // 并且是自定义对象才需要转换

        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {

            // 字典转换成模型 userDict => User模型

            // 转换成哪个模型


            // 获取类

            Class modelClass = NSClassFromString(ivarType);

            

            value = [modelClass modelWithDict:value];

        }

        

        // 给模型中属性赋值

        if (value) {

            [objc setValue:value forKey:key];

        }

    }

        

    return objc;

}



问题:

说说什么是runtime

1>OC 是一个全动态语言,OC 的一切都是基于 Runtime 实现的

平时编写的OC代码, 在程序运行过程中, 其实最终都是转成了runtimeC语言代码, runtime算是OC的幕后工作者

比如:

OC :

[[Person alloc] init]

runtime :

objc_msgSend(objc_msgSend("Person" , "alloc"), "init")

2>runtime是一套比较底层的纯C语言API, 属于1C语言库, 包含了很多底层的C语言API

3>runtimeAPI的实现是用 C++ 开发的(源码中的实现文件都是mm),是一套苹果开源的框架




3.技术点和应用场景

在以下的代码中,都用到了Person类,Person类知识简单的定义了一个成员变量和两个属性

@interface Person : NSObject

{

    @private

    float _height;

}

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) int age;

@end

3_1.获取属性\成员变量列表

回到顶部

对于获取成员变量的列表可以使用class_copyIvarList函数,如果想要获取属性列表可以使用class_copyPropertyList函数,使用的示例如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    

    Class classPerson = NSClassFromString(@"Person"); // 与下面一句效果一样,可以不用导入头文件

//    Class clazz = Person.class;

    unsigned int count = 0;

    Ivar *ivarList = class_copyIvarList(classPerson, &count); // 获取成员变量数组

    for (int i = 0; i < count; i++) {

        const char *cname = ivar_getName(ivarList[i]); // 获取成员变量的名字

        NSString *name = [NSString stringWithUTF8String:cname];

        NSLog(@"%@", name);

    }

    NSLog(@"-------------------分割线------------------");

    objc_property_t *propertyList = class_copyPropertyList(classPerson, &count); // 获取属性数组

    for (int i = 0; i < count; i++) {

        const char *cname = property_getName(propertyList[i]);

        NSString *name = [NSString stringWithUTF8String:cname];

        NSLog(@"%@", name);

    }


}

以上代码的输出为:

2015-06-05 22:28:16.194 runtime终极[4192:195757] _height

2015-06-05 22:28:16.195 runtime终极[4192:195757] _age

2015-06-05 22:28:16.195 runtime终极[4192:195757] _name

2015-06-05 22:28:16.195 runtime终极[4192:195757] -------------------分割线------------------

2015-06-05 22:28:16.195 runtime终极[4192:195757] name

2015-06-05 22:28:16.195 runtime终极[4192:195757] age

为什么会有上面的输出结果,因为@property会做三份工作:
1.生成一个带下划线的成员变量
2.生成这个成员变量的get方法
3.生成这个成员变量的set方法

因此会输出三个成员变量_height_age_name。需要注意的是属性名是不带下划线的,和定义时的名字一样。因此可以说:ivarList可以获取到@property关键字定义的属性 ,而propertyList不可以获取到成员变量。也就是:使用ivarList是可以将所有的成员变量和属性都获取的

当属性是readonly的而且重写了getter时,这种情况还是会遇见的,比如一个属性是计算型属性,需要依赖其他属性的值计算而来。此时生成的带下划线的成员变量就不在了 通过ivarList不能获取该属性了。因此当有这种值的时候,无论使用ivarList还是使用propertyList都无法获取全部的属性或变量

在进行下一个话题之前:先需要弄清楚另一个问题:对于一个readonly的属性,到底是didSet+set好,还是重写getter?

大部分的readonly的属性是计算型的,依旧是依赖于其他属性,因此可以使用didSet+set,也就是在其他属性的set方法内,将本属性set 但是didSet+set有时候完全没有必要,不符合懒加载的规则,浪费了计算能力,用重写getter的方法好一些。 因此重写getter总是会好一点。

回归正题:在KVC时,想要获取全部的成员变量和属性, 怎么办呢?

首先要了解setValue: forKeyPath:方法的底层实现:name属性为例

1.首先先去类的方法列表去寻找有木有setName:,如果有,就直接调用[person setName:value]

2.找找有没有带下划线的成员变量_name,如果有 _name = value;

3.找有没有成员变量name,如果有 name = value;

4.如果都没有找到,就直接报错。

因此对于readonly的又重写了getter的属性而言:如果对propertyList的属性一次使用kvc,就会报错,因此为保证代码正常,不能使用propertyList的属性进行kvc

另外:这种属性本来就是计算型的了,为什么还有为它赋值呢,因此对它进行kvc也不合情理。

当使用ivaList时,直接就无法获取到这种属性,因此是kvc的最佳方案。再者,使用propertyList无法获取成员变量(_height),无法对成员变量进行赋值。而使用ivaList是可以将该赋值的成员变量都获取的。

以上就是使用ivar还是使用property进行kvc的论证。

话题外: 很多类 有些成员变量 既没有暴露给外部调用的getter又没有setter,只是用@private声明了一下:为什么??
猜测是:是方法调用时使用的中间变量,因为是跟随对象产生,不适合使用静态static,又因为外部不会使用,所以没必要给外部提供接口,但是可能有好几个方法都需要这个量,不适合做局部变量,所以就这样定义了。

对于这种情况,要想不对这种成员变量赋值,在KVC时又可以这样改进一下,通过ivarList获取,去掉propertyList中没有的成员变量,这样就过滤掉了上面的那种成员变量了。

3_1_1.应用1:KVC字典转模型

回到顶部

获取属性\成员列表一个重要的应用就是,一次取出模型中的属性\成员变量,根据它的名字获取字典中的key然后取出字典中这个key对应的value,使用setValue: forKeyPath:方法设置值。为什么要这样,而不再使用方法setValuesForKeysWithDictionary:。因为在setValuesForKeysWithDictionary:方法内部会执行这样一个过程

遍历字典里面的所有key,一个一个取出来,遍历每个key按照以下过程

1.取出key,

2.取出keyvalue,即dict[key],直接给模型的属性\成员变量赋值

3.怎么给模型的属性赋值,使用方法setValue:value forKeyPath:key进行赋值,这个方法的执行过程在前面已经提到。

因此,开发中经常遇到的字典中的key比模型中多时,会出现的 this class is not key-value compliant for ‘xxx’这个bug就很好解释了,通常是因为字典中的key,比模型中的属性\成员变量多。那么当模型中的属性比字典中多时,使用setValuesForKeysWithDictionary:会不不会有bug呢?经测试:当多出来的属性是对象数据类型时,为null,当属性是基本数据类型时,会有一个系统默认值(int0)

因此使用逐一为属性赋值的方法进行KVC

Class clazz = Person.class;

unsigned int count = 0;


Person *person = [[Person alloc]init];

NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75};


Ivar *ivars = class_copyIvarList(clazz, &count);

// NSLog(@"%tu", count); // 3

for (int i = 0; i < count; i++) {

    const char *cname = ivar_getName(ivars[i]);

    NSString *name = [NSString stringWithUTF8String:cname];

    NSString *key = [name substringFromIndex:1]; // 去掉'_'

    [person setValue:dict[key] forKey:key];

}

NSLog(@"%@", person); // 已经重写了description方法

输出是:

<Person, 0x7ff15b80f230>{ name = zhangsan, height = 1.750000, age = 19}

使用这种方式进行kvc,即使字典中的key多的时候也不会有bug但是新的问题出现了,如果模型中的属性比字典中的key多便会出现bug而且:如果多的是对象类型不会有bug,该属性的值为null,如果是基本数据类型就会出错could not set nil as the value for the key ‘xxx’。例如,将上面的字典改为:

NSDictionary *dict = @{@"age":@19, @"height": @1.75}; // 去掉了name NSString类型

修改之后输出为:

<Person, 0x7f996263fbd0>{ name = (null), height = 1.750000, age = 19}

如果将字典改为:

NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19}; // 去掉了height float类型

程序直接崩溃。

如何解决上面的bug:可以在setValue:value forKeyPath:key方法调用之前进行如下处理:取出属性对应的类型,如果类型是基本数据类型,value替换为默认值(int对应默认值为0)

runtime提供的ivar_getTypeEncoding函数可以获取到属性的类型,返回值代表的含义如下:

Snip20150606_3.png

heightfloat类型对应的TypeCode"f"因此可以进行过滤一下,代码改动如下:

Class clazz = Person.class;

unsigned int count = 0;


Person *person = [[Person alloc]init];

NSDictionary *dict = @{@"name":@"zhangsan",@"age":@19, @"height": @1.75};


Ivar *ivars = class_copyIvarList(clazz, &count);

for (int i = 0; i < count; i++) {

    const char *cname = ivar_getName(ivars[i]);

    NSString *name = [NSString stringWithUTF8String:cname];

    NSString *key = [name substringFromIndex:1];

    

    const char *coding = ivar_getTypeEncoding(ivars[i]); // 获取类型

    NSString *strCode = [NSString stringWithUTF8String:coding];

    id value = dict[key];

    if ([strCode isEqualToString:@"f"]) {// 判断类型是否是float

        value = @(0.0);

    }

    

    [person setValue:value forKey:key];

}

NSLog(@"%@", person);

这样就可以正常执行了,输出为:

<Person, 0x7fc75d004a00>{ name = zhangsan, height = 0.000000, age = 19}

3_1_2.应用2:NSCoding归档和解档

回到顶部

获取属性\成员列表另外一个重要的应用就是进行归档和解档,其原理和上面的kvc基本上一样,这里只是展示一些代码:

- (void)encodeWithCoder:(NSCoder *)aCoder {

    unsigned int count = 0;

    Ivar *ivars = class_copyIvarList(self.class, &count);

    for (int i = 0; i < count; i++) {

        const char *cname = ivar_getName(ivars[i]);

        NSString *name = [NSString stringWithUTF8String:cname];

        NSString *key = [name substringFromIndex:1];

        

        id value = [self valueForKey:key]; // 取出key对应的value

        [aCoder encodeObject:value forKey:key]; // 编码

    }

}

- (id)initWithCoder:(NSCoder *)aDecoder {

    if (self = [super init]) {

        unsigned int count = 0;

        Ivar *ivars = class_copyIvarList(self.class, &count);

        for (int i = 0; i < count; i++) {

            const char *cname = ivar_getName(ivars[i]);

            NSString *name = [NSString stringWithUTF8String:cname];

            NSString *key = [name substringFromIndex:1];

            

            id value = [aDecoder decodeObjectForKey:key]; // 解码

            [self setValue:value forKey:key]; // 设置key对应的value

        }

    }

    return self;    

}

3_2.交换方法实现

回到顶部

交换方法实现的需求场景:自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)

可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码 的情况下,就完成了项目的改进。

交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。

// 程序一运行的时候调用

+ (void)load

{

// 如果是类方法,使用的是class_getClassMethod,如果是对象方法使用的是class_getInstanceMethod

    Method methodOne = class_getInstanceMethod(self, @selector(methodOne:));

    Method methodTwo = class_getInstanceMethod(self, @selector(methodTwo:));

    // 交换两个方法的实现

    method_exchangeImplementations(methodOne, methodTwo);

}

注意的是

1.可以交换的两个方法的参数必须是匹配的,参数的类型一致。

2.如果在方法one的内部想要调用方法two,此时在方法one的内部应该用one调用,而实际上是在调用two,否则会造成死循环。

例如:

// 交换前

- (NSString *) methodOne:(NSString *)str{

NSLog(@"%@", [self methodTwo:str]);

return "suc";

}

// 交换后 在方法的实现中,要注意将调用two的地方,换成自己的名字

- (NSString *) methodOne:(NSString *)str{

NSLog(@"%@", [self methodOne:str]);

return "suc";

}

任何一个方法都有两个重要的属性:SEL是方法的编号 ,IMP是方法的实现,方法的调用过程实际上去根据SEL去寻找IMP

在这个例子中,假设在交换之前SELmethodOne:的方法指向着IMP1SELmethodTwo的方法指向着IMP2

交换实现实际上是在底层是交换了方法编号的指向,也就是让methodOne:指向了IMP2methodTwo指向了IMP1

3_3.\对象的关联对象

回到顶部

关联对象不是为类\对象添加属性或者成员变量(因为在设置关联后也无法通过ivarList或者propertyList取得) ,而是为类添加一个相关的对象,通常用于存储类信息,例如存储类的属性列表数组,为将来字典转模型的方便。 例如,将属性的名称存到数组中设置关联

const char* propertiesKey = "propertiesKey";

unsigned int count = 0;

Ivar *ivars = class_copyIvarList([Person class], &count);


NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];


for (unsigned int i = 0; i < count; ++i) {

    Ivar pty = ivars[i];

    const char *cname = ivar_getName(ivars[i]);

    NSString *name = [NSString stringWithUTF8String:cname];

    NSString *key = [name substringFromIndex:1]; // 去掉_

    [arrayM addObject:key];

}

free(ivars);

objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC);

NSLog(@"%@", arrayM);

输出是

(

    age,

    height,

    name

)

objc_setAssociatedObject方法的参数解释:

第一个参数id object, 当前对象

第二个参数const void *key, 关联的key,可以是任意类型 

第三个参数id value, 被关联的对象 

第四个参数objc_AssociationPolicy policy关联引用的规则,取值有以下几种:

enum {

   OBJC_ASSOCIATION_ASSIGN = 0,

   OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,

   OBJC_ASSOCIATION_COPY_NONATOMIC = 3,

   OBJC_ASSOCIATION_RETAIN = 01401,

   OBJC_ASSOCIATION_COPY = 01403

};

如果想要获取已经关联的对象,通过key取得即可

NSArray *pList = objc_getAssociatedObject(Person, propertiesKey);

可以将以上两种操作封装起来,Person类增加一个properties类方法,封装上面的操作,用于方便获取类的属性列表。

const char* propertiesKey = "propertiesKey";


@implementation Person

+ (NSArray *)properties {

    // 如果已经关联了,就依据key取出被关联的对象并返回

    NSArray *pList = objc_getAssociatedObject(self, propertiesKey);

    if (pList != nil) {

        return pList;

    }

    // 如果没有关联,则设置关联对象,并将对象返回

    unsigned int count = 0;

    Ivar *ivars = class_copyIvarList([self class], &count);

    

    NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];

    

    for (unsigned int i = 0; i < count; ++i) {

        Ivar pty = ivars[i];

        const char *cname = ivar_getName(ivars[i]);

        NSString *name = [NSString stringWithUTF8String:cname];

        NSString *key = [name substringFromIndex:1];

        [arrayM addObject:key];

    }

    free(ivars);

    objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC);

    return arrayM.copy;

}

@end

3_4.动态添加方法,拦截未实现的方法

回到顶部

每个类都有都有一下两个类方法(来自NSObject)

+ (BOOL)resolveClassMethod:(SEL)sel 

+ (BOOL)resolveInstanceMethod:(SEL)sel

以上两个一个使用于类方法,一个适用于对象方法。在代码中调用没有实现的方法时,也就是sel标识的方法没有实现 都会现调用这两个方法中的一个(如果是类方法就调用第一个,如果是对象方法就调用第二个)拦截。 通常的做法是在resolve的内部指定sel对应的IMP,从而完成方法的动态创建和调用两个过程,也可以不指定IMP打印错误信息后直接返回。

假如在Person类中没有-sayHi这个方法,如果对象p使用[p performSelector:@selector(sayHi) withObject:nil];那么就会必须经过Person类的resolveInstanceMethod:(SEL)sel方法,在这里为-sayHi指定实现。

void abc(id self, SEL _cmd){

    NSLog(@"%@说了hello", [self name]);

}


@implementation Person


//动态添加方法:在resolve中添加相应的方法,注意是类方法还是对象方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

    if ([NSStringFromSelector(sel) isEqualToString:@"sayHi"]) {

        class_addMethod(self, sel, abc, "v@:"); // sel指定实现为abc

    }

    return YES;

}

@end

对实现(abc)的前两个参数的说明

每个方法的内部都默认包含两个参数,被称为隐式参数

id类型self(代表类或对象)SEL类型的_cmd(方法编号)

class_addMethod函数参数的含义:

第一个参数Class cls, 类型

第二个参数SEL name, 被解析的方法

第三个参数 IMP imp, 指定的实现

第四个参数const char *types,方法的类型,具体参照类型的codeType那张图,但是要注意一点:Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).译为:因为函数必须至少有两个参数self_cmd,第二个和第三个字符必须是“@:”。如果想要再增加参数,就可以从实现的第三个参数算起,看下面的例子就明白。

返回值:YES if the method was found and added to the receiver, otherwise NO.

-sayHi方法的实现增加参数

调用时:

Person *p = [[Person alloc] init];

    

p.name = @"zhangsan";

p.age = 10;

[p performSelector:@selector(sayHi:) withObject:@"world"]; // 增加了一个参数,多了冒号

Person类中的代码做了修改

void abc(id self, SEL _cmd, NSString *content){ // 增加了一个参数content

    NSLog(@"%@说了hello%@", [self name], content);

}


@implementation Person


//动态添加方法:在resolve中添加相应的方法,注意是类方法还是对象方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

    if ([NSStringFromSelector(sel) isEqualToString:@"sayHi:"]) {

        class_addMethod(self, sel, abc, "v@:@"); // 增加了一个对象类型参数 增加了@

    }

    return YES;

}

@end

输出为:

zhangsan说了helloworld

3_5.动态创建一个类

回到顶部

动态创建一个类,为这个类添加成员变量和方法,并创建这个类型的对象:

#import "ViewController.h"

#import <objc/runtime.h>

#import <objc/message.h>

#import "Person.h"


static void printSchool(id self, SEL _cmd) {

    NSLog(@"我的学校是%@", [self valueForKey:@"schoolName"]);

}


@implementation ViewController


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    Class classStudent = objc_allocateClassPair(Person.class, "Student", 0);

    

    // 添加一个NSString的变量,第四个参数是对其方式,第五个参数是参数类型

    if (class_addIvar(classStudent, "schoolName", sizeof(NSString *), 0, "@")) {

        NSLog(@"添加成员变量schoolName成功");

    }

    

    // Student类添加方法 "v@:"这种写法见参数类型连接

    if (class_addMethod(classStudent, @selector(printSchool), (IMP)printSchool, "v@:")) {

        NSLog(@"添加方法printSchool:成功");

    }

    

    // 注册这个类到runtime系统中就可以使用他了

    objc_registerClassPair(classStudent); // 返回void

    

    

    

    // 使用创建的类

    id student = [[classStudent alloc] init];

    NSString *schoolName = @"清华大学";

    // 给刚刚添加的变量赋值

    // object_setInstanceVariable(student, "schoolName", (void *)&str);ARC下不允许使用

    [student setValue:schoolName forKey:@"schoolName"];

    // 调用printSchool方法,也就是给student这个接受者发送printSchool:这个消息

//    objc_msgSend(student, "printSchool"); // 我尝试用这种方法调用但是没有成功

    [student performSelector:@selector(printSchool) withObject:nil]; // 动态调用未显式在类中声明的方法

    

}

@end

输出的结果是:

添加成员变量schoolName成功

添加方法printSchool成功

我的学校是清华大学



文章链接:

http://www.cnblogs.com/jys509/p/5207159.html

http://www.cnblogs.com/Mike-zh/p/4557014.html#anchor3_1

http://blog.csdn.net/cerastes/article/details/52690775



weixin073智慧旅游平台开发微信小程序+ssm后端毕业源码案例设计 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值