Objective-C runtime 的简单理解与使用(二)

简书上的所有内容都可以在我的个人博客上找到(打个广告😅)


在我们知道了 Objective-C 中类的本质,以及它的消息分发机制后,我们就可以来看看那些与 runtime 相关的的函数了。当然,我们只会讲比较常见的那些。

关联对象(Associated Object)


关联对象,顾名思义,就是给某对象关联许多其他的对象。这些对象通过 key 来区分。

与关联对象相关的函数有三个:

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void *key);
objc_removeAssociatedObjects(id object);

从函数名我们也可以看出来,这三个函数分别是用来设置,获取和移除关联对象的。这里要解释一下的是他们的参数。

  • 第一个参数 id object 显然就是你要设置关联对象的那个对象。
  • 第二个参数 const void *key 就是用来区分不同的关联对象的 key,因为想让两个 key 匹配到同一个关联对象就必须是完全相等的指针,所以我们一般用静态全局变量来作为 key。
static const void *AssociatedKey = "AssociatedKey";
  • 第三个参数 id value 就是要关联的对象了。
  • 第四个参数 objc_AssociationPolicy policy 指的是关联对象的存储策略,它是一个枚举,可以与 property 的 attribute 相对应:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,                         // assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,             // nonatomic, retain
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,                 // nonatomic, copy
    OBJC_ASSOCIATION_RETAIN = 01401,                     // retain
    OBJC_ASSOCIATION_COPY = 01403                       // copy
};

大家知道,在 category 中,我们无法添加 property,因为无法添加实例变量。那么,我们现在就可以通过关联对象来实现在 category 中添加属性的功能了。

我们现在 CYClass 类的拓展中声明了一个属性

@interface CYClass (Property)
@property (nonatomic, copy)NSString *aString;
@end

如果这个时候我们直接在外部访问这个属性, 那个程序是会 crash 的,不信你可以试试😅,编译器会说:

'-[CYClass setAString:]: unrecognized selector sent to instance 0x1001060a0'

所以我们给它加上 setter 和 getter 方法, 并且在这两个方法中给它设置关联对象:

static void *aStringKey = "aStringKey";

@implementation CYClass (Property)

- (void)setAString:(NSString *)newString{
    objc_setAssociatedObject(self, aStringKey, newString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)aString{
    return objc_getAssociatedObject(self, aStringKey);
}
@end

现在我们再进行读写操作,程序就不会 crash 了。当然,没有必要的情况下,还是不要滥用关联对象, 否则有可能会出现一些难以发现的bug。

方法调配(Method Swizzling)


在前一篇博客中我们知道了每个类中的方法是以 objc_method 结构体的形式放在 methodLists 中的。每一个 selector 对应了一个实现的函数的指针 IMP。而 method swizzling 技术就是通过交换这个函数指针来实现的。

我们最好在 +load 方法中使用 method swizzling,因为 +load 方法对于加入运行期中的每个类及分类都会调用且只调用一次。所以在这里交换方法是最安全的。

我们来看一下苹果为我们提供了哪些API来实现 method swizzling:

IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);

可以直接替换方法,当需要的方法不存在时,会先调用 class_addMethod 来添加一个新的方法。会返回替换前的实现函数指针 。

Method class_getInstanceMethod(Class cls, SEL name);

根据类和 selector 得到 method,用来作为下面两个方法的参数。

IMP method_setImplementation(Method m, IMP imp);

直接为一个方法设置它的实现,返回之前的实现函数指针

void method_exchangeImplementations(Method m1, Method m2)

交换两个方法的实现,实际上就是调用了两次 method_setImplementation,并且是线程安全的。

我们用 method_exchangeImplementations 来简单的尝试一下 method swizzling,我添加了一个 NSString 的分类,用我自己的方法交换了系统的 lowercaseString 方法:

@implementation NSString (Swizzling)
+ (void)load {
    Method originalMethod = class_getInstanceMethod([self class], @selector(lowercaseString));
    Method swappedMthod = class_getInstanceMethod([self class], @selector(swizzle_lowercaseString));
    method_exchangeImplementations(originalMethod, swappedMthod);
}

- (NSString *)swizzle_lowercaseString {
    NSString *lowercase = [self swizzle_lowercaseString];
    NSLog(@"FROM: %@  TO:  %@", self, lowercase);
    return lowercase;
}
@end

