聊聊Objective-C的Runtime

栏目: Objective-C · 发布时间: 7年前

内容简介:聊聊Objective-C的Runtime

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。

对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。Runtime基本上是用C和汇编写的,这个库使得 C语言 有了面向对象的能力。

在Runtime中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,让Objective-C的面向对象编程变为可能。

找出方法的最终执行代码:当程序执行 [object doSomething] 时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。

1 消息机制

与古老的C语言不同,Objective-C虽然源自C语言,但是它却是面向对象的,在这之中,消息机制发挥着重大作用。

C语言和Objective-C编译时的区别:

C语言在编译的时候,已经知道调用哪一个函数。

Objective-C不一样,只有在运行时才知道需要调用的方法和函数。

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);

使用这个方法要 #import <objc/message.h> ,另外,Apple在Xcode5开始,不建议使用底层方法,而恰巧以上方法就是底层方法。此时,Xcode就会报错,那么,如何解决呢?

解决方案如下:

  • 1)打开Project的 Build Settings ,搜索“msg”。
  • 2)将 Enable Strict Checking of objc_msgSend Calls 的值设置为NO。

发送无参消息

@interface Sample : NSObject
+ (void)run;
- (void)run;
- (void)eatWithFood:(NSString *)food;
@end

@implementation Sample
+ (void)run
{
    NSLog(@"类方法 run");
}

- (void)run
{
    NSLog(@"实例方法 run");
}

- (void)eatWithFood:(NSString *)food
{
    NSLog(@"实例方法 eat:%@", food);
}
@end

- (void)test {
	Sample *t = [[Sample alloc] init];
	objc_msgSend(t, @selector(run));

	objc_msgSend([Sample class], @selector(run));
}

发送带参消息

- (void)test {
	Sample *t = [[Sample alloc] init];
	objc_msgSend(t, @selector(eatWithFood:), @"apple");
}

附加:将Objective-C转换出Runtime代码方法

clang -rewrite-objc xxxx.m

交换方法的实现

class_getInstanceMethod(__unsafe_unretained Class cls, SEL name) 获取对象方法。

class_getClassMethod(__unsafe_unretained Class cls, SEL name) 获取类方法。

method_exchangeImplementations(Method m1, Method m2) 交换方法的实现方式。

常见示例代码:

NSURL *url = [NSURL URLWithString:@"https://charsdavy.github.io"];
[NSURLRequest requestWithURL:url];

url 可能返回nil,而此段代码未能对返回值 url 进行合法判断。但每次使用以上类似代码都需要进行合法性判断,那么有什么更好的方法使 URLWithString: 能够做合法性判断呢?这个时候就需要使用Runtime的特有方法了。

@interface NSURL (DD)
+ (instancetype)dd_URLWithString:(NSString *)URLString;
@end

@implementation NSURL (DD)
// 加载此分类时调用
+ (void)load
{
    //获取方法名称
    Method urlMethod = class_getClassMethod([NSURL class], @selector(URLWithString:));
    Method ddUrlMethod = class_getClassMethod([NSURL class], @selector(dd_URLWithString:));
    //交换方法的实现
    method_exchangeImplementations(urlMethod, ddUrlMethod);
}
//此方法与URLWithString:交换了实现方式
+ (instancetype)dd_URLWithString:(NSString *)URLString
{
//    NSURL *url = [NSURL URLWithString:URLString]; 此处不能再调用此方法,否则会死循环
    NSURL *url = [NSURL dd_URLWithString:URLString];
    if (!url) {
        NSLog(@"url is nil");
    }
    return url;
}
@end

归档和解档

先来理解几个Objective-C中的概念:

序列化:将自定义的Objective-C的对象转化成二进制文件数据。

反序列化:将二进制文件数据转化成自定义的Objective-C的对象。

归档:将自定义的Objective-C的对象存储到本地磁盘。

解档:将存储在本地磁盘的数据转换成自定义的Objective-C的对象。

Ivar类型:成员属性。

Method类型:成员方法。

