runtime

引言

对于从事 iOS 开发人员来说,太多数人都会说「 runtime 是运行时 」

什么情况下用 runtime ?大部分人能说出「 给分类动态添加属性 || 交换方法」

runtime – 运行时(iOS的黑魔法!!)

runtime是OC的底层实现,可以静心一些非常底层的操作(OC无法办到的)

一.runtime简介

二.消息机制<了解/熟悉>

2.1消息机制原理

2.2消息调用流程

三.常用开发场景

3.1 UITextField占位文字的颜色以及字体大小

3.2 给分类动态添加属性

四.面试须知

4.1 动态添加方法(开发中几乎不用,但面试须知)

4.2 使用runtime实现自动归档和解档(之前傻逼式的一个一个写,昨天看了hanK的公开课,顿时打开眼界,原来还能这么搞,大写的服)

4.3 Method Swizzling方法交换(俗称黑魔法,也有说法叫方法欺骗)

4.4 runtime 字典转模型(这个本文不会讲到,因为解析都是用第三方框架了,成熟且实用便利。MJExtension字典转模型实现也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来)

五.利用runtime进行实用性封装

5.1 UIAlertView的封装

5.2 Method Swizzling方法交换封装

5.3  自动归档和解档定义成宏

一. runtime简介:

runtime简称运行时,OC就是运行机制,也就是在运行时候的一些机制,其中最主要的是消息机制。

对于C语言,函数的调用在编译的时候回决定调用哪个函数

对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用

二.消息机制

2.1消息机制原理

方法调用本质:让对象发送消息

objc_msgSend只有对象才能发送消息,故以objc开头

使用消息机制,必须导入#import

runtime都有一个前缀,谁的事情使用谁

解决提示步骤

查找build setting-> 搜索msg->设为NO

查看生成runtime代码,首先终端切换到该目录回车后输入clang -rewrite-objc main.m

2.2 消息调用流程

- 1.通过isa去对应的类中查找

- 2.注册方法编号

- 3.根据方法编号去查找对应方法

- 4.找到只是最终函数实现地址,根据地址去方法区调用对应函数

runtime_第1张图片

消息机制原理图

