runtime和RunLoop的使用

本文深入探讨Objective-C运行时机制(runtime),包括消息传递、方法交换、动态添加方法及属性等核心概念。同时,详细解析RunLoop的工作原理及其在iOS应用中的关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、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、自动释放池

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值