iOS Runtime介绍和使用

  1. Runtime 简介
  2. Runtime 消息机制和相关函数
  3. Runtime 三次转发流程
  4. Runtime 应用
  5. Runtime 面试题

1. Runtime 简介

Objective-C 是一个动态语言,这意味着它不只须要一个编译器,也须要一个运行时系统来动态得建立类和对象、进行消息传递和转发。RuntimeObjective-C 面向对象和动态机制的基石,能够从系统层面解决一些设计或技术问题。它基本是用 C 和汇编写的,属于1个 C 语言库,包含了不少底层的 C 语言 API ,如跟类、成员变量、方法相关的API。它的核心是 - 消息传递 ( Messaging )。html

  • 动态绑定(在运行时肯定要调用的方法)
    动态绑定将调用方法的肯定推迟到运行时。在编译时,方法的调用并不和代码绑定在一块儿,只有在消实发送出来以后,才肯定被调用的代码。经过动态类型和动态绑定技术,您的代码每次执行均可以获得不一样的结果。运行时所以负责肯定消息的接收者和被调用的方法。
    运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型肯定了的对象发送消息时,运行环境系统会经过接收者的isa指针定位对象的类,并以此为起点肯定被调用的方法,方法和消息是动态绑定的。
  • Runtime 交互
    Objective-C 从三种不一样的层级上与 Runtime 系统进行交互:
    • Objective-C 源代码
    • Foundation 框架的 NSObject 类定义的方法
    • runtime 函数的直接调用
  • NSProxy
    Cocoa 中大多数类都继承于 NSObject 类,也就天然继承了它的方法。最特殊的例外是 NSProxy ,它是个抽象超类,它实现了一些消息转发有关的方法,能够经过继承它来实现一个其余类的替身类或是虚拟出一个不存在的类。

2. Runtime 消息机制和相关函数

  • Runtime 详细消息发送步骤:
    • 检测这个 selector 是否是要忽略的。好比 Mac OS X 开发,有了垃圾回收就不理会 retain , release 这些函数了。
    • 检测这个 target 是否是 nil 对象。Objective-C 的特性是容许对一个 nil 对象执行任何一个方法不会 Crash ,由于会被忽略掉。
    • 若是上面两个都过了,那就开始查找这个类的 IMP ,先从 cache 里面找,完了找获得就跳到对应的函数去执行。
    • 若是 cache 找不到就找一下方法分发表。
    • 若是分发表找不到就到超类的分发表去找,一直找,直到找到 NSObject 类为止。
    • 若是还找不到就要开始进入动态方法解析了。
    • 若是仍是找不到而且消息转发都失败了就回执行 doesNotRecognizeSelector: 方法报 unrecognized selector 错。
  • 举例:
    一个对象的方法像这样[obj eat],编译器转成消息发送objc_msgSend(obj, eat)Runtime时执行的流程是这样的:
    1. 经过 objisa 指针找到它的 class
    2. classmethod listeat
    3. 若是 class 中没找到 eat,继续往它的 superclass 中找,一旦找到 eat 这个函数,就去执行它的实现IMP
  • 头文件
    • <objc/runtime.h>
    • <objc/message.h>
  • 消息传递用到的一些概念:
    实例 objc_object
    类对象 objc_class
    元类 Meta Class
    Method objc_method
    SEL objc_selector
    类缓存 objc_cache
    Category objc_category
    IMP

objc_msg

id objc_msgSend ( id self, SEL op, ... );
复制代码
  • id
    objc_msgSend 第一个参数类型为id,是一个指向类实例的指针
    typedef struct objc_object *id;
    复制代码
  • SEL(objc_selector)
    objc_msgSend 第二个参数类型为SEL,它是 selectorObjective-C 中的表示类型( Swift 中是 Selector 类)。selector 是方法选择器,能够理解为区分方法的 ID,而这个 ID 的数据结构是SEL。能够看到selectorSEL的一个实例
    typedef struct objc_selector *SEL;
    复制代码
    @property SEL selector;
    复制代码
    其实它就是个映射到方法的C字符串,你能够用 Objc 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来得到一个 SEL 类型的方法选择器。
    注意:写 C 代码的时候,常常会用到函数重载,就是函数名相同,参数不一样,可是这在Objc中是行不通的,由于selector只记了 methodname ,没有参数,因此无法区分不一样的 method
  • 举例
    OC: [[Person alloc] init]
    Runtime: objc_msgSend(objc_msgSend("Person" , "alloc"), "init")

