OC底层相关

runtime

2019-02-24  本文已影响192人  wxhan

一.什么是runtime(运行时)

Objective-C是一个动态语言,一个由Objective-C编写的程序跑起来分为编译过程和运行过程,当程序在运行的时候,才能做一些处理,比如说类的创建,方法的调用,消息的传递和转发等。也就是说runtime就是程序正在运行的时候的状态。(C是一种静态语言,它在编译的时候就完成这些处理)

可以简单的理解为:Objective-C编写的程序在运行的过程,其实就是runtime状态下的C语言代码。

实例说明:在.h文件声明一个对象方法,如果在.m文件中不做任何实现的话,然后在外部对这个对象方法进行调用。程序编译的时候不会报错,运行的时候再报错。

二.runtime中的class(类)

Objective-C中,任何类的定义都是对象。类和类的实例(对象)在本质上是没有区别的。

1.打开Xcode,然后按下 shift + command + O,搜索 objc.h

#if !OBJC_TYPES_DEFINED
/// 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;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// 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

可以看出 Class (类)是一个 objc_class 结构类型的指针。
2.搜索 objc_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;

三.runtime中消息的传递

对象方法的调用:[obj method] ,在runtime机制中,其实就是消息的发送objc_msgSend(obj, method)

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

- (void)setName:(NSString *)name;

- (void)setAge:(NSString *)age;

@end

NS_ASSUME_NONNULL_END
Person *p = [[Person alloc] init];
[p setName:@"wxh"];

当程序运行中,执行到[p setName:@"wxh"];这条语句时候,在runtimeC做了这样的消息传递objc_msgSend(p, setName:),具体如下:

1.通过 p (obj)isa指针找到Person这个类(class)

注:isa指针 是一个Class类型的指针,每个实例对象有个isa的指针,他指向对象的类。

2.在Person这个类(class)methodLists(存放这个类对象函数的一个数组列表)中找到setName:这个函数(method),如果找不到,就到Person的父类(super_class)中去找。

3.找到setName: 这个函数(method),通过setName:这个函数(method)IMP指针 ,调用这个函数(method)

注:IMP指针 IMP是一个函数指针,指向函数实现的地址。

4.另外一个重要成员, objc_cache类型的cache。它把经常调用的函数缓存起来,当再次收到setName:的消息的时候,直接从cache里面找,大大提高了效率。

总结:Objective-C中,函数的调用,其实就是在runtimeC做了消息传递。runtimeC做消息传递的函数。
objc_msgSend(void /* id self, SEL op, ... */ )
说到这里,顺便看一下底层中的定义函数

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp   
}                                 

SEL是指向 函数名的指针 IMP是指向函数实现地址的指针 char函数的类型
由此可以看出,在Objective-C底层中定义一个函数的时候,并没有涉及到参数,这就是为什么Objective-C中不可以使用函数重载的原因。

四.runtime的应用

1. 关联对象。

runtime给出的方法有

//关联对象
①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)

(1)给分类category添加属性
举个例子:给NSObject添加一个分类 NSObject+wxh,记得#import "objc/runtime.h"

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (wxh)

@property (nonatomic,copy)NSString *name;

@property (nonatomic,strong)NSArray *datas;

@end

NS_ASSUME_NONNULL_END
#import "NSObject+wxh.h"
#import "objc/runtime.h"

@implementation NSObject (wxh)
// 用一个字节来存储key值,设置为静态私有变量,避免外界修改
static char nameKey;
- (void)setName:(NSString *)name{
    // 将某个值与某个对象关联起来,将某个值存储到某个对象中
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
    return objc_getAssociatedObject(self, &nameKey);
}

