iOS runtime 知识点总结

1.说说OC的消息机制?

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)

objc_msgSend底层有3大阶段

消息发送(当前类、父类中查找)、动态方法解析、消息转发

2. 什么是Runtime?平时项目中有用过么?

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行

OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数

平时编写的OC代码,底层都是转换成了Runtime API进行调用

3.与runtime打交道主要有三种方式:

(1)平常使用OC写代码,当代码中使用到OC的类与方法,runtime系统其实已经在隐式的被使用着。

(2)使用NSObject的某些方法如:isKindOfClass:、isMemberOfClass:等等的这些接口时,就是显示使用runtime接口提供的功能。

(3)runtime提供的一套C语言API。

4.runtime具体应用

利用关联对象(AssociatedObject)给分类添加属性

遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)

交换方法实现(交换系统的方法)

利用消息转发机制解决方法找不到的异常问题

5.runtime 常见作用

动态交换两个方法的实现 

动态添加属性 

实现字典转模型的自动转换 

发送消息 

动态添加方法 

拦截并替换方法 

实现 NSCoding 的自动归档和解档

6.runtime 方法调用流程「消息机制」

1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。

2.注册方法编号(这里用方法编号的好处,可以快速查找)。

3.根据方法编号去查找对应方法。

4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

7.isa 

在oc里面有一个叫 isa 的指针,它指向元类,元类的 isa 又会指向根元类,最终指向了 NSObject 的元类,这样形成一个环。所谓元类,就是类对象,例如类A,实例化出一个对象 a,那么对象a的isa指针就是指向 A,类 A 本身也是一个对象,叫类对象。



打开objc/runtime.h,可以看到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;// 类的版本信息,默认为0

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;

        从上述代码中可以看到 class 的本质,它是一个含有 isa 指针的结构体,里面有父类、类名、方法列表、成员变量列表等变量。也就是说通过 runtime 我们可以知道该对象拥有的方法列表和成员变量列表,runtime 提供了相应的 api。 

8.对象模型

类对象(即类,这样称乎是因为在OC中类也是一个对象),实例对象(通过某个类创建的具体实例)

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

typedefstructobjc_class *Class;

查看objc/runtime.h中objc_class结构体的定义如下:

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;

#if!__OBJC2__    Class super_class                      OBJC2_UNAVAILABLE;  // 父类constchar*name                        OBJC2_UNAVAILABLE;// 类名longversion                            OBJC2_UNAVAILABLE;// 类的版本信息,默认为0longinfo                              OBJC2_UNAVAILABLE;// 类信息,供运行期使用的一些位标识longinstance_size                      OBJC2_UNAVAILABLE;// 该类的实例变量大小structobjc_ivar_list *ivars            OBJC2_UNAVAILABLE;// 该类的成员变量链表structobjc_method_list **methodLists  OBJC2_UNAVAILABLE;// 方法定义的链表structobjc_cache *cache                OBJC2_UNAVAILABLE;// 方法缓存structobjc_protocol_list *protocols    OBJC2_UNAVAILABLE;// 协议链表#endif

} OBJC2_UNAVAILABLE;

在__OBJC2__以前,我们是可以看到结构体的各个成员的,下面简单描述一下与对象模型相关的两个字段:

isa:每个对象(包括类对象和实例对象)中都会包含它,表明当前的对象是属于哪个类的。实例对象的isa指针指向它的类对象。而类对象的isa指针指向它的元类(metaclass)。后面会介绍到。

super_class:指向它的父类的指针。

meta-class是一个类对象的类。

1、当我们向一个对象发送消息时,会到这个实例对象所属的这个类对象的方法列表中查找方法

2、而向一个类对象发送消息时,会在这个类对象的meta-class的方法列表中查找。

每个类对象都会有一个单独的meta-class。meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个的闭环。

假设有如下代码:

@interface SuperClass : NSObject@end@interface SubClass : SuperClass@end

@implementation SubClass

- (void)message

{

}

@end


 对于NSObject继承体系来说,图中的Root class即为NSObject.

9.消息机制

在iOS中,我们要区分一下方法与函数。方法是属于类或者对象的,而函数则不一定,可以独立于类与对象之外。函数的调用是一步到位,程序直接跳到函数的地址去执行。而方法的调用,它是对类和对象而言,向对应的类或对象发送一条消息,通过runtime系统的消息机制,找到实际要调用的函数地址,然后跳到地址中执行。当我们执行上一部分Subclass类的如下代码时

