ios开发Runtime的简单使用方法4887王中王鉄算盘奖结

来源:http://www.smjxgs.com 作者:操作系统 人气:152 发布时间:2019-08-08
摘要:ios开发Runtime的简单使用方法,ios开发runtime ios开发Runtime的简单使用方法,其实在OC底层本质上就是C语言函数的调用,去动态的发送消息。下面就拿一个allocinit的方法来举个例子,我们完

ios开发Runtime的简单使用方法,ios开发runtime

ios开发Runtime的简单使用方法,其实在OC底层本质上就是C语言函数的调用,去动态的发送消息。下面就拿一个alloc init的方法来举个例子,我们完全可以去调用运行时的函数,这样就可以去创建一个对象了。

   Person * p = objc_msgSend(objc_getClass("Person"),sel_registerName("alloc"));

  objc_msgSend(p, sel_registerName("init"));

其实我们在终端也可以去输入下面的命令,就可以去查看OC的C语言的实现,就会发现其实创建对象就和上面的运行时代码是很像的

clang -rewrite-objc main.m

在这里也需要了解两个概念性的问题就是SEL和IMP,其实SEL就是方法的编号,而IMP就是需要去执行方法的指针,我们可以通过方法的编号去找到这个代码的具体实现。

我们在下面可以进行交换两个对象的方法,其中Method就是指向struct objc_method的指针。其实SEL和IMP就相当于是一本书里面的标题和页码,我们可以通过标题知道页码,当然也可以去根据页码知道具体的内容。

    Method method1 = class_getInstanceMethod([self class], @selector(eat));
    Method method2 = class_getInstanceMethod([self class], @selector(sleep));
    //进行方法交换
    method_exchangeImplementations(method1, method2);

有两个方法是我们需要知道的一个就是当我们没有实现某个我们调用的类方法的时候回来到下面的这个函数

 (BOOL)resolveClassMethod:(SEL)sel

另外一个就是在我们实现我们调用的对象方法的时候如果没找到会调用下面的这个函数

 (BOOL)resolveInstanceMethod:(SEL)sel

如果我们想要在运行时动态的添加函数就可以在这里面进行添加,并且下面需要注意的是如果我们要写的函数要带有参数的话,那么函数的前面就要带有两个参数,一个是self,还有一个就是SEL _cmd,当前_cmd你可以改名字。self其实就是方法的调用者,而SEL其实就是方法的编号

 (BOOL)resolveInstanceMethod:(SEL)sel
{

    //动态的添加方法
    /*方法的参数一:是这个类
     参数二:方法
     参数三 函数指针
     参数四 就是返回值的类型加参数
     */

    class_addMethod(self, sel,hello,"");

    return [super resolveInstanceMethod:sel];
}
void hello(id self,SEL _cmd,NSString *str1,NSString * str2)
{
    NSLog(@"%@------%@",str1,str2);
    NSLog(@"hello world");
}

还有一个需要注意的是死循环可能不会导致程序直接崩溃,但是如果是函数的递归调用就肯定会导致程序的崩溃的,因为函数执行的时候有自己的临时栈,所以会导致堆栈溢出了,导致程序崩溃。

ios开发Runtime的简单使用方法,其实在OC底层本质上就是C语言函数的调用,去动态的发送消息。下...

本文中所使用的参考链接:
ios开发-Runtime详解
ios Runtime几种基本用法简记
iOS运行时详解
ios runtime理解

自己最近在研究Runtime,研究好久才知道了一些大概和简单的应用。在这里做一个笔记。RunTime被称为iOS开发的黑魔法,功能之强大,简直就是装逼神器啊。自己也是摸索着前人的步伐,一步一步探索Runtime机制在开发中的使用。

1.Runtime

什么是Runtime
Runtime又叫运行时,是一套底层的C语言api,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的.
比如

[receiver message];
//带参数[receiver message:(id)arg,...];

底层编译时会被编译器转化为

objc_msgSend(receiver, selector);
//带参数obj_msgSend(receiver, selector,arg1,arg2,...);

以上你可能还看不出来它的价值,但是我们需要了解的是OC是一门动态的语言,他会将一些工作放到代码运行时才处理而并非编译时,也就是说,有很多类和成员变量在我们编译时是不知道,而在运行时,我们的代码会转换成完整的确定的代码.
在OC代码中,使用runtime,需要引入#import<objc/runtime.h>

1.什么是Runtime

2.类在Runtime中的表示

OC类是由Class类型来表示的,他实际上是一个指向objc_class的结构体指针.

typedef struct object_class *Class;

objc_class结构体如下

