Runtime学习笔记整理

[toc]

一、基本概念

Runtime是一套比较底层的纯C语言API,包含了很多底层的C语言API。在我们平时编写的OC代码中,程序运行时,其实最终都是转成了Runtime的C语言代码。Runtime是开源的,你可以去这里下载Runtime的源码。

实例方法被调用的过程分析

实例方法被调用的时候,会通过其持有的isa指针找到对应的类,然后在其中的class_data_bits_t中查找对应的方法。

*执行NSArray array = [[NSArray alloc] init];的流程:

  • 1、[NSArray alloc]先被执行,由于NSArray没有+alloc方法,所以去分类NSObject中查找
  • 2、检查NSArray是否能响应alloc方法,发现响应后,检查NSArray类,开辟NSArray所需的内存空间,然后把isa指向NSArray。同时+alloc方法被添加到cache列表中。
  • 3、接着执行-init方法,如果NSArray不响应,则继续去父类NSObject中查找。找到后同时加入到cache列表中。
  • 4、以后再使用[[NSArray alloc] init]初始化数组,直接从cache中获取方法执行。

realizeClass方法的主要作用是对类进行第一次初始化(分配可读写数据空间、返回真正的类结构) 类在内存中的位置是编译期确定的,只要代码不改变,类在内存中的位置就会不变

  • ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t
  • 当前类在编译期就已经确定的属性、方法以及遵循的协议都保存在 class_ro_t
  • 类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。
  • class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods

一、相关术语

1、id:表示Objective-C的任意对象类型

struct objc_object {
    Class isa;
} *id;
复制代码

2、isa:实例的一个属性,用来指向实例所属的类

typedef struct objc_class *Class;
struct objc_class { 
    Class isa                                 OBJC_ISA_AVAILABILITY; 
}
复制代码
  • 实例方法调用时,通过对象的 isa 在类中获取方法的实现
  • 类方法调用时,通过类的 isa 在元类中获取方法的实现

3、SEL:SEL又叫选择器,是表示一个方法的selector的指针

typedef struct objc_selector *SEL;
复制代码

4、Method:方法(方法名+方法类型+方法实现)

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name         OBJC2_UNAVAILABLE;  // 方法名
    char *method_types      OBJC2_UNAVAILABLE;  // 方法类型,主要存储着方法的参数类型和返回值类型
    IMP method_imp          OBJC2_UNAVAILABLE;  // 方法的实现,函数指针
}
复制代码

class_copyMethodList(Class cls, unsigned int *outCount)可以使用这个方法获取某个类的成员方法列表。

5、IMP:函数指针,指向方法的实现,由编译器生成,决定代码最终在何处执行。

typedef id (*IMP)(id, SEL, ...);
复制代码

6、Ivar:实例变量

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
}
复制代码

class_copyIvarList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的成员变量列表。

// ivar 的修饰信息存放在了 Class 的 Ivar Layout 中
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;           // <- 记录了哪些是 strong 的 ivar

    const char * name;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;     // <- 记录了哪些是 weak 的 ivar
    const property_list_t *baseProperties;
};
复制代码

7、objc_property_t:实例属性 = Ivar + setter + getter

typedef struct objc_property *objc_property_t;
复制代码

class_copyPropertyList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的属性列表。

8、objc_category

typedef struct objc_category *Category;

typedef struct objc_category {
    const char *name;                            // 类的名字
    classref_t cls;                              // 类
    struct method_list_t *instanceMethods;       // category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods;          // category中所有添加的类方法的列表
    struct protocol_list_t *protocols;           // category实现的所有协议的列表
    struct property_list_t *instanceProperties;  // category中添加的所有属性
};
复制代码

9、Cache:缓存提高查找效率

typedef struct objc_cache *Cache
struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};
复制代码

每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。

10、metaClass(元类):类对象所属的类,类对象的isa指针指向元类

11、根元类:所有的元类的基类,根元类的isa指针指向自己

二、所有实例、类以及元类(meta class)都继承自一个基类,关系如下图所示:

上图中:superclass指针代表继承关系,isa指针代表实例所属的类。 类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。 注意:

  • 1、Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
  • 2、isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass:方法来确定实例对象的类。因为KVO的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。

