Objective-C中的Runtime(一) 文章总结

第一篇:摘抄自 Objective-C Runtime的那点事儿一消息机制

RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。假如在OC中写了这样的一个代码:

 [obj makeText];

其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成

objc_msgSend(obj,@selector(makeText));

obj解释

首先看obj这个对象,iOS中的obj都继承于NSObject。我们看到在NSObject中存在一个Class的isa指针:

  @interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
  }

isa的类型为结构体

  //An opaque type that represents an Objective-C class.
  typedef struct objc_class *Class;
-->
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;  //指向metaclass

#if !__OBJC2__
    Class super_class;      //指向其父类
    const char *name;      //类名
    long version;    //类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion 和 class_getVersion 进行修改、读取
    long info;   //一些标识信息,如CLS_CLASS(0x1L)表示该类为普通class,其中包含对象方法和成员变量;CLS_META(0x2L)表示该类为 metaclass,其中包含类方法
    long instance_size; // 该类的实例变量大小(包括从父类继承下来的实例变量)
    struct objc_ivar_list *ivars; //用于存储每个成员变量的地址
    struct objc_method_list **methodLists; //与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法
    struct objc_cache *cache;   // 指向最近使用的方法的指针,用于提升效率;
    struct objc_protocol_list *protocols;  // 存储该类遵守的协议
#endif
} OBJC2_UNAVAILABLE;

解释:
Class isa : 指向metaclass,也就是静态的Class
一般<u>Object对象中的isa指向普通的Class</u>,这个Class中存储普通成员变量和对象方法(- 开头的方法),<u>普通Class中的isa指针指向静态Class</u>,静态Class中存储static类型成员变量和类方法(+ 开头的方法)。

Class super_class: 指向父类,如果这个类是根类,则为NULL。

注意:
所有metaclass中isa指针都指向根metaclass,而根metaclass则指向自身。

if !__OBJC2__ endif标记的属性是Ojective-C 2.0不支持的,但实际上可以用响应的函数获取这些属性,例如:如果想要获取Class的name属性,可以按如下方法获取:

Class classPerson = Person.class;
// printf("%s\n", classPerson->name); //用这种方法已经不能获取name了 因为OBJC2_UNAVAILABLE
const char *cname  = class_getName(classPerson);
printf("%s", cname); // 输出:Person

@selector(makeText) :

这是一个SEL方法选择器。SEL主要作用是快速地通过方法名字(makeText)查找对应方法的函数指针,然后调用其函数。

SEL本身是一个Int类型的一个地址,地址中存放着方法的名字。在一个类中,每个方法对应着一个SEL,所以不能存在多个名称相同的方法,即使参数类型不同。因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

那么消息发送之后是怎么来动态查找对应的方法的?