static char datasKey;
- (void)setDatas:(NSArray *)datas{
    objc_setAssociatedObject(self, &datasKey, datas, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)datas{
    return objc_getAssociatedObject(self, &datasKey);
}

@end
 NSString *string = [[NSString alloc] init];
 string.name = @"123";
 NSLog(@"%@",string.name);
 string.datas = @[@"w",@"x",@"h"];
 NSLog(@"%@",string.datas);

解释:KVC的字典转模型差不多逻辑。

OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC 等价于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC 等价于 @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN 等价于 @property(strong,atomic)。
OBJC_ASSOCIATION_COPY 等价于 @property(copy, atomic)。

(2)使分散的代码通过block的形式集中到一起。这样可读性更强,代码更简洁。
举个例子:button添加点击事件。

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [btn setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnClick{
    NSLog(@"我被点击了");
}

也就是说我们创建按钮按钮点击事件是分开的处理,如果页面逻辑比较多,调试的时候就会比较麻烦,有时候找一个按钮的点击事件找了大半天。下面将两者关联起来。

#import <UIKit/UIKit.h>

typedef void (^event)(id sender);

NS_ASSUME_NONNULL_BEGIN

@interface UIButton (wxh)

- (void)handelWithEvent:(event)block;

@end

NS_ASSUME_NONNULL_END
#import "UIButton+wxh.h"
#import <objc/runtime.h>

@implementation UIButton (wxh)

- (void)handelWithEvent:(event)block {
    if (block) {
        objc_setAssociatedObject(self, @selector(btnAction:), block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [self addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction:(id)sender{
    event block = objc_getAssociatedObject(self,@selector(btnAction:));
    if (block) {
        block(sender);
    }
}

@end
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [btn setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:btn];
    [btn handelWithEvent:^(id sender) {
        NSLog(@"我被点击了");
    }];

也就是说在开发中,我们可以利用这一点代替比较繁琐的代理事件。

2. 交换方法

class_getClassMethod 得到类的类方法
class_getInstanceMethod 得到类的对象方法

import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (wxh)
+ (instancetype)wxhURLWithString:(NSString *)URLString;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+wxh.h"
#import <objc/runtime.h>
@implementation NSURL (wxh)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = object_getClass([NSURL class]);
        
        SEL SEL_old = @selector(URLWithString:);
        SEL SEL_new = @selector(wxhURLWithString:);
        
        Method method_old = class_getClassMethod([NSURL class], SEL_old);
        Method method_new = class_getClassMethod([NSURL class], SEL_new);
        
        IMP imp_old = method_getImplementation(method_old);
        IMP imp_new = method_getImplementation(method_new);
        
        BOOL beMethod_old = class_addMethod(cls, SEL_old, imp_new, method_getTypeEncoding(method_new));
        if (beMethod_old) {
            class_replaceMethod(cls, SEL_new, imp_old, method_getTypeEncoding(method_old));
        }
        else{
            method_exchangeImplementations(method_old, method_new);
        }
    });
}
+ (instancetype)wxhURLWithString:(NSString *)URLString{
    if (!URLString.length) {
        URLString = @"www.baidu.com";
    }
    NSURL *url = [NSURL wxhURLWithString:URLString];
    return url;
}
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSURL *url = [NSURL URLWithString:@""];
    NSLog(@"打印URL:%@",url);
}
@end

打印结果:打印URL:www.baidu.com

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSMutableArray (wxh)
- (void)wxhAddObject:(id)object;
@end
NS_ASSUME_NONNULL_END
#import "NSMutableArray+wxh.h"
#import <objc/runtime.h>
@implementation NSMutableArray (wxh)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSMutableArray *obj = [[NSMutableArray alloc] init];
        [obj exchangeImplementationsMethod:@selector(addObject:) WithMethod:@selector(wxhAddObject:)];
    });
}
- (void)wxhAddObject:(id)object{
    if (object) {
        [self wxhAddObject:object];
    }
    else{
        NSLog(@"=== object is nil ===");
    }
}
- (void)exchangeImplementationsMethod:(SEL)SEL_old WithMethod:(SEL)SEL_new{
    Class cls = [self class];
    Method method_old = class_getInstanceMethod(cls, SEL_old);
    Method method_new = class_getInstanceMethod(cls, SEL_new);
    
    IMP imp_old = method_getImplementation(method_old);
    IMP imp_new = method_getImplementation(method_new);
    
    BOOL beMethod_old = class_addMethod(cls, SEL_old, imp_new, method_getTypeEncoding(method_new));
    if (beMethod_old) {
        class_replaceMethod(cls, SEL_new, imp_old, method_getTypeEncoding(method_old));
    }
    else{
        method_exchangeImplementations(method_old, method_new);
    }
}
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *r = nil;
    NSMutableArray *arr = [NSMutableArray array];
    [arr addObject:r];
}
@end