实例(objc_object)

objc_msgSend 第一个参数类型为 id 指向类实例的指针,即 objc_objectios

objc_object 结构体包含一个 isa 指针,类型为 isa_t 联合体。根据 isa 指向对象所属的类。isa 这里还涉及到 tagged pointer 等概念。由于 isa_t 使用 union 实现,因此可能表示多种形态,既能够当成是指针,也能够存储标志位置。git

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ... 此处省略其余方法声明
}
复制代码

注意: isa 指针不老是指向实例对象所属的类,不能依靠它来肯定类型,而是应该用 class 方法来肯定实例对象的类。由于 KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫作 isa-swizzling 的技术。github

objc_class

Objective-C 类是由 Class 类型来表示的,它其实是一个指向 objc_class 结构体的指针。面试

typedef struct objc_class *Class;
复制代码

objc/runtime.hobjc_class 结构体的定义以下:objective-c

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                         OBJC2_UNAVAILABLE;
    const char * _Nonnull name                          OBJC2_UNAVAILABLE;
    long version                                        OBJC2_UNAVAILABLE;
    long info                                           OBJC2_UNAVAILABLE;
    long instance_size                                  OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars             OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                  OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
复制代码

结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵照的协议列表等。json

对象在内存中的排布能够当作一个结构体,该结构体的大小并不能动态变化,因此没法在运行时动态给对象增长成员变量。相对的,对象的方法定义都保存在类的可变区域中。以下图所示为 Class 的描述信息,其中 methodList 为可访问类中定义的方法的指针的指针,经过修改该指针所指向的指针的值,咱们能够实现为类动态增长方法实现。数组

objc_class 继承于 objc_object,也就是说一个 Objective-C 类自己同时也是一个对象,咱们称之为类对象,类对象就是一个结构体 struct objc_class ,这个结构体存放的数据称为元数据。为了处理类和对象的关系,runtime 库建立了一种叫作元类 (Meta Class) 的东西,类对象所属类型就叫作元类,它用来表述类对象自己所具有的元数据。类方法就定义于此处,由于这些方法能够理解成类对象的实例方法。每一个类仅有一个类对象,而每一个类对象仅有一个与之相关的元类。缓存

当你发出一个相似 [NSObject alloc] 的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。全部的元类最终都指向根元类为其超类。全部的元类的方法列表都有可以响应消息的类方法。因此当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找可以响应消息的方法,若是找到了,而后对这个类对象执行方法调用。bash

元类(Meta Class)

元类(Meta Class)是一个类对象的类。 在上面咱们提到,全部的类自身也是一个对象,咱们能够向这个对象发送消息(即调用类方法)。 为了调用类方法,这个类的 isa 指针必须指向一个包含这些类方法的一个 objc_class 结构体,这就引出了 meta-class 的概念。

类对象中的元数据存储的都是如何建立一个实例的相关信息,那么类对象和类方法应该从哪里建立呢? 就是从 isa 指针指向的结构体建立,类对象的 isa 指针指向的咱们称之为元类(metaclass),元类中保存了建立类对象以及类方法所需的全部信息。

  1. 每一个 Class 都有一个 isa 指针指向一个惟一的 Meta Class
  2. 每个 Meta Classisa 指针都指向最上层的 Meta Class(图中的 NSObjectMeta Class
  3. 最上层的 Meta Classisa 指针指向本身,造成一个回路
  4. 每个 Meta Classsuper_class 指针指向它本来 Classsuper_classMeta Class 。可是最上层的 Meta Classsuper_class 指向 NSObject Class 自己
  5. 最上层的 NSObject Classsuper_classnil ,也就是它没有超类

Method(objc_method)

objc/runtime.h :

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                         OBJC2_UNAVAILABLE;
    char *method_types                      OBJC2_UNAVAILABLE;
    IMP method_imp                          OBJC2_UNAVAILABLE;
}
复制代码
  • objc_method 结构体的内容:
    SEL method_name : 方法名,相同名字的方法即便在不一样类中定义,它们的方法选择器也相同
    char *method_types : 方法类型,是个char指针,其实存储着方法的参数类型和返回值类型
    IMP method_imp : 方法实现,本质上是一个函数指针

