iOS runtime详解

简介

objective-c(简写objc)属于动态语言,不像C语言一样静态编译期,就确定了调用方法的指针,而objc所谓的方法调用只是消息的发送,如下:

[recevier message];
//转换为
objc_msgSend(recevier, selector);
//如果存在参数如下:
objc_msgSend(recevier, selector, arg1, arg2, ...);

因此具体的调用的函数指针是在运行期确定的,并且在此期间还可以动态修改最终调用的函数指针位置,如isa-swizzlingmethod-swizzling技术,或者若未存在此方法,可以动态的添加方法;

另外,objc源码是开源的,且几乎全部使用C语言实现(有些使用了汇编实现),可以从苹果开源官方网站获取此代码。

与runtime交互

objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码

大部分都是编写objc代码,其他都交由runtime系统来后台执行具体的操作,如[recevier message]转为调用objc_msgSend方法来执行;

NSObject类

Cocoa中大多数类都继承自NSObject类(NSProxy类是个例外,它是一个抽象超类,来充当其他对象或尚不存在的对象的替代者),自然也继承了其方法,如description方法可重载实现定义类描述;还提供了一些运行时获取类信息并检查一些特性:如class获取类对象; isMemberOfClass检查类对象是否在给定类的实例,isKindOfClass检查类实例是否为给定类或者类的继承类实例;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime函数

runtime系统是一个由一系列数据结构和函数组成,具有公共接口的动态共享库。其构成了NSObject类的基础,大部分还是使用更上层的接口编程,一般会用在hook接口或者与其他语言桥接等场景;

消息

objc方法调用是以消息发送的形式传递的并获取到相应的函数指针,从而实现函数的直接调用,具体使用的objc方法为:

void objc_msgSend(receiver, selector, arg1, ....);

其中发送调用时隐含了receiver=self, selector=_cmd,这两个参数是由编译器编译时自动添加上的;

而方法中的super关键词接收到消息时,编译器会创建一个objc_super的结构体,如下:

struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向selfid指针和class的SEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

划重点以上表明:

在同一对象中调用[self class][super class]返回的都是selfClass类isa指针

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

方法的调用流程

image.png
  • 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。

  • 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。

  • 如果上面两个都过了,那就通过获取self实例对象的isa获取类结构体(包含父类super_classcache缓存及方法列表);

  • 优先从cache缓存中查找,若查找到就跳转到对应的IMP函数指针去执行;

  • cache缓存中未找到,就去methodLists方法列表中查找;

  • 如果方法列表中未找到,就去super_class父类结构体中查找,一直找到NSObject类为止;

  • 如果还找不到就要开始进入消息转发流程了,后面会提到。

获取方法地址

直接获取到方法地址(IMP函数指针)可有效避免runtime消息发送流程,对于大量同函数调用的情况,可提升调用效率,但不太常见;

NSObject提供了通过idselector来获取对应IMP函数指针(包括实例对象或者类对象的函数指针)的方法:

- (IMP)methodForSelector:(SEL)aSelector;

该方法不属于objc的本身的特性,而为cocoa runtime的特性,查看源码实际为NSObject的类方法,通过class_getMethodImplementation方法获取,源码如下:

+ (IMP)instanceMethodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return class_getMethodImplementation(self, sel);
}

具体的IMP函数指针调用,需要转换为具体的函数指针类型,包括返回值类型,参数列表及其类型,如下:

IMP imp = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
//无返回值
void (*setter)(id, SEL, BOOL) = (void *)imp;
//存在返回值,则指定返回值类型即可
id (*setter)(id, SEL, BOOL) = (void *)imp;
//or
int (*setter)(id, SEL, BOOL) = (void *)imp;

消息转发

对于[objc message]消息发送调用形式,若找不到该方法,在编译期就会报错;若是通过[objc performSelector:@selectro(message)]形式,需要在运行期才能确定是否能响应该消息,若无法响应就会报找不到该实例方法,进而引发崩溃;

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[KVCTest test1]: unrecognized selector sent to instance 0x600000c4e8e0'
*** First throw call stack:
xxxx

因此通常会通过respondsToSelector:方法来判定是否能响应此消息,才触发消息的发送;

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

消息转发步骤如下:

  • 动态方法解析
  • 备用接收者
  • 完整转发


    image.png

动态方法解析

