在IOS开发和学习过程当中,咱们常常会接触到一个词: Runtime 。不少开发者对之既熟悉又陌生,基本都是浅尝辄止,达不到灵活使用的水平(话说开发中也确实不常常用。。)本文和你们一块儿研究一下,Runtime究竟是什么,还有他的一些应用场景,毕竟Runtime是OC动态特性的核心,熟练掌握它能够帮助咱们更好的控制类的属性及方法,编写出更高效的代码。html
1、什么是Runtime ios
无论你以前如何理解的Runtime,先把他扔一边,咱们从头梳理一下:数组
一、有一种大气而准确的说法 : 缓存
Objective-C是C语言的扩展,并加入了面向对象特性和Smalltalk式的消息传递机制。OC中的Runtime实现了将C语言转化为面向对象语言的做用,实际上咱们的每一条OC代码的执行都会转换为Runtime的函数调用。Runtime是OC底层的实现,其函数的调用是高效的,基于Runtime的代码编写也是高效的!框架
二、核心: 函数
是一个用C和汇编语言写的Runtime库(开源),这个库所作的事情就是加载类信息,进行方法的分发和转发,正是这个库赋予了Objective-C动态特性。布局
三、所谓的“动态特性” :性能
(1)咱们比较下C++ 和OC,C++没有动态特性,编译时直接将代码转换为机器语言,而OC是在运行的时候,经过Runtime把程序转为可令计算机读懂的语言。二者都是对C进行了面向对象的扩展,可是实现机制不一样。学习
(2)虽然RunTime赋予了OC动态特性,使得开发和使用变得至关灵活,可是归根结底OC仍是一种编译型的语言,其具备必定的动态性,可是其动态特性也比不上JavaScript这种解释型的语言。atom
四、Runtime 其实有两个版本(也就听听。。):
(1)“Modern” 和 “Legacy”。咱们如今用的 Objective-C 2.0 采用的是现行(Modern)版的 Runtime 系统,只能运行在 iOS 和 OS X 10.5 以后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中得(早期) Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你须要从新编译它的子类,而现行版本就不须要。
(2)苹果开源了Runtime库的代码,同时GNU也维护着一个开源的版本,这两个版本之间都在努力的保持一致。
五、常见的简单使用场景,后面详细罗列:
(1)动态的建立、改变类
(2)动态的建立、改变、遍历属性
(3)动态的建立、改变、交互、遍历方法
2、OC中的对象模型
真正开始了解Runtime,有个基础工做须要作,就是咱们要重温一下OC的对象和类的结构
一、打开<objc/objc.h>看看objc_object的定义 (截图看起来比较清晰,呵呵)
总结一下上面的:
(1)经常使用的id类型其实是一个指向objc_object(实例对象)结构体的指针,id一般指代一个对象,也就是说OC对象其实就一个指向objc_object结构体的指针
(2)看objc_object结构体定义,得知其结构体内有一个类型为Class的字段isa,这就是常说的isa指针了。
(3)Class声明为一个指向objc_class的指针
关于 SEL、IMP的补充
(1)SEL是selector在Objective-C中的表示类型。selector能够理解为区别“方法”的ID。
typedef struct objc_selector *SEL; struct objc_selector { char *name; OBJC2_UNAVAILABLE;// 名称 char *types; OBJC2_UNAVAILABLE;// 类型 };
name和types都是char类型。
(2)IMP是“implementation”的缩写,它是由编译器生成的一个函数指针。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。
二、接下来打开<objc/runtime.h>,看看objc_class的定义
研究一下objc_class中的几个字段:
(1)isa:这里的isa指针一样是一个指向objc_class(类对象)的指针,代表该Class的类型,这里的isa指针指向的就是常说的meta-class(元类)了。不难看出,类自己也是一个对象。一样的,元类也是一个对象,为了设计上的完整,元类的isa指针都会指向一个root metaclass(根元类)。根元类自己的isa指针指向本身,这样就造成了一个闭环。
(2)super_class:这个指针就是指向该class的super class,即指向父类,若是该类已是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
(3)cache:用于缓存最近使用的方法。消息发送时,系统会根据isa指针去查找可以响应这个消息的对象。在实际使用中,这个对象只有一部分方法是经常使用的,若是每次消息来时,都是在methodLists中遍历一遍,性能必定不好。这时,在每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,若是cache没有,才去methodLists中查找方法。这样,对于那些常常用到的方法的调用,提升了调用的效率。
(4)version:咱们可使用这个字段来提供类的版本信息。这对于对象的序列化很是有用,它可让咱们识别出不一样类定义版本中实例变量布局的改变(不太明白的做用。。。)
(5)objc_method_list:方法链表中存放的是该类的成员方法(-方法),类方法(+方法)存在meta-class的objc_method_list链表中(就是元类的“实例方法”)。
关于结构体指针 Method、Ivar的补充
(1)Method表明类中的某个方法的类型
声明: typedef struct objc_method *Method;
objc_method的定义以下:
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; // 方法类型 IMP method_imp OBJC2_UNAVAILABLE; // 方法实现 }
方法名method_name类型为SEL。
方法类型method_types是一个char指针,存储着方法的参数类型和返回值类型。
方法实现method_imp的类型为IMP
(2)Ivar表明类中实例变量的类型
声明:typedef struct objc_ivar *Ivar
objc_ivar的定义以下:
struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; // 变量名 char *ivar_type OBJC2_UNAVAILABLE; // 变量类型 int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节 #ifdef __LP64__ int space OBJC2_UNAVAILABLE; // 占用空间 #endif }
三、经典配图展现:oc对象继承模型
(1)上图的几个注意点
(2)举个方法查找过程的例子:
调用respondsToSelector: 的时候,实例对象只须要根据其isa指针,找到其所属的class,而后遍历其methodLists,若是没有,那么根据这个类的super_class找到其父类,再看其父类是否能相应这个方法就能够了,直到super_class为nil时,就没法响应这个方法了,return NO。
(3)调用类方法的不一样:
当咱们使用类名调用类方法(+方法)时,只须要根据class的isa指针,找到其meta-class,而后经过meta-class的methodLists找到相应的方法既可(“类”是“元类”的对象)。
3、消息机制
一、OC中调用一个方法的本质就是在给对象发送消息,好比初始化一个NSObject对象:
NSObject *object = [[NSObject alloc] init];
事实上,在编译时这句话会翻译成一个C的函数调用,即:
objc_msgSend(objc_msgSend([NSObject?class],@selector(alloc)),@selector(init));
看看官方文档:
二、关于消息执行的时机问题:
(1)思考:就如上文所述,OC的代码翻译成C的函数调用以后,就是把OC代码转换成C代码了,那OC的动态特性体如今哪里?不就和C的静态特性同样了么?
(2)回答:对于C语言,函数的调用在编译的时候就会去决定调用哪一个函数。而OC是一种动态语言,它会尽量的把代码执行的决策从编译和连接的时候,推迟到运行时。给一个对象发送的一个消息并不会当即执行,而是在运行的时候再去寻找他对应的实现。这样就能够把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。
三、全部使用objc_msgSend函数,会执行如下步骤(也体现了objc_cache的做用)
(1)经过对象(类)的isa指针去找到他的class
(2)在class的method list 找到该消息的实现
(3)若是class中没有该消息的实现,就继续到它的super_class中去找
(4)一旦找到这个这个消息的实现,那么就去执行他的IMP(函数指针,代码所在空间)
四、常见的函数、头文件
(1) #import <objc/runtime.h> : 主要包括 成员变量、类、方法
(2) #import <objc/message.h> : 消息机制
4、RunTime的使用实例
RunTime能够很灵活的实现改变系统的方法及属性的效果,灵活度之大以致于也形成了一些隐患 ——— 破坏了系统的封装及代码的可读性,因此你们仍是谨慎使用,也不要在开发过程当中给队友挖坑(不要问我怎么知道的。。。)
下面搜罗了一些经常使用的场景:
一、动态建立一个类
#import <objc/runtime.h> // 自定义一个方法 void reportFunction (id self, SEL _cmd) { NSLog(@"This object is %p", self); } int main(int argc, const char * argv[]) { @autoreleasepool {?? ? ? ? // 1.动态建立对象 建立一个Person 继承自 NSObject类 Class newClass = objc_allocateClassPair([NSObject class], “Person”, 0);? // 为该类增长名为Report的方法 class_addMethod(newClass, @selector(report), (IMP)reportFunction, @"v@:");?? ? ? ? // 注册该类 objc_registerClassPair(newClass); // 建立一个 Student 类的实例 instantOfNewClass = [[newClass alloc] init]; // 调用方法 [instantOfNewClass report]; } return 0; }
二、关联对象(分类中动态添加成员变量)
(1)对象在内存中的排布能够当作是一个结构体,该结构体的大小并不能动态的变化,因此没法在运行时动态的给对象增长成员变量,可是咱们能够经过关联对象的方法变相的给对象增长一个成员变量。
(2)好比,咱们想给NSObject新增一个关联对象(就是添加成员变量):
建立一个NSObject的分类AssociatedObject,并声明一个新属性
@interface NSObject (AssociatedObject) @property (nonatomic, strong) id associatedObject;? @end 在NSObject+AssociatedObject.m文件里面进行关联 #import "NSObject+AssociatedObject.h" #import <objc/runtime.h> @implementation NSObject (AssociatedObject) @dynamic associatedObject; - (void)setAssociatedObject:(id)object { // 设置关联对象 objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)associatedObject { // 获得关联对象 return objc_getAssociatedObject(self, @selector(associatedObject)); } @end
(3)咱们能够经过上面的方法,给类动态的添加属性,不过咱们更经常使用的是给一个类动态的添加block回调(想让这个类的实例实现什么业务逻辑,均可以经过赋值block,而后调用,来灵活的实现,使用起来也很方便,和添加属性相似)。
三、交换方法(可更改系统方法)
咱们在开发过程当中会遇到一种常见的错误:给数组元素赋值nil,系统会崩溃。下面咱们参照这个案例,解释下runtime交换方法的实现
(1)先建立一个数组
NSMutableArray *arrayM = [NSMutableArray array]; [arrayM addObject:@"1111"]; [arrayM addObject:@"2222"]; [arrayM addObject:nil]; //这里会形成程序崩溃 [arrayM addObject:@"33333"];
(2)交换方法,将系统的addObject和自定义的方法进行交换,咱们先写一个NSMutableArray的分类,给其添加新的方法,而后在其中实现与系统方法交换。注意:“交换”不等同于“替换”
(3)这里有一个坑,addObject 其实是 调用 insertObject :atIndex:方法, 而且在运行过程当中,能够看到这个方法是_NSArrayM的方法(不是NSMutableArray的方法,这里有点像KVO的状况),因此咱们要拿到_NSArrayM类,而后和它交换方法。能够参照下面的代码:NSClassFromString(@"__NSArrayM”)就是动态过程当中获取这个类。
(4)代码实现
@implementation NSMutableArray (XL) + (void)initialize //补充下这个方法只执行一次,类比load方法都是执行一次,可是后者编译完就会执行 { //当前类被初始化的时候调用 Method m1 = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(addObject:)); Method m2 = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(new_AddObject:)); //下面这样写也能够 //Method m2 = class_getInstanceMethod([NSMutableArray class], @selector(new_AddObject:)); method_exchangeImplementations(m1, m2); } - (void)new_AddObject:(id)objc { if (objc == nil) { //这里的方法已经经过交换变成 addObject: [self new_AddObject:@"此处为空"]; }else{ [self new_AddObject:objc]; } } @end
四、KVO的底层实现(自定义KVO)
(1)KVO的底层实现也是利用了RunTime机制,简单点说,KVO机制就是在运行时,动态派生出被检测对象的子类(NSKVONotifying_XXX),将被观察对象的isa指向该子类,而后在新子类中重写观察属性的set方法,接着在set方法里调用观察者的observeValueForKeyPath方法实现的监听机制(晕了吧,这就对了。。。看例子)
(2)给出示例(Person类就不写了,就一个属性age),在控制器中进行监测,下面代码执行结束,会显示监听到的age的变化
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *p = [[HMPerson alloc] init]; p.age = 20; self.p = p; //添加观察者 [p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; //属性改变了! p.age = 30; } - (void)dealloc { [self.p removeObserver:self forKeyPath:@"age"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@对象的%@属性改变了:%@", object, keyPath, change); } @end
(3)分析上面的例子,系统在运行时动态的进行了四项操做:
<1> 生成了Person的子类 NSKVONotifying_Person
<2> 在该子类中重写了age的set方法
<3> 调用了观察者的 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 方法
<4> 在p.age = 30 的时候,修改了对象中的isa指针,指向子类NSKVONotifying_Person,这样,在调用set方法的时候就会去子类的空间中寻找方法的地址并调用(很关键的一步,你们能够打断点验证)
(4)模拟KVO过程,咱们手动建立一个子类:NSKVONotifying_Person,注意,此时运行会报出“坏内存”的错误,由于你的建立的类和系统自动生成的子类重名了(这也是一种验证KVO原理的方式)
#import "NSKVONotifying_Person.h" @implementation NSKVONotifying_HMPerson -(void)setAge:(int)age { //必须先调用父类的setAge方法,保证父类的set方法正常运行 [super setAge:age]; //伪代码,调用监听者的方法,实现监听到属性改变后的逻辑操做,而且传递参数 [监听者 observeValueForKeyPath:@"age" ofObject:super change:@{ 监听属性的键值 } context:nil]; } @end
五、模型归档(遍历成员属性的应用)
(1)Runtime能够动态获取成员属性名列表。
(2)下面代码中,Ivar表示的就是成员属性,ivars是指向属性的指针或者说地址(也能够理解为数组,但不是数组)。
(3)归档的实现可能会遇到对象中有不少属性,逐个手动去匹配归档哪些属性很麻烦,因此使用运行时,经过循环实现。
(4)代码示例:对于一个有不少属性的Person类,遵照了NSCoding协议以后,咱们能够利用RunTime遍历模型对象的全部属性进行归档,关键代码以下:
// 利用runtime机制进行属性的归档接档 - (void)encodeWithCoder:(NSCoder *)aCoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Person class], &count); for (int i = 0; i<count; i++) { // 取出i位置对应的成员变量 Ivar ivar = ivars[i]; // 查当作员变量 const char *name = ivar_getName(ivar); // 归档 NSString *key = [NSString stringWithUTF8String:name]; //KVC 得到对象属性值 id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } // 若是函数名中包含了copy\new\retain\create等字眼,那么这个函数返回的数据就须要手动释放 free(ivars); } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Person class], &count); for (int i = 0; i<count; i++) { // 取出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; }
六、字典和对象模型之间的转换,例如MJExtension
(1)如今流行的不少字典转模型的框架,基本上都是利用Runtime原理实现(效率高):遍历模型属性列表—>根据得到的属性名做为key去字典中取出value —>而后用KVC给模型对象属性赋值
(2)这只是简单原理,框架中还包含了不少状况的判断,好比模型套模型的状况,这就须要检测成员属性的类型是不是对象类型,那就继续转模型。
(3)一个简单的示例代码
-(void)modelToolWithDict:(NSDictionary *)dict andModel:(Model*)model { // Ivar : 成员变量 unsigned int count = 0; // 得到全部的成员变量 Ivar *ivars = class_copyIvarList([HMPerson class], &count); for (int i = 0; i<count; i++) { // 取得i位置的成员变量 Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); // 得到成员变量的类型,若是须要根据类型判断是否有模型嵌套,能够经过这个变量 const char *type = ivar_getTypeEncoding(ivar); //得到字典中的值 id value = dict[[NSString stringWithUTF8String:name]]; //使用KVC给模型赋值(KVC底层也是Runtime) [model setValue:value forKeyPath:[NSString stringWithUTF8String:name]]; NSLog(@"%d %s %s", i, name, type); } }
七、避免数组越界
开发中,数组在访问时若是越界会形成崩溃,为了不这种潜在的崩溃风险,咱们能够采用多种方法“强制”它不越界,好比重写get方法进行内部判断,这里咱们用Runtime来作一个比较完全的解决。
(1)越界的状况:names数组有10个元素, 调用 self.names[10] ,崩溃 ;
这行代码的本质是: [self.names objectAtIndex:10],因此,咱们用运行时进行这个方法的交换。
(2)添加以下分类,后面出现数组访问越界的状况,将返回nil;
(3)由于是在load方法中实现的交换,因此,程序启动内存中加载了这个分类后,自动执行交换,不须要导入任何头文件了。
(4)核心代码
@implementation NSArray(Extension) + (void)load { Method otherMehtod = class_getInstanceMethod(class, otherSelector); Method originMehtod = class_getInstanceMethod(class, originSelector); // 交换2个方法的实现 method_exchangeImplementations(otherMehtod, originMehtod); } - (id)new_ObjectAtIndex:(NSUInteger)index { if (index < self.count) { return [self new_ObjectAtIndex:index]; } else { return nil; } } @end
八、自动显示“空”的tableView
当tableView的数据源为空时,咱们通常会将tableView隐藏,同时贴上去一个“没有加载到内容”之类的提示视图;或者编写一个cell来显示“空”,来给用户一个友善的交互提示。这样写能够,可是比较麻烦,如今用运行时,直接给tableView添加特性,能够自动判断数据源是否为空,而且展现出用户想要展现的“空”视图。
@interface UIScrollView (YWEmptyView) /** 空页面 ,开发者自定义*/ @property (nonatomic, strong) UIView *emptyView; @end
#import "UIScrollView+YWEmptyView.h" #import <objc/runtime.h> static const char emptyKey; @implementation UIScrollView (YWEmptyView) - (UIView *)emptyView { return objc_getAssociatedObject(self, &emptyKey); } - (void)setEmptyView:(UIView *)emptyView { objc_setAssociatedObject(self, &emptyKey, emptyView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self addSubview:self.emptyView]; } //获取当前数据源数量 - (NSInteger)getTotalCount { NSInteger totalCount = 0; if ([self isKindOfClass:[UITableView class]]) { UITableView *tableView = (UITableView *)self; for (NSInteger section = 0; section < tableView.numberOfSections; section++) { totalCount += [tableView numberOfRowsInSection:section]; } } else if ([self isKindOfClass:[UICollectionView class]]) { UICollectionView *collectionView = (UICollectionView *)self; for (NSInteger section = 0; section < collectionView.numberOfSections; section++) { totalCount += [collectionView numberOfItemsInSection:section]; } } return totalCount; }
//判断是否显示“空”视图 - (void)showEmptyView { [self bringSubviewToFront:self.emptyView]; if ([self getTotalCount] > 0) { self.emptyView.hidden = YES; }else { self.emptyView.hidden = NO; } } @end @implementation UITableView (YWEmptyView) + (void)load { SEL reloadSEL = @selector(reloadData); SEL shareReloadSEL = @selector(shareReloadData); Method reloadData = class_getInstanceMethod(self, reloadSEL); Method shareReloadData = class_getInstanceMethod(self, shareReloadSEL); BOOL success = class_addMethod(self, reloadSEL, method_getImplementation(shareReloadData), method_getTypeEncoding(shareReloadData)); if (success) { class_replaceMethod(self, shareReloadSEL, method_getImplementation(reloadData), method_getTypeEncoding(reloadData)); } else { method_exchangeImplementations(reloadData, shareReloadData); } }
//新的刷新方法,和原来的reload进行交换 - (void)shareReloadData { [self shareReloadData]; [self showEmptyView]; } @end @implementation UICollectionView (YWEmptyView) + (void)load { Method reloadData = class_getInstanceMethod(self, @selector(reloadData)); Method shareReloadData = class_getInstanceMethod(self, @selector(shareReloadData)); method_exchangeImplementations(reloadData, shareReloadData); } - (void)shareReloadData { [self shareReloadData]; [self showEmptyView]; } @end
九、按钮的防暴力点击
就是给按钮设置防暴力点击的方法,原理:设置一个时间clickInterval和一个标记位ignoreClick,点击按钮的时候标记位被设置“YES”,以后任何点击事件再也不响应,直到过了clickInterval时长以后,还原标记位为“No”,而且按钮能够再次响应点击事件。
@interface UIControl (ClickRepeatedly) /** * 设置点击的间隔(防止反复点击) */ @property (nonatomic, assign)NSTimeInterval clickInterval; @property (nonatomic, assign)BOOL ignoreClick; @end
#import "UIControl+ClickRepeatedly.h" #import <objc/runtime.h> static const char *ClickIntervalKey; static const char *IgnoreClick; @implementation UIControl (ClickRepeatedly) - (void)setClickInterval:(NSTimeInterval)clickInterval{ objc_setAssociatedObject(self, &ClickIntervalKey, @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSTimeInterval)clickInterval{ return [objc_getAssociatedObject(self, &ClickIntervalKey) doubleValue]; } - (void)setIgnoreClick:(BOOL)ignoreClick{ objc_setAssociatedObject(self, &IgnoreClick, @(ignoreClick), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)ignoreClick{ return [objc_getAssociatedObject(self, &IgnoreClick) boolValue]; } + (void)load{ //替换点击事件 Method a = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method b = class_getInstanceMethod(self, @selector(rc_sendAction:to:forEvent:)); method_exchangeImplementations(a, b); } - (void)rc_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ if (self.ignoreClick) { return; } else{ [self rc_sendAction:action to:target forEvent:event]; } if (self.clickInterval > 0) { self.ignoreClick = YES; [self performSelector:@selector(setIgnoreClick:) withObject:@(NO) afterDelay:self.clickInterval]; } } @end
5、总结
一、Runtime是OC代码能够编译运行的关键,自己是纯C的函数库,它的存在赋予了OC动态特性。
二、Runtime提供的一系列方法,能够在程序运行时操做类以及它的方法和属性,使用Runtime进行代码构建,效率较高。
三、Runtime是底层运行机制,实际开发中咱们使用Runtime的时候并很少,可是关键时刻,仍是能很好的解决不少问题。
四、熟悉Runtime须要了解几项东西:
(1)OC的对象模型
(2)isa指针
(3)消息机制
五、Runtime水很深,各位请根据本身的水性选择区域。。。。。
参考:http://honglu.me/2014/12/29/浅谈OC运行时-RunTime/
https://www.ianisme.com/ios/2019.html?sukey=ecafc0a7cc4a741bfade6848774c88b7eeefdecd843c4a6c6f7da7fc65d2ed4ef4a188f7f68ab13e9ee80007d7ffb919