Objective-C Runtime 1小时入门教程

2023-11-18

原文出处:  ian(@ianisme)   

一、前言

如果你没有Objective-C基础,请学习了基础的iOS开发再来,这个1小时是给有一定iOS基础的童鞋的。如果你是大牛或者你感觉Objective-C Runtime太简单不用1小时学习的,也请您绕道,这或许只是我的私人笔记了。

请跟着教程“一步步来”,请不要大概地扫两眼就说看不懂——以这种态度写成什么样你也看不懂。这是1小时入门教程,请不要试图在1分钟内入门!

二、本文目标

1小时让你知道什么是Objective-C Runtime,并对它有一定的基本了解,可以在开发过程中运用自如。

三、Objective-C Runtime到底是什么东西?

简而言之,Objective-C Runtime是一个将C语言转化为面向对象语言的扩展。
我们将C++和Objective进行对比,虽然C++和Objective-C都是在C的基础上加入面向对象的特性扩充而成的程序设计语言,但二者实现的机制差异很大。C++是基于静态类型,而Objective-C是基于动态运行时类型。也就是说用C++编写的程序编译时就直接编译成了可令机器读懂的机器语言;用Objective-C编写的程序不能直接编译成可令机器读懂的机器语言,而是在程序运行的时候,通过Runtime把程序转为可令机器读懂的机器语言。Runtime是Objective不可缺少的重要一部分。
传送门->runtime源码

四、Objective-C的元素认知

4.1 id和Class

打开/Public Headers/objc.h文件可以看到如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class * Class ;
 
/// Represents an instance of a class.
struct objc_object {
     Class isa   OBJC_ISA_AVAILABILITY ;
} ;
 
/// A pointer to an instance of a class.
typedef struct objc_object * id ;
#endif

Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。

打开/Public Headers/runtime.h文件
objc_class的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct objc_class * Class ;
struct objc_class {
Class isa                                 OBJC_ISA_AVAILABILITY ; // metaclass
#if !__OBJC2__
Class super_class                         OBJC2_UNAVAILABLE ; // 父类
const char * name                           OBJC2_UNAVAILABLE ; // 类名
long version                               OBJC2_UNAVAILABLE ; // 类的版本信息,默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取
long info                                 OBJC2_UNAVAILABLE ; // 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size                         OBJC2_UNAVAILABLE ; // 该类的实例变量大小(包括从父类继承下来的实例变量)
struct objc_ivar_list * ivars               OBJC2_UNAVAILABLE ; // 该类的成员变量地址列表
struct objc_method_list * * methodLists     OBJC2_UNAVAILABLE ; // 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache * cache                   OBJC2_UNAVAILABLE ; // 缓存最近使用的方法地址,用于提升效率;
struct objc_protocol_list * protocols       OBJC2_UNAVAILABLE ; // 存储该类声明遵守的协议的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */

由以上代码可见,类与对象的区别就是类比对象多了很多特征成员,类也可以当做一个objc_object来对待,也就是说类和对象都是对象,分别称作类对象(class object)和实例对象(instance object),这样我们就可以区别对象和类了。

isa:objc_object(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类)其中存放着普通成员变量与动态方法(“-”开头的方法);此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法)。

super_class: 指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为nil。

类与对象的继承层次关系如图(图片源自网络):

objective-runtime-1

所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身。

4.2 SEL

SEL是selector在Objective-C中的表示类型。selector可以理解为区别方法的ID。

1
typedef struct objc_selector * SEL ;

objc_selector的定义如下:

1
2
3
4
struct objc_selector {
     char * name ;                        OBJC2_UNAVAILABLE ; // 名称
     char * types ;                        OBJC2_UNAVAILABLE ; // 类型
} ;

name和types都是char类型。

4.3 IMP

终于到IMP了,它在objc.h中得定义如下:

1
typedef id ( * IMP ) ( id , SEL , . . . ) ;

IMP是“implementation”的缩写,它是由编译器生成的一个函数指针。当你发起一个消息后(下文介绍),这个函数指针决定了最终执行哪段代码。

4.4 Method

Method代表类中的某个方法的类型。

1
typedef struct objc_method * Method ;

objc_method的定义如下:

1
2
3
4
5
struct objc_method {
     SEL method_name                   OBJC2_UNAVAILABLE ; // 方法名
     char * method_types                 OBJC2_UNAVAILABLE ; // 方法类型
     IMP method_imp                     OBJC2_UNAVAILABLE ; // 方法实现
}

方法名method_name类型为SEL,上文提到过。
方法类型method_types是一个char指针,存储着方法的参数类型和返回值类型。
方法实现method_imp的类型为IMP,上文提到过。

4.5 Ivar

Ivar代表类中实例变量的类型

1
typedef struct objc_ivar * Ivar ;

objc_ivar的定义如下:

