阅读 1726

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 )。

  • 动态绑定(在运行时确定要调用的方法)
    动态绑定将调用方法的确定推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,您的代码每次执行都可以得到不同的结果。运行时因此负责确定消息的接收者和被调用的方法。
    运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的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_object

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

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 的技术。

objc_class

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

typedef struct objc_class *Class;
复制代码

objc/runtime.hobjc_class 结构体的定义如下:

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

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

对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态给对象增加成员变量。相对的,对象的方法定义都保存在类的可变区域中。如下图所示为 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() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

元类(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 去执行。

注意:在类不能处理某个selector的情况下,如果类重载了该函数,并使用class_addMethod添加了相应的selector,并返回YES,那么后面forwardingTargetForSelector就不会被调用,如果在该函数中没有添加相应的selector,那么不管返回什么,后面都会继续调用 forwardingTargetForSelector,如果在forwardingTargetForSelector并未返回能接受该selector的对象,那么resolveInstanceMethod会再次被触发,这一次,如果仍然不添加selector,程序就会报异常.

重定向

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 首先它会发送 -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