对象在缓存及方法列表中未找到相应的方法后,runtime首先会调用+ (BOOL)resolveInstanceMethod:(SEL)sel(实例对象)或者+ (BOOL)resolveClassMethod:(SEL)sel(类对象),若在此方法通过class_addMethod方法添加方法并且返回YES(或者在父类往上继承体系添加也可),则会调用添加的方法,具体的方法使用如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

其中class_addMethod涉及到类型编码(Type Encodings),其指定的是imp(id, SEL, ...)方法返回值+输入参数组合类型,为字符数组(每个对应一个字符);types变量第一个输入参数一定是id对象类型,则类型为@;第二个参数为SEL方法选择器类型,则为:;对于其他类型,具体见官网,不过runtime也提供了相应的获取类型编码的方法:

 const char * method_getTypeEncoding(Method m);

使用如下:

@property(nonatomic, copy) NSString *propertyName;
@property(nonatomic, copy, class) NSString *className;

@dynamic propertyName;
@dynamic className;

+ (NSString *)name {
    return  @"class test";
}

- (NSString *)description {
    return @"test";
}

//动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(propertyName)) {
//        const char *typeCode = method_getTypeEncoding(class_getInstanceMethod([self class], @selector(description)));
        const char *typeCode = "@@:";
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(description)), typeCode);
        return YES;
    } else if (sel == @selector(test1)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(test)), "i@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(className)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(name)), "@@:");
        return YES;
    }
    
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

@dynamic关键字在类的实现文件中修饰一个属性,表明系统不用自动生成setter/getter方法,由自己去动态实现;

注意:[self class] object_getClass(self) object_getClass([self class])的区别,先上源码:

+ (id)self {
    return (id)self;
}

- (id)self {
    return self;
}

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

self为实例对象时,[self class]返回的类对象,等价于object_getClass(self)object_getClass([self class])返回的是元类;

self为类对象时,[self class]返回的是self自身即类对象,且object_getClass(self)object_getClass([self class])等价,且都返回的是元类对象,不同于类对象;

动态方法解析,一般用于@dynamic属性,且需要指定已经实现了的处理方法,如上descriptionname

备用接收者

如果动态方法无法解析,runtime会继续调用如下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

方法解析:

如果对象实现(或者继承)此方法,并且返回非nil,则消息会转发至新的接收者;返回的对象不能为self,会导致无限循环;

如果在非根类对象实现此方法且未返回任何内容(即未指定新的接收者),应该调用[super forwardingTargetForSelector]将其转发给父类,以此往上继承调用;

该方法无法对消息进行处理,如操作消息的参数和返回值;

该方法适合将消息转发给能处理该消息的接收者,如实现多继承;

使用如下:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

完整消息转发

当动态方法不能解析且未转发给其他接收者时,runtime就启动完整消息转发机制:通过调用如下方法来转发给其他接收者,并且可以修改该消息;

- (void)forwardInvocation:(NSInvocation *)invocation;

方法解析如下:

NSObject对象实现了该方法,但只是调用doesNotRecognizeSelector:方法,若继承对象未实现该方法(前提是未动态解析方法或转发备用接收者),就会抛出异常;

该消息的唯一参数为NSInvocation类型的对象,该对象封装了原始的消息及消息参数;该参数来源于methodSignatureForSelector:方法返回的方法签名,该方法必须被重写,否则会抛出异常;

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
使用如下:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}

转发与多继承

image.png

objective-c只支持单继承,但是通过消息转发机制可以实现“多继承”的效果;如上图所示,WarriorDiplomat没有继承关系,但是Warriornegotiate消息转发给了Diplomat后,就好似DiplomatWarrior的超类一样;

注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

转发与继承

不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:isKindOfClass:只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector])
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;  
}

方法调配 Method Swizzling

Method Swizzling方法调配技术是苹果的“黑魔法”,可以不用继承或者重写方法,就可以修改类的方法实现;

常用的方法如下:

Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);

具体使用如下:

#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];         
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end

