目录:
- Runtime简介
- runtime实战应用
- 代码一:OC代码对象调用代码 -> 消息发送机制代码 的转换
- 代码二:验证OC底层实现
- 代码三:Runtime项目中的实用
- 代码四:归档解档
- 代码五:用runtime解耦取消依赖
- 代码六:利用 runtime 一键改变字体
- 代码七:用runtime-关联对象,使用Category添加属性
- 代码八:利用Runtime减少应用崩溃-例如数组越界
- 代码九:iOS-UIButton同时点击&&重复点击规避
- 代码十:iOS利用Runtime自定义控制器POP手势动画
- 代码十一:利用runtime对一些常用并且容易导致崩溃的方法进行处理
零、demo地址
https://github.com/lionsom/LXRunTimeAll
一、Runtime简介
- OC运用的是消息发送机制
- OC代码底层变成了C语言运行时代码
- OC编译的时候没有加载进内存
Runtime:就是苹果提供的一个API
1、利用Runtime,在程序运行过程中,动态的创建一个类
2、利用Runtime,在程序运行过程中,动态的修改一个类的属性、方法
3、遍历一个类所有的成员变量
头文件:
<obj/message.h> 包含了runtime的API
<obj/runtime.h>
两个概念
1、Method:成员方法
2、Ivar:成员变量
二、runtime实战应用
代码一:OC代码对象调用代码 -> 消息发送机制代码 的转换
第一步:先将项目对消息发送机制的检测给关了
第二步:导入“消息发送机制”头文件
#import <objc/message.h>
第三步:代码
由一个简单的对象调用函数,逐步进行“消息发送机制”代码的替换
- (void)viewDidLoad {
[super viewDidLoad];
//方式一:OC标准写法
// Person * p = [[Person alloc]init];
//方式二:进行拆分
// Person * p =[Person alloc];
// p = [p init];
//方式三:拆分后,逐步采用消息发送机制进行替换 此步骤之后可完全不用导入头文件也可以进行Person的调用
//NSClassFromString(@"Person") == [Person class] == objc_getRequiredClass("Person")
NSObject * p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));
objc_msgSend(p, @selector(init));
//方式四: 最后组合
// NSObject * p = objc_msgSend(objc_msgSend(NSClassFromString(@"Person"), @selector(alloc)),@selector(init));
//方式一:标准的OC对象调用函数
// [p eat];
//方式二:
// [p performSelector:@selector(eat)];
//方式三:调用消息发送机制
//消息发送机制
objc_msgSend(p, @selector(eat));
}
代码二:验证OC底层实现
第一步:创建命令行项目
第二步:创建一个Person类
第三步:打开main.m函数,将Person添加到里面
第四步:进入终端,进入该目录下,将mian.m转成C语言文件
clang -rewrite-objc的作用是把oc代码转写成c/c++代码
clang -rewrite-objc main.m
执行之后,目录下多出一个main.cpp文件
第五步:打开main.cpp到最后,进行OC代码与C语言代码的比较
代码三:Runtime项目中的实用
案例:因为NSURL出现汉字后,url为空,但是又不会报错
//目的:给系统NSURL这个类的URLWithString 方法添加一个功能,创建URL又能判断是否为空
NSURL * url = [NSURL URLWithString:@"www.baidu.com/你好"];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
NSLog(@"AAA == %@",url);
- 解决方案一:使用Category换一个
+(instancetype)LX_URLWithString:(NSString *)URLString {
NSURL * url = [NSURL LX_URLWithString:URLString];
if (url == nil) {
NSLog(@"url为空");
}
return url;
}
然后调用新的方法,新方法内部加上判断
NSURL * url = [NSURL LX_URLWithString:@"www.baidu.com/你好"];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
NSLog(@"AAA == %@",url);
弊端:项目较大的时候,要讲全局都替换,很麻烦,并且代码不好维护,其他人也不知道这个函数。
- 解决方案二:利用runtime动态进行函数实现交换
第一步:为什么使用runtime??
优点:不需要进行太大的修改,而且继续使用原生方法名,只不过方法实现改变了!
弊端:要进行及时的注释,不然很容易忘记,出错
第二步:原理
runtime:可以交换方法的实现!!
第三步:代码实现
在Category刚刚加载的时候就需要对函数方法进行交换
1、导入头文件#import <objc/runtime.h>
2、在load函数中进行函数实现的交换
3、拿到这两个方法
- class_getClassMethod 获取类方法
- class_getInstanceMethod 获取对象方法
4、交换方法
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)
+(void)load {
NSLog(@"%s",__func__);
//交换我们的URLWithString和LX_URLWithString方法
//第一步:拿到这两个方法
//class_getClassMethod 获取类方法
//class_getInstanceMethod 获取对象方法
Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
Method LXURLWithStr = class_getClassMethod(self, @selector(LX_URLWithString:));
//
第二步:交换方法
method_exchangeImplementations(URLWithStr, LXURLWithStr);
}
//重新创建一个
+(instancetype)LX_URLWithString:(NSString *)URLString {
NSURL * url = [NSURL LX_URLWithString:URLString];
if (url == nil) {
NSLog(@"url为空");
}
return url;
}
@end
代码四:归档解档
其他的参考文档://www.greatytc.com/p/fed1dcb1ac9f
问题引入:在iOS中一个自定义对象是无法直接存入到文件中的,必须先转化成二进制流才行。从对象到二进制数据的过程我们一般称为对象的序列化(Serialization),也称为归档(Archive)。同理,从二进制数据到对象的过程一般称为反序列化或者反归档。在序列化实现中不可避免的需要实现NSCoding以及NSCopying(非必须)协议的以下方法:
- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;
- (id)copyWithZone:(NSZone *)zone;
假设我们现在需要对直接继承自NSObject的Person类进行序列化,代码一般长这样子://对变量编码
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.name forKey:@"name"];
[coder encodeint:self.age forKey:@"age"];
[coder encodeObject:_father forKey:@"_father"];
//... ... other instance variables
}
//对变量解码
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
self.name = [coder decodeObjectForKey:@"name"];
self.age = [coder decodeintForKey:@"age"];
_father = [coder decodeObjectForKey:@"_father"];
//... ... other instance variables
}
return self;
}
弊端:
- 工程代码中冗余代码很多
- 父类层级复杂容易导致遗漏点一些父类中的属性变量
解决方案:Runtime
第一步:使用Runtime获取变量以及属性
runtime中获取某类的所有变量(属性变量以及实例变量)API:
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
获取某类的所有属性变量API:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
Ivar是runtime对于变量的定义,本质是一个结构体:
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
#ifdef __LP64__
int space;
#endif
}
typedef struct objc_ivar *Ivar;
- ivar_name:变量名,对于一个给定的Ivar,可以通过*const char ivar_getName(Ivar v)函数获得 char * 类型的变量名;
- ivar_type: 变量类型,在runtime中变量类型用字符串表示,例如用@表示id类型,用i表示int类型...。这不在本文讨论之列。类似地,可以通过*const char ivar_getTypeEncoding(Ivar v)函数获得变量类型;
- ivar_offset: 基地址偏移字节数,可以不用理会
获取所有变量的代码一般长这样子:
unsigned int numIvars; //成员变量个数
Ivar *vars = class_copyIvarList(NSClassFromString(@"UIView"), &numIvars);
NSString *key=nil;
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = vars[i];
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)]; //获取成员变量的名字
NSLog(@"variable name :%@", key);
key = [NSString stringWithUTF8String:ivar_getTypeEncoding(thisIvar)]; //获取成员变量的数据类型
NSLog(@"variable type :%@", key);
}
free(vars);//记得释放掉
objc_property_t是runtime对于属性变量的定义,本质上也是一个结构体(事实上OC是对C的封装,大多数类型的本质都是C结构体)。在runtime.h头文件中只有*typedef struct objc_property objc_property_t,并没有更详细的结构体介绍。虽然runtime的源码是开源的,但这里并不打算深入介绍,这并不影响我们今天的主题。与Ivar的应用同理,获取类的属性变量的代码一般长这样子:
unsigned int outCount, I;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [[[NSString alloc] initWithCString:property_getName(property)] ;
NSLog(@"property name:%@", propertyName);
}
free(properties);
最后代码:
#import "Person.h"
//第一步:导入runtime头文件
#import <objc/runtime.h>
@implementation Person
//解档
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
/*
unsigned int count1 = 0;
objc_property_t * objArr = class_copyPropertyList([Person class], &count1);
for (int j = 0; j<count1; j++) {
const char * name1 = property_getName(objArr[j]);
NSString * key1 = [NSString stringWithUTF8String:name1];
NSLog(@"AAAAAA == %@",key1);
}
*/
//如果属性过多,这样写就比较麻烦
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
//for 搞定
for (int i = 0; i < count; i++) {
//拿个每一个ivar
Ivar ivar = ivars[I];
//ivar对应的名称
const char * name = ivar_getName(ivar);
//转成 OC
NSString * key = [NSString stringWithUTF8String:name];
//解档
id value = [coder decodeObjectForKey:key];
//设置到属性 -- KVC
[self setValue:value forKey:key];
NSLog(@"BBBBB == %@",key);
}
//这个变量不归OC管理,ARC处理不了,所以需要手动释放
free(ivars);
}
return self;
}
//告诉系统需要归档的属性
- (void)encodeWithCoder:(NSCoder *)coder
{
//如果属性过多,这样写就比较麻烦
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
//for 搞定
for (int i = 0; i < count; i++) {
//拿个每一个ivar
Ivar ivar = ivars[I];
//ivar对应的名称
const char * name = ivar_getName(ivar);
//转成 OC
NSString * key = [NSString stringWithUTF8String:name];
//获取属性值 -- KVC
id value = [self valueForKey:key];
//归档
[coder encodeObject:value forKey:key];
}
//这个变量不归OC管理,ARC处理不了,所以需要手动释放
free(ivars);
}
@end
更新20180104
代码五:用runtime解耦取消依赖
+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
Class cls = NSClassFromString(@"BookDetailComponent");
return [cls performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"bookId":bookId}];
}
更新20180123
代码六:利用 runtime 一键改变字体
思路 :写一个UILabel+FontChange
的扩展,在load函数中进行系统函数
与自定义函数
的交换,从而达到目的。
如果只是针对个别UILabel做特殊处理的话,可以使用Label的Tag
来进行区分!!!
+ (void)load {
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获得viewController的生命周期方法的selector
SEL systemSel = @selector(willMoveToSuperview:);
//自己实现的将要被交换的方法的selector
SEL swizzSel = @selector(myWillMoveToSuperview:);
//两个方法的Method
Method systemMethod = class_getInstanceMethod([self class], systemSel);
Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
if (isAdd) {
//如果成功,说明类中不存在这个方法的实现
//将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
} else {
//否则,交换两个方法的实现
method_exchangeImplementations(systemMethod, swizzMethod);
}
});
}
- (void)myWillMoveToSuperview:(UIView *)newSuperview {
[self myWillMoveToSuperview:newSuperview];
if ([self isKindOfClass:NSClassFromString(@"UIButtonLabel")]) {
return;
}
if (self) {
if (self.tag == 10086) {
self.font = [UIFont systemFontOfSize:self.font.pointSize];
} else {
if ([UIFont fontNamesForFamilyName:CustomFontName])
self.font = [UIFont fontWithName:CustomFontName size:self.font.pointSize];
}
}
}
更新20180126
代码七:用runtime-关联对象,使用Category添加属性
了解OC的都应该知道,在一般情况下,我们是不能向Category中添加属性的,只能添加方法,但有些情况向,我们确实需要向Category中添加属性,而且很多系统的API也有一些在Category添加属性的情况,例如我们属性的UITableView
的section
和row
属性,就是定义在一个名为NSIndexPath
的分类里的,如下
那这到底是怎么实现的呢?
更新20180201
代码八:利用Runtime减少应用崩溃-例如数组越界
我们首先把__NSArrayI
的objectAtIndex
方法换成我们的ls_objectAtIndex
,然后方法里面判断但是否越界,是的话直接返回nil
:
[NSClassFromString(@"__NSArrayI") swapMethod:@selector(objectAtIndex:) currentMethod:@selector(ls_objectAtIndex:)];
- (id)ls_objectAtIndex:(NSUInteger)index
{
if (index >= [self count])
{
return nil;
}
return [self ls_objectAtIndex:index];
}
然后当我们想下面这样写的时候就不会崩溃了:
NSArray *array = @[@"aa",@"ddd"];
array[5];
更新20180211
代码九:iOS-UIButton同时点击&&重复点击规避
更新20180212
代码十:iOS利用Runtime自定义控制器POP手势动画
更新20180312
代码十一:利用runtime对一些常用并且容易导致崩溃的方法进行处理