一、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、自動釋放池