针对使用的答疑如下:

  • 为啥使用load方法

    +load方法是类初始加载时(应用启动就会加载)调用且只调用一次,load方法按照父类到子类,类自身到Category的顺序被调用,且若都实现了该方法,都会被调用;不同于+initialize方法,该方法是在第一次调用类方法或者实例方法是调用,有可能被调用多次(若子类未实现此方法或者子类调用[super initialize]都会导致被调用多次);

    注意:在方法中不能调用[super load],因此+load方法在父类、子类是被分别调用的,且存在顺序;如果调用,若父类已经交换IMP,导致又被交换回来,进而失效;

  • 是否需要dispatch_once

    +load方法系统会自动调用且只调用一次,但不保证被手动调用,为防止被多次调用并发问题,建议添加dispatch_once来保证唯一性;

  • 为啥调用class_addMethod,直接调用method_exchangeImplementations交换方法不就行了

    class_getInstanceMethod返回的可能是父类的实现,即子类未实现被交换的方法,导致父类的实现指向了子类交换的方法,进而导致父类调用被交换的方法(交换后实际调用的是子类交换的方法)引发崩溃;

    对于该方法调用不同场景的影响,可见Runtime Method Swizzling 实战

  • xxx_viewWillAppear方法中调用[self xxx_viewWillAppear:animated]是否会造成死循环

    不会,[self xxx_viewWillAppear:animated]实际调用的被交换的方法viewWillAppear:;

对于不同类方法交换的场景,可见Method Swizzling的各种姿势
Method Swizzling方法调配使用的场景如下:

AOP 面向切面编程

AOP(Aspect Oriented Program)面向切面编程,如下是百度百科及维基百科的解释:

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

百度百科:

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

主要目的:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

理解:

横切关注点不同于主业务逻辑代码,应该是主业务逻辑代码中的非业务逻辑代码通用部分,如主业务逻辑中添加行为统计,“行为统计”就是横切关注点,将其抽离出来;主业务逻辑(可能是多个业务模块)需要横切关注点时可统一插入到主业务逻辑中,而不影响主业务逻辑。

典型案例就是日志记录,因为日志功能往往横跨系统中的每个业务模块,即“横切”所有有日志需求的类及方法体。

针对ios的面向切面编程就是Method Swizzling黑魔法,可以在不改变原有代码(或者函数)逻辑上,添加非业务逻辑,如添加行为日志记录及上报;

在 Objective-C 的实现结构中 Runtime 的动态派发机制保证了这么语言的灵活性,而在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是AOP(面向切面编程)。

Method Swizzling如上所述,不做阐述,不过有人通过该技术实现了优秀的AOP库,如Aspects

Aspects

Aspects 就是一个不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                          withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

