runtime和RunLoop的使用

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章