[subClassInstance message]

编译器调用的其实是

objc_msgSend(subClassInstance, selector) //selector参数,编译器传递的会是sel_registerName("message")。

如果方法带有参数,那么调的会是

objc_msgSend(subClassInstance, selector, arg1, arg2, ...)

对象模型 objc_class中有一个属性

struct objc_method_list **methodLists;这个里面存储的是类的方法链表。链表内每个元数都是一个方法。那么方法又是什么,我们可以在runtime.h中可以看到,每个方法其实也是一个结构体。

struct objc_method {

    SEL method_name                                          OBJC2_UNAVAILABLE;  //选择器,Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL

    char*method_types                                      OBJC2_UNAVAILABLE;

    IMP method_imp                                          OBJC2_UNAVAILABLE;  //IMP实际上是一个函数指针,指向方法实现的地址

}   


就上一部分SubClass中的而然,SubClass中的methodLists中就会有一个方法。method_name的值为:sel_registerName("message"),method_imp则指向messge方法实现的地址。(这个地址是编译链接期已经决定了的,这里我还这样理解,我们可以运行时为类添加新的方法,修改方法的实现把它修改成另一个已经存在的实现(这就是后面会说到的Method swizzling),但是不能在运行时创建一个新的实现。)  

  那么objc_msgSend会帮我们完成动态绑定的所有事情:通过传进来的receiver subClassInstance,及selector找到对应的Method。method中已经关联了方法实现的地址,接下来就把参数作为方法实现的参数传进去进行调用,把调用的反回值作为objc_msgSend的返回值。这就是OC中的消息发送。

下图演示了这样一个消息的基本框架,以下基本来自官方文档截图及翻译:


当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个我们在稍后讨论。

  现在先回头看看对象模型中objc_class中的struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存

上面说的是方法第一次被调用的流程。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法,走上述流程。所以OC的对象模型是消息传递机制中方法查找的基础。

接下来我们会讲消息的转发机制,如上面所说:如果最后没有定位到selector,则会走消息转发流程。实在没必要重复造轮子,对于这部分详情可以参考消息转发,作者已经写得够好了。但为了不跳链接也能有个好的理解,我下面是对链接内容的删简版,去掉具体代码的实现。


1、当没有找到SEL的IMP时,resolveInstanceMethod方法就会被调用,它给类利用class_addMethod添加方法的机会。

2、经过resolveInstanceMethod如果对象还是不能执行到对应的IMP那么就进入下一个阶段,进入到

流程到了这里,系统给了个将这个SEL转给其他对象的机会。

3、如果第二步返回的是nil或Self,那就就会进入methodSignatureForSelector这个函数和后面的forwardInvocation:是最后一个寻找IML的机会。这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行。

真正执行从methodSignatureForSelector:返回的NSMethodSignature。在这个函数里可以将NSInvocation多次转发到多个对象中,这也是这种方式灵活的地方。(forwardingTargetForSelector只能以Selector的形式转向一个对象)。

具体调用过程:

消息机制是运行时里面最重要的机制,OC中任何方法的调用,本质都是发送消息。使用运行时,发送消息需要导入框架并且xcode5之后,苹果不建议使用底层方法,如果想要使用运行时,需要关闭严格检查objc_msgSend的调用,BuildSetting->搜索msg 改为NO。

实例方法调用底层实现:

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

[p eat];

// 底层会转化成

//SEL:方法编号,根据方法编号就可以找到对应方法的实现。

[p performSelector:@selector(eat)];

//performSelector本质即为运行时,发送消息,谁做事情就调用谁

objc_msgSend(p, @selector(eat));

// 带参数

objc_msgSend(p, @selector(eat:),10);

类方法的调用底层

// 本质是会将类名转化成类对象,初始化方法其实是在创建类对象。

[Person eat];

// Person只是表示一个类名,并不是一个真实的对象。只要是方法必须要对象去调用。

// RunTime 调用类方法同样,类方法也是类对象去调用,所以需要获取类对象,然后使用类对象去调用方法。

Class personclass = [Persion class];

[[Persion class] performSelector:@selector(eat)];

// 类对象发送消息

objc_msgSend(personclass, @selector(eat));