iOSRuntime 中,Method 经过 selectorIMP 两个属性,实现了快速查询方法及实现,相对提升了性能,又保持了灵活性

类缓存(objc_cache)

cache 为方法调用的性能进行优化。每一个消息都须要遍历一次 isa 指向的类的方法列表(objc_method_list),这样效率过低了。Runtime 系统会把被调用的方法存到 cache 中( method_name 做为keymethod_imp 做为value)。下次查找的时候会优先在 cache 中查找,效率更高。
objc_cache 是存在 objc_class 结构体中的。

cache_t_buckets_mask_occupied:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ... 省略其余方法
}
复制代码

bucket_t 中存储了 指针IMP 的键值对:

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};
复制代码

Category(objc_category)

Category 为现有的类提供了拓展性,它是 category_t 一个指向分类的结构体的指针。

typedef struct category_t *Category;
复制代码
struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};
复制代码
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段经过name对 应到对应的类对象。
instanceMethods:category中全部给类添加的实例方法的列表。
classMethods:category中全部添加的类方法的列表。
protocols:category实现的全部协议的列表。
instanceProperties:表示Category里全部的properties,这就是咱们能够经过objc_setAssociatedObject和objc_getAssociatedObject增长实例变量的缘由,不过这个和通常的实例变量是不同的。
复制代码

从上边category_t的结构体中能够看出,分类中能够添加实例方法,类方法,甚至能够实现协议,添加属性,不能够添加成员变量。

Ivar

Ivar 是一种表明类中实例变量的类型。

typedef struct ivar_t *Ivar;
复制代码

ivar_t

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};
复制代码

class_copyIvarList 函数获取的不只有实例变量,还有属性。但会在本来的属性名前加上一个下划线。

objc_property_t

@property 标记了类中的属性,它是一个指向objc_property结构体的指针:

typedef struct property_t *objc_property_t;
复制代码

能够经过 class_copyPropertyListprotocol_copyPropertyList 方法来获取类和协议中的属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
复制代码

返回类型为指向指针的指针,由于属性列表是个数组,每一个元素内容都是一个 objc_property_t 指针,而这两个函数返回的值是指向这个数组的指针。

class_copyIvarListclass_copyPropertyList 对比:

- (void)runtimeGetPropertyList {
    id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(RuntimeExploreInfo, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        fprintf(stdout, "runtimeGetPropertyList---%s %s\n", property_getName(property), property_getAttributes(property));
    }
}

- (void)runtimeGetIvarList {
    id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
    unsigned int numIvars = 0;
    Ivar *ivars = class_copyIvarList(RuntimeExploreInfo, &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        fprintf(stdout, "runtimeGetIvarList---%s\n", ivar_getName(thisIvar));
    }
}
复制代码

打印结果:

IMP

就是指向最终实现程序的内存地址的指针。

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

它就是一个函数指针,这是由编译器生成的。当你发起一个 Objective-C 消息以后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 idSEL 类型。每一个方法名都对应一个 SEL 类型的方法选择器,而每一个实例对象中的 SEL 对应的方法实现确定是惟一的,经过一组 idSEL 参数就能肯定惟一的方法实现地址;反之亦然。

3. Runtime的三次转发流程

进行一次发送消息会在相关的类对象中搜索方法列表,若是找不到则会沿着继承树向上一直搜索直到继承树根部(一般为 NSObject ),若是仍是找不到而且消息转发都失败了就回执行 doesNotRecognizeSelector: 方法报 unrecognized selector 错。