1
2
3
4
5
6
7
8
struct objc_ivar {
     char * ivar_name                   OBJC2_UNAVAILABLE ; // 变量名
     char * ivar_type                   OBJC2_UNAVAILABLE ; // 变量类型
     int ivar_offset                   OBJC2_UNAVAILABLE ; // 基地址偏移字节
#ifdef __LP64__
     int space                         OBJC2_UNAVAILABLE ; // 占用空间
#endif
}

4.6 objc_property_t

objc_property_t是属性,它的定义如下:

1
typedef struct objc_property * objc_property_t ;

objc_property是内置的类型,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等。它的定义如下:

1
2
3
4
typedef struct {
     const char * name ; // 名称
     const char * value ;    // 值(通常是空的)
} objc_property_attribute_t ;

4.7 Cache

Catch的定义如下:

1
typedef struct objc_cache * Cache

objc_cache的定义如下:

1
2
3
4
5
struct objc_cache {
     unsigned int mask                   OBJC2_UNAVAILABLE ;
     unsigned int occupied               OBJC2_UNAVAILABLE ;
     Method buckets [ 1 ]                    OBJC2_UNAVAILABLE ;
} ;

mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置。
occupied: 实际占用cache buckets的总数。
buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
objc_msgSend(下文讲解)每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

4.8 Catagory

这个就是我们平时所说的类别了,很熟悉吧。它可以动态的为已存在的类添加新的方法。
它的定义如下:

1
typedef struct objc_category * Category ;

objc_category的定义如下:

1
2
3
4
5
6
7
struct objc_category {
     char * category_name                           OBJC2_UNAVAILABLE ; // 类别名称
     char * class_name                               OBJC2_UNAVAILABLE ; // 类名
     struct objc_method_list * instance_methods     OBJC2_UNAVAILABLE ; // 实例方法列表
     struct objc_method_list * class_methods         OBJC2_UNAVAILABLE ; // 类方法列表
     struct objc_protocol_list * protocols           OBJC2_UNAVAILABLE ; // 协议列表
}

因为是入门,以上就列举这些吧!

五、Objective-C的消息传递

5.1 基本消息传递

在面向对象编程中,对象调用方法叫做发送消息。在编译时,程序的源代码就会从对象发送消息转换成Runtime的objc_msgSend函数调用。
例如某实例变量receiver实现某一个方法oneMethod

1
[ receiver oneMethod ] ;

Runtime会将其转成类似这样的代码

1
objc_msgSend ( receiver , selector ) ;

具体会转换成什么代码呢?
Runtime会根据类型自动转换成下列某一个函数:
objc_msgSend:普通的消息都会通过该函数发送
objc_msgSend_stret:消息中有数据结构作为返回值(不是简单值)时,通过此函数发送和接收返回值
objc_msgSendSuper:和objc_msgSend类似,这里把消息发送给父类的实例
objc_msgSendSuper_stret:和objc_msgSend_stret类似,这里把消息发送给父类的实例并接收返回值
当消息被发送到实例对象时,是如图所示处理的(图片源自网络):

objective-runtime-2

objc_msgSend函数的调用过程:

  • 第一步:检测这个selector是不是要忽略的。
  • 第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉。
  • 第三步:
    1.调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根class;
    2.当我们调用某个某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根metaclass;
  • 第四部:前三部都找不到就会进入动态方法解析(看下文)。

5.2 消息动态解析

动态解析流程图(图片来自网络):

objective-runtime-6

请参照图片品味以下步骤(实例请看下文《6.6 苍老师的唱歌篇》):

  • 第一步:通过resolveInstanceMethod:方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;
  • 第二步:这步会进入forwardingTargetForSelector:方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三部;
  • 第三部:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;
  • 第四部:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash。

到这里大家可能晕乎乎的,下面看实战篇吧!苍老师必须让你懂!

六、Runtime实战

请大家放心,以下所有实战篇,在最后都会分享Demo给大家!

6.1 苍老师问好篇

苍老师见到我们广大的粉丝们,第一反应当然是:大家好!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
// 自定义一个方法
void sayFunction ( id self , SEL _cmd , id some ) {
     NSLog ( @ "%@岁的%@说:%@" , object_getIvar ( self , class_getInstanceVariable ( [ self class ] , "_age" ) ) , [ self valueForKey : @ "name" ] , some ) ;
}
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         // 动态创建对象 创建一个Person 继承自 NSObject类
         Class People = objc_allocateClassPair ( [ NSObject class ] , "Person" , 0 ) ;
 
         // 为该类添加NSString *_name成员变量
         class_addIvar ( People , "_name" , sizeof ( NSString* ) , log2 ( sizeof ( NSString* ) ) , @ encode ( NSString* ) ) ;
         // 为该类添加int _age成员变量
         class_addIvar ( People , "_age" , sizeof ( int ) , sizeof ( int ) , @ encode ( int ) ) ;
 
         // 注册方法名为say的方法
         SEL s = sel_registerName ( "say:" ) ;
         // 为该类增加名为say的方法
         class_addMethod ( People , s , ( IMP ) sayFunction , "v@:@" ) ;
 
         // 注册该类
         objc_registerClassPair ( People ) ;
 
         // 创建一个类的实例
         id peopleInstance = [ [ People alloc ] init ] ;
 
         // KVC 动态改变 对象peopleInstance 中的实例变量
         [ peopleInstance setValue : @ "苍老师" forKey : @ "name" ] ;
 
         // 从类中获取成员变量Ivar
         Ivar ageIvar = class_getInstanceVariable ( People , "_age" ) ;
         // 为peopleInstance的成员变量赋值
         object_setIvar ( peopleInstance , ageIvar , @ 18 ) ;
 
         // 调用 peopleInstance 对象中的 s 方法选择器对于的方法
         // objc_msgSend(peopleInstance, s, @"大家好!"); // 这样写也可以,请看我博客说明
         ( ( void ( * ) ( id , SEL , id ) ) objc_msgSend ) ( peopleInstance , s , @ "大家好" ) ;
 
         peopleInstance = nil ; //当People类或者它的子类的实例还存在,则不能调用objc_disposeClassPair这个方法;因此这里要先销毁实例对象后才能销毁类;
 
         // 销毁类
         objc_disposeClassPair ( People ) ;
 
     }
     return 0 ;
}

