Runtime整理

1.典型应用场景:

  1. 分类增加属性(关联对象(Objective-C Associated Objects))
  2. 消息转发(热更新)解决Bug(JSPatch)
  3. 方法魔法(Method Swizzling)方法添加和替换和KVO实现
  4. 实现NSCoding的自动归档和自动解档
  5. 实现字典和模型的自动转换(MJExtension)

2.相关技术链接

整理runtime文档
苹果维护的开源代码
苹果和GNU各自维护一个开源的Runtime开源

3.基本介绍

Runtime 基本是用 C 和汇编写的,它使用来处理OC转换C的中间结构体。
高级编程语言 -> 汇编语言 - > 机器语言
OC(面向对象)-> 过渡runtime实现->C语言(面向过程)

[obj foo] 编译器转换objc_msgSend(obj, foo),runtime执行流程:

  • 首先,通过obj的isa指针找到它的 class ;
  • 在 class 的 method list 找 foo ;
  • 如果 class 中没到 foo,继续往它的 superclass 中找 ;
  • 一旦找到 foo 这个函数,就去执行它的实现IMP 。
  • 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list

4.数据结构

//实例(objc_object)
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

//类
struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父类
     const char *name                         OBJC2_UNAVAILABLE;  // 类名
     long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
     long info                                OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
     long instance_size                       OBJC2_UNAVAILABLE;  // 该类的实例变量大小
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表
     struct objc_method_list *methodLists     OBJC2_UNAVAILABLE;  // 方法定义的链表
     struct objc_cache *cache                 OBJC2_UNAVAILABLE;  // 方法缓存
     struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE;  // 协议链表
#endif
}OBJC2_UNAVAILABLE;

//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}  

// Method(objc_method)
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;      方法名
    char *method_types                                       OBJC2_UNAVAILABLE;      方法类型
    IMP method_imp                                           OBJC2_UNAVAILABLE;        方法实现
}

元类(Meta Class)
是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass), 元类中保存了创建类对象以及类方法
所需的所有信息。
objc_object结构体实例它的isa指针指向了类对象objc_class,它的isa指针指向了metaclass,元类isa指向了NSobject元类。

类缓存(objc_cache)
当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

Category(objc_category)
Category是表示一个指向分类的结构体的指针,其定义如下:
struct objc_category{
     char *category_name                         OBJC2_UNAVAILABLE; // 分类名
     char *class_name                            OBJC2_UNAVAILABLE;  // 分类所属的类名
     struct objc_method_list *instance_methods   OBJC2_UNAVAILABLE;  // 实例方法列表
     struct objc_method_list *class_methods      OBJC2_UNAVAILABLE; // 类方法列表
     struct objc_protocol_list *protocols        OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性

复制代码

5.方法与消息

  1. SEL 又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
复制代码

通过下面三种方法可以获取SEL:
a、sel_registerName函数

SEL deallocSelector = sel_registerName("dealloc");
复制代码

b、Objective-C编译器提供的@selector()

SEL deallocSelector = @selector(dealloc);
复制代码

c、NSSelectorFromString()方法

SEL selector = NSSelectorFromString(@"setOrientation:");
复制代码
  1. IMP实际上是一个函数指针,指向方法实现的地址。 其定义如下:
id (*IMP)(id, SEL,...)  
复制代码

第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)接下来的参数:方法的参数列表。

  1. Method用于表示类定义中的方法,则定义如下:
typedef struct objc_method *Method  
struct objc_method{  
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名  
    char *method_types   OBJC2_UNAVAILABLE;   
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现  
}
复制代码
  1. 方法调用流程 消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。
objc_msgSend(receiver, selector)
复制代码

如果消息中还有其它参数,则该方法的形式如下所示:

objc_msgSend(receiver, selector, arg1, arg2,...)
复制代码
  1. 消息转发处理 消息转发机制基本上分为三个步骤: 1>、动态方法解析 2>、备用接收者 3>、完整转发 没有处理就程序崩溃

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。 在这个方法中,我们有机会为该未知消息新增一个“处理方法”,通过运行时class_addMethod函数动态添加到类里面就可以了。 这种方案更多的是为了实现@dynamic属性。