三、类对象在runtime中的数据结构

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;  // 类的版本信息,默认为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;
复制代码
  • 1、isa: 结构体的首个变量也是isa指针,这说明Class本身也是Objective-C中的对象。
  • 2、super_class: 结构体里还有个变量是super_class,它定义了本类的超类。类对象所属类型(isa指针所指向的类型)是另外一个类,叫做“元类”。
  • 3、name:类名称。
  • 4、version:类的版本信息。
  • 5、info:运行期使用的标志位,比如0x1(CLS_CLASS)表示该类为普通class,0x2(CLS_META)表示该类为 metaclass。
  • 6、instance_size:实例大小,即内存所占空间。
  • 7、ivars: 成员变量列表,类的成员变量都在ivars里面。
  • 8、methodLists: 方法列表,根据标志位的不同可能指向不同,比如可能指向实例方法列表,或者指向类方法列表。类的实例方法都在methodLists里,类方法在元类的methodLists里面。methodLists是一个指针的指针,通过修改该指针指向指针的值,就可以动态的为某一个类添加成员方法。这也就是Category实现的原理,同时也说明了Category只可以为对象添加成员方法,不能添加成员变量。
  • 9、cache: 方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,会优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。提高效率。
  • 10、protocols:类需要遵守的协议。

四、运行时创建类,只需要三步:

  • 1、为类和类的元类分配内存空间objc_allocateClassPair
  • 2、为类添加方法和成员属性class_addMethodclass_addIvarclass_addProperty
  • 3、注册新创建的类objc_registerClassPair

注意:运行时只能添加属性,不能添加成员变量,否则会打乱类的内存结构。

参考博客

Objective-C中的Runtime

深入解析 ObjC 中方法的结构

二、动态特性

[TOC]

@(runtime)[runTime, 温故而知新]

转自:iOS动态特性初研究(利用JSON动态创建类型和对象)

1.什么是动态特性?

程序可以访问,检测和修改它本身状态或行为的能力。用我自己的理解,这里的状态和行为,理解成变量,属性和方法,会更加形象一点。

2.与动态特性相关的概念,selector,IMP,Class

Class: 从语法形式上看,和UIButton,NSString一样,是一种类型。

Class被定义为一个指向objc_class的结构体指针。

它是指向对象的类结构体的指针,该类结构体含有一个指向其父类类结构的指针,访类方法的链表,该类方法的缓存以及其他必要信息。见下图

除了静态方法来创建对象,还可以使用string来创建,NSClassFromString。

SEL:定义成一个指向objc_selector指针

运行时,会在方法链表中根据SEL查找具体的实现方法IMP。为什么不用函数指针直接调用,而加了一层SEL?我的理解,首先Object-C的类不能直接应用函数指针,这样只能做一个@selector语法来取(本人在OC中写过状态机,用函数指针形式写action,但一直报错,只能用selector代替);其次,SEL还可以配合动态方法来使用,例如NSSelectorFromString,performSelector,动态添加方法,并执行。

IMP:就是定义一个函数指针的形式

它包含一个接受消息的对象(self指针),调用方法SEL,以及若干参数,并返回一个id。

3. 举例子,如何将JSON直接映射成对象,如何将对象直接映射成DB(coreData原理)

3.1定义该类的属性,方法,生成对象。

用动态方法,获得该对象的属性/变量列表(class_copyPropertyList/class_copyIvarList),遍历获得每个属性的名称(property_getName),然后将JSON转换Dic,用key-value(setvalueForkey,valueForKey)方法,对对象进行赋值,取值操作。

此种方法,抽象出了公用的setter方法(用dictionary給对象赋值),但是缺点是,类型要事先定义。无法动态生成类型。这种例子,网上很多,而且不明白为什么例子中都把property name和attribute值打印出来,至于怎么用,半个字都没提?

(上面是最长见的使用方式,有人问我能否不事先定义类型,然后利用JSON来创建类型呢?这个还把我问住了)后来查阅OC runtime guide,发现有动态添加变量的方法(class_addIvar),于是思路由此打开:

3.2、首先定义一个空的类

(没有属性,变量,方法),只有一个类名,然后运行时,給该类添加变量(当时没有查到可以动态添加属性的方法,后来发现有,但是要到iOS4.3以后才行),随后用給变量赋值。但是结果让人失望,无法动态添加变量。原因是class_addIvar只能在动态创建类型的时候,添加变量,也就是“class_addIvar"This function may only be called after objc_allocateClassPair and beforeobjc_registerClassPair.Adding an instance variable to an existing class is notsupported”,而事先定义类是静态创建的类,故无法在runtime时添加变量objective-c-add-property-in-runtime

于是,只能放弃事先定义类的方式,转而利用在动态创建类时(objc_allocateClassPair),添加变量 。然后用給变量赋值和取值的方式(object_setInstanceVariable,object_getIvar,注意,无法用key-value的方式操作,这种方法只有静态定义属性后才行),但这种方式,就只能用纯C的方式封装,赋值,取值都要传进obj参数,比较繁琐,没有面向对象那么方便。

