Runtime ----- 带你上道
在IOS开发和学习过程中,我们经常会接触到一个词: Runtime 。很多开发者对之既熟悉又陌生,基本都是浅尝辄止,达不到灵活使用的水平(话说开发中也确实不经常用。。)本文和大家一起研究一下,Runtime到底是什么,还有他的一些应用场景,毕竟Runtime是OC动态特性的核心,熟练掌握它可以帮助我们更好的控制类的属性及方法,编写出更高效的代码。
一、什么是Runtime
不管你之前如何理解的Runtime,先把他扔一边,我们从头梳理一下:
1、有一种大气而准确的说法 :
Objective-C是C语言的扩展,并加入了面向对象特性和Smalltalk式的消息传递机制。OC中的Runtime实现了将C语言转化为面向对象语言的作用,实际上我们的每一条OC代码的执行都会转换为Runtime的函数调用。Runtime是OC底层的实现,其函数的调用是高效的,基于Runtime的代码编写也是高效的!
2、核心:
是一个用C和汇编语言写的Runtime库(开源),这个库所做的事情就是加载类信息,进行方法的分发和转发,正是这个库赋予了Objective-C动态特性。
3、所谓的“动态特性” :
(1)我们比较下C++ 和OC,C++没有动态特性,编译时直接将代码转换为机器语言,而OC是在运行的时候,通过Runtime把程序转为可令计算机读懂的语言。两者都是对C进行了面向对象的扩展,但是实现机制不同。
(2)虽然RunTime赋予了OC动态特性,使得开发和使用变得相当灵活,但是归根结底OC还是一种编译型的语言,其具有一定的动态性,但是其动态特性也比不上JavaScript这种解释型的语言。
补充:
(3)编译型和解释型语言
- 编译型:先一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;与特定平台相关,一般无法移植到其他平台。
- 解释型:不需要事先编译,直接将源代码解释成机器码并立即执行,只要平台提供了相应的解释器即可运行该程序。每次运行时编译,运行效率低,但是只要有解释器,跨平台方便。
- JAVA的特殊:编译后(.class)运行在JVM上(解释器)。所以两种类型都属于,更倾向解释型。
4、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也维护着一个开源的版本,这两个版本之间都在努力的保持一致。
5、常见的简单使用场景,后面详细罗列:
(1)动态的创建、改变类
(2)动态的创建、改变、遍历属性
(3)动态的创建、改变、交互、遍历方法
扩展补充:
6、OC编译过程 :
- 编译器:Clang(前端)+ LLVM(后端),前端完成语法语义分析,生成中间文件(和机器无关)。后端先代码优化(和机器无关),再生成机器语言并优化代码(和机器相关)。
- bitcode:可以理解为上面说的前端输出产物“中间文件”。有独立的语法,和机器架构无关。在xcode7之后,允许在二进制后面嵌入bitcode(Archive打包),默认开启。
- 核心过程
<1> 预处理(Pre-process):把宏替换,删除注释,展开头文件,产生.i文件。
<2> 编译(Compliling):把之前的.i文件转换成汇编语言,产生.s文件。
<3> 汇编(Asembly):把汇编语言文件转换为机器码文件,产生.o文件(目标文件)。
<4> 链接(Link):对.o文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个.o文件进行 link)。
- 整体过程
<1>写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
<2>运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;
<3>编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
<4>链接文件:将项目中的多个可执行文件合并成一个文件;
<5>拷贝资源文件:将项目中的资源文件拷贝到目标包;
<6>编译 storyboard 文件:storyboard 文件也是会被编译的;
<7>链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
<8>编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
图片管理方式(Asset和直接拖拽到项目里)的区别:
Asset:
- 1、Xcode创建了图片组,使用图片可省略@2x的后缀写法,直接用图片名来加载图片
- 2、使用imageNamed方法,有缓存,name相当于key。这样可以减少I/O操作,但是内存会一直占用。
- 3、有一定安全性,图片打包后无法轻易直接打开
- 4、适合icon类型小图片
拖拽(resource):
- 1、图片名称是带有@2x扩展名的完整名称(没有图片组管理)。
- 2、在mainBundle里面,可以用imageWithContentsOfFile:path 加载,随着对象释放而释放,所以适合放大图
<9>运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
<10>生成 .app 包
<11>将 Swift 标准库拷贝到包中
<12>对包进行签名
<13>完成打包
二、OC中的对象模型
真正开始了解Runtime,有个基础工作需要做,就是我们要重温一下OC的对象和类的结构
1、打开<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”的缩写,它是由编译器生成的一个函数指针。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。
2、接下来打开<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
}
3、经典配图展示:oc对象继承模型
(1)上图的几个注意点
- 注意竖直方向和虚线方向代表的不同意义
- meta-class的isa指针指向的是root meta-class
- root meta-class的isa指针指向的是其本身
- root meta-class的super class指向的是root class(这里也是不明白意义所在。。)
- root class的super class 指向的是nil
(2)举个方法查找过程的例子:
调用respondsToSelector: 的时候,实例对象只需要根据其isa指针,找到其所属的class,然后遍历其methodLists,如果没有,那么根据这个类的super_class找到其父类,再看其父类是否能相应这个方法就可以了,直到super_class为nil时,就无法响应这个方法了,return NO。
(3)调用类方法的不同:
当我们使用类名调用类方法(+方法)时,只需要根据class的isa指针,找到其meta-class,然后通过meta-class的methodLists找到相应的方法既可(“类”是“元类”的对象)。
三、消息机制
1、OC中调用一个方法的本质就是在给对象发送消息,比如初始化一个NSObject对象:
NSObject *object = [[NSObject alloc] init];
事实上,在编译时这句话会翻译成一个C的函数调用,即:
objc_msgSend(objc_msgSend([NSObject?class],@selector(alloc)),@selector(init));
看看官方文档:
2、关于消息执行的时机问题:
(1)思考:就如上文所述,OC的代码翻译成C的函数调用之后,就是把OC代码转换成C代码了,那OC的动态特性体现在哪里?不就和C的静态特性一样了么?
(2)回答:对于C语言,函数的调用在编译的时候就会去决定调用哪个函数。而OC是一种动态语言,它会尽可能的把代码执行的决策从编译和链接的时候,推迟到运行时。给一个对象发送的一个消息并不会立即执行,而是在运行的时候再去寻找他对应的实现。这样就可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。
3、所有使用objc_msgSend函数,会执行以下步骤(也体现了objc_cache的作用)
(1)通过对象(类)的isa指针去找到他的class
(2)在class的method list 找到该消息的实现
(3)如果class中没有该消息的实现,就继续到它的super_class中去找
(4)一旦找到这个这个消息的实现,那么就去执行他的IMP(函数指针,代码所在空间)
4、常见的函数、头文件
(1) #import <objc/runtime.h> : 主要包括 成员变量、类、方法
- class_copyIvarList : 获得某个类内部的所有成员变量
- class_copyMethodList : 获得某个类内部的所有方法
- class_getInstanceMethod : 获得某个实例方法(对象方法,减号-开头)
- class_getClassMethod : 获得某个类方法(加号+开头)
- class_addMethod : 添加方法
- class_addProperty : 为类添加属性
- class_respondsToSelector: 类实例是否响应指定的selector
- class_setSuperclass: 调整一个类的继承关系
- class_replaceProperty:替换类的属性
- class_replaceMethod:替换类的方法
- ivar_getTypeEncoding : 获得成员变量的类型
- ivar_getName : 获得成员变量名
- method_exchangeImplementations : 交换2个方法的具体实现
(2) #import <objc/message.h> : 消息机制
- objc_msgSend(....)
四、RunTime的使用实例
RunTime可以很灵活的实现改变系统的方法及属性的效果,灵活度之大以至于也造成了一些隐患 ——— 破坏了系统的封装及代码的可读性,所以大家还是谨慎使用,也不要在开发过程中给队友挖坑(不要问我怎么知道的。。。)
下面搜罗了一些常用的场景:
1、动态创建一个类
#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;
}
2、关联对象(分类中动态添加成员变量)
(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,然后调用,来灵活的实现,使用起来也很方便,和添加属性类似)。
3、交换方法(可更改系统方法)
我们在开发过程中会遇到一种常见的错误:给数组元素赋值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
4、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
5、模型归档(遍历成员属性的应用)
(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;
}
6、字典和对象模型之间的转换,例如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);
}
}
- 补充:value for key 和 object for key的区别
1、两者都是键值对应。
2、valueforkey是KVC的方法,只允许使用NSString类型。
3、objectforkey是NSDictionary的方法,可以是任意类型。
4、如果key不是以@开头,则这两个方法是“等价”的。但如果key以@开头,valueForKey去掉@符号后剩下部分作为key值去执行方法(可能会crash)
7、避免数组越界
开发中,数组在访问时如果越界会造成崩溃,为了避免这种潜在的崩溃风险,我们可以采用多种方法“强制”它不越界,比如重写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
8、自动显示“空”的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 = ;
if ([self isKindOfClass:[UITableView class]]) {
UITableView *tableView = (UITableView *)self;
for (NSInteger section = ; section < tableView.numberOfSections; section++) {
totalCount += [tableView numberOfRowsInSection:section];
}
} else if ([self isKindOfClass:[UICollectionView class]]) {
UICollectionView *collectionView = (UICollectionView *)self;
for (NSInteger section = ; section < collectionView.numberOfSections; section++) {
totalCount += [collectionView numberOfItemsInSection:section];
}
}
return totalCount;
}
//判断是否显示“空”视图
- (void)showEmptyView {
[self bringSubviewToFront:self.emptyView];
if ([self getTotalCount] > ) {
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
9、按钮的防暴力点击
就是给按钮设置防暴力点击的方法,原理:设置一个时间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 > )
{
self.ignoreClick = YES;
[self performSelector:@selector(setIgnoreClick:) withObject:@(NO) afterDelay:self.clickInterval];
} } @end
五、总结
1、Runtime是OC代码可以编译运行的关键,本身是纯C的函数库,它的存在赋予了OC动态特性。
2、Runtime提供的一系列方法,可以在程序运行时操作类以及它的方法和属性,使用Runtime进行代码构建,效率较高。
3、Runtime是底层运行机制,实际开发中我们使用Runtime的时候并不多,但是关键时刻,还是能很好的解决很多问题。
4、熟悉Runtime需要了解几项东西:
(1)OC的对象模型
(2)isa指针
(3)消息机制
5、Runtime水很深,各位请根据自己的水性选择区域。。。。。
参考:http://honglu.me/2014/12/29/浅谈OC运行时-RunTime/
https://www.ianisme.com/ios/2019.html?sukey=ecafc0a7cc4a741bfade6848774c88b7eeefdecd843c4a6c6f7da7fc65d2ed4ef4a188f7f68ab13e9ee80007d7ffb919
Runtime ----- 带你上道的更多相关文章
- ajax方式提交带文件上传的表单,上传后不跳转
ajax方式提交带文件上传的表单 一般的表单都是通过ajax方式提交,所以碰到带文件上传的表单就比较麻烦.基本原理就是在页面增加一个隐藏iframe,然后通过ajax提交除文件之外的表单数据,在表单数 ...
- Yii2表单提交(带文件上传)
今天写一个php的表单提交接口,除了基本的字符串数据,还带文件上传,不用说前端form标签内应该有这些属性 <form enctype="multipart/form-data&quo ...
- C#中富文本编辑器Simditor带图片上传的全部过程(MVC架构的项目)
描述:最近c#项目中使用富文本编辑器Simditor,记录一下以便以后查看. 注:此项目是MVC架构的. 1.引用文件 项目中引用相应的css和js文件,注意顺序不能打乱,否则富文本编辑器不会正常显示 ...
- c#中富文本编辑器Simditor带图片上传的全部过程(项目不是mvc架构)
描述:最近c#项目中使用富文本编辑器Simditor,记录一下以便以后查看. 注:此项目不是MVC架构的. 1.引用文件 项目中引用相应的css和js文件,注意顺序不能打乱,否则富文本编辑器不会正常显 ...
- wangEditor - 轻量级web富文本编辑器(可带图片上传)
业务需求: 通过后台编辑文章和图片,上传到前端界面,展示新闻消息模块.这个时候,需要一款简洁的编辑器,百度编辑器是最常用的一种,但是功能太过于复杂,而wangEditor - 轻量级web富文本编辑器 ...
- SpringMVC使用MultipartFile文件上传,多文件上传,带参数上传
一.配置SpringMVC 二.单文件与多文件上传 三.多文件上传 四.带参数上传 一.配置SpringMVC 在spring.xml中配置: <!-- springmvc文件上传需要配置的节点 ...
- IIS301重定向:将不带www的域名跳转到带www上
首先你的域名有这两条解析记录 进入服务器IIS,添加2个站点,如下图 第一个正常绑定你的域名:www.baidu.com 第二个绑定不带www的域名:baidu.com 然后点开ncgd-no-www ...
- React+wangeditor+node富文本处理带图片上传
最近有个需求出现在我的视野中,因为我的另外的博客需要上传文章,但是我不想每次都在我的数据库中慢慢的修改格式,所以我另做了一个后台去编辑文本后发送给服务器,那么这里就涉及到两点,一个是富文本,一个是需要 ...
- netcore3.1 + vue (前后端分离) ElementUI多文件带参数上传
vue前端代码 前端主要使用了ElementUI的el-uploda插件,除去业务代码需要注意的是使用formdata存储片上传时所需的参数 <el-upload class="upl ...
随机推荐
- 学习Python一年,基础忘记了,看看面试题回忆回议,Python面试题No3
这边有几个面试题,好棒 第1题:你如何管理不同版本的代码? git,svn两个都要说到,github,码云也要提及,面试官想要的就是版本管理工具,你只要选择一个你熟悉的,疯狂的说一通就可以了,最好说一 ...
- stark组件之添加、修改页面内容搭建(七)
如何快速的进行数据的添加以及修改呢?modelform来实现是可以达到效果的,在这里就是应用了modelform,每一个表都不同,所以需要创建不同的modelform. def get_model_f ...
- 杭电 5363 求集合的非空子集中key的数量
Description soda has a set S with n integers {1,2,…,n}. A set is called key set if the sum of intege ...
- Python基础(十)re模块
Python基础阶段快到一段落,下面会陆续来介绍python面向对象的编程,今天主要是补充几个知识点,下面开始今天的内容. 一.反射 反射的作用就是列出对象的所有属性和方法,反射就是告诉我们,这个对象 ...
- selenium IDE断言设置实践
断言: 验证应用程序的状态是否同所期望的一致. 常见的断言包括:验证页面内容,如标题是否为X或当前位置是否正确等等. 断言被用于4种模式+5种手段: Assert Assert 断言失败时,该测试将终 ...
- Query on a string
You have two strings SS and TT in all capitals. Now an efficient program is required to maintain a o ...
- POJ 3041_Asteroids
题意: N*N网格中有小行星,光束能将一整行或者一整列的行星消灭,问消灭所有行星至少需要多少光束? 分析: 最小顶点覆盖问题,将每个小行星看成边,左右顶点为横纵坐标,可以转化为二分图,利用二分图中最小 ...
- 百度语音识别API初探
近期想做个东西把大段对话转成文字.用语音输入法太慢,所以想到看有没有现成的API,网上一搜,基本就是百度和讯飞. 这里先看百度的 笔者使用的是Java版本号的 下载地址:http://bos.nj.b ...
- JAVA 小程序之ATM
一个JAVA的小程序,主要要求有模块化编程的思想,能够把ATM中各个功能独立成为一个一个的方法. ATM主要功能有: 查询余额: 取款: 存款: 修改密码: 退出. 以上功能均由独立的方法给出,具体实 ...
- iOS音频播放 (二):AudioSession 转
原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/ 前言 本篇为<iOS音频播放>系列的第二篇. 在实施前一篇中所 ...