iOS开发-初识runtime

一.runtime的基本属性


SEL

  • objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
  • 其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

id

  • objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
  • objc_object又是啥呢:
struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling的技术,详见官方文档

Class

  • 之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;
  • objc_class就是我们摸到的那个瓜,里面的东西多着呢:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
   #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    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;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议

  • PS:
    objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)
    其中objc_ivar_listobjc_method_list分别是成员变量列表和方法列表:
   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;
}                                                            OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *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;
}

如果你C语言不是特别好,可以直接理解为objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

  • 最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。
  • 不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个ObjC类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc]这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

Method

  • Method是一种代表类中的某个方法的类型。
typedef struct objc_method *Method;

objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

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

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

Ivar

  • Ivar是一种代表类中实例变量的类型。
typedef struct objc_ivar *Ivar;
  • objc_ivar在上面的成员变量列表中也提到过:
-(NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}

class_copyIvarList 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。

IMP

  • IMPobjc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法。
你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含idSEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址;反之亦然。

Cache

  • runtime.hCache的定义如下:
typedef struct objc_cache *Cache
  • 还记得之前objc_class结构体中有一个struct objc_cache *cache吧,它到底是缓存啥的呢,先看看objc_cache的实现:
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。

Property

  • @property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyListprotocol_copyPropertyList方法来获取类和协议中的属性:

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

返回类型为指向指针的指针,因为属性列表是个数组,每个元素内容都是一个objc_property_t指针,而这两个函数返回的值是指向这个数组的指针。
举个栗子,先声明一个类:

@interface Lender : NSObject {
    float alone;
}
@property float alone;
@end

你可以用下面的代码获取属性列表:

id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你可以用property_getName函数来查找属性名称:

const char *property_getName(objc_property_t property)

你可以用class_getPropertyprotocol_getProperty通过给出的名称来在类和协议中获取属性的引用:

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函数来发掘属性的名称和@encode类型字符串:

const char *property_getAttributes(objc_property_t property)

把上面的代码放一起,你就能从一个类中获取它的属性啦:

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));
}

对比下 class_copyIvarList 函数,使用 class_copyPropertyList函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。

二.runtime常用用法


1.获取类的成员变量

- (IBAction)getClassMember:(UIButton *)sender {
    
    // 定义一个变量接收属性数目
    unsigned int outCount = 0;
    
    // 获得person类的成员变量列表
    Ivar *ivarArray = class_copyIvarList([UIView class], &outCount);
    
    // 遍历列表
    for (int i = 0; i < outCount; ++i) {
        Ivar ivar = ivarArray[i];
        // 获得成员变量名
        const char *ch = ivar_getName(ivar);
        // 获得成员变量类型
        const char *type = ivar_getTypeEncoding(ivar);
        // C字符串转OC字符串
        NSString *varName = [NSString stringWithUTF8String:ch];
        NSString *varType = [NSString stringWithUTF8String:type];
        NSLog(@"%@  %@",varType,varName);
    }
    // 释放
    free(ivarArray);
}

2.获得类中的所有属性

- (IBAction)getClassProperty:(UIButton *)sender {
    
    unsigned int count = 0;
    
    objc_property_t *propertyArray = class_copyPropertyList([Person class], &count);
    
    for (int i = 0; i < count; ++i) {
        
        objc_property_t property = propertyArray[i];
        
        const char *name = property_getName(property);
        
        NSString *proName = [NSString stringWithUTF8String:name];
        
        NSLog(@"%@",proName);
    }
    // 释放
    free(propertyArray);
}

3.获得类的全部方法

- (IBAction)getClassMethod:(UIButton *)sender {
    // 定义一个变量接收方法数量
    unsigned int count = 0;
    // 获得类的方法
    Method *methodArray = class_copyMethodList([Person class], &count);
    // 遍历类的方法
    for (int i = 0; i < count; ++i) {
        Method method = methodArray[i];
        // 获得方法名
        SEL sel = method_getName(method);
        // 获得方法的实现
//        IMP imp = method_getImplementation(method);
        NSLog(@"%@",NSStringFromSelector(sel));
    }
    // 释放
    free(methodArray);
}

4.获得类的全部协议