结论:3.2中的结论,如果编译前定义类,那么无法用runtime添加变量,这种方法行不通;只有在runtime时,在objc_allocateClassPairobjc_registerClassPair之间用class_addIvar添加变量

3.3、后来查到有动态添加property的方法

(class_addProperty),在4.3之后。于是想到一种动态创建类型,并且可以用OC语法的方式访问变量。

首先,动态创建类型,添加变量(这个很重要,因为当我们访问property时,实际上是要对变量操作,如果没有添加变量,那么就是null),注册类型,然后往里动态添加属性,随后就可以象OC一样方便访问属性了 (因为静态类中属性会默认有一个和它同名的变量,对属性操作,实际上是对该变量操作)。

但实际上对该属性赋值后,取值却是null。因为只有在编译前定义的属性才会默认一个变量,property实际上只是提供了setter和getter的方法,至于你要把值存贮在哪里,需要自己设定,所以还需要在class_addProperty方法后,添加property的setter,getter,并在其中确定需要把值保存到哪里,从哪里取值。

getter

setter

这样我们就能用ClassA objA; [objAsetxxx:xxx]; [objA xxx]的方法来访问属性了(本人写了一个简单的实现,但暂时无法上传github,稍后会上传,请各位上传)

3.4、使用动态创建类,对象,以及ORM的优点,缺点

这个例子有如下几个特点:1.可以动态生成类型 2.可以用OC的方式访问属性。纯粹的“动态”。

当然也有美中不足的地方,首先动态创建对象的类型都是id类型(因为是动态创建,事先没有定义具体类型),视觉上不直观。其次编译过程中,会报warning,因为property是动态添加的,不是编译之前确定的,所以编译器不知道setter,getter方法哪里来的。(当然可以用performSelector来调用就没有warning问题,但是调用方式太繁琐)

但是不影响使用。

结果

结论:3.3的方法比3.2,3.1的方法牛逼,直接动态创建类型和对象,但是牺牲的是code的可读性和可维护性,研究的意义大于实用意义。

注意:这里需要大家研究的是,如何通过JSON的值,确定动态添加的变量和property的类型,我的思路是,可以容易区分NSString和NSNumber,但是如果确定int,long,float, long long等类型?应该可以通过值的大小范围来确定,例如int -256~255

3.5、如何将对象映射进DB中,其实原理是一样的,可以运行时,获得类名,属性名,属性类型,值,然后用sqlite3的接口创建表,列,值,类型等等。其实Coredata也是运用了这个动态的原理来实现的。

三、 动态添加属性

@property 和 Ivar 的区别@property = Ivar + setter + getter

第一种:通过runtime动态关联对象

相关函数objc_setAssociatedObjectobjc_getAssociatedObjectobjc_removeAssociatedObjects,下面的代码通过给UIButton添加一个分类的方式关联两个属性clickIntervalclickTime,来实现按钮的防连点操作。


// .h文件
#import 
@interface UIButton (FixMultiClick)
@property (nonatomic, assign) NSTimeInterval clickInterval;
@end
复制代码
// .m文件
#import "UIButton+FixMultiClick.h"
#import 
#import 

@interface UIButton ()
@property (nonatomic, assign) NSTimeInterval clickTime;
@end