最后的结果是:18岁的苍老师说:大家好!
在使用

1
objc_msgSend ( peopleInstance , s , @ "大家好!" ) ;

默认会出现以下错误:
objc_msgSend()报错Too many arguments to function call ,expected 0,have3
直接通过objc_msgSend(self, setter, value)是报错,说参数过多。
请这样解决:
Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改为 NO
当然你也可以这样写(推荐):

1
( ( void ( * ) ( id , SEL , id ) ) objc_msgSend ) ( peopleInstance , s , @ "大家好" ) ;

强制转换objc_msgSend函数类型为带三个参数且返回值为void函数,然后才能传三个参数。
此实战内容是,动态创建一个类,并创建成员变量和方法,最后赋值成员变量并发送消息。其中成员变量的赋值使用了KVC和object_setIvar函数两种方式,这些东西大家举一反三就可以了。

Demo传送门->6.1苍老师问好篇Demo

6.2 苍老师的特征篇

苍老师在大家心目中应该有很多特征吧,下面我们通过代码来获取苍老师的特征。
People.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ interface People : NSObject
{
     NSString * _occupation ;
     NSString * _nationality ;
}
 
@ property ( nonatomic , copy ) NSString * name ;
@ property ( nonatomic ) NSUInteger age ;
 
- ( NSDictionary * ) allProperties ;
- ( NSDictionary * ) allIvars ;
- ( NSDictionary * ) allMethods ;
 
@ end

People.m文件

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@implementation People
 
- ( NSDictionary * ) allProperties
{
     unsigned int count = 0 ;
 
     // 获取类的所有属性,如果没有属性count就为0
     objc_property_t *properties = class_copyPropertyList ( [ self class ] , & count ) ;
     NSMutableDictionary *resultDict = [ < a href = "http://www.jobbole.com/members/www821839432" > @ { } < / a > mutableCopy ] ;
 
     for ( NSUInteger i = 0 ; i < count ; i ++ ) {
 
         // 获取属性的名称和值
         const char *propertyName = property_getName ( properties [ i ] ) ;
         NSString *name = [ NSString stringWithUTF8String :propertyName ] ;
         id propertyValue = [ self valueForKey :name ] ;
 
         if ( propertyValue ) {
             resultDict [ name ] = propertyValue ;
         } else {
             resultDict [ name ] = @"字典的key对应的value不能为nil哦!" ;
         }
     }
 
     // 这里properties是一个数组指针,我们需要使用free函数来释放内存。
     free ( properties ) ;
 
     return resultDict ;
}
 
- ( NSDictionary * ) allIvars
{
     unsigned int count = 0 ;
 
     NSMutableDictionary *resultDict = [ < a href = "http://www.jobbole.com/members/www821839432" > @ { } < / a > mutableCopy ] ;
 
     Ivar *ivars = class_copyIvarList ( [ self class ] , & count ) ;
 
     for ( NSUInteger i = 0 ; i < count ; i ++ ) {
 
         const char *varName = ivar_getName ( ivars [ i ] ) ;
         NSString *name = [ NSString stringWithUTF8String :varName ] ;
         id varValue = [ self valueForKey :name ] ;
 
         if ( varValue ) {
             resultDict [ name ] = varValue ;
         } else {
             resultDict [ name ] = @"字典的key对应的value不能为nil哦!" ;
         }
 
     }
 
     free ( ivars ) ;
 
     return resultDict ;
}
 