//类在runtime中的表示
struct objc_class {
    Class isa;//指针,顾名思义,表示是一个什么实例的isa指向类对象,类对象的isa指向元类

#if !__OBJC2__
    Class super_class;  //指向父类
    const char *name;  //类名
    long version;
    long info;
    long instance_size
    struct objc_ivar_list *ivars //成员变量列表
    struct objc_method_list **methodLists; //方法列表
    struct objc_cache *cache;//缓存,一种优化,调用过的方法存入缓存列表,下次调用先找缓存
    struct objc_protocol_list *protocols //协议列表
    #endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

同样的,我们看看其他的一些相关定义

//描述类中的一个方法
typedef struct objc_method *Method;
//实例变量
typedef struct objc_ivar *Ivar;
// 类别Category
typedef struct objc_category *Category;
// 类中声明的属性
typedef struct objc_property *objc_property_t;

在OC中,一切都被设计成了对象,OC中的类的本质也是一个对象,在runtime中用结构体表示.

Runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数。平时我们编写的OC代码,都是基于Runtime实现的。OC是运行时语言,也只有在运行的时候才可以确定对象的类型,并调用类的对象的相应的方法,其中最主要的是消息机制。所以利用Runtime机制可以在程序运行的时候动态修改类的方法、类的对象的属性、方法、创建类别。这些应该是Runtime的基本用法吧,也是我们在平时的开发中用到的。

3.OC与Runtime的交互

例如下边的这个方法在运行时会被转化:

a.OC代码

只需编写OC代码即可,Runtime在幕后搞定一切,编译器会将OC代码转换成运行时代码

/* OC方法调用 */

[obj makeTest];

/* 编译时Runtime会将上面的代码转为下面的消息发送 */

objc_msgSend(obj, @selector(makeText));

b.通过Foundation框架和NSObject类定义方法.

NSObject的一些方法可以从Runtime系统中获取信息

  • -class 返回对象的类;
  • -isKindOfClass 和 - isMemerOfClass 检查对象是否存在于指定的类的继承体系中
  • -respondsToSelector 检查对象能否相应指定的消息
  • -conformsToProtocol 检查对象是否实现了指定的协议
  • -methodForSelector 返回指定方法实现的地址.

iOS的顶层基类NSObject含有一个指向objc_class结构体的isa指针:

c.通过Runtime库objc/runtime.h库函数直接调用.

详细参考后面的实际用法.

@interface NSObject{

Class isa;

}

typedef struct objc_class *Class;

struct objc_class {

Class isa; // 指向metaclass,也就是静态的Class

Class super_class ; // 指向其父类

const char *name ; // 类名

long version ; // 类的版本信息,初始化默认为0

/* 一些标识信息,如CLS_CLASS(0x1L)表示该类为普通class;

CLS_META(0x2L)表示该类为metaclass */

long info;

long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);

struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址

/* 与info的一些标志位有关,如是普通class则存储对象方法,如是metaclass则存储类方法; */

struct objc_method_list **methodLists ;

struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;

struct objc_protocol_list *protocols; // 存储该类遵守的协议

}

4.Runtime的术语

[receiver message];
id objc_msgSend ( id self, SEL op, ... );

在objc_msgSend函数的调用过程:

1.SEL(实际上是@selector方法取到的类方法的函数地址)

Objc特性-Runtime
SEL和IMP究竟是什么
SEL就是对方法的一种包装。包装的SEL类型数据它对应相应的方法地址,找到方法地址就可以调用方法.
其实它就是映射到方法的C字符串,你可以通过Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。
如果你知道selector对应的方法名是什么,可以通过NSString* NSStringFromSelector(SEL aSelector)方法将SEL转化为字符串,再用NSLog打印。
它的数据结构是:

typedef struct objc_selector *SEL;

SEL的创建:

SEL s1 = @selector(test1); // 将test1方法包装成SEL对象  
SEL s2 = NSSelectorFromString(@"test1"); // 将一个字符串方法转换成为SEL对象 

一些其他的用法:

// 将SEL对象转换为NSString对象 
NSString *str = NSStringFromSelector(@selector(test)); 

Person *p = [Person new]; 
// 调用对象p的test方法 
[p performSelector:@selector(test)]; 

/* 
 调用方法有两种方式: 
 1.直接通过方法名来调用 
 2.间接的通过SEL数据来调用 
 */ 
Person *person = [[Person alloc] init]; 
    // 1.执行这行代码的时候会把test2包装成SEL类型的数据 
    // 2.然后根据SEL数据找到对应的方法地址(比较耗性能但系统会有缓存) 
    // 3.在根据方法地址调用对应的方法 
[person test1]; 
// 将方法直接包装成SEL数据类型来调用 withObject:传入的参数  
[person performSelector:@selector(test1)]; 
[person performSelector:@selector(test2:) withObject:@"传入参数"]; 

