理解Runtime和RunLoop

Posted by lingjye on June 16, 2018

Runtime

Objective-C运行时是一个运行时库,它提供对Objective-C语言的动态属性的支持,它会尽可能地将许多决定从编译期推迟到运行时。这意味着Objective-C不仅需要编译器,还需要运行时系统来执行编译代码。

Runtime是由C和汇编写成,它实现了Objective-C到C的转化,即面向对象到面向过程的转化。

运行时应用

  • 发送消息
  • 动态方法解析
  • 消息转发

参考Objective-C消息传递与消息转发机制

  • KVO

参考Demo

  • 属性关联

由于某些原因,有时我们不能直接向一个类中添加属性,例如第三方SDK,但是可以通过像其类别(Category)中添加属性关联来达到目的。通常使用的方法为:

1
2
3
4
5
6
7
8
9
10
11
12
// 设置关联
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy);

// 获取关联对象
id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);

// 删除关联
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object);
                         
  • 方法交换

通常采用AOP方式,来hook原来的方法,达到某种效果。例如在不侵入原工程代码的情况下集成友盟统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(analysis_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 添加方法
        BOOL didAddMethod = class_addMethod(class, swizzledMethod, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        // 判断是否已存在该方法
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
			// 直接交换方法的实现
			method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void) analysis_viewWillAppear:(BOOL)animated {
	// 调用该方法自身,实际上是调用交换前的方法
    [self analysis_viewWillAppear:animated];
    // TODO analysis method
    // ...
}

  • 声明属性

使用的Property属性描述符:

1
typedef struct objc_property * Property;

使用class_copyPropertyListprotocol_copyPropertyList获取一个和协议中属性列表,函数原型:

1
2
objc_property_t * class_copyPropertyList(Class cls,unsigned int * outCount)
objc_property_t * protocol_copyPropertyList(Protocol * proto,unsigned int * outCount)

获取属性列表示例:

1
2
3
id LenderClass = objc_getClass(“Lender”);
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList(LenderClass,&outCount);

使用property_getName获取属性名称:

1
2
const char * property_getName(objc_property_t property)

使用class_getPropertyprotocol_getProperty分别获取类和协议中给定名称的属性:

1
2
objc_property_t class_getProperty(Class cls,const char * name)
objc_property_t protocol_getProperty(Protocol * proto,const char * name,BOOL isRequiredProperty,BOOL isInstanceProperty)

使用property_getAttributes获取属性的类型字符串。它返回objc_property_attribute_t结构体列表,包含了name和value。 参考官方文档

查看Lender类的属性列表:

1
2
3
4
5
6
7
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
  • 归档解档

归档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    //  获取实例变量的列表
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i ++) {
        Ivar ivar = ivars[i];
        //  获取实例变量的名字,C字符串
        const char *name = ivar_getName(ivar);
        //  将C字符串转化为NSString类型
        NSString *nameStr = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
        //  利用KVC取出属性对应的值
        id value = [self valueForKey:nameStr];
        //  实现归档
        [aCoder encodeObject:value forKey:nameStr];
    }
    // 释放,不要忘记他们是Copy来的
    free(ivars);
}

解档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i ++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
            id value = [aDecoder decodeObjectForKey:key];
           //  通过KVC来设置值
            [self setValue:value forKey:key];
        }

        free(ivars);
    }
    return self;
}

Runtime官方文档

RunLoop

RunLoop即运行循环,用来对程序运行期间的的输入源进行调度处理。基于RunLoop可以用来监控卡顿以及节省CPU资源,提升程序性能。通过RunLoop的调度,使得线程在无事件处理时进入休眠,有时间处理事保持活跃状态。主线程默认携带一个mainRunLoop,由UIApplicationMain函数开启,在处理大量占用CPU的任务时,可以将其放到空闲的RunLoop模式中执行,以免阻塞。

RunLoop在iOS中由CFRunLoop实现RunLoop对象。Foundation中的NSRunLoop,实际上是基于CoreFoundation中的CFRunLoopRef的封装。

RunLoop模式:

  • NSDefaultRunLoopMode:默认Mode,空闲状态;
  • NSRunLoopCommonModes:Mode集合;
  • UITrackingRunLoopMode:ScrollView滑动时的模式,此时基于NSTimer的定时器不执行;
  • UIInitializationRunLoopMode:启动时进入的第一个Mode,之后会切换到默认Mode;
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

可以使用[NSRunLoop currentRunLoop]来获取当前的RunLoop,scrollView滑动式,基于NSTimer的定时器不执行可以将当前RunLoop模式切换到NSRunLoopCommonModes来解决。如下:

1
2
3
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:timer forMode:NSRunLoopCommonModes];

相对应的获取方法还有:

1
2
3
4
NSRunLoop *mainLoop = [NSRunLoop mainRunLoop];
// CoreFoundation
CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();
CFRunLoopRef mainLoopRef = CFRunLoopGetMain();

CFRunLoopSource

  • source0:触摸事件,如UIEvent,CFSocket这样的事件。
  • source1:基于Port的线程间通信,Mach port驱动,CFMachport,CFMessagePort等。

CFRunLoopObserver

在RunLoop运行过程中,有六种状态:

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 进入 loop
    kCFRunLoopBeforeTimers , // 触发 Timer 回调
    kCFRunLoopBeforeSources , // 触发 Source0 回调
    kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
    kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
    kCFRunLoopExit , //  loop退出
    kCFRunLoopAllActivities  // loop 所有状态
}

流程:

  • 通知Observers: 进入Runloop。
  • 开启do while来保活线程,通知Observers:RunLoop将要处理Timer回调,Source0回调,执行添加的相应Block。
  • 然后如果有Source1需要处理,则处理Source1事件。
  • 回调执行后,通知Observers:RunLoop的线程将要进入休眠。
  • 进入休眠后等待mach_port消息,如果有新消息,则RunLoop再次唤醒。
  • RunLoop线程被唤醒,然后开始处理消息(Timer,dispatch,source1)等事件,及block
  • 根据当前RunLoop的状态来判断是否需要走下一个Loop,当外部强制终止(用户退出,线程终止等)或超时,退出loop。

应用

  • AFNetworking 2.x 中的常驻线程
  • 卡顿检测及处理
  • 程序崩溃检测
  • tableView图片延迟加载