- ( NSDictionary * ) allMethods
{
     unsigned int count = 0 ;
 
     NSMutableDictionary *resultDict = [ < a href = "http://www.jobbole.com/members/www821839432" > @ { } < / a > mutableCopy ] ;
 
     // 获取类的所有方法,如果没有方法count就为0
     Method *methods = class_copyMethodList ( [ self class ] , & count ) ;
 
     for ( NSUInteger i = 0 ; i < count ; i ++ ) {
 
         // 获取方法名称
         SEL methodSEL = method_getName ( methods [ i ] ) ;
         const char *methodName = sel_getName ( methodSEL ) ;
         NSString *name = [ NSString stringWithUTF8String :methodName ] ;
 
         // 获取方法的参数列表
         int arguments = method_getNumberOfArguments ( methods [ i ] ) ;
 
         resultDict [ name ] = @ ( arguments - 2 ) ;
     }
 
     free ( methods ) ;
 
     return resultDict ;
}
 
@end

在main.m中运行以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         People * cangTeacher = [ [ People alloc ] init ] ;
         cangTeacher . name = @ "苍井空" ;
         cangTeacher . age = 18 ;
         [ cangTeacher setValue : @ "老师" forKey : @ "occupation" ] ;
 
         NSDictionary * propertyResultDic = [ cangTeacher allProperties ] ;
         for ( NSString * propertyName in propertyResultDic . allKeys ) {
             NSLog ( @ "propertyName:%@, propertyValue:%@" , propertyName , propertyResultDic [ propertyName ] ) ;
         }
 
         NSDictionary * ivarResultDic = [ cangTeacher allIvars ] ;
         for ( NSString * ivarName in ivarResultDic . allKeys ) {
             NSLog ( @ "ivarName:%@, ivarValue:%@" , ivarName , ivarResultDic [ ivarName ] ) ;
         }
 
         NSDictionary * methodResultDic = [ cangTeacher allMethods ] ;
         for ( NSString * methodName in methodResultDic . allKeys ) {
             NSLog ( @ "methodName:%@, argumentsCount:%@" , methodName , methodResultDic [ methodName ] ) ;
         }
 
     }
     return 0 ;
}

最后的输出结果如下:

objective-runtime-3

是不是有点失望,我没有加一些特殊的技能,留给下文了。此实战内容是通过苍老师的一些特征学习:如何获取对象所有的属性名称和属性值、获取对象所有成员变量名称和变量值、获取对象所有的方法名和方法参数数量。

Demo传送门->6.2苍老师的特征篇Demo

6.3 苍老师增加新技能篇

苍老师要通过Category和Associated Objects增加技能了,快看!
创建People+Associated.h文件如下:

1
2
3
4
5
6
7
8
9
10
#import "People.h"
 
typedef void ( ^ CodingCallBack ) ( ) ;
 
@ interface People ( Associated )
 
@ property ( nonatomic , strong ) NSNumber * associatedBust ; // 胸围
@ property ( nonatomic , copy ) CodingCallBack associatedCallBack ;    // 写代码
 
@ end

People+Associated.m如下:

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#import "People+Associated.h"
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@implementation People ( Associated )
 
- ( void ) setAssociatedBust : ( NSNumber * ) bust
{
     // 设置关联对象
     objc_setAssociatedObject ( self , @selector ( associatedBust ) , bust , OBJC_ASSOCIATION_RETAIN_NONATOMIC ) ;
}
- ( NSNumber * ) associatedBust
{
     // 得到关联对象
     return objc_getAssociatedObject ( self , @selector ( associatedBust ) ) ;
}
 
- ( void ) setAssociatedCallBack : ( CodingCallBack ) callback {
     objc_setAssociatedObject ( self , @selector ( associatedCallBack ) , callback , OBJC_ASSOCIATION_COPY_NONATOMIC ) ;
}
 
- ( CodingCallBack ) associatedCallBack {
     return objc_getAssociatedObject ( self , @selector ( associatedCallBack ) ) ;
}
 
@end

在main.m中运行以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#import "People.h"
#import "People+Associated.h"
 
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         People * cangTeacher = [ [ People alloc ] init ] ;
         cangTeacher . name = @ "苍井空" ;
         cangTeacher . age = 18 ;
         [ cangTeacher setValue : @ "老师" forKey : @ "occupation" ] ;
         cangTeacher . associatedBust = @ ( 90 ) ;
         cangTeacher . associatedCallBack = ^ ( ) {
 
             NSLog ( @ "苍老师要写代码了!" ) ;
 
         } ;
         cangTeacher . associatedCallBack ( ) ;
 
         NSDictionary * propertyResultDic = [ cangTeacher allProperties ] ;
         for ( NSString * propertyName in propertyResultDic . allKeys ) {
             NSLog ( @ "propertyName:%@, propertyValue:%@" , propertyName , propertyResultDic [ propertyName ] ) ;
         }
 
         NSDictionary * methodResultDic = [ cangTeacher allMethods ] ;
         for ( NSString * methodName in methodResultDic . allKeys ) {
             NSLog ( @ "methodName:%@, argumentsCount:%@" , methodName , methodResultDic [ methodName ] ) ;
         }
 
     }
     return 0 ;
}

这次运行结果多出了一个associatedBust(胸围)和一个associatedCallBack(写代码)属性。
如图:

objective-runtime-4

我们成功的给苍老师添加个一个胸围的属性和一个写代码的回调,但是添加属性没有什么意义,我们平时在开发过成功中用的比较多的就是添加回调了。