/*************ViewController.m******************/-(void)setupobjc_msgSend{/*

objc_msgSend

<#id self#> :谁发送消息

<#SEL op, ...#>:发送什么消息

*///  id objc = objc_msgSend([NSObject class], @selector(alloc));//  objc = objc_msgSend(objc, @selector(init));//    OC: Person *p = [Person alloc];// 底层的实际写法Person  *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));// 调用对象方法(本质:让对象发送消息)//  OC:  p = [p init];//runtimeobjc_msgSend(p, sel_registerName("init"));//OC://    [p hungry];//    [p run:20];//runtimeobjc_msgSend(p,@selector(hungry));    objc_msgSend(p,@selector(run:),20);}Person.m文件-(void)run:(NSUInteger)m{NSLog(@"他跑了%zdM",m);}-(void)hungry{NSLog(@"他饿了");}

打印如下:

打印结果

三.常用开发场景

3.1 UITextField占位文字的颜色以及字体大小

[_textField setValue:kFont(14) forKeyPath:@"_placeholderLabel.font"];    [_textField setValue:kThemeColor forKeyPath:@"_placeholderLabel.textColor"];

3.2 给分类动态添加属性

注意:我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

原理:给一个类声明属性,其本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

需求:给系统 NSObject 类动态添加属性 name 字符串。

#import"NSObject+Person.h"#importstaticcharconst*constkName ="kName";@implementationNSObject(Person)- (void)setName:(NSString*)name{/*

    objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)


    <#id  _Nonnull object#>: 给哪个对象添加属性

    <#const void * _Nonnull key#>:属性名称

    <#id  _Nullable value#>:属性值

    <#objc_AssociationPolicy policy#>:保存策略

    **/objc_setAssociatedObject(self, kName, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);    }- (NSString*)name{//objc_getAssociatedObject:获取某个对象的值returnobjc_getAssociatedObject(self, kName);}/*************调用******************/NSObject*obj =[NSObjectnew];    obj.name =@"flowerflower";NSLog(@"我是分类obj中的%@",obj.name);/*************打印输出******************/我是分类obj中的flowerflower

小结:

属性赋值的本质:就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让name和NSObject产生关联,而runtime可以做到这一点。

四.面试须知

4.1 动态添加方法(开发中几乎不用,但面试须知)

需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。

案例代码

/*************UserModel.h******************/- (void)eat;- (void)run:(NSNumber*)m;/*************UserModel.m******************/@implementationUserModel// 没有返回值,1个参数// void,(id,SEL)voidaaa(idself, SEL _cmd,NSNumber*meter) {NSLog(@"跑了%@米", meter);}+ (BOOL)resolveInstanceMethod:(SEL)sel{if(sel ==NSSelectorFromString(@"run:")) {// 动态添加run方法// class: 给哪个类添加方法// SEL: 添加哪个方法,即添加方法的方法编号// IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))// type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmdclass_addMethod(self, sel, (IMP)aaa,"v@:@");returnYES;    }return[superresolveInstanceMethod:sel];}- (void)eat{NSLog(@"我吃了");}@end/*************ViewController.m******************/- (void)viewDidLoad {    [superviewDidLoad];    UserModel *user = [UserModel new];    [user performSelector:@selector(run:) withObject:@121];    [user eat];    objc_msgSend(user,@selector(eat));    objc_msgSend(user,@selector(run:),@1);}

runtime_第2张图片

图片.png

4.2 使用runtime实现自动归档和解档(之前傻逼式的一个一个写,昨天看了hanK的公开课,顿时打开眼界,原来还能这么搞,大写的服)

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。

runtime_第3张图片

图片.png

假设这里有100个属性,那是不是我们也只能把100个属性都给写一遍。看着HanK老师的公开课之后,我会觉得自己挺傻乎乎的,不管模型中再多属性,轻轻松松50行搞定。

首先要看个小示例

/*************UserModel.h******************/@interfaceUserModel:NSObject@property(nonatomic,copy)NSString*name;@property(nonatomic,assign)NSIntegerage;/*************UserModel.m******************/#import"UserModel.h"#import@interfaceUserModel()@property(nonatomic,copy)NSString*userId;@property(nonatomic,copy)NSString*phone;//注册手机号@end@implementationUserModel@end/*************调用******************/unsignedintcount =0;//成员变量的个数Ivar *ivarList =  class_copyIvarList(UserModel.class, &count);        Ivar ivar1 = ivarList[0];NSLog(@"%s",ivar_getName(ivar1));//_nameIvar ivar2 = ivarList[1];NSLog(@"%s",ivar_getName(ivar2));//_ageIvar ivar3 = ivarList[2];//  打印UserModel里面的成员属性的个数NSLog(@"%d",count);

图片.png

也就是说即使在.m声明的私有属性,也可以使用runtime获取到成员变量的属性

回归正题(使用runtime实现归档和接档,不管属性是10个,20个,甚至上100个)

/*************UserModel.h******************/@interfaceUserModel:NSObject//定义一堆属性  假设这里有22个属性    这里就不全部展示了。就简单写了几个@property(nonatomic,copy)NSString*userId;//用户Id@property(nonatomic,copy)NSString*phone;//注册手机号@property(nonatomic,copy)NSString*nickName;//昵称@property(nonatomic,copy)NSString*loginPSW;//登陆的md5密码@end/*************UserModel.m******************/#import"UserModel.h"#import@interfaceUserModel()@end@implementationUserModel- (void)encodeWithCoder:(NSCoder*)aCoder{/* 获取类中的ivar列表

    count:  count为ivar总数

    class_copyIvarList:  获取类的全部属性

    **/unsignedintcount =0;    Ivar *ivarList = class_copyIvarList(self.class, &count);for(inti  =0; i< count; i++) {//取出IvarIvar ivar = ivarList[i];//属性名称  ivar_getName:获取类实例成员变量,只能取到本类的,父类的访问不到NSString*key = [NSStringstringWithUTF8String:ivar_getName(ivar)];//归档  通过KVC取的 就没有int类型了[aCoder encodeObject:[selfvalueForKey:key] forKey:key];    }//但凡在C语言里面 看到New Creat Copy 都需要释放free(ivarList);//释放}- (instancetype)initWithCoder:(NSCoder*)coder{if(self= [superinit]) {unsignedintcount =0;        Ivar *ivarList = class_copyIvarList(self.class, &count);for(inti  =0; i< count; i++) {//取出IvarIvar ivar = ivarList[i];//属性名称NSString*key = [NSStringstringWithUTF8String:ivar_getName(ivar)];//解档    设置到成员变量上[selfsetValue: [coder decodeObjectForKey:key] forKey:key];        }        free(ivarList);//释放}returnself;}/*************ViewController******************///宏#define UserDataFilePath ([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"user.data"])- (void)viewDidLoad {    [superviewDidLoad];unsignedintcount =0;//成员变量的个数Ivar *ivarList =  class_copyIvarList(UserModel.class, &count);    Ivar ivar1 = ivarList[0];NSLog(@"%s",ivar_getName(ivar1));//_userIdIvar ivar2 = ivarList[1];NSLog(@"%s",ivar_getName(ivar2));//_phoneIvar ivar3 = ivarList[2];//  打印UserModel里面的成员属性的个数NSLog(@"%d",count);//打印结果:22}- (IBAction)save:(id)sender{    UserModel *model = [UserModel new];        model.nickName =@"flowerflower";        model.phone =@"1234567890";//归档[NSKeyedArchiverarchiveRootObject:model toFile:UserDataFilePath];}- (IBAction)read:(id)sender{  UserModel *model = [NSKeyedUnarchiverunarchiveObjectWithFile:UserDataFilePath];NSLog(@"%@----%@",model.nickName,model.phone);}

图片.png

4.3 方法交换(俗称黑魔法,也有说法叫方法欺骗)

系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能

简单粗暴方式: 继承系统的类,重写方法.

高端大气上档次的runtime:使用runtime,交换方法.

简单粗暴方式:

//Image继承于UIImage//重写系统方法+(UIImage*)imageNamed:(NSString*)name{UIImage*image =[superimageNamed:name];if(image) {NSLog(@"加载成功");    }else{NSLog(@"加载失败");    }returnimage;}//ViewController中导入头文件直接调用//有图片则加载成功,无图片则加载失败。UIImage*img = [Image imageNamed:@"brand_foreclock"];

高端大气上档次的runtime交换方法

UIImage+yhp_Image.m文件#import"UIImage+yhp_Image.h"#import@implementationUIImage(yhp_Image)// 把类加载进内存的时候调用,只会调用一次+(void)load{//交换方法Method imageNameMethod = class_getClassMethod(self,@selector(imageNamed:));    Method yhp_imageNameMethod = class_getClassMethod(self,@selector(yhp_imageNamed:));//交换方法地址,相当于交换实现方法method_exchangeImplementations(imageNameMethod, yhp_imageNameMethod);}//不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super. //注意:不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉+(UIImage*)yhp_imageNamed:(NSString*)name{// 图片UIImage*image = [UIImageyhp_imageNamed:name];if(image) {NSLog(@"加载成功");    }else{NSLog(@"加载失败");    }returnimage;}//ViewController不需要头文件UIImage*image = [UIImageimageNamed:@"1.peng"];

五.利用runtime进行实用性封装

5.1 UIAlertView的封装

正常写法

/**

使用步骤:

1.初始化

2.设置代理

3.实现代理方法

**/@interfaceViewController:UIViewController@end- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{UIAlertView*alert = [[UIAlertViewalloc] initWithTitle:@"AlertViewTest"message:@"message"delegate:selfcancelButtonTitle:@"取消"otherButtonTitles:@"确定",nil];      [alert show];  }#pragma marks -- UIAlertViewDelegate --  //根据被点击按钮的索引处理点击事件  -(void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex  {NSLog(@"clickButtonAtIndex:%d",buttonIndex);  }

使用runtime对UIAlertView进行封装

/*************UIAlertView+HHAddition.h******************/#importtypedefvoid(^AlertCallBack)(NSIntegerbuttonIndex);@interfaceUIAlertView(HHAddition)-(void)showAlertWithHandler:(AlertCallBack)callback;@end/*************UIAlertView+HHAddition.m******************/#import"UIAlertView+HHAddition.h"#importstaticcharkUIAlertViewBlockAddress;@implementationUIAlertView(HHAddition)-(void)showAlertWithHandler:(void(^)(NSInteger))callback{self.delegate =self;//为某个对象设置关联对象的值//第一个参数是主对象,第二个参数是键,第三个参数是关联的对象,第四个参数是存储策略:是枚举,定义了内存管理语义objc_setAssociatedObject(self, &kUIAlertViewBlockAddress, callback, OBJC_ASSOCIATION_COPY);    [selfshow];  }- (void)alertView:(UIAlertView*)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex{//根据给定的键从某对象中获取相应的关联对象值AlertCallBack alertCallBack = objc_getAssociatedObject(self, &kUIAlertViewBlockAddress);if(alertCallBack) {        alertCallBack(buttonIndex);        objc_setAssociatedObject(self, &kUIAlertViewBlockAddress,nil, OBJC_ASSOCIATION_COPY);    }}@end/*************调用******************/UIAlertView*alertView = [[UIAlertViewalloc] initWithTitle:@"是否联系客服"message:message delegate:selfcancelButtonTitle:@"取消"otherButtonTitles:@"确定",nil];    [alertView showAlertWithHandler:^(NSIntegerbuttonIndex) {NSLog(@"clickButtonAtIndex:%d",buttonIndex);      }];

5.2 Method Swizzling方法交换封装

/*************NSObject+Swizzling.h******************/#import@interfaceNSObject(Swizzling)+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector                        bySwizzledSelector:(SEL)swizzledSelector;@end/*************NSObject+Swizzling.m******************/#import"NSObject+Swizzling.h"@implementationNSObject(Swizzling)+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{    Classclass= [selfclass];//原有方法Method originalMethod = class_getInstanceMethod(class, originalSelector);//替换原有方法的新方法Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);/**

先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况.

class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。

也就是说如果class_addMethod返回YES,说明子类中没有方originalSelector,

通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。

*/BOOLdidAddMethod = class_addMethod(class,originalSelector,                                        method_getImplementation(swizzledMethod),                                        method_getTypeEncoding(swizzledMethod));if(didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMPclass_replaceMethod(class,swizzledSelector,                            method_getImplementation(originalMethod),                            method_getTypeEncoding(originalMethod));    }else{//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可method_exchangeImplementations(originalMethod, swizzledMethod);    }}@end

5.3  自动归档和解档定义成宏

两句代码搞定#import"UserModel.h"#import#define encodeRuntime(className) \\unsignedintcount =0;\Ivar *ivars = class_copyIvarList([Aclass], &count);\for(inti =0; i

你可能感兴趣的:(runtime)