@implementation UIButton (FixMultiClick)
-(NSTimeInterval)clickTime {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickTime:(NSTimeInterval)clickTime {
    objc_setAssociatedObject(self, @selector(clickTime), @(clickTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)clickInterval {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickInterval:(NSTimeInterval)clickInterval {
    objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load {
    [UIButton aspect_hookSelector:@selector(sendAction:to:forEvent:)
                      withOptions:AspectPositionInstead
                       usingBlock:^(id info){
        UIButton *obj = info.instance;
        if(obj.clickInterval <= 0){
            [info.originalInvocation invoke];
        }
        else{
            if ([NSDate date].timeIntervalSince1970 - obj.clickTime < obj.clickInterval) {
                return;
            }
            obj.clickTime = [NSDate date].timeIntervalSince1970;
            [info.originalInvocation invoke];
        }
    } error:nil];
}
@end
复制代码

优点:

可以快速为一个已有的class添加一个动态属性或者block块

缺点:

不能遍历所有的关联对象列表,不能移除指定的关联对象,只能通过objc_removeAssociatedObjects一次移除所有的关联对象。

第二种:通过runtime动态创建类的时候添加Ivar

相关函数objc_alloctateClassPairclass_addIvarobjc_registerClassPaire

// 一:为Class分配内存空间
Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);
// 二:添加方法
class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");
// 三:注册Class
objc_registerClassPair(myClass);
// 创建对象调用方法
id obj = [[myClass alloc] init];
[obj performSelector:@selector(method)];
复制代码

优点:

动态添加Ivar我们能够通过遍历Ivar得到我们所添加的属性

缺点:

必须通过class_allocatePair动态创建一个class,才能调用class_addIvar创建Ivar,最后通过class_registClassPair注册class。不能为已存在的类添加Ivar,否则会涉及到OC中类的成员变量的偏移量问题,如果在类注册之后class_adddIvar的话会破坏原来类成员变量的正确偏移量,这样的话会导致你访问的成员变量并不是你想访问的成员变量(用KVC赋值和取值直接报错, 用getIvar的话取值为null),如图:

第三种:通过runtime动态添加property

相关函数class_addPropertyclass_addMethodobjc_getAssociatedObjectobjc_getAssociatedObject

仅仅添加属性是没什么用的,因为还需要添加属性对应的实例变量。虽然runtime提供了class_addIvar方法来给类添加实例变量,但是注意,该方法只能在创建新的类的时候才能使用;对于已经存在的类,是不允许添加实例变量的。鉴于上述原因,所以可以采用动态添加关联对象来存储属性对应的实例变量。实现策略如下:

  • 1、由于我们肯定会在interface 中提供生的property(由于没有合成实现与ivar,在此称为生的),所以这样对于在外部访问时和普通property相同。
  • 2、由于缺乏的是实现以及可以存取的数据量,这里我们可以直接实现这些set与get。
  • 3、set与get的实现可以通过 associatedObject 进行对对象的存取操作。
#import "RuntimeTest.h"
#import 

@interface RuntimeTest()
{
    NSString* _address;
}
@end

@implementation RuntimeTest

+(void)load {
    [self runtimeTest];
}

void myMethod(id self, SEL _cmd) {
    NSLog(@"self = %@", self);
    NSLog(@"self.name = %@", [self valueForKey:NSStringFromSelector(@selector(name))]);
    NSLog(@"self.addres = %@", [self valueForKey:NSStringFromSelector(@selector(addres))]);
}

NSString *nameGetter(id self, SEL _cmd) {
    NSString* result = objc_getAssociatedObject(self, _cmd);
    return result;
}

void nameSetter(id self, SEL _cmd, NSString *value) {
    NSString *propertyStr = NSStringFromSelector(_cmd);
    // 去掉 set
    NSString *realProperty = [propertyStr substringFromIndex:3];
    // 去掉 :
    realProperty = [realProperty substringToIndex:realProperty.length - 1];
    // 首字母小写
    realProperty = [realProperty lowercaseString];
    // 关联对象
    objc_setAssociatedObject(self, NSSelectorFromString(realProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
    
+ (void) runtimeTest {
    // 1、Class分配内存空间
    Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);
    // 2.1、添加方法
    class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");
    // 2.2、添加变量(ivar)
    class_addIvar(myClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
    // 三:注册Class
    objc_registerClassPair(myClass);
    
    // 2.3、添加属性(property)(可以在类的注册完成之后)
    NSString* propertyName = @"addres";
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
    objc_property_attribute_t ownership = { "N", "" };  // N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] };  //variable name
    objc_property_attribute_t attrs[] = { type, ownership0, ownership, backingivar };
    if (class_addProperty(myClass, [propertyName UTF8String], attrs, sizeof(attrs)/sizeof(objc_property_attribute_t))) {
        //添加get和set方法
        NSString *setFunc = [NSString stringWithFormat:@"set%@:",[propertyName capitalizedString]];
        class_addMethod(myClass, NSSelectorFromString(propertyName), (IMP)nameGetter, "@@:");
        class_addMethod(myClass, NSSelectorFromString(setFunc), (IMP)nameSetter, "v@:@");
    }
    
    // 创建对象调用方法
    id obj = [[myClass alloc] init];
    [obj setValue:@"xiaoMing" forKey:NSStringFromSelector(@selector(name))];
    [obj setValue:@"宇宙1" forKey:NSStringFromSelector(@selector(addres))];
    NSLog(@"addres1 = : %@", [obj valueForKey:NSStringFromSelector(@selector(addres))]);
    [obj setValue:@"宇宙2" forKey:@"addres"];
    NSLog(@"addres1 = : %@", [obj valueForKey:@"addres"]);

    [obj performSelector:@selector(method)];
}
@end
复制代码

优点:

能都在已有的类中添加property,并且能能够遍历到动态添加的属性。这种操作由于提供了生的property,所以在第三方的json转model库遍历property时可以直接遍历到,由于手动实现了set和get方法,所以在遍历后的KVC赋值时也能起到作用,保证了和普通成员变量操作的一致性。

缺点:

比较麻烦class_addProperty只是声明了get和set方法(缺少实现和Ivar),get和set方法需要自己实现,值也需要自己存储(可以使用关联对象或者存储到已存在的ivar上)。

第四种:通过setValue:forUndefinedKey:动态添加键值

这种方法类似于property,需要重写setValue:forUndefinedKeyvalueForUndefinedKey:,存值方式也一样,需要借助一个其他对象。由于这种方式没有借助于runtime,所以也比较容易理解。

参考资料:

ios动态添加属性的几种方法

iOS-Runtime-实践篇

老生常谈category增加属性的几种操作

Objective-C runtime - 应用和示例

四、 KVC分析

Key-Value Coding(KVC)实现分析

[obj setValue:@"张三" forKey:@"name"];
// =======================================
// 就会被编译器处理成:
// =======================================
SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (obj->isa, sel);
method(obj, sel, @"张三", @"name");
复制代码

KVC运用了isa_swizzling(类型混合指针机制)技术,来实现其内部查找定位的。

- (void)setValue:(id)value forKey:(NSString *)key;
复制代码
  • ① 首先搜索 setter 方法,有就直接赋值。
  • ② 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
    • 返回 NO,则执行setValue:forUNdefinedKey:
    • 返回 YES,则按_key,_isKey,key,isKey的顺序搜索成员名。
  • ③ 还没有找到的话,就调用setValue:forUndefinedKey。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
- (id)valueForKey:(NSString *)key;
复制代码
  • ① 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
  • ② 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
    • 返回 NO,则执行valueForUNdefinedKey:
    • 返回 YES,则按_key,_isKey,key,isKey的顺序搜索成员名。
  • ③ 还没有找到的话,调用valueForUndefinedKey:

KVC 主要方法

设置值

// value的值为OC对象,如果是基本数据类型要包装成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;
// keyPath键路径,类型为xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 它的默认实现是抛出异常,可以重写这个函数做错误处理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
复制代码

获取值

- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
复制代码

NSKeyValueCoding 类别中还有其他的一些方法

// 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
+ (BOOL)accessInstanceVariablesDirectly;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果你在setValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
复制代码

实用技巧

//JSON数据:
//{
//    "username": "lxz",
//    "age": 25,
//    "id": 100
//}

@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSString age;
@property (nonatomic, assign) NSInteger userId;
@end

@implementation User
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        self.userId = [value integerValue];
    }
}
@end
复制代码

赋值时会遇到一些问题,例如服务器会返回一个id字段,但是对于客户端来说id是系统保留字段,可以重写setValue:forUndefinedKey:方法并在内部处理id参数的赋值。

转换时需要服务器数据和类定义匹配,字段数量和字段名都应该匹配。如果User比服务器数据多,则服务器没传的字段为空。如果服务端传递的数据User中没有定义,则会导致崩溃。

在KVC进行属性赋值时,内部会对基础数据类型做处理,不需要手动做NSNumber的转换。需要注意的是,NSArray和NSDictionary等集合对象,value都不能是nil,否则会导致Crash。

异常处理

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        [self setValue:@"" forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}
复制代码

当通过KVC给某个非对象的属性赋值为nil时,此时KVC会调用属性所属对象的setNilValueForKey:方法,并抛出NSInvalidArgumentException的异常,并使应用程序Crash。

我们可以通过重写下面方法,在发生这种异常时进行处理。例如给name赋值为nil的时候,就可以重写setNilValueForKey:方法并表示name是空的。

应用场景:

1、访问修改私有变量(替换系统自带的导航栏,tabBar,替换UIPageControl的image等等)

KVC修改readonly的系统隐藏变量。将UIPageControl的圆形替换为长条形。

[pageControler setValue:[UIImage imageNamed:@"line"] forKeyPath:@"pageImage"];
[pageControler setValue:[UIImage imageNamed:@"current"] forKeyPath:@"currentPageImage"];
复制代码
2、valueForKeyPath的使用更加广泛,功能也更加强大

1、对数组求和、平均值、最大值、最小值。

NSArray *array = @[@1, @3, @5, @7, @9,@11, @13];
NSInteger sumPath = [[array valueForKeyPath:@"@sum.floatValue"] integerValue];
NSInteger avgPath = [[array valueForKeyPath:@"@avg.floatValue"] integerValue];
NSInteger maxPath = [[array valueForKeyPath:@"@max.floatValue"] integerValue];
NSInteger minPath = [[array valueForKeyPath:@"@min.floatValue"] integerValue];
NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sumPath, (long)avgPath, (long)maxPath, (long)minPath);
// 上述例子经验证是可取的,但下面的写法不可取(将引起崩溃)
NSInteger sum = [[array valueForKey:@"@sum.floatValue"] integerValue];
NSInteger avg = [[array valueForKey:@"@avg.floatValue"] integerValue];
NSInteger max = [[array valueForKey:@"@max.floatValue"] integerValue];
NSInteger min = [[array valueForKey:@"@min.floatValue"] integerValue];
NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sum, (long)avg, (long)max, (long)min);
复制代码

2、删除数组中重复的数据

NSArray *array = @[@1, @3, @5, @7, @9, @11, @13, @7, @9,@11];
NSLog(@"deleteKeyPath = %@",[array valueForKeyPath:@"@distinctUnionOfObjects.self"]);
// 下述写法不可取,会引起崩溃
NSLog(@"deleteKey = %@",[array valueForKey:@"@distinctUnionOfObjects.self"]);
复制代码

3、深层次取出字典中的属性

NSDictionary *dic = @{@"dic1":@{@"dic2":@{@"name":@"zhangsanfeng",@"info":@{@"age":@"13"}}}};
NSLog(@"KeyPath = %@",[dic valueForKeyPath:@"dic1.dic2.info.age"]); // 可以深层次的取到子层级属性
NSLog(@"Key = %@",[dic valueForKey:@"dic1.dic2.info.age"]);         // 无法深层次取到子层级属性
复制代码

参考资料:

KVC/KVO原理详解及编程指南

ios动态添加属性的几种方法

KVC, KVO实现原理剖析

KVC 与 KVO 使用姿势和原理解析

KVC原理剖析

KVC 中的 valueForKeyPath 高级用法

iOS 关于KVC的一些总结

五、IMP

method_t的结构

struct method_t {
    SEL name;           // 方法名(一个类里面可能有多个name相同的method,比如分类中重写的方法)
    const char *types;  // 存储着方法的参数类型和返回值类型的描述字串 
    IMP imp;            // 方法的函数指针(方法实现,相同的name可能对应不同的实现)
};
复制代码

IMP与SEL的区别?

  • IMP 它是一个指向方法实现的指针,每一个方法都一个对应的IMP指针
  • SEL 是方法的名字,不同的方法可能名字(SEL)相同,实现(IMP)不同。比如 category中重写了类方法,则可能出现不同的method对应相同的名字(SEL),但是实现(IMP)不同

怎么获取IMP

1、根据method获取IMP(唯一)

// Method method = class_getInstanceMethod([self class], sel);
Method method = someMethod;
IMP imp = method_getImplementation(method);
复制代码

2、根据SEL获取IMP(可能不是想要的mehtod的IMP)

// 第一种:methodForSelector(SEL) (内部是用 class_getMethodImplementation 实现)
SEL sel = @selector(myFunc);
IMP imp = [self methodForSelector:sel];

// 第二种:class_getMethodImplementation(Class, SEL)
SEL sel = @selector(myFunc);
IMP imp = class_getMethodImplementation(self, sel);
复制代码

执行一个selector的几种方法

SEL sel = @selector(someFunc:);
复制代码

1、使用objc_msgSend(接受者+选择器+参数)

#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
#else
OBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
#endif
复制代码

2、使用performSelector(尽量不使用)

performSelector系列方法在内存管理上容易有缺失,它无法确定将要执行的选择子是什么,因而ARC编译器也无法插入适当的内存管理方法,这是一个大坑,使用GCD则不存在这个问题。

// 没有参数
- (id)performSelector:(SEL)aSelector;
// 传递一个参数
- (id)performSelector:(SEL)aSelector withObject:(id)object;
// 传递两个参数
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
复制代码

实现performSelector 传递多个参数

3、直接调用IMP(相当于C语言函数指针)

// 不同的返回值使用不同的宏,否则会报EXC_BAD_ACCESS错误
typedef id (*_IMP) (id, SEL, ...);
typedef int (*_INT_IMP) (id, SEL, ...);
typedef bool (*_BOOL_IMP) (id, SEL, ...);
typedef void (*_VOID_IMP) (id, SEL, ...);

Method mthod = class_getInstanceMethod([Obj class], sel);
_IMP imp = (_IMP)method_getImplementation(mthod);
imp(Obj, sel, 参数列表)
复制代码

4、通过NSInvocation调用

NSMethodSignature * methodSignature  = [[myObj class] instanceMethodSignatureForSelector:@selector(myFunc)];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:myObj];
[invocation setSelector:@selector(myFunc)];
NSString *a=@"111";
int b=2;
[invocation setArgument:&a atIndex:2];
[invocation setArgument:&b atIndex:3];
[invocation retainArguments];
[invocation invoke];
复制代码

objc_msgSend的方法实现的伪代码

id objc_msgSend(id self, SEL op, ...) {
   if (!self) return nil;
   // 关键代码(a)
   IMP imp = class_getMethodImplementation(self->isa, SEL op);
   imp(self, op, ...); // 调用这个函数,伪代码...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) {
      ... // 执行动态绑定
    }
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; // 这个是用于消息转发的
    return imp;
}
// 遍历继承链,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
    Class curClass = cls;
    IMP imp = nil;
    do { // 先查缓存,缓存没有时重建,仍旧没有则向父类查询
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass); // 关键代码(b)
    return imp;
}
复制代码