**@selector (SEL):是一个SEL方法选择器。**SEL其主要作用是快速的通过方法名字查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以一个类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

运行时发送消息的底层实现每一个类都有一个方法列表 Method List,保存这类里面所有的方法,根据SEL传入的方法编号找到方法,相当于value - key的映射。然后找到方法的实现。去方法的实现里面去实现。如图所示。


我们来到objc_class中查看,其中包含着类的一些基本信息。

struct objc_class {

  Class isa; // 指向metaclass


  Class super_class ; // 指向其父类

  const char *name ; // 类名

  long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取

  long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;

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

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

  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;

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

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

}

下面我们就以p实例的eat方法来看看具体消息发送之后是怎么来动态查找对应的方法的。

实例方法[p eat];底层调用[p performSelector:@selector(eat)];方法,编译器在将代码转化为objc_msgSend(p, @selector(eat));

在objc_msgSend函数中。首先通过p的isa指针找到p对应的class。在Class中先去cache中通过SEL查找对应函数method,如果找到则通过method中的函数指针跳转到对应的函数中去执行。

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

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

注:对象如何找到对应的方法去调用

1.方法保存到什么地方?

对象方法保存到类中,类方法保存到元类(meta class),每一个类都有方法列表methodList。

2明确去哪个类中调用?

通过isa指针

1.根据对象的isa去对应的类查找方法,isa:判断去哪个类查找对应的方法 指向方法调用的类。

2.根据传入的方法编号SEL,里面有个哈希列表,在列表中找到对应方法Method(方法名)。

3.根据方法名(函数入口)找到函数实现,函数实现在方法区。

10.使用RunTime交换方法

系统自带的方法功能不够,需要给系统自带的方法扩展一些功能,并且保持原有的功能时,可以使用RunTime交换方法实现。这里要实现image添加图片的时候,自动判断image是否为空,如果为空则提醒图片不存在。

方法一:自定义UIImage类,缺点:每次用要导入自己的类。

方法二:使用分类,UIImage分类扩充一个这样方法,缺点:需要导入,无法写super和self,会干掉系统方法,解决:给系统方法加个前缀,与系统方法区分,如:xmg_imageNamed。

+ (nullable UIImage *)xx_ccimageNamed:(NSString *)name

{

    // 加载图片    如果图片不存在则提醒或发出异常

  UIImage *image = [UIImage imageNamed:name];

    if (image == nil) {

        NSLog(@"图片不存在");

    }

    return image;

}

缺点:每次使用都需要导入头文件,并且如果项目比较大,之前使用的方法全部需要更改。

方法三 :RunTime交换方法交换方法的本质其实是交换两个方法的实现,即调换xx_ccimageNamed和imageName方法,达到调用xx_ccimageNamed其实就是调用imageNamed方法的目的;交互方法实现,步骤: 1.提供分类 2.写一个有这样功能方法 3.用系统方法与这个功能方法交互实现,在+load方法中实现。

那么首先需要明白方法在哪里交换,因为交换只需要进行一次,所以在分类的load方法中,当加载分类的时候交换方法即可。

+(void)load

{

    // 获取要交换的两个方法

    // 获取类方法  用Method 接受一下

    // class :获取哪个类方法

    // SEL :获取方法编号,根据SEL就能去对应的类找方法。

    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));

    // 获取第二个类方法

    Method xx_ccimageNameMrthod = class_getClassMethod([UIImage class], @selector(xx_ccimageNamed:));

    // 交换两个方法的实现 方法一 ,方法二。

    method_exchangeImplementations(imageNameMethod, xx_ccimageNameMrthod);

    // IMP其实就是 implementation的缩写:表示方法实现。

}

交换方法内部实现:

根据SEL方法编号在Method中找到方法,两个方法都找到

交换方法的实现,指针交叉指向。如图所示:


注意:交换方法时候 xx_ccimageNamed方法中就不能再调用imageNamed方法了,因为调用imageNamed方法实质上相当于调用 xx_ccimageNamed方法,会循环引用造成死循环。

RunTime也提供了获取对象方法和方法实现的方法。

// 获取方法的实现

class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>)

// 获取对象方法

class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)

此时,当调用imageNamed:方法的时候就会调用xx_ccimageNamed:方法,为image添加图片,并判断图片是否存在,如果不存在则提醒图片不存在。

