Runtime
大家都知道Runtine是比较底层的东西,平时用得少,但是一旦用起来,威力是巨大的,常常有四两拨千斤的用处。
什么是Runtime
运行时刻是指一个程序在运行(或者在被执行)的状态。也就是说,当你打开一个程序使它在电脑上运行的时候,那个程序就是处于运行时刻。在一些编程语言中,把某些可以重用的程序或者实例打包或者重建成为“运行库"。这些实例可以在它们运行的时候被连接或者被任何程序调用。
- Runtime从字面上理解是运行时的意思,OC是一门动态编程语言,归因于继承和多态,代码的最终实现在运行的时候才最终确定。而Runtime可以通过这种特性,在程序运行前对代码自定义。Runtime是一个开源库,通过这个库,OC中的[target doMethodWith:var1]转换为objc_msgSend(target,@selector(doMethodWith:),var1),下面就来看看这个转换是怎么发生的。
-
注意使用Runtime需要导入objc/runtime.h头文件
1.类与对象的存储结构
-
对象的结构:首先了解一点,OC中对象、类、Block这些在运行的时候最终都是已结构体的形式存储的,其中对象的结构体如下:
struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; typedef struct objc_object *id;
isa指针:对象的isa指针是一个指向该对象类(Class)的指针,翻译过来就是“是一个”,这个对象“是一个”Class的对象。在代码执行过程中,通过isa指针找到对象的类,在类中存储着许多的信息,包括属性列表和方法列表以及其他信息。
-
类的结构:在代码执行过程中通过对象的isa指针找到类的地址后,会在类的结构体中寻找执行函数所需要的信息,类的结构体如下:
struct objc_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;
isa指针:类的isa指针指向该类的元类,也就是说我们使用的类其实也是一个对象,在类的结构体中存储实例方法,在元类的结构体中存储类方法。
objc_ivar_list:成员变量列表,存储类的成员变量,可以通过操作这个成员变量列表来添加属性。
objc_method_list:实例方法列表,存储类的实例方法,可以用过实例方法列表来交换方法的实现,自定义方法的实现。
cache:方法缓存,这个缓存的主要作用是将该类使用过的方法缓存起来,我们平时使用xCode编程的方法提示就是缓存在这个cache中。 -
元类:上面说了,类是元类的对象,类的isa指针指向类的元类,元类中存储着类方法。元类的结构和类是一样的,元类的isa指针指向根元类,根元类的isa指针指向自身。除了isa指针,类和元类的结构体中还有一个super_class指针,指向各自的父类,当方法在本类中找不到时,就会在父类中寻找,值得注意的是:根元类的父类是根类,根类的父类是nil。了解这些关系,就可以了解在代码执行过程中消息发送机制转换的函数如何实现。说到底就是在类的继承体系中找到实现方法的指针,实例方法在类中寻找,类方法在元类中寻找。具体结构如下图所示:
#### 2. [target doMethodWith:var1]是怎么运行的
- 首先代码运行时会将[target doMethodWith:var1]编译为objc_msgSend(target,@selector(doMethodWith:),var1),然后通过target对象结构体中的isa指针寻找到对象的结构体地址。
- 在寻找到类的结构体后,如果方法doMethodWith:是实例方法,就在类结构体中的methodLists寻找这个方法来执行,如果没有找到,就会通过类的super_class指针到父类的methodLists去寻找,一直向上,如果在根类没有找到,就会crash;如果是类方法,就会通过类结构体中的isa指针找到元类结构体的地址,在元类的methodLists寻找类方法,如果没有找到也会像上面的父类去寻找,最终没有找到也会crash。
- var1是执行doMethodWith:方法需要的参数,不必多说。
用处一:表单提交移除Emoji巧用Runtime
- 在上一个项目中,有许多界面需要提交表单,每个表单有很多的输入框,用户在输入过程中可能会输入Emoji表情,后台要求在上传数据的时候要移除这些表情。这种情况下在限定输入框的样式有损用户体验,而在每次输入后移除字符中的输入框都很麻烦,需要做大量的工作。这种大规模重复的工作第一反应就是将其封装起来,这个时候怎么封装就成了主要的问题。
- 怎么封装:在这些表单的提交中,一开始我就将需要的参数放到了一个model中,每次输入完成就给其中的一个参数赋值,在提交前通过判断model的属性也能轻松的做到判断参数是否输入,最后在提交表单数据前将model转化为字典。在这种数据集中在一处的情况下就便于封装,考虑将model与Runtime结合,通过Runtime获取ivars属性列表,通过对这些属性做出操作来移除表单填写过程中输入的Emoji。
-
类目:首先构造一个参数model,model的属性就是需要提交的参数,在输入数据的过程中,对于纯数字的输入选项可以将键盘设为纯数字键盘,这样就不会有Emoji输入,所以这里只考虑字符串移除Emoji。但是如何才能让每一个model都能用一句代码移除model中所有的Emoji,这个时候类目的作用就显现出来了,通过类目为NSString添加RemoveEmoji类目,在类目中实现判断和移除字符串的方法,为NSObject添加RemoveEmoji类目,通过遍历model的属性,来判断属性是否含有Emoji,然后移除Emoji。该类目移除Emoji方法如下:
-(void)removeEmoji{ NSSet *arrPropertyKeys = objc_getAssociatedObject(self, ATModelCachedPropertyKeysKey); // 获取model所有属性 for (NSString *key in arrPropertyKeys) { id valueKey = [self valueForKey:key]; if ([valueKey isKindOfClass:[NSString class]]) { valueKey = (NSString *)valueKey; if (valueKey) { if ([valueKey isIncludingEmoji]) { valueKey = [valueKey removedEmojiString]; [self setValue:valueKey forKey:key]; } }else{ if ([key hasPrefix:@"_"]) { valueKey = [self valueForKey:[key substringFromIndex:1]]; if ([valueKey isIncludingEmoji]) { valueKey = [valueKey removedEmojiString]; [self setValue:valueKey forKey:key]; } } } } } }
-
removeEmoji方法可以实现移除工程中所有model中含有的Emoji,但是需要引入NSString+RemoveEmoji类目,该类目实现如下(将isIncludingEmoji和removedEmojiString设置为接口即可):
- (BOOL)isEmoji { const unichar high = [self characterAtIndex: 0]; if (0xd800 <= high && high <= 0xdbff) { const unichar low = [self characterAtIndex: 1]; const int codepoint = ((high - 0xd800) * 0x400) + (low - 0xdc00) + 0x10000; return (0x1d000 <= codepoint && codepoint <= 0x1f77f); } else { return (0x2100 <= high && high <= 0x27bf); } }
- (BOOL)isIncludingEmoji { BOOL __block result = NO; [self enumerateSubstringsInRange:NSMakeRange(0, [self length]) options:NSStringEnumerationByComposedCharacterSequences usingBlock: ^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop) { if ([substring isEmoji]) { *stop = YES; result = YES; } }]; return result; }
- (instancetype)removedEmojiString { NSMutableString* __block buffer = [NSMutableString stringWithCapacity:[self length]]; [self enumerateSubstringsInRange:NSMakeRange(0, [self length]) options:NSStringEnumerationByComposedCharacterSequences usingBlock: ^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop) { [buffer appendString:([substring isEmoji])? @"": substring]; }]; return buffer; }
-
完成以上两个类目后,只需在提交表单时,在将model转换为NSDictionary之前执行[model removeEmoji]就可以顺利移除表单中的Emoji,使用这种模式移除Emoji复用性高,操作简便,充分体现Runtime的优势,真正达到了四两拨千斤的效果。
用处二:利用Runtime替换SDK的方法
有的时候我们在开发过程中需要自定义SDK或者修改公司代码库的代码,但是SDK的以库的形式给出,不能更改,公司代码库有时候为了全局照相也不能更改,这个时候就需要Runtime来解决这个棘手的问题。通过以上的介绍大家都知道了在类的结构体和元类的结构体中有一个存储方法的地方,类的方法都存储在这个地方,需要更改方法的实现可以对methodLists方法列表进行操作已达到目的。
- 怎么更改方法的实现:在不更改库方法的情况下自定义方法,最容易想到的就是通过Runtime交换方法。但是以什么形式来交换实现的方法,怎么执行实现交换方法的方法,什么时候执行都是需要考虑的问题。首先,要交换一个类的方法,在无法给该类添加方法的情况下,能想到的首先就是添加类目了,通过添加类目,实现一个交换方法的方法就可以顺利交换一个方法,并自定义它的实现。然后,怎么实现交换方法呢,这个在后面以代码的形式给出。最后一个问题,什么时候实现这个方法,这个问题比较复杂,在平时我们使用类目的时候,都需要导入类目的头文件,然后才能使用相应的方法,但是这样使用很麻烦,怎么样才能只执行一次就能在所有的地方改变这个方法的实现,甚至不用手动调用就能执行这个交换两个方法的方法,我想到了load方法。
-
load方法:load方法是一个类方法,每个类的load方法会在编译时执行一次,通过在load方法中实现交换两个方法,就可以不用导入头文件,也不用调用方法,轻松的实现库文件中方法的交换。具体实现如下(在类目中load方法执行交换,ed_initWithJavaRespone:自定义需要交换的方法):
+(void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method method1 = class_getInstanceMethod([self class], NSSelectorFromString(@"initWithJavaRespone:")); Method method2 = class_getInstanceMethod([self class], @selector(ed_initWithJavaRespone:)); method_exchangeImplementations(method1, method2); }); }
-(void)ed_initWithJavaRespone:(NSDictionary *)dic{ // code here }
总结
- Runtime是个好东西,它也确实是个好东西,虽然平时用得不多,关键时刻却能迸发无穷的能量,所以在掌握基本的iOS编程技巧之后,可以多了解一下这些底层的东西,对平时开发会有很大的帮助。
- 除了以上介绍的两种用法,Runtime还有很大的用处,Runtime的使用能大幅度减少代码量,能解决普通方法不能解决的问题。关于其他的Runtime函数,大家就勇敢的去探索吧!
- 学习路线建议是:Runtime是什么?->Runtime能干什么?->怎么使用Runtime?->我好牛逼哈哈哈