使用举例如下:

  1. viewWillAppear`中添加日志
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
  1. 调试查看点击状态

    [_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
    } error:NULL];
    
  2. 为系统类添加处理程序,如UIVieweController,官方demo:

    @implementation UIViewController (DismissActionHook)
    
    // Will add a dismiss action once the controller gets dismissed.
    - (void)pspdf_addWillDismissAction:(void (^)(void))action {
        PSPDFAssert(action != NULL);
    
        [self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
            if ([aspectInfo.instance isBeingDismissed]) {
                action();
            }
        } error:NULL];
    }
    
    @end
    

isa swizzling

isa swizzlingKVO(键值观察)机制的实现技术,其通过修改object对象的isa指针指向生成的中间代理类NSKVONotifying_xxx(官方的类名称)NSKVONotifying_xxxsuper_class指针指向原有的观察类对象object class

image.png

NSKVONotifying_xxx生成的中间类,重写被观察的对象四个方法:classsetterdealloc_isKVOA;

重写setter

重写class方法目的是让被观察者对象调用[object class]时返回的原有的类实例;

官方文档上对于KVO的实现的最后,给出了需要我们注意的一点是,永远不要用用isa来判断一个类的继承关系,而是应该用class方法来判断类的实例。

重写setter

重写setter方法目的是能监听到被观察者调用属性设置方法,如setXxxx(Xxxx为属性名称)或者调用setValude:forKey:时,能添加通知消息方法:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

didChangValueForKey:中调用观察者必须重写的方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

因此,若- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法生效,即观察者能通过上述方法受到属性被改变的通知,则需要满足如下:

  • 自动通知,NSObject实现了自动通知的方法;

    • 存在setter访问器方法,并且通过setter方法或者self.xxx间接调用setter方法,则中间类会添加will/didChangeValueForKey:触发事件通知;
    • 不存在setter访问器方法,需要通过setValude:forKey:方法来修改属性,中间类会添加will/didChangeValueForKey:触发事件通知;
    • 对于集合类,如NSMutalArray,需要通过mutableArrayValueForKey来获取中间代理类,触发通知,否则直接通过addObject:无法收到通知;
    • 对于存在依赖关系的属性,具体可查看官方文档;
  • 手动通知:手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。这可以帮助你最少限度触发不必要的通知,或者一组改变值发出一个通知,想要使用手动通知必须实现automaticallyNotifies-ObserversForKey: 方法;并且手动调用will/didChangeValueForKey:来触发通知;

重写dealloc

用来销毁新生成的NSKVONotifying_类;

重写_isKVOA方法

这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。

YYModel

具体的实现机制可参考YYModel对于setter封装:

/// 获取监控的属性
objc_property_t getKVOProperty(Class cls, NSString *keyPath) {
    if (!keyPath || !cls) {
        return NULL;
    }
    
    objc_property_t res = NULL;
    unsigned int count = 0;
    const char *property_name = keyPath.UTF8String;
    objc_property_t *properties = class_copyPropertyList(cls, &count);
    
    for (unsigned int idx = 0; idx < count; idx++) {
        objc_property_t property = properties[idx];
        if (strcmp(property_name, property_getName(property)) == 0) {
            res = property;
            break;
        }
    }
    free(properties);
    return res;
}

/// 检测属性是否存在setter方法
BOOL ifPropertyHasSetter(objc_property_t property) {
    BOOL res = NO;
    unsigned int attrCount;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'S') {
            res = YES;
        }
    }
    free(attrs);
    return res;
}

/// 获取属性的数据类型
YYEncodingType getPropertyType(objc_property_t) {
    unsigned int attrCount;
    YYEncodingType type = YYEncodingTypeUnknown;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'T') {
            type = YYEncodingGetType(attrs[idx].value);
        }
    }
    free(attrs);
    return type;
}

/// 根据setter名称获取属性名
NSString *getPropertyNameFromSelector(SEL selector) {
    NSString *selName = [NSStringFromSelector(selector) substringFromIndex: 3];
    NSString *firstAlpha = [[selName substringToIndex: 1] lowercaseString];
    return [selName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstAlpha];
}

/// 根据属性名获取setter名称
SEL getSetterFromKeyPath(NSString *keyPath) {
    NSString *firstAlpha = [[keyPath substringToIndex: 1] uppercaseString];
    NSString *selName = [NSString stringWithFormat: @"set%@", [keyPath stringByReplacingCharactersInRange: NSMakeRange(0,  1) withString: firstAlpha]];
    return NSSelectorFromString(selName);
}

/// 设置bool属性的kvo setter
static void setBoolVal(id self, SEL _cmd, BOOL val) {
    NSString *name = getPropertyNameFromSelector(_cmd);
    void (*objc_msgSendKVO)(void *, SEL, NSString *) = (void *)objc_msgSend;
    void (*objc_msgSendSuperKVO)(void *, SEL, BOOL) = (void *)objc_msgSendSuper;
    
    objc_msgSendKVO(self, @selector(willChangeValueForKey:), val);
    objc_msgSendSuperKVO(self, _cmd, val);
    objc_msgSendKVO(self, @selector(didChangeValueForKey:), val);
}

/// KVO实现
static void addObserver(id observedObj, id observer, NSString *keyPath) {
    objc_property_t observedProperty = getKVOProperty([observedObj class], keyPath);
    if (!ifPropertyHasSetter(observedProperty)) {
        return;
    }
    
    NSString *kvoClassName = [@"SLObserved_" stringByAppendString: NSStringFromClass([observedObj class])];
    Class kvoClass = NSClassFromString(kvoClassName);
    if (!kvoClass)) {
        kvoClass = objc_allocateClassPair([observedObj class], kvoClassName.UTF8String, NULL);
        
        Class(^classBlock)(id) = ^Class(id self) {
            return class_getSuperclass([self class]);
        };
        class_addMethod(kvoClass, @selector(class), imp_implementationWithBlock(classBlock), method_getTypeEncoding(class_getMethodImplementation([observedObj class], @selector(class))));
        objc_registerClassPair(kvoClass);
    }
    
    YYEncodingType type = getPropertyType(observedProperty);
    SEL setter = getSetterFromKeyPath(observedProperty);
    switch (type) {
        case YYEncodingTypeBool: {
            class_addMethod(kvoClass, setter, (IMP)setBoolVal, method_getTypeEncoding(class_getMethodImplementation([observedObj class], setter)));
        }   break;
        ......
    }
}

其中实现点包含了TypeCode键值编码runtime中创建类、消息发送、获取属性列表、添加类对象方法等知识点;
YYModel 源码

神经病院 Objective-C Runtime 出院第三天——如何正确使用 Runtime

Key-Value Observing Programming Guide

iOS KVO(键值观察) 总览

概念及数据结构

id

/// A pointer to an instance of a class.
typedef struct objc_object *id;

//objc-private.h
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ... 此处省略其他方法声明
}
//objc.h
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

指向类实例的指针;

objc_object 结构体包含一个 isa 指针,类型为 isa_t 联合体。根据 isa 就可以顺藤摸瓜找到对象所属的类。isa 这里还涉及到 tagged pointer 等概念。因为 isa_t 使用 union 实现,所以可能表示多种形态,既可以当成是指针,也可以存储标志位。有关 isa_t 联合体的更多内容可以查看 Objective-C 引用计数原理

PS: isa 指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档

SEL

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

称为方法选择器,是一个方法selector的指针,用于类查找方法列表中的对应的IMP

objc源码中未找到objc_selector结构体的定义,objc在编译时会根据方法的名字及参数列表,生成一个整型标识,这个标识就是SEL;本质上SEL是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法,不同类查找的方法列表不同,因此不会导致类方法指向同一个IMP),为了加快方法的查询速度;

不同类中相同方法名及参数名的SEL相同,即使参数的类型不同,因此在一个类中方法名及参数名相同但参数类型不同的方法编译错误,objc使用了大量带参数类型的方法名称,导致objc方法名都很长;

工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。

可在运行时添加新的selector,也可以通过如下三种方法获取方法的SEL

  • sel_registerName函数
  • Objective-C编译器提供的@selector()
  • NSSelectorFromString()方法

IMP

IMPobjc.h中的定义是:

typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一个函数指针,这是由编译器生成的。第一个参数为self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则指向元类的指针);第二个SEL为方法选择器;接下来就是参数列表;

Class

Class 其实是一个指向 objc_class 结构体的指针:

typedef struct objc_class *Class;
//objc/objc-private.h定义
struct objc_class : objc_object {
    // Class ISA;                           //add, 指向类对象的指针
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    ... 省略其他方法
}
// objc/runtime.h定义
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;                                                   //注意此处,类自身存在指向类对象的指针,称为元类
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;

objc_class继承自objc_object,因此类本身也是类对象,为了处理类本身及类对象的关系,runtime创建了元类(Meta-Class),类对象所属的类型叫做元类,用来标识类对象具备的元数据。类方法(可以理解为类对象的实例方法,区别于类本身的实例方法)就定义在类对象中,每个类仅有一个类对象,每个类对象仅有一个与之相关的元类,所有的元类最终指向根元类(即为NSObject),最终根元类的isa指针指向自己,见下图,如[NSObject alloc]消息发送是,会在类对象中查询能够响应消息的方法。
同时,可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议;

image.png

image.png

图中类实例的isa指向类本身,类本身的isa指向元类,元类的isa指向根元类Root class,根元类的isa指向自身;其他的super_class指向父类的类本身及元类,最终父类指向nil
image.png

实例对象

typedef struct objc_object *id;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

实例对象是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属类的成员变量并初始化,其中isa指针也会被初始化,让对象可以访问类及类的继承体系,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法。

实例对象即id类型,其是一个objc_object结构类型的指针。该类型的对象可以转换为任何一种对象,类似于C语言中void *指针类型的作用;

runtime - iOS类对象、实例对象、元类对象

类对象

类对象即类本身,即实例对象的isa指向的地址。类对象存储着类的成员变量、缓存及实例方法列表,但不存储类方法;

元类对象

元类对象即类对象的isa指向的地址,存储着类方法,其isa指向根元类(Metal-Class,NSObject),根元类的isa指向自身;

cache

接收者收到消息,优先去cache缓存中查找,如果没有就通过isa指针去类本身的实例方法列表中methodLists查找,提升查找速度;

该字段指向struct objc_cache的结构体,具体如下:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};
    • mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。
  • occupied:一个整数,指定实际占用的缓存bucket的总数。

  • buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

Method

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}  

对于struct objc_method结构体描述如下:

  • 方法名类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同;
  • 方法类型 types 是个char指针,其实存储着方法的参数类型和返回值类型;
  • imp 指向了方法的实现,本质上是一个函数指针;

相关的runtime调动方法如下:

// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );

Ivar

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}  

Ivar是类中实例变量的类型,可以根据实例来查找类中的名字,也称“反射”;

成员变量的方法调用如下:

// 获取成员变量名
const char * ivar_getName ( Ivar v );
// 获取成员变量类型编码
const char * ivar_getTypeEncoding ( Ivar v );
// 获取成员变量的偏移量
ptrdiff_t ivar_getOffset ( Ivar v );

ivar_getOffset函数,对于类型id或其它对象类型的实例变量,可以调用object_getIvarobject_setIvar`来直接访问成员变量,而不使用偏移量;
类成员变量支持权限控制:

  • @protected是受保护的,只能在本类及其子类中访问,在{}声明的变量默认是@protect;
  • @private是私有的,只能在本类访问;
  • @public公开的,可以被在任何地方访问;
    objc_property_t**

