一、runtime的简介
RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。runtime一般是针对系统的类。
可以参照“runtime中文文档”
对于C语言,函数的调用在编译的时候会决定调用哪个函数。
对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
事实证明:
在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
在编译阶段,C语言调用未实现的函数就会报错。
二、runtime的作用
1、发送消息
方法调用的本质,就是通过runtime发送消息。
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现
objc_msgSend,只有对象才能发送消息,因此以objc开头.
oc代码最终会转换为C++,通过在终端输入“clang -rewrite-objc main.m ”查看最终生成代码
使用消息机制前提,必须导入#import <objc/message.h>
xcode6.0以后不推荐使用runtime,需要关闭编译器的消息检查,若不关闭则没有代码匹配提示,如下所示:
消息机制使用场景:1、调用私有方法
#import <Foundation/Foundation.h>
@interface Dog : NSObject
- (void)speak:(NSString *)str;
@end
#import "Dog.h"
@implementation Dog
- (void)run{
NSLog(@"狗跑了");
}
- (void)speak:(NSString *)str {
NSLog(@"狗说了:%@", str);
}
+ (void)see {
NSLog(@"狗狗在看门");
}
@end
通过runtime运行时调用对象方法,私有方法,类方法
#import "ViewController.h"
#import <objc/message.h>
#import "Dog.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//消息机制
Dog *g = objc_msgSend(objc_getClass("Dog"), sel_registerName("alloc"));
g = objc_msgSend(g, sel_registerName("init"));
//调用方法
objc_msgSend(g, @selector(run));
objc_msgSend(g, @selector(speak:), @"hello world!");
objc_msgSend([Dog class], @selector(see));
方法调用的流程:
对象方法保存在类对象的方法列表中,类方法保存在元类的方法列表中
1、通过对象的isa指针找到对应的类对象
2、把方法名转换为方法编号SEL
3、根据方法编号去查找对应方法的函数地址
4、根据函数地址去方法区调用方法
2、交换方法
经常使用
开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
重写系统的方法: 想给系统的方法添加额外的功能;系统的方法不需要。
方式一:继承系统的类,重写方法。注:最好不要在分类中重写父类的方法,会把父类的方法覆盖掉。
方式二:使用runtime,交换方法。
创建分类,并且交换方法,使得调用imageName时直接调用到了"zm_imageName:"方法
#import <UIKit/UIKit.h>
@interface UIImage (image)
@end
#import "UIImage+image.h"
#import <objc/message.h>
@implementation UIImage (image)
//把类加载到内存时会调用一次,只会调用一次
+ (void)load
{
//1、获取imageNamed类方法
Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
//2、获取分类扩展的方法
Method zmImageNameMethod = class_getClassMethod(self, @selector(zm_imageNamed:));
//3、开始交换方法
method_exchangeImplementations(imageNameMethod, zmImageNameMethod);
}
+ (UIImage *)zm_imageNamed:(NSString *)name {
UIImage *image = [UIImage zm_imageNamed:name];
if (image == nil) {
NSLog(@"找不到图片");
} else {
NSLog(@"加载图片成功");
}
return image;
}
@end
使用分类
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"1.png"];
}
3、动态添加方法
平时用的不是很多
开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表。可以使用动态给某个类添加方法解决,使得方法在用的时候才加载方法。oc都是懒加载机制,只要一个方法实现了,就会被添加到方法列表中。
#import "Person.h"
//导入运行时框架
#import <objc/message.h>
@implementation Person
//类方法
//+ (BOOL)resolveClassMethod:(SEL)sel{
//
//}
//要想实现动态添加方法,首先要实现如下方法
//调用:当一个方法没有实现,但是又调用了这个方法,系统会调用这个方法
//作用:知道类里面的那个方法没有实现,从而动态的添加方法
//@prama1 sel:表示没有实现的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"调用了没有实现的:%@", NSStringFromSelector(sel));
//动态添加方法
if(sel == @selector(eat:)){
/**
* @param1 cls: 给那个类添加方法
* @param2 name: 添加方法的方法编号是什么
* @param3 imp: 方法实现,函数入口,函数名称
* @param4 types: 方法的类型,需要查文档
*/
class_addMethod(self, sel, (IMP)eats, "v@:@");
}
return [super resolveInstanceMethod:sel];
}
@end
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
//动态添加方法
[p performSelector:@selector(eat:) withObject:@"好吃"];
//[p eat];
}
4.动态添加属性
原理:给一个类声明属性,其实本质就是给这个类添加关联外面的内存,并不是直接把这个值的内存空间添加到类存空间。
分类添加属性:因为在分类里面,@property只会生成setter和getter方法的声明,而没有实现,无法保存属性的值,所以需要通过runtime动态给分类添加属性。
使用的场景:当给系统的类添加属性的时候,可以使用runtime动态添加属性。
给一个NSOject动态添加属性:通过分类的方式。
#import <Foundation/Foundation.h>
@interface NSObject (property)
//@property只会生成getter/setter方法的声明和下划线的成员变量
@property NSString *nameStr;
@end
#import "NSObject+property.h"
#import <objc/message.h>
@implementation NSObject (property)
- (void)setNameStr:(NSString *)nameStr {
/** 让当前字符串和对象产生联系
* param1 给那个对象添加属性
* param2 属性名称
* param3 属性值
* param4 保存的策略
*/
objc_setAssociatedObject(self, @"nameStr", nameStr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)nameStr {
return objc_getAssociatedObject(self, @"nameStr");
}
@end
5.字典转模型
KVC的底层实现:遍历字典中所有的key,去模型中找对应的属性,并通过“setter”方法给属性赋值,如果找不到就报错。当报错时,重写“- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key”即可避免崩溃。
MJExtension的原理:通过runtime把模型中的属性都遍历出来,去字典中取出对应的value给模型的属性赋值。从而达到了,模型中需要多少就去字典中拿多少。
5.1、简单的字典转模型
#import <Foundation/Foundation.h>
@interface NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end
#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
//runtime:遍历模型中所有的属性名,去字典中查
+ (instancetype)modelWithDict:(NSDictionary *)dict{
//每个类里面有属性列表(数组)
id objc = [[self alloc] init];
//1.获取模型中的属性列表
/** 把所有的成员属性复制一份给你,避免修改系统的东西
* Ivar:表示成员变量(即带下划线的),不是属性
* Ivar *:表示指向一个ivar数组的指针
* cls:表示获取那个类的成员属性列表
* outCount:表示获取成员属性的总数
*/
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
//获取所有的属性,一般不用,会漏掉成员变量
//class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
//2.遍历模型中所有的成员属性
for (int i = 0 ; i < count; i++) {
//获取成员属性
Ivar ivar = ivarList[i];
//获取成员变量名称
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
//获取属性名称
//NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
//3.给模型的属性赋值
//value:字典的值
//key:属性名
//获取key,因为获取的成员变量有下划线
NSString *key = [propertyName substringFromIndex:1];
//获取字典中的value
id value = dict[@"key"];
if(value){
//通过kvc赋值不能传空
[objc setValue:value forKey:key];
}
}
return objc;
}
@end
5.2、字典转模型二级
#import <Foundation/Foundation.h>
@interface NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end
#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
// 获取类里面所有方法
// class_copyMethodList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)// 本质:创建谁的对象
// 获取类里面属性
// class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
// Ivar:成员变量 以下划线开头
// Property:属性
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
id objc = [[self alloc] init];
// runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 1.获取模型中所有成员变量 key
// 获取哪个类的成员变量
// count:成员变量个数
unsigned int count = 0;
// 获取成员变量数组
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 获取成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 获取key
NSString *key = [ivarName substringFromIndex:1];
// 去字典中查找对应value
// key:user value:NSDictionary
id value = dict[key];
// 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
// 并且是自定义对象才需要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型 userDict => User模型
// 转换成哪个模型
// 获取类
Class modelClass = NSClassFromString(ivarType);
value = [modelClass modelWithDict:value];
}
// 给模型中属性赋值
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
@end
二、runLoop运行循环
一、 runLoop的简介
1.runLoop的基本作用:
保持程序的持续运行
处理App中的各种事件(触摸事件、定时器事件、Selector事件)
节省CPU的资源,提高程序的性能(它能让主线程有事做事,没事休息)
2.main函数中的RunLoop
第14行代码的UIApplicationMain函数内部启动了一个runLoop,所以UIApplicationMain函数一直没有返回,保持了程序的持续运行。这个默认启动的runLoop是和主线程相关,runLoop主要处理主线程的事件。
一、 runLoop对象的使用
ios中提供了2套API来使用和访问runLoop:Foundation(NSRunLoop类)和Core Foundation(CFRunLoopRef)。
NSRunLoop是基于CFRunLoopRef的一层oc包装,CFRunLoopRef是开源的。
CFRunLoopRef资料:https://opensource.apple.com/source/CF/CF-1151.16
NSRunLoop文档:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
1.runLoop和线程的关系
每一个线程都有一个与之对应的runLoop对象;
主线程的runLoop系统已经创建好了,子线程的runLoop需要手动创建。
runLoop在第一次访问时创建,在线程结束时销毁
2.获得runLoop
- (void)viewDidLoad {
[super viewDidLoad];
//方式一:
CFRunLoopRef runloop1 = CFRunLoopGetMain();
CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
//方式二:
//1.获得当前线程的runLoop对象
[NSRunLoop currentRunLoop];
//2.获得主线程的runLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop];
runloop.getCFRunLoop 转化为 CFRunLoopRef类型
//3.为子线程创建runLoop对象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run{ //runLoop是懒加载的,获取一次就会创建当前线程的runloop。
[NSRunLoop currentRunLoop];
NSLog(@"*******");
}
3.runLoop相关的类
Core Foundation中关于Runloop的5个类:
CFRunLoopRef:
CFRunLoopModeRef:线程的运行模式
CFRunLoopSourceRef:事件源
CFRunLoopTimerRef:定时器
CFRunLoopObserverRef
注意:如果runLoop中没有这红色的四个类,则它会立即结束,不会跑圈,因为runLoop是事件驱动的。
3.1、CFRunLoopModeRef
3.1.1、CFRunLoopModeRef代表RunLoop的运行模式:
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
如果需要切换Mode,只能退出Loop(线程),再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
3.1.2、系统默认注册了5个Mode:
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行,比较常用
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响,比较常用
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode,表示凡是标记为kCFRunLoopCommonModes模式的runloop会工作
3.2、CFRunLoopSourceRef
CFRunLoopSourceRef是事件源(输入源),分别为:
Source0:非基于Port的,用于用户主动触发的事件,如按钮的点击事件
Source1:基于Port的,通过内核和其它线程相互发送消息
3.3、CFRunLoopTimerRef是定时器事件,基于时间的触发器
基本上说的就是NSTimer,它会受到runloop的mode的影响,若NSTimer所在的地方遇到UI界面有UITextView拖动的情况,NSTimer会不准确,需要进行如下设置。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//把定时器添加到当前runloop中,并设置该runloop的运行模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)run {
NSLog(@"-----------");
}
GCD的定时器不受Runloop的mode的影响,非常精准
@interface ViewController ()
//必须把GCD定时器进行强引用,否则不会循环调用方法
@property (nonatomic, strong)dispatch_source_t timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1、创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//2、创建GCD定时器
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//3、设置定时器的开始时间、间隔时间、精准度
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//4、定时器调用的方法
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"----*----");
});
//5、调用
dispatch_resume(_timer);
}
3.4、CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
它可以监听的时间点有以下几个:
即将进入Loop
即将处理Timer
即将处理Source
即将进入休眠
刚从休眠中唤醒
即将退出Loop
- (void)viewDidLoad {
[super viewDidLoad];
// 1、创建observer
/* 参数1: 分配存储空间
* 参数2: 要监听的状态,kCFRunLoopAllActivities监听所有状态
* 参数3: 是否要持续监听
* 参数4: 优先级
* 参数4: 回调
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即将进入Loop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@" 即将处理Timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理Source");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将进入休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"刚从休眠中唤醒");
break;
case kCFRunLoopExit:
NSLog(@"即将退出Loop");
break;
default:
break;
}
});
// 2、给runLoop添加一个监听者
/* 参数1: 要监听那个runloop
* 参数2: 监听者
* 参数3: 要监听runloop在那种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 3、释放Observer
CFRelease(observer);
}
4、runloop的应用
4.1、NSTimer
4.2、ImageView显示
4.3、PerformSelector
//让UIImageView在UI被拖拽时,不加载图片,避免UI界面卡顿
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode, UITrackingRunLoopMode]];
4.4、常驻线程,让线程一直运行
- (void)viewDidLoad {
[super viewDidLoad];
//一个线程只能执行第一封装的任务,该任务执行完了,则无法再在该线程中添加任务,若要让它能再次执行任务,需要runloop。
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(show) object:nil];
[thread start];
}
- (void)show {
NSLog(@"让该方法一直运行!");
//1、给子线程的runloop添加source,让它不要退出
//注:每一个runloop必须要有一个source 或者 timer
//[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(texts) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//1、创建子线程的runloop,并开启runloop
[[NSRunLoop currentRunLoop] run];
}
- (void)texts {
}
4.5、自动释放池