Demo传送门->6.3苍老师增加新技能篇Demo

6.4 苍老师的资料归档篇

苍老师的资料总要整理一下吧!
创建People.h

Objective-C
1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>
 
@interface People : NSObject <NSCoding>
 
@property ( nonatomic , copy ) NSString *name ; // 姓名
@property ( nonatomic , strong ) NSNumber *age ; // 年龄
@property ( nonatomic , copy ) NSString *occupation ; // 职业
@property ( nonatomic , copy ) NSString *nationality ; // 国籍
 
@end
P

People.m

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#import "People.h"
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@implementation People
 
- ( void ) encodeWithCoder : ( NSCoder * ) aCoder {
     unsigned int count = 0 ;
     Ivar *ivars = class_copyIvarList ( [ People class ] , & count ) ;
     for ( NSUInteger i = 0 ; i < count ; i ++ ) {
         Ivar ivar = ivars [ i ] ;
         const char *name = ivar_getName ( ivar ) ;
         NSString *key = [ NSString stringWithUTF8String :name ] ;
         id value = [ self valueForKey :key ] ;
         [ aCoder encodeObject :value forKey :key ] ;
     }
     free ( ivars ) ;
}
 
- ( id ) initWithCoder : ( NSCoder * ) aDecoder {
     self = [ super init ] ;
     if ( self ) {
         unsigned int count = 0 ;
         Ivar *ivars = class_copyIvarList ( [ People class ] , & count ) ;
         for ( NSUInteger i = 0 ; i < count ; i ++ ) {
             Ivar ivar = ivars [ i ] ;
             const char *name = ivar_getName ( ivar ) ;
             NSString *key = [ NSString stringWithUTF8String :name ] ;
             id value = [ aDecoder decodeObjectForKey :key ] ;
             [ self setValue :value forKey :key ] ;
         }
         free ( ivars ) ;
     }
     return self ;
}
 
@end

Demo传送门->6.4苍老师的资料归档篇Demo

6.5 苍老师的资料转换篇

服务器返回了大量苍老师的数据,手机端这边接收后如何去转换呢?当然是要将JSON转换为Model啦!
相信平时你们的项目中也用到过这些三方库,下面我们去了解下runtime实现JSON和Model互转。
创建People.h

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>
 
@interface People : NSObject
 
@property ( nonatomic , copy ) NSString *name ; // 姓名
@property ( nonatomic , strong ) NSNumber *age ; // 年龄
@property ( nonatomic , copy ) NSString *occupation ; // 职业
@property ( nonatomic , copy ) NSString *nationality ; // 国籍
 
// 生成model
- ( instancetype ) initWithDictionary : ( NSDictionary * ) dictionary ;
 
// 转换成字典
- ( NSDictionary * ) covertToDictionary ;
 
@end

People.m的代码如下:

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#import "People.h"
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@implementation People
 
- ( instancetype ) initWithDictionary : ( NSDictionary * ) dictionary
{
     self = [ super init ] ;
 
     if ( self ) {
         for ( NSString *key in dictionary . allKeys ) {
             id value = dictionary [ key ] ;
 
             SEL setter = [ self propertySetterByKey :key ] ;
             if ( setter ) {
                 // 这里还可以使用NSInvocation或者method_invoke,不再继续深究了,有兴趣google。
                 ( ( void ( * ) ( id , SEL , id ) ) objc_msgSend ) ( self , setter , value ) ;
             }
         }
     }
     return self ;
}
 
- ( NSDictionary * ) covertToDictionary
{
     unsigned int count = 0 ;
     objc_property_t *properties = class_copyPropertyList ( [ self class ] , & count ) ;
 
     if ( count != 0 ) {
         NSMutableDictionary *resultDict = [ < a href = "http://www.jobbole.com/members/www821839432" > @ { } < / a > mutableCopy ] ;
 
         for ( NSUInteger i = 0 ; i < count ; i ++ ) {
             const void *propertyName = property_getName ( properties [ i ] ) ;
             NSString *name = [ NSString stringWithUTF8String :propertyName ] ;
 
             SEL getter = [ self propertyGetterByKey :name ] ;
             if ( getter ) {
                 id value = ( ( id ( * ) ( id , SEL ) ) objc_msgSend ) ( self , getter ) ;
                 if ( value ) {
                     resultDict [ name ] = value ;
                 } else {
                     resultDict [ name ] = @"字典的key对应的value不能为nil哦!" ;
                 }
 
             }
         }
 
         free ( properties ) ;
 
         return resultDict ;
     }
 
     free ( properties ) ;
 
     return nil ;
}
 
#pragma mark - private methods
 
// 生成setter方法
- ( SEL ) propertySetterByKey : ( NSString * ) key
{
     // 首字母大写,你懂得
     NSString *propertySetterName = [ NSString stringWithFormat : @"set%@:" , key . capitalizedString ] ;
 
     SEL setter = NSSelectorFromString ( propertySetterName ) ;
     if ( [ self respondsToSelector :setter ] ) {
         return setter ;
     }
     return nil ;
}
 