Runtime的三次转发流程:

  1. 动态方法解析: +resolveInstanceMethod:, +resolveClassMethod:
  2. 消息转发: forwardingTargetForSelector
  3. 重定向: methodSignatureForSelector:, forwardInvocation:

动态方法解析

Objective-C 运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。若是你添加了函数并返回YES, 那运行时系统就会从新启动一次消息发送的过程。

- (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //执行foo函数
        [self performSelector:@selector(foo:)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(foo:)) {//若是是执行foo函数,就动态解析,指定新的IMP
            class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void fooMethod(id obj, SEL _cmd) {
        NSLog(@"Doing foo");//新的foo函数
    }
复制代码

若是resolve方法返回 NO ,运行时就会移到下一步 :forwardingTargetForSelector

消息转发

若是目标对象实现了 -forwardingTargetForSelector:Runtime 这时就会调用这个方法,给你把这个消息转发给其余对象的机会。
Controller :

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self performSelector:@selector(runtimeMessageTest)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(runtimeMessageTest)) {
        return [RuntimeExploreInfo new]; // 返回RuntimeExploreInfo对象,让RuntimeExploreInfon对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
复制代码

RuntimeExploreInfo :

#import "RuntimeExploreInfo.h"
    
    @implementation RuntimeExploreInfo
    
    - (void)runtimeMessageTest {
        NSLog(@"runtimeMessageTest---");
    }
    
    @end
复制代码

经过 forwardingTargetForSelector 把当前 Controller 的方法转发给了 RuntimeExploreInfo 去执行。

重定向

若是在上一步还不能处理未知消息,则惟一能作的就是启用完整的消息转发机制了。 首先它会发送 -methodSignatureForSelector: 消息得到函数的参数和返回值类型。若是 -methodSignatureForSelector: 返回 nilRuntime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。若是返回了一个函数签名,Runtime 就会建立一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。

- (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
        [self performSelector:@selector(runtimeMessageTest)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES; // 返回YES,进入下一步转发
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return nil; // 返回nil,进入下一步转发
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"runtimeMessageTest"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 签名,进入forwardInvocation
        }
        
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;
        
        RuntimeExploreInfo *p = [RuntimeExploreInfo new];
        if([p respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:p];
        }else {
            [self doesNotRecognizeSelector:sel];
        }
    }
复制代码

咱们实现了完整的转发。经过签名,Runtime 生成了一个对象 anInvocation ,发送给了 forwardInvocation ,咱们在 forwardInvocation 方法里面让 RuntimeExploreInfo 对象去执行了 runtimeMessageTest 函数。

4. Runtime 应用

  1. 关联对象( Objective-C Associated Objects )给分类增长属性
  2. 方法魔法( Method Swizzling )方法添加和替换
  3. KVO 实现
  4. 实现 NSCoding 的自动归档和自动解档
  5. 实现字典和模型的自动转换( MJExtensionYYModel )
  6. 用于封装框架(想怎么改就怎么改)

关联对象( Objective-C Associated Objects )给分类增长属性

RuntimeExploreInfo+RuntimeAddProperty.h 添加了 phoneNum 属性

#import "RuntimeExploreInfo+RuntimeAddProperty.h"
    #import "objc/runtime.h"
    
    @implementation RuntimeExploreInfo (RuntimeAddProperty)
    
    static char kPhoneNumKey;
    
    - (void)setPhoneNum:(NSString *)phoneNum {
        objc_setAssociatedObject(self, &kPhoneNumKey, phoneNum, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id)phoneNum {
        return objc_getAssociatedObject(self, &kPhoneNumKey);
    }
    
    @end
复制代码
- (void)runtimeAddProperty {
        RuntimeExploreInfo *test = [RuntimeExploreInfo new];
        test.phoneNum = @"12342424242";
        NSLog(@"RuntimeAddProperty---%@", test.phoneNum);
    }
复制代码

方法魔法( Method Swizzling )方法添加和替换和 KVO 实现

  • 添加方法
    /**
     class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
     cls 被添加方法的类
     name 添加的方法的名称的SEL
     imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
     类型编码
     */
    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    复制代码
  • 替换方法
    class_replaceMethod 替换类方法的定义
    method_exchangeImplementations 交换两个方法的实现
    method_setImplementation 设置一个方法的实现
    注意:class_replaceMethod 试图替换一个不存在的方法时候,会调用 class_addMethod 为该类增长一个新方法
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            SEL originalSelector = @selector(viewDidLoad);
            SEL swizzledSelector = @selector(runtimeReplaceViewDidLoad);
            
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            
            //judge the method named  swizzledMethod is already existed.
            BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            // if swizzledMethod is already existed.
            if (didAddMethod) {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    - (void)runtimeReplaceViewDidLoad {
        NSLog(@"替换的方法");
        //[self runtimeReplaceViewDidLoad];
    }
    复制代码
    swizzling应该只在 +load 中执行一次( dispatch_once )完成。在 Objective-C 的运行时中,每一个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,而且只有在方法被实现的状况下才会被调用。

KVO实现

Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态建立一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVONSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法以前和以后,通知全部观察对象属性值的更改状况。

NSKVONotifying_A 类剖析

NSLog(@"self->isa:%@",self->isa);  
    NSLog(@"self class:%@",[self class]);  
复制代码

在创建KVO监听前,打印结果为:

self->isa:A
    self class:A
复制代码

在创建KVO监听以后,打印结果为:

self->isa:NSKVONotifying_A
    self class:A
复制代码

子类setter方法剖析:
KVO 的键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:didChangeValueForKey: ,在存取数值的先后分别调用 2 个方法: 被观察属性发生改变以前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变动;当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变动;以后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工做原理在代码中至关于: - (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO 在调用存取方法以前总调用 [super setValue:newName forKey:@"name"]; //调用父类的存取方法 [self didChangeValueForKey:@"name"]; //KVO 在调用存取方法以后总调用 }

实现NSCoding的自动归档和自动解档

原理描述:用 runtime 提供的函数遍历 Model 自身全部属性,并对属性进行 encodedecode 操做。

核心方法:在Model的基类中重写方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            unsigned int outCount;
            Ivar * ivars = class_copyIvarList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                Ivar ivar = ivars[i];
                NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
            }
        }
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [aCoder encodeObject:[self valueForKey:key] forKey:key];
        }
    }
复制代码

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

原理描述:用runtime提供的函数遍历Model自身全部属性,若是属性在json中有对应的值,则将其赋值。
核心方法:在NSObject的分类中添加方法

- (instancetype)initWithDict:(NSDictionary *)dict {
    
        if (self = [self init]) {
            //(1)获取类的属性及属性对应的类型
            NSMutableArray * keys = [NSMutableArray array];
            NSMutableArray * attributes = [NSMutableArray array];
    
            unsigned int outCount;
            objc_property_t * properties = class_copyPropertyList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                objc_property_t property = properties[i];
                //经过property_getName函数得到属性的名字
                NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
                [keys addObject:propertyName];
                //经过property_getAttributes函数能够得到属性的名字和@encode编码
                NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
                [attributes addObject:propertyAttribute];
            }
            //当即释放properties指向的内存
            free(properties);
    
            //(2)根据类型给属性赋值
            for (NSString * key in keys) {
                if ([dict valueForKey:key] == nil) continue;
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
        return self;
    
    }
复制代码

5. Runtime 面试题

  • Self & Super

    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self)
        {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    复制代码

    答案:都输出 Son
    解惑:这个题目主要是考察关于 objc 中对 selfsuper 的理解。

    self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。上面的例子无论调用 [self class] 仍是 [super class] ,接受消息的对象都是当前 Son *xxx 这个对象。而不一样的是,super 是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

    当使用 self 调用方法时,会从当前类的方法列表中开始找,若是没有,就从父类中再找;
    而当使用 super 时,则从父类的方法列表中开始找。而后调用父类的这个方法。

    当调用 [self class] 时,实际先调用的是 objc_msgSend 函数,第一个参数是 Son 当前的这个实例,而后在 Son 这个类里面去找 - (Class)class 这个方法,没有,去父类 Father 里找,也没有,最后在 NSObject 类中发现这个方法。而 - (Class)class的实现就是返回 self 的类别,故上述输出结果为 Son

    当调用 [super class] 时,会转换成 objc_msgSendSuper 函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。第二步是去 Father 这个类里去找 - (Class)class ,没有,而后去 NSObject 类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去调用,此时已经和 [self class] 调用相同了,故上述输出结果仍然返回 Son

  • Object & Class & Meta Clas

    @interface Sark : NSObject
    @end
    @implementation Sark
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
            BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
            BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
            BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
            NSLog(@"%d %d %d %d", res1, res2, res3, res4);
        }
        return 0;
    }
    复制代码

    答案: 1 0 0 0
    咱们看到在 Objective-C 的设计哲学中,一切都是对象。Class在设计中自己也是一个对象。而这个 Class 对象的对应的类,咱们叫它 Meta Class 。即 Class 结构体中的 isa 指向的就是它的 Meta Class
    Meta Class 理解为 一个 Class 对象的 Class 。简单的说:
    当咱们发送一个消息给一个 NSObject 对象时,这条消息会在对象的类的方法列表里查找;
    当咱们发送一个消息给一个类时,这条消息会在类的 Meta Class 的方法列表里查找

  • 消息 和 Category

    @interface NSObject (Sark)
    + (void)foo;
    @end
    @implementation NSObject (Sark)
    - (void)foo
    {
        NSLog(@"IMP: -[NSObject(Sark) foo]");
    }
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [NSObject foo];
            [[NSObject new] foo];
        }
        return 0;
    }
    复制代码

    答案:
    IMP: -[NSObject(Sark) foo]
    IMP: -[NSObject(Sark) foo]
    解释:

    1. objc runtime 加载完后,NSObjectSark Category 被加载。而 NSObjectSark Category 的头文件 + (void)foo 并无实质参与到工做中,只是给编译器进行静态检查,全部咱们编译上述代码会出现警告,提示咱们没有实现 + (void)foo 方法。而在代码编译中,它已经被注释掉了。
    2. 实际被加入到 Classmethod list 的方法是 - (void)foo ,它是一个实例方法,因此加入到当前类对象 NSObject 的方法列表中,而不是 NSObject Meta class 的方法列表中。
    3. 当执行 [NSObject foo] 时,咱们看下整个 objc_msgSend 的过程:
    • objc_msgSend 第一个参数是 (id)objc_getClass("NSObject") ,得到 NSObject Class 的对象。
    • 类方法在 Meta Class 的方法列表中找,咱们在 load Category 方法时加入的是 - (void)foo 实例方法,因此并不在 NSOBject Meta Class 的方法列表中
    • 继续往 super class 中找,NSObject Meta Classsuper classNSObject 自己。因此,这个时候咱们可以找到 - (void)foo 这个方法。
      因此正常输出结果。
    1. 当执行 [[NSObject new] foo] ,咱们看下整个 objc_msgSend 的过程:
      [NSObject new] 生成一个 NSObject 对象。直接在该对象的类( NSObject )的方法列表里找。可以找到,因此正常输出结果。
  • 成员变量与属性

    @interface Sark : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    @implementation Sark
    - (void)speak
    {
        NSLog(@"my name is %@", self.name);
    }
    @end
    @interface Test : NSObject
    @end
    @implementation Test
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            id cls = [Sark class];
            void *obj = &cls;
            [(__bridge id)obj speak];
        }
        return self;
    }
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [[Test alloc] init];
        }
        return 0;
    }
    复制代码

    答案: my name is

 

更多实用详见 Demo Runtime文件夹下

 

参考文章

Objective-C Runtime
刨根问底Objective-C Runtime

相关文章
相关标签/搜索