IMP实战

一:如果分类中重写了类的方法,找到原有方法,并且执行获取结果

/**
 如果分类中重写了类的方法,找到原有方法,并且执行获取结果

 @param aString 需要比较的NSString
 @return YES or NO
 */
-(BOOL)excuteoRiginalIsEqualToString:(NSString*)aString {
    unsigned int count;
    Method originalMethod = {0};
    // 获取类的所有方法列表,根据SEL匹配,可能找到多个method,最后一个即原有method
    Method *methods = class_copyMethodList([self class], &count);
    for (int i = 0; i < count; i++) {
        const char* funcName = sel_getName(method_getName(methods[i]));
        if ( 0 == strcmp(funcName, "isEqualToString:") ) {
            // category中的方法在方法列表中的下标小,最后一个为原来的方法
            originalMethod = methods[i];
        }
    }
    _BOOL_IMP imp = (_BOOL_IMP)method_getImplementation(originalMethod);
    BOOL res = NO;
    if (imp) {
        res = imp(self, method_getName(originalMethod), aString);
    }
    free(methods);
    return res;
}
复制代码

二:当每个Controller执行完ViewDidLoad以后就在控制台把自己的名字打印出来,方便去做调试或者了解项目结构

#import "UIViewController+viewDidLoad.h"
#import 