// 测试消息转发
- (void)runtimeTestMethodForward
{
    //执行foo函数
    [self performSelector:@selector(foo:) withObject:@10 withObject:@20];  // 最多传递2个参数
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(foo:)) {
        //如果是执行foo函数,就动态解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)newFoo, “@@:”);     /// ”v@:”意思就是这已是一个void类型的方法,没有参数传入,,  “i@:”就是说这是一个int类型的方法,没有参数传入,,”i@:@”就是说这是一个int类型的方法,又一个参数传入;@@:传递对象
        return YES;
    }
    return NO;      //  运行时就会移到下一步:forwardingTargetForSelector
}

void newFoo(id obj, SEL _cmd, id params)
{
    NSLog(@"doing new foo");
}
复制代码

备用接收者

  • (id)forwardingTargetForSelector:(SEL)aSelector 如果在上一步无法处理消息,则Runtime会继续调以下方法: 如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (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 {
    return YES;//返回YES,进入下一步转发
}

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

完整消息转发 如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 我们首先要通过,指定方法签名,若返回nil,则表示不处理。 如下代码:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,进入下一步转发
}

// 完整消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"@@:"];//签名,进入forwardInvocation
    }

    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL sel = anInvocation.selector;

    Person *p = Person.new;
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }else
    {
        [self doesNotRecognizeSelector:sel];
    }
}

void newFoo(id obj, SEL _cmd, id params)
{
    NSLog(@"doing new foo %@", params);
}
复制代码

应用 1.关联对象(Objective-C Associated Objects)给分类增加属性 说明:分类是不能自定义属性和变量的 方法:

//关联对象
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)
参数说明:
id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略

内存管理策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0,             // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401,         // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403            // 表示关联对象copy,不是线程安全的
};

@interface UIView (DefaultColor)
@property (nonatomic, strong) UIColor *defaultColor;
@end

#import <objc/runtime.h>
@implementation UIView (DefaultColor)
@dynamic defaultColor;
static char kDefaultColorKey;

- (void)setDefaultColor:(UIColor *)defaultColor
{
    objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIColor *)defaultColor
{
   return objc_getAssociatedObject(self, &kDefaultColorKey);
}

@end
复制代码

通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。

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

class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
参数说明:
	cls 被添加方法的类
	name 添加的方法的名称的SEL
        imp 方法的实现。该函数必须至少要有两个参数,self,_cmd
        类型编码
方法替换
// 方法替换
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(crviewDidLoad);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // 校验交换的方法是否已经存在
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        }else
        {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)crviewDidLoad
{
    NSLog(@"自定义的方法");
}
复制代码

+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用. swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

查找消息的唯一依据是selector的名字。所以,我们可以利用Objective-C的runtime机制,实现在运行时交换selector对应的方法实现以达到我们的目的。 即运行时交换方法名。

每一个SEL与一个IMP一一对应,正常情况下通过SEL可以查找到对应消息的IMP实现。 交换后

实现NSCoding的自动归档和自动解档 原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。 核心方法:在Model的基类中重写方法:

- (instancetype)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];
        }
        free(ivars);
    }

    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];
    }
    free(ivars);
}
复制代码

测试整形,数据没有都可以正常存储数据

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

- (instancetype)initWithDic:(NSDictionary *)dic
{
    if (self = [super init]) {
        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 * key = [NSString stringWithUTF8String:property_getName(property)];
            NSString *type = [NSString stringWithUTF8String:property_getAttributes(property)];
            NSLog(@"type %@, key %@", type, key);
            if ([dic valueForKey:key] == nil) continue;
            [self setValue:[dic valueForKey:key] forKey:key];
        }
        //立即释放properties指向的内存
        free(properties);
    }

    return self;
}
复制代码
总结:ivar和property区别
ivar  = instanc variable 实例变量,仅仅是一个变量;必须存在;带下划线的变量”_name”
property  = objc声明的属性,操作变量函数setter和getter;是可选的;没有带下滑线的变量“name”
复制代码

转载于:https://juejin.im/post/5afe2bde518825672f1a1a31

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值