objc_property_t是表示Objective-C声明的属性的类型,其实际是指向objc_property结构体的指针,其定义如下:

typedef struct objc_property *objc_property_t;

属性相关的方法如下:

// 获取属性名
const char * property_getName ( objc_property_t property );
// 获取属性特性描述字符串
const char * property_getAttributes ( objc_property_t property );
// 获取属性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 获取属性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
  • property_copyAttributeValue函数,返回的char *在使用完后需要调用free()释放。
  • property_copyAttributeList函数,返回值在使用完后需要调用free()释放。
    objc_property_attribute_t**

objc_property_attribute_t定义了属性的特性(attribute),它是一个结构体,定义如下:

typedef struct {
    const char *name;           // 特性名
    const char *value;          // 特性值
} objc_property_attribute_t;

protocol协议

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;
        //省略一些封装的便捷 get 方法
    ....
};

@interface Protocol : NSObject
@end

Protocol就是继承自NSObject的对象,其id结构体类型为struct protocol_t

runtime提供了一系列关于协议的方法,如下:

// 返回指定的协议
Protocol * objc_getProtocol ( const char *name );
// 获取运行时所知道的所有协议的数组
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 创建新的协议实例
Protocol * objc_allocateProtocol ( const char *name );
// 在运行时中注册新创建的协议
void objc_registerProtocol ( Protocol *proto );
// 为协议添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一个已注册的协议到协议中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 为协议添加属性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回协议名
const char * protocol_getName ( Protocol *p );
// 测试两个协议是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 获取协议中指定条件的方法的方法描述数组
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 获取协议中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 获取协议中的属性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 获取协议的指定属性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 获取协议采用的协议
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看协议是否采用了另一个协议
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
  • objc_getProtocol函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil。
  • objc_copyProtocolList函数,获取到的数组需要使用free来释放
  • objc_allocateProtocol函数,如果同名的协议已经存在,则返回nil
  • objc_registerProtocol函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议