@implementation UIViewController (viewDidLoad)

+ (void)load
{
    //保证交换方法只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //获取原始方法
        Method viewDidLoad = class_getInstanceMethod(self, @selector(viewDidLoad));
        //获取方法实现
        _VIMP viewDidLoad_IMP = (_VIMP)method_getImplementation(viewDidLoad);
        //重新设置方法实现
        method_setImplementation(viewDidLoad,imp_implementationWithBlock(^(id target,SEL action){
            viewDidLoad_IMP(target,@selector(viewDidLoad));
            //自定义代码
            NSLog(@"%@ did load",target);
        }));
    });
}
复制代码

参考博客

GCD实践(二)少用performSelector系列方法

刨根究底iOS—调戏Category

六、KVO分析

一次简单的KVO操作

@interface Man : NSObject
@property (nonatomic, assign) NSInteger p_mustacheLength;
// 直接修改成员变量
- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength;
// 手动触发
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength;
@end
@implementation Man
// 直接修改成员变量
- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength {
    _p_mustacheLength = p_mustacheLength;
}
// 手动触发
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength {
    [self willChangeValueForKey:@"p_mustacheLength"];
    _p_mustacheLength = p_mustacheLength;
    [self didChangeValueForKey:@"p_mustacheLength"];
}
// 重写set方法
- (void)setP_mustacheLength:(NSInteger)p_mustacheLength {
    _p_mustacheLength = p_mustacheLength;
}
// 是否自动对属性p_mustacheLength触发KVO
+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength {
    // 默认返回YES
    return YES;
}
@end
@interface KVOVC : UIViewController
@end