// 生成getter方法
- ( SEL ) propertyGetterByKey : ( NSString * ) key
{
     SEL getter = NSSelectorFromString ( key ) ;
     if ( [ self respondsToSelector :getter ] ) {
         return getter ;
     }
     return nil ;
}
 
@end

main.m中运行以下代码:

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import <Foundation/Foundation.h>
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
#import "People.h"
 
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         NSDictionary *dict = @ {
                               @"name" : @"苍井空" ,
                               @"age"    : @ 18 ,
                               @"occupation" : @"老师" ,
                               @"nationality" : @"日本"
                               } ;
 
         // 字典转模型
         People *cangTeacher = [ [ People alloc ] initWithDictionary :dict ] ;
         NSLog ( @"热烈欢迎,从%@远道而来的%@岁的%@%@" , cangTeacher . nationality , cangTeacher . age , cangTeacher . name , cangTeacher . occupation ) ;
 
         // 模型转字典
         NSDictionary *covertedDict = [ cangTeacher covertToDictionary ] ;
         NSLog ( @"%@" , covertedDict ) ;
 
     }
     return 0 ;
}

最后输出内容如下:

objective-runtime-5

相信通过前面的学习,这些代码不用写过多的注释你也可以看懂了,我把假设是网络返回的苍老师的资料转化为了model,然后又将model转回字典。这是一个JSON转Model相互转换的一个思路,大家稍后运行Demo细细品味。

Demo传送门->6.5苍老师的资料转换篇Demo

6.6 苍老师的唱歌篇

这个实例主要是验证一下上文《5.2 消息动态解析》

第一首:

添加sing实例方法,但是不提供方法的实现。验证当找不到方法的实现时,动态添加方法。
创建People.h

Objective-C
1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>
 
@interface People : NSObject
 
@property ( nonatomic , copy ) NSString *name ;
 
- ( void ) sing ;
 
@end

创建People.m

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "People.h"
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@implementation People
 
+ ( BOOL ) resolveInstanceMethod : ( SEL ) sel
{
     // 我们没有给People类声明sing方法,我们这里动态添加方法
     if ( [ NSStringFromSelector ( sel ) isEqualToString : @"sing" ] ) {
         class_addMethod ( self , sel , ( IMP ) otherSing , "v@:" ) ;
         return YES ;
     }
     return [ super resolveInstanceMethod :sel ] ;
}
 
void otherSing ( id self , SEL cmd )
{
     NSLog ( @"%@ 唱歌啦!" , ( ( People * ) self ) . name ) ;
}

在main.m中运行以下代码:

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <Foundation/Foundation.h>
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
#import "People.h"
 
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         People *cangTeacher = [ [ People alloc ] init ] ;
         cangTeacher . name = @"苍老师" ;
         [ cangTeacher sing ] ;
 
     }
     return 0 ;
}

结果如下:

objective-runtime-7

我们没有提供苍老师唱歌的方法实现,因此在调用此方法的时候,会调用resolveInstanceMethod方法,我们动态添加了方法。我们也可以返回No,继续向下传递。(此处请返回《5.2 消息动态解析》第一步品味下)

Demo传送门->6.6苍老师唱歌篇(第一首)Demo

第二首

外面的小鸟在唱歌,但是苍老师的歌声盖过了小鸟,我们只能听到苍老师唱歌了。
这里我们不声明sing方法,将调用途中动态更换调用对象。
在第一首代码的基础上,创建Bird的model
Bird.h

1
2
3
4
5
6
7
#import  <Foundation/Foundation.h>
 
@ interface Bird : NSObject
 
@ property ( nonatomic , copy ) NSString * name ;
 
@ end

Bird.m

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#import "Bird.h"
#import "People.h"
 
@implementation Bird
 
// 第一步:我们不动态添加方法,返回NO,进入第二步;
+ ( BOOL ) resolveInstanceMethod : ( SEL ) sel
{
     return NO ;
}
 
// 第二部:我们不指定备选对象响应aSelector,进入第三步;
- ( id ) forwardingTargetForSelector : ( SEL ) aSelector
{
     return nil ;
}
 
// 第三步:返回方法选择器,然后进入第四部;
- ( NSMethodSignature * ) methodSignatureForSelector : ( SEL ) aSelector
{
     if ( [ NSStringFromSelector ( aSelector ) isEqualToString : @"sing" ] ) {
         return [ NSMethodSignature signatureWithObjCTypes : "v@:" ] ;
     }
 
     return [ super methodSignatureForSelector :aSelector ] ;
}
 
// 第四部:这步我们修改调用对象
- ( void ) forwardInvocation : ( NSInvocation * ) anInvocation
{
     // 我们改变调用对象为People
     People *cangTeacher = [ [ People alloc ] init ] ;
     cangTeacher . name = @"苍老师" ;
     [ anInvocation invokeWithTarget :cangTeacher ] ;
}
 
@end