需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescriptionprotocol_addProtocolprotocol_addProperty往协议中添加方法等。

category类别

/// An opaque type that represents a category.
typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}  

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

Runtime并没有在<runtime.h>头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class中,我们可以通过针对objc_class的操作函数来获取分类的信息;

可以通过类别增加类的方法,但不能通过类别增加实例变量,不过objc提供了解决方案--关联对象(Associated Object);

关联对象

关联对象类似字典,通过key关键词类设定类中的成员变量并关联相关的对象,并通过key来获取关联的对象,不过需要手动指定内存策略,来告知runtime如何管理关联的对象,具体的内存策略如下:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

具体的<runtime.h>定义的方法如下:

//若value为nil,则移除指定的存在的关联对象
void 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);
//移除所有的关联对象
void objc_removeAssociatedObjects(id _Nonnull object);

typedef void (*objc_hook_setAssociatedObject)(id _Nonnull object, const void * _Nonnull key,
                                              id _Nullable value, objc_AssociationPolicy policy);

void objc_setHook_setAssociatedObject(objc_hook_setAssociatedObject _Nonnull newValue,
                                       objc_hook_setAssociatedObject _Nullable * _Nonnull outOldValue)

关联对象使用起来并不复杂,它让我们可以动态地增强类现有的功能,如关联UIAlertView点击按钮关联响应的block,在按钮点击代理实现中直接获取关联的按钮block执行;

参考资料

Objective-C Runtime Programming Guide

Objective-C Runtime

NSProxy

Objective-C Runtime 运行时之一:类与对象

Objective-C Runtime 运行时之二:成员变量与属性

Objective-C Runtime 运行时之三:方法与消息

Objective-C Runtime 运行时之四:Method Swizzling

Objective-C Runtime 运行时之五:协议与分类

《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 引导 runtime是运行时,对于从事iOS开发,想要深入学习OC的人,runtime是必须熟悉掌握的东西。 ru...
    叫我小黑阅读 857评论 1 4
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 719评论 0 1
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,109评论 0 7
  • 1、runtime(运行时机制)是什么 runtime是属于OC的底层,是一套比较底层的纯C语言API, 属于1个...
    AKyS佐毅阅读 548评论 0 16
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 770评论 0 4