// 同一个属性观察了多次,用来区分是哪一次观察操作
// const*:不能改变内容
// const:不能改变地址
char const* const context_p_man_p_mustacheLength_1 = "context_p_man_p_mustacheLength_1";
char const* const context_p_man_p_mustacheLength_2 = "context_p_man_p_mustacheLength_2";
@interface KVOVC ()
@property (nonatomic, strong) Man* p_man;
@end
@implementation KVOVC
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    NSLog(@"-----------------------------------------");
    NSLog(@"keyPath = %@", keyPath);
    NSLog(@"object = %@", object);
    NSLog(@"change = %@", change);
    NSLog(@"context = %s", context);
}
- (void)viewDidLoad {
    [super viewDidLoad];
	self.p_man = [[Man alloc] init];
	
    [self.p_man addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                    options:NSKeyValueObservingOptionNew
                    context:(void*)context_p_man_p_mustacheLength_1];
    
    [self.p_man addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                    options:NSKeyValueObservingOptionNew
                    context:(void*)context_p_man_p_mustacheLength_2];

    // (触发)set方法可以触发(无论是否重写Man的p_mustacheLengthset方法。因为此时p_man的isa = NSKVONotifying_Man而不是Man 查看链接)
    self.p_man.p_mustacheLength = 10;
    // (触发)kvc可以触发(kvc首先查找调用的也是set方法 查看链接)
    [self.p_man setValue:@20 forKey:NSStringFromSelector(@selector(p_mustacheLength))];
    // (不能触发)直接修改成员变量不能触发(没有走set方法)
    [self.p_man set_P_mustacheLength:30];
    // (触发)手动触发
    [self.p_man set_P_mustacheLength_manual:40];
}
- (void)dealloc {
    NSLog(@"%@-%s-%d", NSStringFromClass([self class]), __func__, __LINE__);
        [self.p_man removeObserver:self
                    forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                       context:(void*)context_p_man_p_mustacheLength_1];
    [self.p_man removeObserver:self
                    forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                       context:(void*)context_p_man_p_mustacheLength_2];
}
@end
复制代码