-->首先编译器会将 [obj makeText]; 转化为 objc_msgSend(obj, @selector(makeText));
然后在 objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。在Class中先去cache中通过SEL查找对应函数method(猜测cache中的method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中没有找到,则到methodLists中查找,若找不到,则取superClass中查找。若找到了,则将method加入到cache中,以便下次查找,并通过method中的函数指针跳转到对应的函数中去执行


第二篇:iOS~runtime理解

我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,例如[target doSomething];会被转化成objc_msgSend(target, @selector(doSomething));

相关的定义

/// An opaque type that represents a method in a class definition.
//类中的一个方法
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
// 实例变量
typedef struct objc_ivar *Ivar;

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

/// An opaque type that represents an Objective-C declared property.
// 类中声明的属性
typedef struct objc_property *objc_property_t;

获取各种列表的方法:

//获取列表
- (void)getList{
    unsigned int count;
    
    //获取属性列表
    objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
    for (unsigned int i=0; i < count; i++) {
        objc_property_t property = propertyList[i];
        const char *propertyName = property_getName(property);
        NSLog(@"property ----> %@",[NSString stringWithUTF8String:propertyName]);
    }
    
    //获取方法列表
    Method *methodList = class_copyMethodList([Person class], &count);
    for (unsigned int i=0; i < count; i++) {
        Method method = methodList[i];
        NSLog(@"method----> %@",NSStringFromSelector(method_getName(method)));
    }

    //获取成员变量列表
    Ivar *ivars = class_copyIvarList([Person class], &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char * ivarName = ivar_getName(ivar);
        NSLog(@"Ivar ----> %@",[NSString stringWithUTF8String:ivarName]);
    }
    
    //获取协议列表
    __unsafe_unretained Protocol ** protocolList = class_copyProtocolList([Person class], &count);
    for (unsigned int i; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
    }
}

方法调用过程

如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。

--> 1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。

--> 2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现并执行。

--> 3.如果没找到,去父类指针所指向的对象中执行1,2。

--> 4.依次类推,如果一直到根类还没找到,转向拦截调用。

--> 5.如果没有重写拦截调用的方法,程序报错。

以上的过程给我带来的启发:

  • 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
  • 如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。

-->拦截调用

拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法。默认返回NO,可以加上自己的处理后返回YES。
  • 第二个方法和第一个方法相似,只不过处理的是实例方法。
  • 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
  • 第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget : 方法让某个target触发这个方法。

动态添加方法

重写拦截调用的方法并且返回YES -> 我们是怎么实现的呢?

<u>有一个办法是根据传进来的SEL类型的selector动态添加一个方法:</u>

     //隐式调用方法
     [target performSelector:@selector(resolveAdd:) withObject:@"test"];

然后,在target对象内部重写拦截调用的方法,动态添加方法。

    void runAddMethod(id self, SEL _cmd, NSString *string){
        NSLog(@"add C IMP  %@",string);
    }
    + (BOOL)resolveInstanceMethod:(SEL) sel{
        //给本类动态添加一个方法
        if([NSStringFromSelector(sel)  isEqualToString:@"resolveAdd:"]){
              class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
         }
    }

其中 class_addMethod 的四个参数分别是:

1、Class cls 给哪个类添加方法,本例中是self
2、SEL name添加的方法,本例中是重写的拦截调用传进来的selector。
3、IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用 +(IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
4、"v@:*"方法的签名,代表有一个参数的方法。

关联对象

现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
这种情况的一般解决办法就是继承。
但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。
这个时候,runtime的关联属性就发挥它的作用了。

    //首先定义一个全局变量,用它的地址作为关联对象的key
    static char associatedObjectKey;
    //设置关联对象
    objc_setAssociatedObject(target, $associatedObjectKey, @"添加的字符串属性", OBJC_ASSOCIATION_RETAIN_NONATOMIC);    
    //获取关联对象
    NSString *string = objc_getAssociatedObject(target, & associatedObjectKey);
    NSLog(@"AssociatedObject = %@", string);

objc_setAssociatedObject的四个参数:

  1. id object 给谁设置关联对象。
    2.const void *key 关联对象唯一的key,获取时会用到。
    3.id value 关联对象。
    4.objc_AssociationPolicy关联策略,有以下几种策略:
如果你熟悉OC,看名字应该知道这几种策略的意思了吧。
  enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403 
};

objc_getAssociatedObject的两个参数
1.id object 获取谁的关联对象。
2.const void *key 根据这个唯一的key获取关联对象。

其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能的类的类别中,方便使用。

//添加关联对象
- (void)addAssociatedObject:(id)object{
    objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//关联策略依情况而定
}
//获取关联对象
- (id)getAssociatedObject{
    return objc_getAssociatedObject(self, _cmd);
}

注意:这里面我们把getAssociatedObject方法的地址作为唯一的key,
     _cmd代表当前调用方法的地址。

方法交换 (详见原文)

方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
话不多说,这是参考Mattt大神在NSHipster上的文章自己写的代码。

方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程。
既然是切面,就一定不要忘记,交换完再调回自己。
一定要保证只交换一次,否则就会很乱。
最后,据说这个技术很危险,谨慎使用。

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

推荐阅读更多精彩内容