美文网首页
IOS基础:运行时

IOS基础:运行时

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-23 10:12 被阅读0次

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 简介
  • 一、消息发送机制 objc_msgSend
    • 1、编译器的转换
    • 2、运行时定义的数据结构
    • 3、obj_msgSend的动作
    • 4、源码解析
  • 二、消息转发机制
    • 1、简介
    • 2、动态方法解析 resolveMethod
    • 3、备用接收者 forwardingTargetForSelector
    • 4、完整消息转发 objc_msgForward
  • 三、动态修改方法
    • 1、Method Swizzling 原理
    • 2、Method Swizzling 的Demo演示
    • 3、Method Swizzle的注意事项
  • 四、分类与扩展
    • 1、分类
    • 2、扩展
  • 五、关联对象
    • 1、特点
    • 2、使用说明
    • 3、实现原理
  • Demo
  • 参考文献

简介

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言写的 Runtime 库。

Runtime基本是用 C 和汇编写的。高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。

消息发送机制的优劣

优势:语言具有动态性,更加灵活。
劣势:导致安全性降低,多层机制势必降低语言执行效率。

Runtime应用

Runtime简直就是做大型框架的利器。下面就介绍一些常见的应用场景。

  • 关联对象(Objective-C Associated Objects)给分类增加属性
  • 方法魔法(Method Swizzling)方法添加和替换和KVO实现
  • 消息转发(热更新)解决Bug(JSPatch)
  • 实现NSCoding的自动归档和自动解档
// 在Model的基类中重写方法,用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作
- (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];
    }
}
  • 实现字典和模型的自动转换(MJExtension)
// 在NSObject的分类中添加方法,用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值
- (instancetype)initWithDict:(NSDictionary *)dict {

    if (self = [self init]) {
        //(1)获取类的属性名字及属性对应的类型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */

        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;

}
  • 内省方法的实现原理都是运用runtime的相关函数实现的
isKindOfClass:Class
isMemberOfClass:Class
respondToSelector:selector
conformsToProtocol:protocol

一、消息发送机制 objc_msgSend

1、编译器的转换

OC对象调用方法[object methodName] ,都会转换为objc msgSend(object, @selector(methodName))函数的调用一即给一个对象发送一个息,这个转换过程是在编译时就完成的,而具体怎么给对象发送消息则是在运行时完成的。

//向receiver发送名为message的消息。
[receiver message];

//将这一句重写为C代码
clang -rewrite-objc MyClass.m

//最终[receiver message]会由编译器转化为以下的纯C调用
//当向一般对象发送消息时,调用objc_msgSend;当向super发送消息时,调用的是objc_msgSendSuper
objc_msgSend(receiver, @selector(message));

例如:

[self class]  ----->  objc_msgSend(self, @selector(class))
[self superclass]  ----->  objc_msgSendSuper(super, @selector(class))
[super methodName]  ----->  objc_msgSendSuper({self, [self class]}, @selector(methodName))

需要注意的是super指的是结构体objc_super,其包括__unsafe_unretained id receiverreceiver指向的self也是当前对象。简单解释下,self代表的是当前对象,可super代表的可不是父类的一个对象啊。super关键字的作用就是告诉当前消息接收者直接去它的父类里的查找方法,而不是从它的类里开始查找,消息接收者还是self

下面是对应Runtime的源码:

// 返回该对象所属的类
- (Class)class {
    return object_getClass(self); 
}

// 返回该对象所属类的父类
- (Class)superclass {
    return [self class]->superclass; 
}

举个例子:

@interface Student : Person
@end

@implementation Student

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"[self class]:%@", [self class]);
        NSLog(@"[self superclass]:%@", [self superclass]);
        
        NSLog(@"[super class]:%@", [super class]);
        NSLog(@"[super superclass]:%@", [super superclass]);
    }
    return self;
}
@end

输出结果为:

2020-07-20 15:43:47.644572+0800 Demo[68310:20639379] [self class]:Student
2020-07-20 15:43:47.645006+0800 Demo[68310:20639379] [self superclass]:Person
2020-07-20 15:43:47.645085+0800 Demo[68310:20639379] [super class]:Student
2020-07-20 15:43:47.645152+0800 Demo[68310:20639379] [super superclass]:Person

[self class][self superclass]就不用说了,消息接收者都是self,会从Student类的方法缓存和方法列表里开始查找classsuperclass方法,而这两个方法都是NSObject类的方法,所以会一层一层往上,找到后发现两者的实现就是返回self的类和父类。

[super class][super superclass]的消息接收者其实都还是self,只不过会跳过Student类,直接从Person类的方法缓存和方法列表里开始查找classsuperclass方法,最后也还是找到NSObject类那里,找到后发现两者的实现就是返回self的类和父类,而self又没变。

2、运行时定义的数据结构

a、运行时
typedef struct objc_object *id;//id其实是一个object结构体的指针,所以id不用加*
typedef struct objc_class *Class;//Class是class结构体的指针

struct objc_object {
    Class isa;
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;     // 用来缓存指针和虚函数表  
    class_data_bits_t bits;  //方法列表等
    //...
}
运行时定义的数据结构
  • objc_object:指代id,每个对象的在内存的结构并不是确定的,但其首地址指向的肯定是isa。通过isa指针,运行时就能获取到objc_class,实例对象的isa指针指向类对象,类对象的isa指针指向元类对象。
  • objc_class:指代Class,继承自objc_object,包括Class superClasscache t cacheclass_ data bits_ t bits