通常我们使用归档和解档的方式如下:

@interface Sample : NSObject<NSCoding>
@property (nonatomic) NSString *name;
@property (nonatomic) NSString *age;
@end

@implementation Sample
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.age forKey:@"age"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeObjectForKey:@"age"];
    }
    return self;
}
@end

- (void)saveObject
{
    Sample *p = [[Sample alloc] init];
    p.name = @"Chars";
    p.age = @"18";
    
    NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"chars.dat"];
    
    BOOL flag = [NSKeyedArchiver archiveRootObject:p toFile:path];
    if (flag) {
        NSLog(@"success");
    } else {
        NSLog(@"falied");
    }
    NSLog(@"%@", path);
}

- (void)readObject
{
    NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"chars.dat"];
    
    Sample *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    if (p) {
        NSLog(@"name:%@,age:%@", p.name, p.age);
    } else {
        NSLog(@"falied");
    }
    NSLog(@"%@", path);
}

Objective-C中归档底层实现方式:将对象拆分为字典(键值对),然后变成二进制存入磁盘。

但是,当model中成员属性数量很多的时候,就沦为了体力劳动。那么,此时我们又能使用Runtime来简化工作。

class_copyIvarList(__unsafe_unretained Class cls, unsigned int *outCount) 获取Class中成员变量的个数。

以下就是使用Runtime消息机制编写的归档与解档方法:

- (void)encodeWithCoder:(NSCoder *)aCoder
{   
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Sample class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
    free(ivars);
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Sample class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

2 KVO

利用Runtime,在运行时动态创建一个对象。

实现原理:

  1. 创建 NSKVONotifying_XXX:XXX 类(XXX为被监听者)。
  2. 重写属性set方法,调用 willChangeValueForKey:didChangeValueForKey: 方法,进而触发调用观察者的 observeValueForKeyPath:ofObject:change:context:

示例代码:

@interface Viewer : NSObject
@property (nonatomic) NSString *name;
@property (nonatomic) NSString *age;
@end

@interface Observer : NSObject
@property (nonatomic) NSString *name;
@property (nonatomic) NSString *age;
@end

@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"object:%@, keyPath:%@, change:%@", object, keyPath, change);
}
@end

Observer *observer = [[Observer alloc] init];
Viewr *viewer = [[Viewer alloc] init];
//注册监听,viewer为被监听者,observer为观察者
[viewr addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
//触发KVO
viewer.age = @"99";

3 动态添加方法

当方法被调用时,才被加载。

+ (BOOL)resolveClassMethod:(SEL)sel; 当一个类被调用了一个没有实现的方法时,则会调用此方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel 当一个类被调用了一个没有实现的实例方法时,则会调用此方法。

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types) 动态添加方法。

参数 cls :类类型。参数 name :方法编号。参数 imp :方法实现,就是一个函数的指针。参数 * types :方法类型

@implementation Sample
//一个类被调用了一个没有实现的实例方法时,则会调用此方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        //添加一个实例方法
        class_addMethod([Sample class], sel, (IMP)eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

//隐式参数,self和_cmd是系统传过来的参数
void eat(id self, SEL _cmd) {
    NSLog(@"调用了%@的%@方法", self, NSStringFromSelector(_cmd));
}
@end

[[[Sample alloc] init] performSelector:@selector(eat)];

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

实用Common Lisp编程

实用Common Lisp编程

Peter Seibel / 田春 / 人民邮电出版社 / 2011-10 / 89.00元

由塞贝尔编著的《实用Common Lisp编程》是一本不同寻常的Common Lisp入门书。《实用Common Lisp编程》首先从作者的学习经过及语言历史出发,随后用21个章节讲述了各种基础知识,主要包括:REPL及Common Lisp的各种实现、S-表达式、函数与变量、标准宏与自定义宏、数字与字符以及字符串、集合与向量、列表处理、文件与文件I/O处理、类、FORMAT格式、符号与包,等等。......一起来看看 《实用Common Lisp编程》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具