Runtime Summary

Posted by syea on July 10, 2018

Runtime Summary

作为一个 iOS 程序员,了解 runtime 是必须的。学习过程中也看过很多很多分析 runtime 的文章,halfrost、杨萧玉等大佬的文章更是读了好几遍。然后一段时间后,对 runtime 的印象还是模模糊糊。runtime 会用吗?会用。动态绑定属性,hook 交换方法,利用反射写路由…够吗?不够。

目录

  • oc 中的类与对象
  • oc 中的方法
  • runtime 应用
  • 总结

源码阅读 objc4-723

oc 中的类与对象

首先,了解一下基本的 oc 对于 objc_classobjc_object 的定义.

1. 来自runtime.h
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;
/* Use `Class` instead of `struct objc_class *` */

这是很经典的一段,网上很多分析都基于这个定义。然而实际上这个片段已经不适用了。

在这里可以看到,在一个类中,有超类的指针,类名,版本的信息。 ivars是objc_ivar_list成员变量列表的指针;methodLists是指向objc_method_list指针的指针。methodLists是指向方法列表的指针。这里如果动态修改methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。

2. 来自objc-runtime-old.h
struct objc_class : objc_object {
    Class superclass;
    const char *name;
    uint32_t version;
    uint32_t info;
    uint32_t instance_size;
    struct old_ivar_list *ivars;
    struct old_method_list **methodLists;
    Cache cache;
    struct old_protocol_list *protocols;
    // CLS_EXT only
    const uint8_t *ivar_layout;
    struct old_class_ext *ext;

    ...
}
3. 来自objc-runtime-new.h
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    ...
}
4. 来自objc.h
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

实不相瞒,看下来云里雾里,所幸有很多前辈已经发表了很多自己的见解,顺着他们的思路,再回头看看就容易明白很多了。

来一波关于对象跟类的总结。

  1. Objective-C 里面,所有的对象底层都是 c 的结构体 objc_object
  2. 类也是对象,可以说包含 isa 指针的就是对象
  3. 类里面有什么? isasuperclasscachebits
  4. isa 指向元类,superclass 指向父类,cache 缓存指针和 vtable (其实就是用来缓存常用的方法),bits 包含了 methodspropertiesprotocols 等信息(在class_rw_t里)。
  5. 对象如何调用方法?不可能每个对象都保存他们可以调用的方法,这样性能太差了。一个实例对象调用实例方法,通过 isa 指针找到它的类,在类的bits里可以找到方法调用。一个类调用类方法也是一样,通过 isa 指针找到这个类的元类,从元类中获取可执行的方法。
  6. 对象的内存管理。oc 通过引用计数管理对象的内存,引用计数保存在isa 指针中,isa 是一个isa_t类型的联合体,内部有一个bits,里面的extra_rc来表示引用计数。

oc 中的方法

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

runtime 的源码看起来真的有些吃不消,不像第三方库的源码,还看的懂一些。自己的确离大神还有很远的距离,他们早在15年、16年就详细得分析了这些源码。废话不多说,继续写一下 oc 中的方法,加深自己的理解。

IMP 是指向方法实现的一个指针,提到 imp 就不得不讲一下 oc 中的消息转发机制。

objc_msgSend 可以说是整个 oc 的核心。此函数是消息发送必经之路。简单的描述就是就是获取 IMP 并调用。它的源码是由汇编实现了,目前还看不太懂,现阶段只能站在前人的肩膀上加深自己的理解。

objc_msgSend 分为好几个阶段:

  1. NilTest
    判断被发送消息的对象是否为 nil 的。如果为 nil,那就直接返回 nil。因此在 oc 中 nil 调用方法并不会导致程序崩溃。
  2. search cache
    前文了解到,在 objc_class 中有一个cache,这里缓存着常用的方法。如果每次调用方法都去所有的方法列表找显然很不明智,cache 是比较好的处理方式。
  3. MethodTableLookup
    在缓存没有命中的情况下,我们就只能在方法表中找 IMP
  4. _class_lookupMethodAndLoadCache3 -> lookUpImpOrForward
    递归向父类查找,方式跟前面一样,先查 cache 再查方法表。
  5. _class_resolveMethod
    如果还是没有找到,就调用此方法,查找是否有开发人员动态添加的方法。
  6. id _objc_msgForward
    如果还是没有,那就只能转发了。
  7. forwardingTargetForSelector
    这一步是转发的补救措施之一:重写- (id)forwardingTargetForSelector:(SEL)aSelector 如果 A 不能执行此方法,就用能执行此方法的 B 来替换,也就是替消息找备援接收者。
  8. methodSignatureForSelector -> forwardInvocation
    补救措施二:重写- (void)forwardInvocation:(NSInvocation *)anInvocation,消息手动重定向

2-8 的每一步只要找到了 imp 就直接执行,还会将 imp 存入 cache,方便下次调用。8后还未找到就程序崩溃。抛出经典的 unrecognized selector sent to instance xxxx 错误。

runtime 应用

1.Method Swizzling

这算是最常用的一个应用,比如要对一个应用的所有 controller 进行埋点,一个一个在代码里写那还算是程序员吗?利用 runtime,只要新建一个 controller 的 category,在 load 方法里面交换 viewWillAppear 实现。 Over

还有,我们经常做有关下标的操作(数组取值,字符串截取),容易产生越界。也只要 Method Swizzling 就可以做好防范。

2.Isa Swizzling

前面已经提到,有 isa 的就是对象,那么 Isa Swizzling 要用来干嘛呢?这个虽然自己用的不多,但是官方的 KVO 却是一个典型案例。

    // 此处 p 的 isa 还是指向 Person
    Person *p = [[Person alloc] init]; 

    // 此处 p 的 isa 已经指向 NSKVONotifying_Person
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

这样乱交换不会出事吗?万一代码中调用object_getClass不是会出错?然而并没有,KVO 还在新类中重写了classsetterdealloc_isKVOA.
重写 class 方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
重写 setter 方法显然就是为了监听,KVO 会在 setter 方法中新增另外两个方法的调用。

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

然后在didChangeValueForKey中调用

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

是不是豁然开朗,整个 KVO 就这么走通了。

3.Associated Object

这个用的也比较多,比如我们有时候需要 button 有一个属性 param ,让它能够携带参数,该怎么处理呢?我们可以写一个子类继承 UIButton,然后声明一个 param 属性就 ok,但是这样比较麻烦。最好可以在原生 button 上就能取到这个参数。这个时候我们就可以利用 runtime 关联一个属性。

- (void)setParam:(NSString *)param
{
    objc_setAssociatedObject(self, &paramKey,param,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)param
{
    return objc_getAssociatedObject(self, &paramKey);
}

4.反射

FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);

FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class _Nullable NSClassFromString(NSString *aClassName);

FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
FOUNDATION_EXPORT Protocol * _Nullable NSProtocolFromString(NSString *namestr) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

应用在启动时,会调用每个类的 load 方法,并将类的信息与对应的字符串存储在一个 map 中,由此我们可以利用这些方法搞点事情。
比如路由:我们可以存储一份 Router : ControllerName 的 map,服务器下发路由后,我们找到对应的ControllerName,再通过NSClassFromString实例化对应的 controller,至此就完成了一个简单的路由。

总结

其实一些心得已经写在文中了,有一些是引用前辈的,有一些是自己的理解。对 runtime 老是一种似懂非懂的感觉,本篇算是好好整理了一下。