objc_getClass:参数是类名的字符串,返回的就是这个类的类对象;
object_getClass:参数是id类型,它返回的是这个id的isa指针所指向的Class,如果传参是Class,isa指针指向的是元类对象,即返回该Class的metaClass。
  • cache_t:用于快速查找方法的执行函数,是可增量扩展的哈希表结构,也是局部性原理的最佳应用。包括多个bucket_t,每个bucket_t包括keyIMP
  • class_data_bits_t:class_ rw_ _t的封装。
  • class_ rw_ t:代表了类相关的读写信息,是对class_ ro_ t的封装。提供了运行时对类扩展的能力,是二维数组。它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。
  • class_ro_ t:代表了类相关的只读信息,包括当前类的属性、实例变量、方法、协议。`存储的是类在编译时就确定的一些信息,是一维数组。
b、method_t 方法体

method_t(objc_method):方法体,包括名称SEL name,返回值和参数const char* types,函数体IMP imp

struct objc_method {
    SEL _Nonnull method_name        ///方法的名称
    char * _Nullable method_types   ///方法的类型
    IMP _Nonnull method_imp         ///方法的具体实现,由 IMP 指针指向
} 

SEL(method_name):表示选择器,这是一个不透明结构体。

Objc.h
/// An opaque type that represents a method selector. 代表一个方法的不透明类型
typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selectorObjective-C中的表示类型。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL

// 可以看到selector是SEL的一个实例
@property SEL selector;

其实selector就是个映射到方法的C字符串,通常可以把它直接理解为一个字符串。例如

// 会打印出 isEqual:
printf(“%s”,@selector(isEqual:))

运行时维护着一张SEL的表,将相同字符串的方法名映射到唯一一个SEL, 通过sel_registerName(char *name)方法,可以查找到这张表中方法名对应的SEL类型的方法选择器。这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了methodname,没有参数,所以没法区分不同的method。比如:

// 会报错
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

// 只能通过命名来区别
- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

需要特别注意的是,在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。

最后补充一下,Dispatch Table是一张SELIMP的对应表。苹果提供了一个语法糖@selector用来方便地调用该函数。

IMP(method_imp):是一个指向方法的实现的函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。在iOS的Runtime中,Method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 

Type Encodings(method_types):v@:voididSEL

SEL aSelector = @selector(update)

id result1 = [obj update];
id result2 = [obj performSelector: @selector(update)];
id result3 = [obj performSelector: aSelector];

在发送消息之前验证一个对象响应了选择器,这个方法很常见,因为我们在委托的时候需要用到,当委托对象需要调用方法的时候,则需要先验证这个方法是否存在于受委托对象里面,再进行调用:

//列数
- (NSUInteger) columnCount
{
    if ([self.delegate respondsToSelector: @selector(columnCountInWaterFallLayout:)]) {
        return [self.delegate columnCountInWaterFallLayout: self];
    }
    else
        return HobenDefaultColumnCount;
}

怎么通过SEL获取方法名称?

NSString *methodName = NSStringFromSelector(methodId);

IMP怎么获得和使用?

IMP methodPoint = [self methodForSelector: methodId];
methodPoint();

3、obj_msgSend的动作

objc_ msgSend 函数内部具体怎么给对象发送消息的呢? (消息发送流程、方法调用流程)。

obj_msgSend的动作

实例(objc_object)
类对象(objc_class):存储实例方法列表等信息
元类对象(Meta Class):存储类方法列表等信息
缓存查找(objc_cache):一个class往往只有20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。给定值是SEL ,目标值是对应bucket t中的IMPcache_ key_ t 通过f(key)哈希算法查找到bucket t
当前类中查找:对于已排序好的列表,采用二分查找算法查找方法对应执行函数。对于没有排序的列表,采用一般遍历查找方法对应执行函数。

缓存查找
  1. 一进入objc_ msgSend函数,系统会首先判断消息接收者是不是nil,如果是nil直接return,结束该方法的调用,程序并不会崩掉。
  2. 如果不是nil,则根据对象的isa指针找到该对象所属的类,去这个类的方法缓存cache里查找方法,方法缓存是通过散列表实现的,所以查找效率非常高,如果找到了就直接调用。
  3. 如果没有找到,则会去类的方法列表methods 里查找,这里对于已排过序的方法列表采用二分查找,对于未排过序的方法列表则采用遍历查找,如果在类的方法列表找到了方法,则首先把该方法缓存到当前类的cache中,然后调用该方法。
  4. 如果没有找到,则会根据当前类的superclass指针找到它的父类,去父类里查找。找到父类后,会首先去父类的方法缓存cache里查找方法,如果找到了,则首先把该方法缓存到当前类的cache中(注意不是父类的cache哦),然后调用该方法。
  5. 如果没有找到,则会去父类的方法列表一methods里查找。如果在父类的方法列表找到了方法,则首先把该方法缓存到当前类的cache中(注意不是父类的cache) 。
  6. 如果到了nil,还是没有找到方法,就会触发动态方法解析。

简单说来:首先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找,父类再逐级查找,如果一直查找到根类仍旧没有实现,则调用_objc_msgForward函数,进入消息转发流程。

从整个消息发送流程,我们也感受到:消息发送关注的仅仅是消息接收者和SEL,无非就是通过消息接收者的isa指针和superclass指针去找SEL嘛,根本就没有什么绝对的标识来表明某个方法是实例方法还是类方法,所以如果出现类调用实例方法也不要惊讶哦,比如[NSObject -test]是没有问题的,你只要抓紧方法调用流程这条线就可以了。

实战演示
// 实例对象,isa指针地址为实例对象obj的地址,但isa指向的是类对象的地址    
NSObject *obj = [[NSObject alloc] init];
// 类对象,一个类对象在整个程序运行过程中只会存在一个,不管你调用多少次class方法,同一个类返回的类对象地址始终是一样的类对象结构体
Class cls = [NSObject class];
//元类对象,元类对象需要通过object_getClass这个runtime函数获取,在整个程序运行过程中也只存在一个该类的元类对象
Class metaCls = object_getClass(cls);
NSLog(@"obj->%p",obj);
NSLog(@"cls->%p",cls);
NSLog(@"metaCls->%p",metaCls);

instance中存放着isa和所有对象的值,instanceisa指向的是class

class存放着isasuperclass以及NSObject的属性信息和对象方法。superclass指向其父类,NSObject(Root class)的类对象的superclass指向nilisa指向的是meta。通过对象调用对象方法的时候,首先会通过instanceisa找到其class,然后在class中找到对象方法并调用,没有找到则会通过superclass一层一层往上找,最终还是找不到的时候就会报错。

metaclass类似,只不过它存储的不是对象方法,而是类方法。并且元类的isa全部指向NSObject的元类对象。元类的superclass指向其父类,但是NSObject的元类对象的superclass指向的是其类对象。通过类对象调用类方法的时候,首先会通过classisa找到其meta,然后在meta中找到类方法并调用,没有找到则会通过superclass一层一层往上找,最终还是找不到的时候就会报错。

@interface NSObject (DVObject)
@end
@implementation NSObject (DVObject)
- (void)eat {
    NSLog(@"吃饭");
}
@end

@interface Person : NSObject
+ (void)eat;
@end
@implementation Person
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person eat];
    }
    return 0;
}

运行这段代码,我们发现程序并没有奔溃,而是打印了”吃饭“,这是为什么呢?其实离不开oc的运行时特性。这里Person调用eat类方法,首先会通过isa找到自己的元类对象,发现没有eat方法,然后通过superclass一直找到NSObject的元类对象继续查找eat方法,依然没有,再通过superclass,注意,此时的superclass指向的是NSObject的类对象,而通过代码可以发现NSObject的类对象中是存在eat方法的,并且调用成功。如果调用一个类方法没有类方法实现,但有对应的实例方法实现,会不会崩溃或者实际调用(元类逐级上找,最后找到rootClass(meta->class))

为什么要设计metaclass

并不是先有MetaClass,而是在万物都是对象的Smalltalk里,向对象发送消息的基本解决方案是统一的,希望复用的。而实例和类之间用的这一套通过isa指针指向的Class单例中存储方法列表和查询方法的解决方案的流程,是应该在类上复用的,而MetaClass就顺理成章出现罢了。

类方法也是类的一种特征描述。而Smalltalk的精髓正在于消息传递,复用消息传递才是根本目的,而MetaClass只不过是因此需要的一个工具罢了。

4、源码解析

_class_lookupMethodAndLoadCache3方法:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls/*当前对象所属的类*/, sel/*消息*/, obj/*当前对象*/,
                              YES/*initialize*/, NO/*标识在cache没有找到方法*/, YES/*resolver*/);
}

lookUpImpOrForward:查找IMP或者进入动态解析。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

 retry:
    // Try this class's cache.
    // 缓存里查找
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    // 在当前类的方法列表里查找方法
    meth = getMethodNoSuper_nolock(cls/*当前类*/, sel/*消息*/);
    if (meth) {// 如果找到了方法
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);// 首先把该方法缓存到当前类的cache中
        imp = meth->imp;
        goto done;// 然后跳转到done
    }

    // Try superclass caches and method lists.
    // 在当前类父类的方法缓存和方法列表里查找方法
    curClass = cls;
    while ((curClass = curClass->superclass)) {// 循环的作用是一层一层往上,直到根类,直到nil
        // Superclass cache.
        // 在父类的方法缓存里查找方法
        imp = cache_getImp(curClass, sel);
        if (imp) {// 如果找到了方法
            // 首先把该方法缓存到当前类的cache中(注意不是父类的cache哦)
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;// 跳转到done
        }

        // Superclass method list.
        // 在父类的方法列表里查找方法
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {// 如果找到了方法
            // 首先把该方法缓存到当前类的cache中(注意不是父类的cache哦)
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;// 跳转到done
        }
    }

    // No implementation found. Try method resolver once.
    // 如果正常的消息发送流程走完,没有找到方法,就会触发动态方法解析
    if (!triedResolver) {// 首先判断之前有没有进行过动态方法解析,有则直接触发消息转发机制,没有则进行动态方法解析
        _class_resolveMethod(cls, sel, inst);// 进行动态方法解析
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;// 标记为已经进行过动态方法解析
        goto retry;// 并重新走一遍消息发送流程来查找方法
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    // 如果正常的消息发送流程和动态方法解析都走完,还是没有找到方法,就会触发消息转发机制
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
}
 done:
    // 返回方法的IMP,就返回到了汇编那里,汇编收到返回值IMP后,会执行它所指向的函数
    return imp;
}

getMethodNoSuper_nolock:找到所有的方法列表。

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
{
    // 根据类的methods成员变量,找到所有的方法列表
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        // 遍历这些方法列表来查找方法
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

search_method_list:在方法列表中查找方法。

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        // 对于排过序的方法列表,使用二分查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
         // 对于未排序的方法列表,使用普通遍历查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}

二、消息转发机制

1、简介

a、转发流程

当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。为了展示消息转发的具体动作,这里尝试向一个对象发送一条错误的消息,并查看一下_objc_msgForward是如何进行转发的。

消息转发流程
  1. 调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。
  2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。
  3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
  4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。

如果消息转发机制都走完了,还是没法处理这个方法的调用,那就彻底没救了,程序才会崩掉,报unrecognized selector sent to instance的错误,也就是说这个错误是消息转发机制报的,而不是消息发送机制或动态方法解析阶段报的。

b、Demo演示

让运行时发送的所有消息全部打印到/tmp/msgSend-xxxx文件里:

//可以在代码里执行下面的方法
(void)instrumentObjcMessageSends(YES);

//或者暂停程序运行,并在gdb中输入下面的命令
call (void)instrumentObjcMessageSends(YES)

结果如下:

+ Test NSObject initialize
+ Test NSObject new
+ Test NSObject alloc
+ Test NSObject allocWithZone:
- Test NSObject init
- Test NSObject performSelector:
+ Test NSObject resolveInstanceMethod:
- Test NSObject forwardingTargetForSelector:
- Test NSObject methodSignatureForSelector:
- Test NSObject class
- Test NSObject doesNotRecognizeSelector:

2、动态方法解析 resolveMethod

如果正常的消息发送流程走完,没有找到方法,就会触发动态方法解析。动态方法解析是指,如果我们在编译时没有为某个方法提供实现,可以在运行时通过类的+resolveInstanceMethod:方法或+resolveClassMethod:方法动态地为这个方法添加实现(利用class_addMethod方法),会添加到类的methods里。动态方法解析完成后,如果你添加了函数并返回YES,那运行时系统会重新走一遍消息发送流程来查找方法。如果动态方法解析也没有解决问题,就会触发消息转发机制。

a、源码解析

_class_resolveMethod

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {// 如果这个类不是元类,即因为没找到实例方法进入动态解析
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {// 如果这个类是元类,即因为没找到类方法进入动态解析
        _class_resolveClassMethod(cls, sel, inst);
    }
}

_class_resolveInstanceMethod

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 首先判断该类有没有实现+resolveInstanceMethod:方法,因为它是个类方法,所以传的是cls->ISA(),没有的话直接return
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    // 调用该类的+resolveInstanceMethod:方法,可以在这个方法里动态地为没找到的方法添加实现,会添加到类的methods里
    objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
}

_class_resolveClassMethod

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    // 此处的cls已经是metaClass了,所以直接传的是cls
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    // 调用该类的+resolveClassMethod:方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到元类的methods里
    objc_msgSend(nonmeta, SEL_resolveClassMethod, sel);
}
b、Demo演示
@interface Person : NSObject

- (void)eat;
+ (void)drink;

@end



#import "Person.h"
#import <objc/runtime.h>

@implementation Person

//动态方法解析,处理没找到的实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    //动态地为这个方法添加实现
    if (sel == @selector(eat)) // 如果是eat方法再添加,别把别的方法也给写了
    {
        Method tempMethod = class_getInstanceMethod(self, @selector(otherEat));
        
        // self:eat方法要添加到当前类的methods里,此处self就是当前类
        // sel:为@selector(eat)添加对应的IMP,即为eat方法添加实现
        // imp、types:我们假设要添加为otherEat方法的IMP和types
        class_addMethod(self, sel,method_getImplementation(tempMethod), method_getTypeEncoding(tempMethod));
        
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

// 动态方法解析,处理没找到的类方法
+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(drink))
    {
        Method tempMethod = class_getClassMethod(self, @selector(otherDrink));
        
        // object_getClass(self):drink方法要添加到当前类元类的methods里,所以此处是object_getClass(self)
        class_addMethod(object_getClass(self), sel, method_getImplementation(tempMethod), method_getTypeEncoding(tempMethod));
        
        return YES;
    }
    return [super resolveClassMethod:sel];
}

#pragma mark - other method

- (void)otherEat
{
    NSLog(@"Person otherEat");
}

+ (void)otherDrink
{
    NSLog(@"Person otherDrink");
}

@end


int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        Person *person = [[Person alloc] init];
        [person eat]; // 我们没有实现-eat方法,所以会触发动态方法解析
        [Person drink];// 我们没有实现+drink方法,所以会触发动态方法解析
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输出结果为:

2020-07-20 11:35:12.127783+0800 Demo[67452:20501286] Person otherEat
2020-07-20 11:35:12.128223+0800 Demo[67452:20501286] Person otherDrink

3、备用接收者 forwardingTargetForSelector

如果正常的消息发送流程和动态方法解析都走完,还是没有找到方法,就会触发消息转发机制。消息转发机制是指,把消息转发给别的对象,让别的对象来调用这个方法,因为到了这一步,就表明你这个类本身已经没有能力调用这个方法了,交给别人吧。(消息转发机制的源码是不开源的)

系统会调用该类的forwardingTargetForSelector:方法,我们可以在这个方法里直接把消息转发给别的对象。

不完整的消息转发Demo演示如下:

int main(int argc, char * argv[]) {
   NSString * appDelegateClassName;
   @autoreleasepool {
       // Setup code that might create autoreleased objects goes here.
       appDelegateClassName = NSStringFromClass([AppDelegate class]);
       Person *person = [[Person alloc] init];
       [person eat]; // 我们没有实现-eat方法,所以会触发动态方法解析
       [Person drink];// 我们没有实现+drink方法,所以会触发动态方法解析
   }
   return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}


@interface Person : NSObject

- (void)eat;
+ (void)drink;

@end

#import "Person.h"
#import "Student.h"
#import <objc/runtime.h>

@implementation Person

// 把消息转发给别的对象
// aSelector:要把哪个方法转发给别的对象——即没找到的方法
// return:要把消息转发给哪个对象——即你觉得能调用该方法的对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
   if (aSelector == @selector(eat))
   {
       // 转发给Student对象,因为它能处理这个方法。
       // 我们猜测这里的底层实现,无非就是拿这个返回的对象调用它相应的方法,
       return [[Student alloc] init];
   }
   return [super forwardingTargetForSelector:aSelector];
}

// 注意:处理类方法时,前面要换成“+”
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
   if (aSelector == @selector(drink))
   {
       return [Student class];
   }
   return [super forwardingTargetForSelector:aSelector];
}

@end



@interface Student : NSObject

- (void)eat;
+ (void)drink;

@end

#import "Student.h"

@implementation Student

- (void)eat {
   NSLog(@"Student eat");
}

+ (void)drink {
   NSLog(@"Student drink");
}

@end

输出结果为:

2020-07-20 14:43:17.385454+0800 Demo[67883:20589127] Student eat
2020-07-20 14:43:17.385875+0800 Demo[67883:20589127] Student drink

4、完整消息转发 objc_msgForward

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

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        Person *person = [[Person alloc] init];
        [person eat]; // 我们没有实现-eat方法,所以会触发动态方法解析
        [Person drink];// 我们没有实现+drink方法,所以会触发动态方法解析
        [person addA:1 andB:2];// Person对象转发一个方法后,想要获取到方法的返回值
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}



@interface Person : NSObject

- (void)eat;
+ (void)drink;
- (NSInteger)addA:(NSInteger)addA andB:(NSInteger)andB;

@end

#import "Person.h"
#import "Student.h"
#import <objc/runtime.h>

@implementation Person

// 提供一个方法签名,用来生成invocation
// aSelector:要把哪个方法转发给别的对象——即没找到的方法
// return:方法签名——即方法类型编码的包装,包含了方法的返回值和参数信息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat))
    {
        // "v16@0:8":eat方法的类型编码
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    
    if (aSelector == @selector(addA:andB:))
    {
        // "i24@0:8i16i20":addA:andB:方法的类型编码
        return [NSMethodSignature signatureWithObjCTypes:"i24@0:8i16i20"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

// 把消息转发给别的对象
// anInvocation 根据上面的方法签名生成的invocation,它是一个对象,里面封装了方法调用者、消息、方法返回值参数等信息
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 转发给Student对象
    [anInvocation invokeWithTarget:[[Student alloc] init]];
    
    
    // 获取返回值
    if (anInvocation.selector == @selector(addA:andB:))
    {
        int returnValue;
        [anInvocation getReturnValue:&returnValue];
        NSLog(@"%ld", (long)returnValue);// 3
    }
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(drink))
    {
        // "v16@0:8":drink方法的类型编码
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 转发给Student对象
    [anInvocation invokeWithTarget:[Student class]];
}

@end




@interface Student : NSObject

- (void)eat;
+ (void)drink;
- (NSInteger)addA:(NSInteger)addA andB:(NSInteger)andB;

@end

#import "Student.h"

@implementation Student

- (void)eat {
    NSLog(@"Student eat");
}

+ (void)drink {
    NSLog(@"Student drink");
}

- (NSInteger)addA:(NSInteger)addA andB:(NSInteger)andB
{
    return (addA + andB);
}

@end

输出结果为:

2020-07-20 15:22:43.280495+0800 Demo[68224:20624471] Student eat
2020-07-20 15:22:43.280906+0800 Demo[68224:20624471] Student drink
2020-07-20 15:22:43.281015+0800 Demo[68224:20624471] 3

完整消息转发可以获取方法的返回值,而直接消息转发就做不到。


三、动态修改方法

1、Method Swizzling 原理

Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的,实现hook消息机制。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系,IMP有点类似函数指针,指向具体的Method实现。

于是我们可以利用method_exchangeImplementations来交换2个方法中的IMP,利用class_replaceMethod来修改类,利用 method_setImplementation来直接设置某个方法的IMP。归根结底,都是偷换了selector的IMP,如下图:

Method Swizzling 原理

2、Method Swizzling 的Demo演示

method_exchangeImplementations的示例
#import "NSArray+Swizzle.h"

@implementation NSArray (Swizzle)

- (id)customLastObject
{
    //乍一看,这不递归了么?别忘记这是调换IMP之后的selector
    //[self myLastObject]执行的其实是[self lastObject]
    id lastObject = [self customLastObject];
    NSLog(@"自定义的LastObject");
    return lastObject;
}

@end


#import <objc/runtime.h>
#import "NSArray+Swizzle.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);

        Method originMethod = class_getInstanceMethod([NSArray class], @selector(lastObject));
        Method customMethod = class_getInstanceMethod([NSArray class], @selector(customLastObject));

        method_exchangeImplementations(originMethod, customMethod);
          
        NSArray *array = @[@"0",@"1",@"2",@"最后一个元素"];
        NSString *string = [array lastObject];
        NSLog(@"结果为: %@",string);
        
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输出结果为:

2020-07-24 10:16:37.103660+0800 Demo[77755:22122757] 自定义的LastObject
2020-07-24 10:16:37.103790+0800 Demo[77755:22122757] 结果为: 最后一个元素
class_addMethod和class_replaceMethod的示例

UIWindowsendEvent重定向到我们自己的写的sendEventMySelf的实现,然后将其原本的实现重定向到了我们给它新添加的方法sendEventOriginal中。而sendEventMySelf中,我们首先可以对这个消息进行我们想要的处理,然后再通过调用sendEventOriginal方法转到正常的执行流程。

#import <UIKit/UIKit.h>
#import "TestHookObject.h"
#import <objc/runtime.h>

@implementation TestHookObject

+ (void)initialize
{
    // 获取到UIWindow中sendEvent对应的method(一个结构体,用来对方法进行描述)
    Method sendEvent = class_getInstanceMethod([UIWindow class], @selector(sendEvent:));
    // 获取到我们自己定义的类中的sendEvent的Method(方法的签名必须一样,否则运行时报错)
    Method sendEventMySelf = class_getInstanceMethod([self class], @selector(sendEventHooked:));
    
    // 通过UIWindow原生的sendEvent的Method获取到对应的IMP(一个函数指针)
    IMP sendEventImp = method_getImplementation(sendEvent);
    // 使用运行时API Class_addMethod给UIWindow类添加了一个叫sendEventOriginal的方法
    // 该方法使用UIWindow原生的sendEvent的实现,并且有着相同的方法签名
    class_addMethod([UIWindow class], @selector(sendEventOriginal:), sendEventImp, method_getTypeEncoding(sendEvent));
    
    // 获取我们自定义类中的sendEventMySelf的IMP
    IMP sendEventMySelfImp = method_getImplementation(sendEventMySelf);
    // 为UIWindow原生的sendEvent指定一个新的实现
    // 将该实现指定到了我们自己定义的sendEventMySelf上,完成了偷梁换柱
    class_replaceMethod([UIWindow class], @selector(sendEvent:), sendEventMySelfImp, method_getTypeEncoding(sendEvent));
}

// 截获到window的sendEvent。可以先处理完以后,再继续调用正常处理流程
- (void)sendEventHooked:(UIEvent *)event
{
    // do something what ever you want
    NSLog(@"haha, this is my self sendEventMethod!!!!!!!");
    NSLog(@"因为在水下呆得太无聊了,陪大伙一块儿冒险更好玩");

    // invoke original implemention
    [self performSelector:@selector(sendEventOriginal:) withObject:event];
}

@end

这块儿你可能有个困惑,我们自定义类中明明是没有sendEventOriginal方法的啊?为什么执行起来不报错,而且还会正常执行?

因为sendEventMySelfUIWindowsendEvent重定向过来的,所以在运行时该方法中的self代表的就是UIWindow的实例,而不再是TestHookObject的实例了,加上sendEventOriginal是我们通过运行时添加到UIWindow的实例方法,所以可以正常调用。当然如果直接通过下面这种方式调用也是可以的,只不过编译器会提示警告(编译器没那么智能),因此我们采用了performSelector的调用方式。

[self sendEventOriginal:event];

以上就是Hook的实现,使用时我们只需要让TestHookObject类执行一次初始话操作就可以了,执行完以后UIWindowsendEvent消息就会hook到我们的sendEventMySelf中了。下面是调用代码:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    RootViewController *rootVC = [[RootViewController alloc] init];
    UINavigationController *mainNC = [[UINavigationController alloc] initWithRootViewController:rootVC];
    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = mainNC;
    [self.window makeKeyAndVisible];
    
    //hook UIWindow‘s SendEvent method
    //只需要初始化就可以了
    TestHookObject *hookSendEvent = [[TestHookObject alloc] init];
    
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    btn.center = CGPointMake(160, 240);
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventAllEvents];
    [self.window addSubview:btn];
    
    return YES;
}

- (void)btnAction
{
    NSLog(@"白龙马要去西天取经,你一条龙为什么还想要吃斋念佛?");
}

输出结果为:

2020-07-24 10:46:14.559637+0800 Demo[77941:22144213] haha, this is my self sendEventMethod!!!!!!!
2020-07-24 10:46:14.559802+0800 Demo[77941:22144213] 因为在水下呆得太无聊了,陪大伙一块儿冒险更好玩
2020-07-24 10:46:14.560347+0800 Demo[77941:22144213] 白龙马要去西天取经,你一条龙为什么还想要吃斋念佛?

3、Method Swizzle的注意事项

  • 第一个风险是,需要在+load方法中进行方法交换。因为如果在其他时候进行方法交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。
  • 第二个风险是,被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。
  • 第三个风险是,交换的方法如果依赖了cmd,那么交换后,如果 cmd发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd
  • 第四个风险是,方法交换命名冲突。如果出现冲突,可能会导致方法交换失败
  • swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatchdispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

四、分类与扩展

1、分类

数据结构
struct category_t { 
    const char *name; // 是指 class_name 而不是 category_name
    classref_t cls; // 要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
    struct method_list_t *instanceMethods; // category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods;// category中所有添加的类方法的列表
    struct protocol_list_t *protocols;// category实现的所有协议的列表
    struct property_list_t *instanceProperties;// 添加属性,不可以添加成员变量。表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的
};
用途
  • 分解体积庞大的类文件(按照功能对类进行分类)
  • 给类添加新的方法
  • 不能给类添加成员变量
  • 通过@property定义的变量,只能生成对应的gettersetter的方法声明,但是不能实现gettersetter方法,同时也不能生成带下划线的成员属性;
  • 注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。
特点
  • 运行时决议(runtime运行时才添加附加方法)
  • 可以为系统类添加分类
添加内容
  • 实例方法
  • 类方法
  • 协议
  • 属性(只声明了get/set方法,并没有添加实例变量,通过关联对象来为分类添加实例变量)
源码分析
  • 分类添加的方法可以“覆盖”(效果上是覆盖的,但是宿主类的同名方法仍然存在)
  • 同名分类方法谁能生效,取决于编译顺序:最后被编译的分类最先生效
  • 名字相同的分类会引起编译报错

category如何被加载的,两个category的load方法的加载顺序? 两个category的同名方法的加载顺序?

  1. runtime时,首先把category的实例方法、协议以及属性添加到类上。
  2. 其次把category的类方法添加到类的metaclass上;
  3. +load的执行顺序是先类,后category,而category+load执行顺序是根据编译顺序决定的;
  4. 我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,对于category同名方法的加载顺序,显然是按照后编译的方法先执行;
  5. 对于调用到原来类中被category覆盖掉的方法,我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法。

2、扩展

用途
  • 可以给类添加成员变量,但是是私有的,可以給类添加方法,但是是私有的
  • 添加的属性和方法是类的一部分,在编译期就决定的。
  • 在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。伴随着类的产生而产生,也随着类的消失而消失, 必须有类的源码才可以给类添加extension,所以对于系统一些类,如NSString,就无法添加类扩展
  • 不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension
和分类区别
  • 分类是运行时决议,扩展是编译时决议
  • 扩展只是以声明的形式存在,多数情况下寄生在宿主类的.m文件中,不是一个独立的文件,而分类有声明有实现,且是独立文件
  • 不能为系统类添加扩展,但是可以添加分类

五、关联对象

分类,可以通过它来扩展方法;Associated Object,可以通过它来扩展属性;在iOS开发中,可能category比较常见,相对的Associated Object,就用的比较少,要用它之前,必须导入<objc/runtime.h>的头文件。

1、特点

  • 关联对象由AssociationsManager管理并在AssociationsHashMap存储。所有对象的关联对象内容都在同一个全局容器中。
  • setAssociatedvalue设置为nil就可以清除掉关联对象关联的值。
  • 由于不改变原类的实现,所以可以给原生类或者是打包的库进行扩展,一般配合Category实现完整的功能。
  • ObjC类定义的变量,由于runtime的特性,都会暴露到外部,使用关联对象可以隐藏关键变量,保证安全。
  • 标记成OBJC_ASSOCIATION_ASSIGN的关联对象和@property (weak)是不一样的,上面表格中等价定义写的是@property (unsafe_unretained),对象被销毁时,属性值仍然还在。如果之后再次使用该对象就会导致程序闪退。
  • objc_removeAssociatedObjects。这个方法是移除源对象中所有的关联对象,并不是其中之一。所以其方法参数中也没有传入指定的key。要删除指定的关联对象,使用objc_setAssociatedObject 方法将 key对应的value设置成nil即可
  • 关联对象要比本对象释放晚一些

2、使用说明

分类无法添加成员变量,在分类中定义了属性,系统没有生成对应的成员变量,也没有实现setget方法。那我们如何实现为分类添加属性呢?通过runtime中提供的关联对象相关API我们可以实现以上功能。可以在不改变类的源码的情况下,为类添加实例变量。需要注意的是,这里指的实例变量,并不是真正的属于类的实例变量,而是一个关联值变量。

/**
* 参数一:id object:给哪个对象添加属性。
* 参数二:const void * key:关联对象中属性值存取过程中对应唯一标识,根据key来设置和取值。
* 参数三:id value:关联的值,也就是set方法传入的值给属性去保存。
* 参数四:objc_AssociationPolicy policy:策略,属性以什么形式保存。
*/

// 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

// 获得关联对象
id objc_getAssociatedObject(id object, const void * key)

// 移除所有的关联对象
void objc_removeAssociatedObjects(id object)

关联策略包括:retainassgincopy, 和普通对象一样进行内存管理。

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     // 指定相关的对象被复制,原子性   
};

// 关联对象实现weak属性的方式有点特殊,用__weak修饰对象,并将其用block包裹,关联block对象
-(void)setWeakvalue:(NSObject *)weakvalue {
    __weak typeof(weakvalue) weakObj = weakvalue;
    typeof(weakvalue) (^block)() = ^(){
        return weakObj;
    };
    objc_setAssociatedObject(self, weakValueKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSObject *)weakvalue {
    id (^block)() = objc_getAssociatedObject(self, weakValueKey);
    return block();
}

通过关联对象来实现分类添加成员变量的方式如下:

@interface NSObject (Extension)

@property (nonatomic,copy  ) NSString *name;

@end


@implementation NSObject (Extension)

- (void)setName:(NSString *)name {
   objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}


- (NSString *)name {
   return objc_getAssociatedObject(self,@selector(name));
}

@end

3、实现原理

a、概述
关联对象
  • AssociationsManager:类似于一个单例对象,保存着整个系统的关联对象数据。包含有一个多线程操作的锁和- AssociationsHashMap的表。
  • AssociationsHashMap:保存对象的地址(一个类对象)和这个类全部关联的对象的hash table
  • ObjectAssociationMap:一个类全部关联的对象,key为索引。
  • ObjcAssociation:最小单元,要关联的value和关联策略。

有个全局哈希表AssociationsHashMap,这个表里面维护了所有对象的关联属性,并且对象的地址作为查询的key,但是我们知道每个对象下面可能有多个关联属性,所有此时又有个哈希表,这个表里面的key是关联属性所对应的key,实际中我们调用objc_setAssociatedObject函数时,传的key,就用在这里的,那么对应的value就是我们所关联的属性了吗,NO(哭了),此时的value是一个结构体,这个结构体里面有个成员变量new_value才是我们关联的具体属性。

可以推测出objc_getAssociatedObject的实现原理了,首先根据对象地址从AssociationsHashMap拿到ObjectAssociationMap,然后根据我们自己设置的keyObjectAssociationMap拿到ObjcAssociation,然后获取成员变量new_value,就是我们要找的目标属性了。

b、数据结构
数据结构
// 存储了AssociationsHashMap对象
class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

// 继承自unordered_map,unordered_map的前两个参数为disguised_ptr_t和ObjectAssociationMap
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
};

// 继承自map,map的前两个参数为void *和ObjcAssociation
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
};

// 存储策略和值
class ObjcAssociation {
        uintptr_t _policy;// 策略
        id _value;// 值
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
        ObjcAssociation() : _policy(0), _value(nil) {}

        uintptr_t policy() const { return _policy; }
        id value() const { return _value; }
        
        bool hasValue() { return _value != nil; }
};
c、方法实现分析
方法分析

我们通过runtime源码来分析底层原理,来到objc-runtime.mm,搜索objc_setAssociatedObjectobjc_getAssociatedObjectobjc_removeAssociatedObjects,分别来看看设置,取值,移除所有关联对象的底层实现:

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

id objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

objc-references.mm文件中找到了上面三个底层方法的具体实现,其中_object_set_associative_reference方法的源码实现如下:

    void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
        // 通过关联的value值判断,有值则取值,没值就取nil
       id new_value = value ? acquireValue(value, policy) : nil;
        {
            AssociationsManager manager;
            AssociationsHashMap &associations(manager.associations());

            // disguise_ object就是用传进来关联的object通过DISGUISE运算得到的
            disguised_ptr_t disguised_object = DISGUISE(object);
            if (new_value) {
                // 通过disguised_ object判断是否有存在的ObjectAssociationMap
                AssociationsHashMap::iterator i = associations.find(disguised_object);
               
                if (i != associations.end()) {
                    // 如果有,则取出ObjectAssociationMap
                    ObjectAssociationMap *refs = i->second;
                    ObjectAssociationMap::iterator j = refs->find(key);
                    if (j != refs->end()) {
                        old_association = j->second;
                        // 用新值替换旧值 
                        j->second = ObjcAssociation(policy, new_value);
                    } else {
                        (*refs)[key] = ObjcAssociation(policy, new_value);
                    }
                } else {
                    // 如果没有存在的ObjectAssociationMap,则创建新的,然后保存新值
                    ObjectAssociationMap *refs = new ObjectAssociationMap;
                    associations[disguised_object] = refs;
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                    object->setHasAssociatedObjects();
                }
            } else {
                // 如果新值为nil,则移除原有关联ObjectAssociationMap
                AssociationsHashMap::iterator i = associations.find(disguised_object);
                if (i !=  associations.end()) {
                    ObjectAssociationMap *refs = i->second;
                    ObjectAssociationMap::iterator j = refs->find(key);
                    if (j != refs->end()) {
                        old_association = j->second;
                        refs->erase(j);// 移除
                    }
                }
            }
        }
        // 释放旧值
        if (old_association.hasValue()) ReleaseValue()(old_association);
    }

_object_get_associative_reference方法的源码实现如下:

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());

        // disguise_ object就是用传进来关联的object通过DISGUISE运算得到的
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 通过disguised_ object判断是否有存在的ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // 如果有,则取出ObjectAssociationMap
            ObjectAssociationMap *refs = i->second;
            // 通过key判断是否存在ObjcAssociation
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
            // 取出ObjcAssociation中的value和policy
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

_object_remove_assocations方法的源码实现如下:

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());

        if (associations.size() == 0) return;
        // disguise_ object就是用传进来关联的object通过DISGUISE运算得到的
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 通过disguised_ object判断是否有存在的ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // 如果有,则删除AssociationsHashMap 中的所有ObjectAssociationMap
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            delete refs;
            // 删除AssociationsHashMap
            associations.erase(i);
        }
    }
    for_each(elements.begin(), elements.end(), ReleaseValue());
}
d、关联属性的释放

关联对象的释放是在dealloc方法中调用void _object_remove_assocations(id object)实现的:

当一个类的引用计数为0时,需要释放这个类,而当这个类在别的类中使用了关联属性把它关联了起来,比如说需要销毁person对象,那么如何处理person被关联的这块?销毁对象这块最终需要调用到NSObject类中的dealloc方法:

//释放对象
- (void)dealloc {
    _objc_rootDealloc(self);
}

沿着调用函数流程dealloc—>_objc_rootDealloc—>rootDealloc object_dispose—>objc_destructInstance走最终会发现:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //---释放 C++ 的实例变量 
        if (cxx) object_cxxDestruct(obj);
        //---移除关联属性
        if (assoc) _object_remove_assocations(obj);
        //---将弱引用置为nil
        obj->clearDeallocating();
    }
    return obj;
}

_object_remove_assocations中会移除掉关联属性,具体怎么移除,简单来说就是从上面说的两个哈希表中去移除对应的keyvalue就行了。

Demo

Demo在我的Github上,欢迎下载。
RuntimeDemo

苹果Runtime开源代码
本文使用的版本是 objc4-680.tar.gz

参考文献

Objective-C 中的消息与消息转发
Runtime:消息发送机制、动态方法解析、消息转发机制
iOS开发中实现hook消息机制的方法探究
iOS Runtime详解

相关文章

网友评论

      本文标题:IOS基础:运行时

      本文链接:https://www.haomeiwen.com/subject/ivzmmktx.html