1.首先通过obj的isa指针找到obj对应的Class。

问题来了:为什么不直接用函数指针,而用SEL走一圈再回到函数指针呢?

有了SEL这个中间过程,我们可以对一个编号和什么方法映射做些操作,也就是说我们可以一个SEL指向不同的函数指针,这样就可以完成一个方法名在不同时候执行不同的函数体。另外可以将SEL作为参数传递给不同的类执行。也就是说我们某些业务我们只知道方法名但需要根据不同的情况让不同类执行的时候,SEL可以帮助我们。

2.在Class中先去cache中通过SEL查找对应函数method

2.id

id是通用数据类型,能够表示任何对象

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

id其实就是一个指向objc_object结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到对象所属的类。

3.若cache中未找到,再去methodLists中查找

3.Class

isa指针的数据类型是Class,Class表示对象所属的类

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

可以查看到Class其实就是一个objc_class结构体指针(如上面2.在runtime中的表示显示的哪有)

4.若methodLists中未找到,则进入superClass按前面的步骤进行递归查找

4.Method

Method表示类中的某个方法

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

其实Method就是一个指向objc_method结构体指针,它存储了方法名(method_name)、方法类型(method_types)和方法实现(method_imp)等信息。而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。

5.若找到method,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

5.Ivar

Ivar表示类中的实例变量

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

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
}

Ivar其实就是一个指向objc_ivar结构体指针,它包含了变量名(ivar_name)、变量类型(ivar_type)等信息。

6.如果一直查找到NSObject还没查找到,则会进入消息动态处理流程。

6. IMP

在上面讲Method时就说过,IMP本质上就是一个函数指针,指向方法的实现

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif
当你向某个对象发送一条信息,可以由这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。

消息动态处理流程:

7.Cache

顾名思义,Cache主要用来缓存,那它缓存什么呢?
Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。


/* 1. 时机处理之一,在这个方法中我们可以利用runtime的特性动态添加方法来处理 */

(BOOL)resolveInstanceMethod:(SEL)sel;

/* 2. 时机处理之二,在这个方法中看代理能不能处理,如果代理对象能处理,则转接给代理对象 */

- (id)forwardingTargetForSelector:(SEL)aSelector;

/* 3. 消息转发之一,该方法返回方法签名,如果返回nil,则转发流程终止,抛出异常 */

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

/* 4. 消息转发之二,在该方法中我们可以对调用方法进行重定向 */

- (void)forwardInvocation:(NSInvocation *)anInvocation;

5.Runtime的实际用法.

2.Runtime的应用场景

1.消息发送.

我们来看看,objc_msgSend是如何具体的发送一个消息的:
1.首先根据receiver对象的isa指针获得他的class
2.有现在class的cache查找message方法,如果找不到,再到methodlists查找.
3.如果在class没有找到,再到super_class查找.
4.一旦找到message这个方法,就执行它实现的IMP.

1>.程序运行过程中动态创建类,如:KVO的实现

问题,self和super的区别

2>.程序运行过程中动态修改对象的属性、方法

问题,隐藏参数self和_cmd

(答案在:OC特效:Runtime)

3>.遍历一个类的所有成员变量、方法

2.方法解析与消息转发