注意:在分类一定不要重写系统方法,就直接把系统方法干掉,如果真的想重写,在系统方法前面加前缀,方法里面去调用系统方法。

思想:什么时候需要自定义,系统功能不完善,就自定义一个这样类,去扩展这个类

11.Method Swizzling (黑魔法)

基于消息机制,才会有Method Swizzling,Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

https://nshipster.com/method-swizzling/

例如,我们想跟踪在程序中每一个 viewcontroller 展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个 viewcontroller 中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

这种情况下,我们就可以使用Method Swizzling

#import@implementation UIViewController (Tracking)+ (void)load {

        static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        Class class= [selfclass];       

        // When swizzling a class method, use the following:

                    // Class class = object_getClass((id)self);        SEL originalSelector = @selector(viewWillAppear:);

        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);

        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {

                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));

        } else {

            method_exchangeImplementations(originalMethod, swizzledMethod);

        }

    });

}

#pragmamark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {

        [self xxx_viewWillAppear:animated];

    NSLog(@"viewWillAppear: %@", self);

}

@end

在这里,我们通过 method swizzling 修改了 UIViewController 的 @selector(viewWillAppear:) 对应的函数指针,使其实现指向了我们自定义的 xxx_viewWillAppear 的实现。这样,当 UIViewController 及其子类的对象调用 viewWillAppear 时,都会打印一条日志信息。

上面的例子很好地展示了使用 method swizzling 来一个类中注入一些我们新的操作。当然,还有许多场景可以使用 method swizzling,在此不多举例。在此我们说说使用 method swizzling 需要注意的一些问题:

Swizzling应该总是在+load中执行

在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

1、总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。

2、避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。

3、明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看头文件以了解事件是如何发生的。

12.动态添加方法

为什么动态添加方法? OC都是懒加载,有些方法可能很久不会调用

应用场景:电商,视频,社交,收费项目:会员机制中,只要会员才拥有这些功能

有没有使用过performSelector,使用,什么时候使用?动态添加方法的时候使用? 为什么动态添加方法?

// 默认OC方法都有两个默认存在的隐式参数,self(哪个类的方法),_cmd(方法编号)

void run(idself, SEL _cmd,NSNumber*metre) {

NSLog(@"跑了%@",metre);

}


什么时候调用?只要调用没有实现的方法 就会调用方法去解决,这里可以拿到那个未实现的方法名

// 作用:去解决没有实现方法,动态添加方法

+(BOOL)resolveInstanceMethod:(SEL)sel{

class:给谁添加方法 

 SEL:添加哪个方法 

 IMP:方法实现,函数入口,函数名,如:(IMP)run,方法名run强转成IMP 

 type:方法类型,通过查苹果官方文档,V:void, 

 class_addMethod(<#__unsafe_unretained Class cls#>,<#SEL name#>,<#IMP imp#>,<#const char *types#>)

// [NSStringFromSelector(sel) isEqualToString:@"eat"];

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

{// 添加方法

class_addMethod(self, sel, (IMP)run,"v@:");

returnYES; 

 }

return[superresolveInstanceMethod:sel];

}


如果一个类方法非常多,其中可能许多方法暂时用不到。而加载类方法到内存的时候需要给每个方法生成映射表,又比较耗费资源。此时可以使用RunTime动态添加方法

动态给某个类添加方法,相当于懒加载机制,类中许多方法暂时用不到,那么就先不加载,等用到的时候再去加载方法。

动态添加方法的方法:首先我们先不实现对象方法,当调用performSelector: 方法的时候,再去动态加载方法。这里同上创建Person类,使用performSelector: 调用Person类对象的eat方法。

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

// 当调用 P中没有实现的方法时,动态加载方法

[p performSelector:@selector(eat)];

此时编译的时候是不会报错的,程序运行时才会报错,因为Person类中并没有实现eat方法,当去类中的Method List中发现找不到eat方法,会报错找不到eat方法。

而当找不到对应的方法时就会来到拦截调用,在找不到调用的方法程序崩溃之前调用的方法。当调用了没有实现的对象方法的时,就会调用**+(BOOL)resolveInstanceMethod:(SEL)sel方法。当调用了没有实现的类方法的时候,就会调用+(BOOL)resolveClassMethod:(SEL)sel**方法。

首先我们来到API中看一下苹果的说明,搜索 Dynamic Method Resolution 来到动态方法解析。


Dynamic Method Resolution的API中已经讲解的很清晰,我们可以实现方法resolveInstanceMethod:或者resolveClassMethod:方法,动态的给实例方法或者类方法添加方法和方法实现。

所以通过这两个方法就可以知道哪些方法没有实现,从而动态添加方法。参数sel即表示没有实现的方法。

一个objective - C方法最终都是一个C函数,默认任何一个方法都有两个参数。self : 方法调用者 _cmd : 调用方法编号。我们可以使用函数class_addMethod为类添加一个方法以及实现。

这里仿照API给的例子,动态的为P实例添加eat对象

+(BOOL)resolveInstanceMethod:(SEL)sel

{

    // 动态添加eat方法

    // 首先判断sel是不是eat方法 也可以转化成字符串进行比较。   

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

    /**

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

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

    第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)

    第四个参数: types :方法类型,需要用特定符号,参考API

    */

      class_addMethod(self, sel, (IMP)eat , "v@:");

        // 处理完返回YES

        return YES;

    }

    return [super resolveInstanceMethod:sel];

}

