runtime

一.什么是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的字典转模型差不多逻辑。

  • 先通过void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy):设置关联对象的方法,相当于setValue:forKey
  • 再使用id objc_getAssociatedObject(id object, const void *key):获取关联对象。相当于valueForKey:
  • objc_AssociationPolicy policy:关联策略。
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

  • 应用场景 一般都是用来改变一些系统方法的实现。
    (1)例如上面NSMutableArray的例子,达到防奔溃处理。
    (2)改变viewDidLoad打印出当前控制器的名称等。

3.动态添加函数

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

  • 动态添加对象方法:创建一个Person继承于NSObject,不添加任何属性和方法,直接在外部给Person添加一个对象方法。
#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

  • 应用场景
    (1)在类的外部动态给类class添加方法。
    (2)重写动态协议,防止调用不存在的方法时,程序崩溃crash
    (3)通过class_addMethod的返回值判断某个类中是否有某个方法。

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

  • 获取属性
    class_copyIvarList
    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);
  • 获取方法
    class_copyMethodList
    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);
  • 修改属性
    通过KVC的方式进行修改即可
[textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];

5.归档&解档

归档&解档

6.热更新

JSPatch

7.逆向开发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容