[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。

4>.交换方法

Method Resolution

首先Objective-C在运行时调用 resolveInstanceMethod:或 resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。

5>.运行时创建类

Fast Forwarding

如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。

3.场景举例:

Normal Forwarding

如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:
方法。

(1).动态创建类---实现自己的KVO监听

3.关联对象

OC中的category无法向既有类添加属性,因此可以用runtime的关联对象来实现.

KVO监听相信大家在平时开发中都有用到过,但是它是怎么监听到属性变化的呢?懒加载大家都有用到吧,自己重写属性的set方法。对,KVO就是监听属性的set方法。让我们先看第一张图:

4.Method Swizzling

通过修改一个已存在类的方法,来实现方法替换是比较常用的runtime技巧.

4887王中王鉄算盘奖结果 1

截图1

图中实例对象p的isa指针指向的是Person类,让我们单步往下走

4887王中王鉄算盘奖结果 2

截图2

现在p的isa指针指向的是NSKVONotifying_Person这个类。在程序运行的时候,苹果利用runtime机制动态的创建了一个继承Person类的NSKVONotifying_Person这个类,并将isa指针动态指向子类方法。苹果在动态创建的NSKVONotifying_Person这个类中重写父类属性的set方法,方法实现中再调用父类的set方法。

4887王中王鉄算盘奖结果 3

重写父类属性的set方法

现在我们自己利用Runtime实现自己的KVO监听,关键代码如下:

-(void)LR_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{

//获取类名

NSString* oldClass = NSStringFromClass([self class]);

NSString* newClass = [@"LR" stringByAppendingString:oldClass];

const char* name = [newClass UTF8String];

//1.动态生成一个类

Class myClass = objc_allocateClassPair([self class], name, 0);

//添加一个方法

class_addMethod(myClass, @selector(setName:), (IMP)setName, "");

//2.注册这个类

objc_registerClassPair(myClass);

//3.修改isa指针

object_setClass(self, myClass);

//4.实现setName方法

void setName(id self, SEL _cmd,NSString* newName){

NSLog(@"我来饿了");

}

(2).程序运行过程中动态修改对象的属性、方法

我们在demo中调用Person类一个没有实现的方法,然后command R会怎样?Crash!!!

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

[p performSelector:@selector(run)];

现在我们可以利用runtime机制实现方法的懒加载。

关键代码如下:

(BOOL)resolveInstanceMethod:(SEL)sel{

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

//1.cls 类类型

//2.name 方法编号

//3.imp 方法实现。函数指针指向一个实现

//4.types 返回值类型

class_addMethod([Person class], sel, (IMP)haha, "v");

}

/*

第四个参数的含义:

v表示void,@表示id类型,:表示SEL类型

"v@:@":表示返回值为void,接受一个id类型、一个SEL类型、一个id类型的方法

"@@:":表示返回值为id类型,接受一个id类型和一个SEL类型参数的方法

*/

return [super resolveInstanceMethod:sel];

}

实现动态添加的方法的实现

void haha(id self ,SEL _cmd){

NSLog(@"%@===%@",self,NSStringFromSelector(_cmd));

NSLog(@"你说啥");

}

当一个类调用了没有实现的方法,就会来到runtime的这个方法resolveInstanceMethod,进行方法的寻找,如果子类中没有方法的实现,就会在父类中寻找,如果父类也没有,就往父类的父类取寻找。所以在这个方法中我们使用class_addMethod方法动态的为Person类添加方法。

注:在所有的方法中都会隐式接收2个参数----self,_cmd。self调用的类,_cmd方法的编码名。这两个参数只有你传入之后才可以在方法实现中拿到。

(3).遍历一个类的所有成员变量、方法

主要用到的方法如下:

class_copyIvarList--->获取类的成员变量列表-->多用于字典转模型,归解档的操作。(有兴趣的可以研究一下MJExtension的内部实现,受益匪浅)

class_copyPropertyList--->获取类的属性列表

代码如图:

4887王中王鉄算盘奖结果 4

截图3

(4).交换方法

通过runtime的method_exchangeImplementations方法来实现方法的互换(实际是利用runtime改变了两个方法的isa指针指向)。一般用自己写的方法(常用在自己封装的类或写的框架中,添加某些防错措施)来替换系统的方法,如:

在数组中的越界访问或数组使用addObject方法添加元素为nil时导致的程序崩溃。可以新建一个分类实现方法的交换来防止程序的崩溃,如图:

4887王中王鉄算盘奖结果 5

新建分类添加方法交换

(5).动态添加一个类

4887王中王鉄算盘奖结果 6

动态添加一个类

4.Runtime的简单应用

当我们的项目越做越大越复杂的时候,建立的控制器也会越来越多,相应的跳转也会增加。特别是你接收一个大项目的时候,对整体的业务逻辑不熟悉,整体的架构体系也是一头雾水,然而你又要修复某个页面的BUG,估计要找到对应的页面都要找好久。有没有一种方式可以快速找到某个页面对应的ciewController呢?

方案一、在整个项目建立初期构建一个基类viewController,此后创建的VC均继承于基类。我们只需要重写基类的viewWillAppear方法。

- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];

NSString *className = NSStringFromClass([self class]);

NSLog(@"%@ will appear", className);

}

方案二、给viewController创建一个分类,在分类里边进行方法的替换。这里所说的方法替换并不是使用method_exchangeImplementations改变两个方法的isa指针指向。而是先拿到系统方法的IMP实现,然后构建一个新的符合我们需求的IMP实现来替代系统方法的IMP实现。如图:

4887王中王鉄算盘奖结果 7

改变方法的IMP实现

具体代码如下:

4887王中王鉄算盘奖结果 8

修改IMP实现

5.Runtime使用心得

Runtime很强大,这只是我的一个初步的了解,对于很多东西不是很了解。这应该算是Runtime的一个基础用法吧。不过黑魔法就是用着酸爽。要理解透彻,并且在开发中熟练应用感觉任重道远啊。

RuntimeDemo传送门

本文由4887王中王鉄算盘奖结果发布于操作系统,转载请注明出处:ios开发Runtime的简单使用方法4887王中王鉄算盘奖结

关键词:

最火资讯