可能有人会觉得在自己新写的 swizzle_lowercaseString 方法中又调用 [self swizzle_lowercaseString] 会导致死循环,其实在交换了方法以后我们调用原来的 lowercaseString 方法就会进入这个方法的实现,而这时候调用 swizzle_lowercaseString 其实调用的是系统原来的方法,所以是不会产生死循环的。这里理解起来可能有点奇怪。

我们在看一下调用的结果

2016-03-11 20:01:05.645 Example[4129:101067] FROM: Hello World  TO:  hello world

当然 method swizzling 是一把双刃剑,我们可以用它来进行黑盒测试,在真正的项目中如果用 method swizzling 一定要格外小心。

消息转发机制(Message Forwarding)


当我们的对象接收到一个无法解读的消息时,就会进入消息转发。消息转发分为两大阶段,第一阶段是动态方法解析,第二阶段是完整的消息转发。

动态方法解析(dynamic method resolution)


要实现动态方法解析只要重写两个方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel; // 处理无法识别的实例方法
+ (BOOL)resolveClassMethod:(SEL)sel;    // 处理无法识别的类方法

这两个方法传进来的参数 selector 就是那个无法解析的方法,我们可以根据这个 selector 来动态的为这个类添加方法。比如像下面这样:

void dynamicMethod(id self, SEL _cmd) {
    // do something here
}

@implementation CYClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(someSelector)) {       // 对selector做一些逻辑判断
        class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");  // 为类添加方法
        return YES;
    } else {
        return NO;
    }
}
@end

还要提一下 class_addMethod 函数:

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types);

它的最后一个参数是用来描述这个函数的返回值和参数类型的,称之为 类型编码(Type Encoding)。在前面那个例子里的 "v@:" 中, v 表示返回值为 void, @ 表示第一个参数是 id, : 表示第二个参数类型是 SEL 。更多的类型编码可以看这里

当 resolveInstanceMethod: 返回 NO 时,就会进入消息转发的第二阶段 完整的消息转发机制。

完整的消息转发机制


完整的消息转发主要涉及两个方法:

- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

如果在 + resolveInstanceMethod: 方法中返回了 NO 那么就会执行 - forwardingTargetForSelector: 方法。在这个方法内我们可以给对象返回一个备援的接受者来处理这个位置的信息。在 CYClass 的实现中我们这么写:

@implementation CYClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(unrecognizedSel)) {
        return [AnotherClass new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

AnotherClass 的实例就是我们用来作为备援接受者的对象,我们在 AnotherClass 中实现了 unrecognizedSel 方法:

@implementation AnotherClass
- (void)unrecognizedSel {
    NSLog(@"forwarding target for unrecognized selector in AnotherClass");
}
@end

然后我们再给 CYClass 的实例发送 unrecognizedSel 的消息就不会 crash 了:

CYClass *c = [CYClass new];
[c performSelector:@selector(unrecognizedSel)];

// 打印结果
2016-03-12 12:46:30.608 example[1577:19943] forwarding target for unrecognized selector in AnotherClass

如果这一步我们也没有提供一个备援的接收者,那么就会进入最后一步 - forwardInvocation: 方法,系统会把所有与那条消息相关的信息全部封装在一个 NSInvocation 对象中,我们可以在直接改变调用的目标, 也可以修改消息的内容后再进行转发。我们把前一个方法去掉,然后重写一下 - forwardInvocation: 方法:

@implementation CYClass
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    if (sel == @selector(unrecognizedSel)) {
        [anInvocation invokeWithTarget:[AnotherClass new]];
    } else {
        [super forwardInvocation: anInvocation];
    }
}
@end

需要注意的是我们还要重写- methodSignatureForSelector: 方法,因为生成 NSInvocation 对象会调用到这个方法,否则会抛出异常。关于 forwardInvocation 了解的还不是很多,所以例子比较简单,以后有了更深的理解后会再加上。

消息转发的全过程

总结


到这里对于 runtime 的简单理解与使用就基本结束了。总的来说,理解了 Objective-C 的运行时会让我们的代码更加灵活,当然也会增大维护的难度。不过想要学好 Objective-C 这门语言,runtime 是必不可少的!

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

推荐阅读更多精彩内容