重点来看一下class_addMethod方法

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

class_addMethod中的四个参数。第一,二个参数比较好理解,重点是第三,四个参数。

cls : 表示给哪个类添加方法,这里要给Person类添加方法,self即代表Person。

SEL name : 表示添加方法的编号。因为这里只有一个方法需要动态添加,并且之前通过判断确定sel就是eat方法,所以这里可以使用sel。

IMP imp : 表示方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)需要自己来实现这个函数。每一个方法都默认带有两个隐式参数self : 方法调用者 _cmd : 调用方法的标号,可以写也可以不写。


void eat(id self ,SEL _cmd)

{

      // 实现内容

NSLog(@"%@的%@方法动态实现了",self,NSStringFromSelector(_cmd));

}

4.types : 表示方法类型,需要用特定符号。系统提供的例子中使用的是**"v@:",我们来到API中看看"v@:"**指定的方法是什么类型的。


从图中可以看出,

v -> void 表示无返回值@ -> object 表示id参数: -> method selector 表示SEL。

至此已经完成了P实例eat方法的动态添加。当P调用eat方法时输出


动态添加有参数的方法如果是有参数的方法,需要对方法的实现和class_addMethod方法内方法类型参数做一些修改。方法实现:因为在C语言函数中,所以对象参数类型只能用id代替。方法类型参数:因为添加了一个id参数,所以方法类型应该为**"v@:@"**来看一下代码

+(BOOL)resolveInstanceMethod:(SEL)sel

{

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

        class_addMethod(self, sel, (IMP)aaaa , "v@:@");

        return YES;

    }

    return [super resolveInstanceMethod:sel];

}

void aaaa(id self ,SEL _cmd,id Num)

{

    // 实现内容

    NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);

}

调用eat:函数

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

[p performSelector:@selector(eat:)withObject:@"xx_cc"];

输出为


 13.RunTime动态添加属性

使用RunTime给系统的类添加属性,首先需要了解对象与属性的关系。


对象一开始初始化的时候其属性name为nil,给属性赋值其实就是让name属性指向一块存储字符串的内存,使这个对象的属性跟这块内存产生一种关联,个人理解对象的属性就是一个指针,指向一块内存区域。

那么如果想动态的添加属性,其实就是动态的产生某种关联就好了。而想要给系统的类添加属性,只能通过分类。

这里给NSObject添加name属性,创建NSObject的分类我们可以使用@property给分类添加属性

@property(nonatomic,strong)NSString *name;

虽然在分类中可以写@property添加属性,但是不会自动生成私有属性,也不会生成set,get方法的实现,只会生成set,get的声明,需要我们自己去实现。

方法一:我们可以通过使用静态全局变量给分类添加属性

static NSString *_name;

-(void)setName:(NSString *)name

{

    _name = name;

}

-(NSString *)name

{

    return _name;

}

但是这样_name静态全局变量与类并没有关联,无论对象创建与销毁,只要程序在运行_name变量就存在,并不是真正意义上的属性。

方法二:使用RunTime动态添加属性RunTime提供了动态添加属性和获得属性的方法。

-(void)setName:(NSString *)name