- (IBAction)getClassProtocal:(UIButton *)sender {
    
    unsigned int count = 0;
    // 获得指向该类遵循的所有协议的数组指针
    __unsafe_unretained Protocol **protocolArray = class_copyProtocolList([self class], &count);
    
    for (int i = 0; i < count; ++i) {
        Protocol *protocol = protocolArray[i];
        
        const char *name = protocol_getName(protocol);
        NSString *protocolName = [NSString stringWithUTF8String:name];
        NSLog(@"%@",protocolName);
    }
    // 释放
    free(protocolArray);
}

5.动态改变成员变量

- (IBAction)changeClassMember:(UIButton *)sender {
    
//    self.student.name = @"willphonez";
    
    // 定义一个变量保存成员变量数量
    unsigned int count = 0;
    // 获得成员变量的数组的指针
    Ivar *ivarArray = class_copyIvarList([self.student class], &count);
    // 遍历成员变量数组
    for (int i = 0; i < count; ++i) {
        Ivar ivar = ivarArray[i];
        // 获得成员变量名
        NSString *ivarName =[NSString stringWithUTF8String:ivar_getName(ivar)];
        // 根据成员变量名找到成员变量
        if ([ivarName isEqualToString:@"_scret"]) {
            // 对成员变量重新赋值
            object_setIvar(self.student, ivar, @"aibaozi");
            break;
        }
    }
    // 释放
    free(ivarArray);
    // 打印结果
//    NSLog(@"%@",self.student.name);
    [self.student run];
}

6.动态交换类方法

- (IBAction)changeClassMethod:(UIButton *)sender {
    // 获得两个方法
    Method run = class_getInstanceMethod([Person class], @selector(run));
    Method eat = class_getInstanceMethod([Person class], @selector(eat));
    // 交换对象方法实现
    method_exchangeImplementations(run, eat);
    // 调用方法,或发现两个方法的实现交换了
    [self.student run];
    [self.student eat];
    
    // runtime修改的是类,不是单一的对象,一次修改,在下次编译前一直有效
    Person *p = [[Person alloc] init];
    [p run];
    [p eat];
    
    // 也可以在category中添加自己的方法去替换 (包括系统类的方法)
    [Person sleep];
}

7.动态添加方法

- (IBAction)addClassMethod:(UIButton *)sender {
    
    // 添加方法的实现到fromCity方法
    class_addMethod([self.student class], @selector(fromCity:),(IMP)fromCityAnswer, nil);
    
    // runtime修改的是类,不是单一的对象,一次修改,在下次编译前一直有效
    
    if ([self.student respondsToSelector:@selector(fromCity:)]) {
        
        [self.student performSelector:@selector(fromCity:) withObject:@"广州"];
        
    } else {
        NSLog(@"无可奉告");
    }
}

void fromCityAnswer(id self, SEL _cmd, NSString *string) {
    
    NSLog(@"我来自:%@",string);
}

8.动态给category扩展属性

- (IBAction)addCategoryProperty:(UIButton *)sender {
    
    self.student.girlFriend = @"baozijun";
    NSLog(@"%@",self.student.girlFriend);
}
  • Person+ZWF.h文件
#import "Person.h"

@interface Person (ZWF)
// 扩展属性
@property (nonatomic, strong) NSString *girlFriend;

@end
  • Person+ZWF.m文件
#import "Person+ZWF.h"
#import <objc/runtime.h>

@implementation Person (ZWF)

char friend;

- (NSString *)girlFriend
{
    // 获取相关连得对象时使用Objc函数objc_getAssociatedObject
   return objc_getAssociatedObject(self, &friend);
}

- (void)setGirlFriend:(NSString *)girlFriend
{
    // 创建关联要使用objc_setAssociatedObject
    // 4个参数依次是(源对象, 关键字, 关联对象, 关联策略)
    objc_setAssociatedObject(self, &friend, girlFriend, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

+ (void)load
{
    // 获得两个类方法
    Method sleep = class_getClassMethod(self, @selector(sleep));
    Method noSleep = class_getClassMethod(self, @selector(noSleep));
    // 交换两个类方法的实现
    method_exchangeImplementations(sleep, noSleep);
}

+ (void)noSleep
{
    NSLog(@"%s",__func__);
}

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 744评论 0 1
  • 简介 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 O...
    专业男神经阅读 901评论 0 2
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,170评论 0 7
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 726评论 0 2