官方KVO文档:Key-Value Observing Implementation Details

自动键值观察是使用isa- swizzle技术实现的。顾名思义,isa指针指向维护分派表的对象的类。这个分派表本质上包含指向类实现的方法以及其他数据的指针。当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。 您永远不应该依赖isa指针来确定类的继承关系。相反,您应该使用类方法来确定对象实例的类。

KVO原理

一:在self.p_man添加KVO之前,查看其继承关系。结果:isa = Man,superClass = NSObject

二:在self.p_man添加KVO之后,查看其继承关系。结果:isa = NSKVONotifying_Man,superClass = NSObject

官方文档中提及做多的关键字就是isa,Objective-C的消息机制就是通过isa查找方法的。其实在添加KVO之后,isa已经替换成了NSKVONotifying_Man。因此调用属性的set方法的时候,根据isa找到的方法其实是NSKVONotifying_Man中的set方法。

KVO是基于runtime机制实现的,当某个实例的属性第一次被观察的时候,系统会在运行时期动态的创建一个该类的子类(类名=NSKVONotifying_XXX)并将isa指针指向新创建的子类。在这个派生类中重写所有被观察属性的set方法,在成员变量被改变前调用NSObject的willChangeValueForKey:,被改变后调用didChangeValueForKey:。从而导致observeValueForKey:ofObject:change:context被调用。

KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

-(void) addObserver: forKeyPath: options: context: 这个部分就是观察者的注册了。通过以下类图可以很方便得看到,所有的类的KVO观察都是通过infoTable管理的。以被观察对象实例作key,GSKVOInfo对象为value的形式保存在infoTable表里,每个被观察者实例会对应多个keypath,每个keypath会对应多个observer对象。顺带提一下,关于Notification的实现也类似,也是全局表维护通知的注册监听者和通知名。 GSKVOInfo的结构可以看出来,一个keyPath可以对应有多个观察者。其中观察对象的实例和option打包成GSKVOObservation对象保存在一起。

如何手动触发一个的KVO

// 手动触发
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength {
    [self willChangeValueForKey:@"p_mustacheLength"];
    _p_mustacheLength = p_mustacheLength;
    [self didChangeValueForKey:@"p_mustacheLength"];
}
复制代码

如果要禁止KVO对某个属性自动触发,返回NO就可以

// 是否自动对属性p_mustacheLength触发KVO
+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength {
    // 默认返回YES
    return YES;
}
复制代码

KVO容易掉进去的坑

  • 1、没有removeObserver (使用KVOController或者不要忘记)
  • 2、同一个对象的同一个属性被重复removeObserver了多次(使用context来removeObserver)
  • 3、keyPath严重依赖于string(@selector弥补)

KVOController实现探索

自己实现一个KVO:ImplementKVO

参考博客

KVO进阶——KVO实现探究

探究KVO的底层实现原理

从自己实现isa-swizzling到说一些Runtime的内容

iOS探索KVO实现原理,重写KVO

如何优雅地使用 KVO

七、消息转发

为啥可以对nil对象发送消息?

NilTest 宏,判断被发送消息的对象是否为 nil 的。如果为 nil,那就直接返回 nil

参考资料:

Objective-C 消息发送与转发机制原理

八、weak的原理

weak不论是用作property修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak修饰的属性或变量均会被设置为nil,这样可以防止野指针错误,本文要讲解的也正是这个特性,runtime如何将weak修饰的变量的对象在销毁时自动置为nil。

那么runtime是如何实现在weak修饰的变量的对象在被销毁时自动置为nil的呢?

一个普遍的解释是:runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

你可能感兴趣的:(Runtime学习笔记整理)