{

    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

-(NSString *)name

{

    return objc_getAssociatedObject(self, @"name");   

}

动态添加属性

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在**objc_getAssociatedObject中通过次key获得属性的值并返回。参数三:id value** : 关联的值,也就是set方法传入的值给属性去保存。参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。有以下几种

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {

    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象

    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性

    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性

    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性

    OBJC_ASSOCIATION_COPY = 01403    // 指定相关的对象被复制,原子性 

};

获得属性

objc_getAssociatedObject(id object, const void *key);

参数一:id object : 获取哪个对象里面的关联的属性。参数二:void * == id key : 什么属性,与**objc_setAssociatedObject**中的key相对应,即通过key值取出value。

此时已经成功给NSObject添加name属性,并且NSObject对象可以通过点语法为属性赋值。

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

objc.name = @"xx_cc";

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

14. RunTime字典转模型

为了方便以后重用,这里通过给NSObject添加分类,声明并实现使用RunTime字典转模型的类方法。

+ (instancetype)modelWithDict:(NSDictionary *)dict

首先来看一下KVC字典转模型和RunTime字典转模型的区别

KVC:KVC字典转模型实现原理是遍历字典中所有Key,然后去模型中查找相对应的属性名,要求属性名与Key必须一一对应,字典中所有key必须在模型中存在。RunTime:RunTime字典转模型实现原理是遍历模型中的所有属性名,然后去字典查找相对应的Key,也就是以模型为准,模型中有哪些属性,就去字典中找那些属性。

RunTime字典转模型的优点:当服务器返回的数据过多,而我们只使用其中很少一部分时,没有用的属性就没有必要定义成属性浪费不必要的资源。只保存最有用的属性即可。

RunTime字典转模型过程首先需要了解,属性定义在类里面,那么类里面就有一个属性列表,属性列表以数组的形式存在,根据属性列表就可以获得类里面的所有属性名,所以遍历属性列表,也就可以遍历模型中的所有属性名。所以RunTime字典转模型过程就很清晰了。

创建模型对象

id objc = [[self alloc] init];

使用**class_copyIvarList**方法拷贝成员属性列表

unsigned int count = 0;

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

参数一:__unsafe_unretained Class cls : 获取哪个类的成员属性列表。这里是self,因为谁调用分类中类方法,谁就是self。参数二:unsigned int *outCount : 无符号int型指针,这里创建unsigned int型count,&count就是他的地址,保证在方法中可以拿到count的地址为count赋值。传出来的值为成员属性总数。返回值:Ivar * : 返回的是一个Ivar类型的指针 。指针默认指向的是数组的第0个元素,指针+1会向高地址移动一个Ivar单位的字节,也就是指向第一个元素。Ivar表示成员属性。3. 遍历成员属性列表,获得属性列表

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

        // 获取成员属性

        Ivar ivar = ivarList[i];

}

使用**ivar_getName(ivar)**获得成员属性名,因为成员属性名返回的是C语言字符串,将其转化成OC字符串

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

通过**ivar_getTypeEncoding(ivar)**也可以获得成员属性类型。5. 因为获得的是成员属性名,是带_的成员属性,所以需要将下划线去掉,获得属性名,也就是字典的key。

// 获取key

NSString *key = [propertyName substringFromIndex:1];

获取字典中key对应的Value。

// 获取字典的value

id value = dict[key];

给模型属性赋值,并将模型返回

if (value) {

// KVC赋值:不能传空

[objc setValue:value forKey:key];

}

return objc;

至此已成功将字典转为模型。

15. RunTime字典转模型的二级转换

在开发过程中经常用到模型嵌套,也就是模型中还有一个模型,这里尝试用RunTime进行模型的二级转换,实现思路其实比较简单清晰。

首先获得一级模型中的成员属性的类型

// 成员属性类型

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

判断当一级字典中的value是字典,并且一级模型中的成员属性类型不是NSDictionary的时候才需要进行二级转化。首先value是字典才进行转化是必须的,因为我们通常将字典转化为模型,其次,成员属性类型不是系统类,说明成员属性是我们自定义的类,也就是要转化的二级模型。而当成员属性类型就是NSDictionary的话就表明,我们本就想让成员属性是一个字典,不需要进行模型的转换。

id value = dict[key];

if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"])

{

      // 进行二级转换。

}

获取要转换的模型类型,这里需要对propertyType成员属性类型做一些处理,因为propertyType返回给我们成员属性类型的是**@\"Mode\",我们需要对他进行截取为Mode**。这里需要注意的是\只是转义符,不占位。