main.m运行代码如下:

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <Foundation/Foundation.h>
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
#import "People.h"
#import "Bird.h"
 
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         Bird *bird = [ [ Bird alloc ] init ] ;
         bird . name = @"小小鸟" ;
 
         ( ( void ( * ) ( id , SEL ) ) objc_msgSend ) ( ( id ) bird , @selector ( sing ) ) ;
     }
     return 0 ;
}

运行结果如下:

objective-runtime-8

成功更换了对象,把对象更换为苍老师了。(此处请返回《5.2 消息动态解析》品味)

Demo传送门->6.6苍老师唱歌篇(第二首)Demo

第三首

苍老师不想唱歌想跳舞了。
这里我是实现不提供声明,不修改调用对象,但是将sing方法修改为dance方法。
创建People.h

1
2
3
4
5
#import <Foundation/Foundation.h>
 
@ interface People : NSObject
 
@ end

People.m

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import "People.h"
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@implementation People
 
// 第一步:我们不动态添加方法,返回NO,进入第二步;
+ ( BOOL ) resolveInstanceMethod : ( SEL ) sel
{
     return NO ;
}
 
// 第二部:我们不指定备选对象响应aSelector,进入第三步;
- ( id ) forwardingTargetForSelector : ( SEL ) aSelector
{
     return nil ;
}
 
// 第三步:返回方法选择器,然后进入第四部;
- ( NSMethodSignature * ) methodSignatureForSelector : ( SEL ) aSelector
{
     if ( [ NSStringFromSelector ( aSelector ) isEqualToString : @"sing" ] ) {
         return [ NSMethodSignature signatureWithObjCTypes : "v@:" ] ;
     }
 
     return [ super methodSignatureForSelector :aSelector ] ;
}
 
// 第四部:这步我们修改调用方法
- ( void ) forwardInvocation : ( NSInvocation * ) anInvocation
{
     [ anInvocation setSelector : @selector ( dance ) ] ;
     // 这还要指定是哪个对象的方法
     [ anInvocation invokeWithTarget :self ] ;
}
 
// 若forwardInvocation没有实现,则会调用此方法
- ( void ) doesNotRecognizeSelector : ( SEL ) aSelector
{
     NSLog ( @"消息无法处理:%@" , NSStringFromSelector ( aSelector ) ) ;
}
 
- ( void ) dance
{
     NSLog ( @"跳舞!!!come on!" ) ;
}
 
@end

在main.m中运行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <Foundation/Foundation.h>
 
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
#import "People.h"
 
int main ( int argc , const char * argv [ ] ) {
     @ autoreleasepool {
 
         People * cangTeacher = [ [ People alloc ] init ] ;
 
         ( ( void ( * ) ( id , SEL ) ) objc_msgSend ) ( ( id ) cangTeacher , @ selector ( sing ) ) ;
 
     }
     return 0 ;
}

结果如图:

objective-runtime-9

成功更换了方法,苍老师由唱歌改为跳舞了(此处请返回《5.2 消息动态解析》品味)

Demo传送门->6.6苍老师唱歌篇(第三首)Demo

总结

好吧,我承认我骗了你,当你读到这里你肯定花了不止1小时。都是我的错,不是因为你笨,之所以说1小时是为了让你有信心,有耐心继续下去。读到这里恭喜你已经在iOS开发的道路上又向前了一步!同时我也要感谢以下参考文献以及文章,是他们让我更好的理解了runtime,再次表示感谢!这篇文章断断续续写了将近一周的时间,您可以读到这里就是对我最大的鼓励,谢谢!

Demo传送门->所有的Demo打包下载

本文参考文献以及文章:

Objective-C Runtime Reference
Object Model
初识Objective-C Runtime
Objective-C Runtime
Objective-C Runtime 运行时之一:类与对象
Runtime Message Forwarding
runtime模型与字典互转
iOS开发之深入探讨runtime机制
Objective-C runtime之消息(二)
Objective-C runtime之消息转发机制(三)
《编写高质量代码:改善Objective-C程序的61个建议》
《正则表达式30分钟入门教程》(参考写作方式)



from:http://ios.jobbole.com/84028/


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Objective-C Runtime 1小时入门教程 的相关文章