打印结果:=== object is nil ===
解释:
(1)全局效果:不需要引入头文件,直接调用原来的方法就可。
(2)只实现一次dispatch_once:交换方法效果是全局的,只能让它实现一次。虽然系统加载程序时+ (void)load只调用一次,但+ (void)load是一个公开的类函数,有可能被手动调用。因此加上dispatch_once是必要的。
(3)class_addMethod:给class动态添加方法sel,如果selclass中已经存在的,返回值是NO,否则返回值为YES
(4)class_replaceMethod:修改函数的IMP指针,使其指向新的实现地址。
(5)原理:首先,通过dispatch_once保证方法交换有且只有一次,然后根据class_addMethod的返回值判断要被交换的方法本身是否存在。如果为NO,说明存在,通过exchangeImplementationsMethod将两个函数的IMP指针指向的函数实现地址交叉互换。如果为YES,说明要在交换的方法本身是不存在的,但是这时候已经通过class_addMethod将它添加到这个class里面,而且将他的IMP指向新的方法。那么最后只需要再通过class_replaceMethod将新的方法的IMP指向要被交换的方法即可。(其实最后这一步是可以省略的,因为如果要被交换的方法本身是不存在的,那这整个过程其实就相当于给class添加方法而已。)
(6)object_getClass获取父类的Class

3.动态添加函数

class_addMethod返回BOOL值,YES表示添加成功,NO表示添加不成功。
+ (BOOL)resolveInstanceMethod:(SEL)sel对象函数动态决议方法。
+ (BOOL)resolveClassMethod:(SEL)sel类函数动态决议方法。

#import "Person.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
    class_addMethod([Person class], @selector(say:WithName:), class_getMethodImplementation([ViewController class], @selector(personSay:WithName:)), method_getTypeEncoding(class_getInstanceMethod([ViewController class], @selector(personSay:WithName:))));
    [p performSelector:@selector(say:WithName:) withObject:@"hello" withObject:@"wxh"];
}
- (void)personSay:(NSString *)string WithName:(NSString *)name{
    NSLog(@"%@,%@",name,string);
}

打印结果为wxh,hello

#import "ViewController.h"
#import "Person.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    class_addMethod(object_getClass([Person class]), @selector(drive), class_getMethodImplementation(object_getClass([ViewController class]), @selector(personDrive)), method_getTypeEncoding(class_getClassMethod(object_getClass([ViewController class]), @selector(personDrive))));
    [Person performSelector:@selector(drive)];
}
+ (void)personDrive{
    NSLog(@"===");
}

打印结果为===
解释:
(1)如果是一个类class要添加对象方法,直接用类class添加。如果一个类class要添加类方法,只需要通过这个类的父类object_getClass,添加成父类的对象方法,也就是该类的类方法。
(2)方法添加成功之后,必须要通过performSelector调用,不能直接调用。因为class_addMethod是在程序运行状态下才执行的,而编译状态下,还找不到动态添加的方法。
(3)当一个对象cls去调用一个函数method的时候,cls通过自身的ipa指针找到该对象的类class,然后通过class中的函数列表methodLists查找method的函数名sel,如果找到sel,通过IMP指针指向的'method'的实现地址,调用该method。那如果找不到呢,这时候就会去到动态决议方法那里,并返回NO值。
(4)class_addMethod当要添加的方法在改类或者该类的父类中已经存在时,class_addMethod返回NO

4.获取属性,修改属性和获取方法。

    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([UITextField class], &count);
    for (int i = 0; i<count; i++) {
        // 取出成员变量
        Ivar ivar = *(ivars + i);
        // 打印成员变量名字 数据类型
        NSLog(@"%s--%s", ivar_getName(ivar),ivar_getTypeEncoding(ivar));
    }
    // 释放
    free(ivars);
    unsigned int methCount = 0;
    Method *meths = class_copyMethodList([UITextField class], &methCount);
    for(int i = 0; i < methCount; i++) {
        Method meth = meths[i];
        SEL sel = method_getName(meth);
        const char *name = sel_getName(sel);
        NSLog(@"%s", name);
    }
    free(meths);
[textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];

5.归档&解档

归档&解档

6.热更新

JSPatch

7.逆向开发

上一篇下一篇

猜你喜欢

热点阅读