// @\"Mode\"去掉前面的@\"

NSRange range = [propertyType rangeOfString:@"\""];

propertyType = [propertyType substringFromIndex:range.location + range.length];

// Mode\"去掉后面的\"

range = [propertyType rangeOfString:@"\""];

propertyType = [propertyType substringToIndex:range.location];

获取需要转换类的类对象,将字符串转化为类名。

Class modelClass =  NSClassFromString(propertyType);

判断如果类名不为空则调用分类的modelWithDict方法,传value字典,进行二级模型转换,返回二级模型在赋值给value。

if (modelClass) {

      value =  [modelClass modelWithDict:value];

这里可能有些绕,重新理一下,我们通过判断value是字典并且需要进行二级转换,然后将value字典转化为模型返回,并重新赋值给value,最后给一级模型中相对应的key赋值模型value即可完成二级字典对模型的转换。

最后附上二级转换的完整方法

+ (instancetype)modelWithDict:(NSDictionary *)dict{

    // 1.创建对应类的对象

    id objc = [[self alloc] init];

    // count:成员属性总数

    unsigned int count = 0;

  // 获得成员属性列表和成员属性数量

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

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

        // 获取成员属性

        Ivar ivar = ivarList[i];

        // 获取成员名

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

        // 获取key

        NSString *key = [propertyName substringFromIndex:1];

        // 获取字典的value key:属性名 value:字典的值

        id value = dict[key];

        // 获取成员属性类型

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

        // 二级转换

        // value值是字典并且成员属性的类型不是字典,才需要转换成模型

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

            // 进行二级转换

            // 获取二级模型类型进行字符串截取,转换为类名

            NSRange range = [propertyType rangeOfString:@"\""];

            propertyType = [propertyType substringFromIndex:range.location + range.length];

            range = [propertyType rangeOfString:@"\""];

            propertyType = [propertyType substringToIndex:range.location];

            // 获取需要转换类的类对象

          Class modelClass =  NSClassFromString(propertyType);

          // 如果类名不为空则进行二级转换

            if (modelClass) {

                // 返回二级模型赋值给value

                value =  [modelClass modelWithDict:value];

            }

        }

        if (value) {

            // KVC赋值:不能传空

            [objc setValue:value forKey:key];

        }

    }

    // 返回模型

    return objc;

}

16. MJExtention 的底层实现

#import"NSObject+Model.h"#import// class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>) 获取属性列表@implementationNSObject(Model)/**

字典转模型

@param dict 传入需要转模型的字典

@return 赋值好的模型

*/+ (instancetype)modelWithDict:(NSDictionary*)dict {

idobjc = [[selfalloc] init];

//思路: runtime遍历模型中属性,去字典中取出对应value,在给模型中属性赋值

// 1.获取模型中所有属性 -> 保存到类

// ivar:下划线成员变量 和 Property:属性

// 获取成员变量列表

// class:获取哪个类成员变量列表

// count:成员变量总数

//这个方法得到一个装有成员变量的数组

//class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)intcount =0;

// 成员变量数组 指向数组第0个元素

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

// 遍历所有成员变量

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

// 获取成员变量 

userIvar ivar = ivarList[i];

// 获取成员变量名称,即将C语言的字符转为OC字符串

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

// 获取成员变量类型,用于获取二级字典的模型名字

NSString*type = [NSStringstringWithUTF8String:ivar_getTypeEncoding(ivar)];

//  将type这样的字符串@"@\"User\"" 转成 @"User"

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

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

// 成员变量名称转换key,即去掉成员变量前面的下划线

NSString*key = [ivarName substringFromIndex:1];/

/ 从字典中取出对应value dict[@"user"] -> 字典

idvalue = dict[key];

// 二级转换

// 并且是自定义类型,才需要转换

if([value isKindOfClass:[NSDictionaryclass]] && ![type containsString:@"NS"]) {

// 只有是字典才需要转换

Class className =NSClassFromString(type);

// 字典转模型value = [className modelWithDict:value];       

 }

// 给模型中属性赋值 key:user value:字典 -> 模型

if(value) {      

      [objc setValue:value forKey:key];  

}    

 }

returnobjc;

}

@end

你可能感兴趣的:(iOS runtime 知识点总结)