随机推荐

  • thinkpad计算机快捷键,Thinkpad笔记本键盘F1-F12功能键详细介绍

    Thinkpad笔记本电脑F1 F12快捷键的功能是什么 笔记本电脑F1 F12总共12个功能键 每个键的作用不一样 同一个键 不同机型的作用也不一样 通过这些快捷键可以快速调整音量 亮度 开启无线功能等等 Thinkpad笔记本电脑机型特
  • 看小白如何解决ajax跨域问题

    由于此前很少写前端的代码 哈哈 不合格的程序员啊 最近项目中用到json作为系统间交互的手段 自然就伴随着众多ajax请求 随之而来的就是要解决ajax的跨域问题 本篇将讲述一个小白从遇到跨域不知道是跨域问题 到知道是跨域问题不知道如何解决
  • elementui把上传的图片转为base64

    使用组件 然后on change绑定一个方法来获取文件信息 auto upload设置为false即可
  • 具有文件操作和滤波功能的Open3D软件界面(附python pyqt 代码)

    安装依赖 pip install pyqtgraph i https pypi tuna tsinghua edu cn simple pip install pyopengl i https pypi tuna tsinghua edu
  • python 100以内的质数

    可以使用for循环 求100以内的质数 i 2 for j in range 1 101 如果100以内的数字除以2的余数等于0 if j i 0 如果是就跳过 继续下一个循环 continue print j 要找出从1到100之间的质数
  • 在 Python 中打印变量之间没有空格

    文章目录 使用 sep 参数打印变量之间没有空格 使用加法 运算符打印不带空格的变量 在 Python 中不使用换行符或空格打印 使用格式化的字符串文字来打印没有空格的变量 例如 print f hello variable 1 格式化字符
  • oracle 重设redo log 和倒入

    重设redo log alter system set allow resetlogs corruption true scope spfile recover database until cancel using bakcup cont
  • [RK3288][Android6.0] 调试笔记 --- 系统第一次开机进入Recovery模式原因

    Platform ROCKCHIP OS Android 6 0 Kernel 3 10 92 描述 当系统全部download开机后 会默认进入Recovery模式 这个其实是misc分区里的内容在作怪 misc分区 misc img是R
  • String、StringBuffer、StringBuilder的区别

    转载 极客时间 JAVA核心技术36讲 https www cnblogs com ygj0930 p 6581009 html 一 String创建机制 JAVA语言在创建一个字符串时 首先检查池中是否有值相同的字符串对象 如果有则不需要
  • 朴素贝叶斯--matlab自带工具箱的使用

    朴素贝叶斯分类使用条件 其数据点的维数 即特征之间相互独立 当属性之间相关性较小时 分类效率好 当属性之间相关性较大时 分类不如决策树 属性之间的相关性获得 用协方差矩阵 matlab自带函数cov获得 1 nb NaiveBays fit
  • git pull时报错Filename too long

    在git bash窗口执行以下命令 全局设置git支持长文件名 git config global core longpaths true
  • ClickHouse实时分析(一)- ClickHouse入门

    目录 1 什么是ClickHouse 1 1 OLAP场景的关键特征 1 2 列式数据库更适合OLAP场景的原因 1 2 1 输入 输出 1 2 2 CPU 2 ClickHouse的特点 2 1 列式存储 2 2 数据压缩 2 3 数据的
  • DALI协议了解

    DALI Digital Addressable Lighting Interface 是一种新的智能照明系统的控制协议 系统具有结构简单 安装方便 操作容易 功能良优等特点 协议定义了电子镇流器与控制器之间的通信方式 DALI协议系统由分
  • 哨兵节点:思想简单,效果很棒的的编程算法

    文章目录 普通的算法 哨兵算法 小结 别人的经验 我们的阶梯 今天和同事一起调代码 定位到一处很耗时的地方 在某个线程中 同步周期需要保证在2毫秒 如果耗时不到2毫秒 那么就让剩下的时间进行sleep 但是在调用一个模块的内部函数时 时不时
  • 轻盈绘图的Excalidraw插件

    朋友们 你们的Obsidian彩虹屁狂魔重新上线 从这篇Obsidian实践开始 我想跟大家陆续分享一下 Obsidian中那些让人眼前一亮的宝藏插件 原本我想从逻辑上最基础 使用上最简单的插件讲起 然鹅 请原谅我实在按捺不住自己迫切的心情
  • JAVA小游戏(国王和大臣)

    棋规则和需求 规则 棋子 2颗国王棋和 8 16 颗大臣棋 分别用黑白两色代表国王和大臣 黑子为国王 白子为大臣 棋盘 5 9的二维棋盘 共37个交点 如图所示 吃子规则 包括移子和添子 1 国王每回合可以选择移动一步 或者跳吃大臣一颗棋子
  • ceph集群警告和错误类型

    指标 说明 级别 noscrub flag s set 防止集群做清洗操作 full flag s set 使集群到达设置的full ratio值 会导致集群阻止写入操作 nodeep scrub flag s set 防止集群进行深度清洗
  • Vue与WebGL结合

    本文将WebGL与当前前端比较火的Vue框架进行初步结合 以备后续项目开发需要 部分代码源自https blog csdn net GISuuser article details 82224057以及 WebGL编程指南 大家可以自己百度
  • 使用IDEA创建springboot项目时,出现错误Cannot download ‘https://start.spring.io‘: connect timed out

    使用IDEA创建springboot项目时 出现错误Cannot download https start spring io connect timed out 解决方案 1 选择 custom 并使用阿里云 https start al
  • Objective-C Runtime 1小时入门教程

    原文出处 ian ianisme 一 前言 如果你没有Objective C基础 请学习了基础的iOS开发再来 这个1小时是给有一定iOS基础的童鞋的 如果你是大牛或者你感觉Objective C Runtime太简单不用1小时学习的 也请