runtime

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

首先当我们调用一个方法时,例如

[obj doSomeThing];

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

objc_msgSend(obj,@selector(doSomeThing));  

我们知道iOS中所有的对象object都是继承自NSObject,我们点进去NSObject里面看看

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

可以看到NSObject的类申明里面存在一个Class的isa指针。然后我们在看看Class

typedef struct objc_class *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

} 

可以看到Class是一个objc_class的结构体,而objc_class的结构体里面又存在很多东西,下面就看看这些都是什么。

  • Class isa: 指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方法)。注意:所有metaclass中isa指针都指向跟metaclass。而跟metaclass则指向自身。Root metaclass是通过继承Root class产生的。与root class结构体成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。

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

  • 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: 存储该类遵守的协议

下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。
首先,编译器将代码[obj doSomeThing];转化为objc_msgSend(obj, @selector
(doSomeThing));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

最后补充一下@selector (doSomeThing):
这是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字(doSomeThing)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

runtime的一下常见使用

  • 设置关联获取对象和为类别添加属性
  • 方法的交换swizzling
  • 获取私有属性和方法
  • 对私有属性修改
  • 归档和解档
  • 动态添加方法
  • 字典转模型
1.设置关联获取对象和为类别添加属性

首先是为一个值设置关联

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

  • id object 设置关联的对象,一般传self就行啦。
  • const void *key 设置key,通过这个key来获取关联的值
  • id value 关联的值。
  • objc_AssociationPolicy policy 关联值的属性是用assign ,retain ,copy等
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. */
};

获取设置好的关联对象

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)

  • id object 对应设置关联的id object 。
  • const void *key key当然是设置关联的key

移除关联对象

OBJC_EXPORT void objc_removeAssociatedObjects(id object)

使用示例

//定义全局的char
 static const char *yoyo;
//设置关联
 objc_setAssociatedObject(self, &yoyo, @"哈哈哈哈", OBJC_ASSOCIATION_COPY_NONATOMIC);
//获取关联值
 objc_getAssociatedObject(self,&yoyo000);
//移除关联
 objc_removeAssociatedObjects(self);

为类别添加属性

我们都知道,类别不能直接添加属性不然会报一下警告提示,如果不重新set和get方法直接使用的话还会crash。

WechatIMG1.jpeg

下面我们为NSObject类添加一个property字符串属性
首先是.h文件

#import <Foundation/Foundation.h>

@interface NSObject (addProperty)
@property (nonatomic,strong)NSString *property;
@end

然后是在.m文件中重写set方法和get方法,但是要利用runtime来写

#import "NSObject+addProperty.h"
#import <objc/runtime.h>

static const char *kProperty;

@implementation NSObject (addProperty)

-(void)setProperty:(NSString *)property
{
    objc_setAssociatedObject(self, &kProperty, property, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)property
{
   return  objc_getAssociatedObject(self, &kProperty);
}
@end
2.方法的交换swizzling

利用runtime我们可以截取方法的响应,然后交换两个方法,在我们添加的方法中加入我们想要的操作,例如我们可以截取button的响应事件,交换成我们自定义的方法,那么button在每次响应之前都会先响应我们的方法,这样子可以实现button的点击统计事件等之类的操作。

首先获取方法

OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)

  • Class cls 传入你想获取的方法的类
  • SEL name 方法名。方法的获取首先会在该类的class_copyMethodList中寻找,如果没有就在其父类的class_copyMethodList中寻找,也没有就返回空。

交换方法,用前面获取到的响应方法和自定义的方法,进行互换,使用method_exchangeImplementations需要注意的是要在dispatch_once_t中执行一次就好,不然偶数执行的话又会调换回来原先的方法

OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 

下面实例一下button的点击事件的截取交换,首先给button添加一个类别.h文件中不用做什么操作

#import <UIKit/UIKit.h>
@interface UIButton (buttonHook)

@end

然后在.m文件中执行交换,可以在类方法initialize或者load中执行交换的,initialize方法是类第一次初始化的时候调用一次。需要注意的是要在dispatch_once_t中执行一次就好,不然偶数执行的话又会调换回来原先的方法

#import "UIButton+buttonHook.h"
#import <objc/runtime.h>

@implementation UIButton (buttonHook)

+(void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //获取button的点击响应事件方法
        Method sendAction = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        //获取自定义的方法
        Method hookSendAction = class_getInstanceMethod(self, @selector(hookSendAction:to:forEvent:));
        //交换两个方法
        method_exchangeImplementations(sendAction, hookSendAction);
    });
}
//自定义的方法
- (void)hookSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    //do something.
    
    //这里执行的是button的响应方法即@selector(sendAction:to:forEvent:)
    [self hookSendAction:action to:target forEvent:event];
}
@end
3. 获取属性和方法

获取属性
方法1

OBJC_EXPORT Ivar *class_copyIvarList(Class cls, unsigned int *outCount) 

  • Class cls 类,想获取那个类就传那个类
  • unsigned int *outCount int类型指针,这里会返回属性个数的长度给该int数据

返回值为lvar数组。里面包含类的属性,lvar是一个结构体,看看里面有什么

typedef struct objc_ivar *Ivar;


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

  • char *ivar_name 属性的名称
  • char *ivar_type 属性的类型
  • int ivar_offset 属性的偏移? 如果是object类型的是8字节,基本数据类型4字节

方法2,使用class_copyPropertyList获取,这个获取和方法1的获取类似,参数都一样

OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

typedef struct objc_property *objc_property_t;

获取方法 使用类似于获取属性

OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount) 

  • Class cls 类,想获取那个类就传那个类
  • unsigned int *outCount int类型指针,这里会返回属性个数的长度给该int数据

返回值为Method数组。里面包含类的方法,Method是一个结构体,看看里面有什么

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}  
  • SEL method_name 方法名
  • char *method_types 方法类型
  • IMP method_imp 一个函数指针,保存了方法的地址

下面我们看一下具体的使用,首先先创建一个Property的类,在.h文件中添加一些属性和方法,在.m文件中也添加一个私有属性和私有方法,看看我们能不能获取得到

#import <Foundation/Foundation.h>

@interface Property : NSObject

@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int age;
@property (nonatomic,assign)float height;
@property (nonatomic,assign)float weight;
@property (nonatomic,assign)NSInteger integer;
@property (nonatomic,strong)NSDictionary *dic;
@property (nonatomic,strong)NSArray *array;

-(void)doSomething;
@end
#import "Property.h"

@interface Property ()
//私有变量
@property (nonatomic,assign)int sex;

@end

@implementation Property

//私有方法
-(void)privateDoSomething
{
    
}

-(void)doSomething
{
    
}
@end

接着我们在viewcontroller里面获取Property类的属性和方法

#pragma mark - 获取所有的属性(包括私有的)
- (void)getAllIvar {
    unsigned int count = 0;
    //Ivar:定义对象的实例变量,包括类型和名字。
    //获取所有的属性(包括私有的)
    Ivar *ivars= class_copyIvarList([Property class], &count);
    for (int i = 0; i < count; i++) {
        //取出成员变量
        Ivar ivar = ivars[i];
        //获取属性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
        //获取属性类型
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        NSLog(@"属性 --> %@ 和 %@ 和 %td ",name,type,ivar_getOffset(ivar));
    }
}
#pragma objc_property_t - 获取所有的属性(包括私有的)
-(void)getAllProperty
{
    unsigned int count = 0;
    objc_property_t *propertys = class_copyPropertyList([Property class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = propertys[i];
        NSString *name = [NSString stringWithUTF8String:property_getName(property)];
        NSLog(@"哈哈哈:%@",name);
    }
}
#pragma mark - 获取所有的方法(包括私有的)
- (void)getAllMethod {
    unsigned int count = 0;
    //获取所有的方法(包括私有的)
    Method *memberFuncs = class_copyMethodList([Property class], &count);
    for (int i = 0; i < count; i++) {
        //获取方法
        SEL address = method_getName(memberFuncs[i]);
        //获取方法指针
        IMP imp = method_getImplementation(memberFuncs[i]);
        //获取方法类型
        const char *type =  method_getTypeEncoding(memberFuncs[i]);
        NSString *methodName = [NSString stringWithCString:sel_getName(address) encoding:NSUTF8StringEncoding];
        NSLog(@"方法 : %@ --->类型%s",methodName,type);
    }
}

最后我们看看打印出来的日志,完美获取到所有的属性和方法(包括私有)。而且属性的set和get方法也都可以获取到。

2017-06-22 11:03:30.573646+0800 runtime[4880:1603707] 属性 --> _age 和 i 和 8 
2017-06-22 11:03:30.573848+0800 runtime[4880:1603707] 属性 --> _height 和 f 和 12 
2017-06-22 11:03:30.574219+0800 runtime[4880:1603707] 属性 --> _weight 和 f 和 16 
2017-06-22 11:03:30.574411+0800 runtime[4880:1603707] 属性 --> _sex 和 i 和 20 
2017-06-22 11:03:30.574807+0800 runtime[4880:1603707] 属性 --> _name 和 @"NSString" 和 24 
2017-06-22 11:03:30.575005+0800 runtime[4880:1603707] 属性 --> _integer 和 q 和 32 
2017-06-22 11:03:30.575115+0800 runtime[4880:1603707] 属性 --> _dic 和 @"NSDictionary" 和 40 
2017-06-22 11:03:30.575217+0800 runtime[4880:1603707] 属性 --> _array 和 @"NSArray" 和 48 
2017-06-22 11:03:30.575361+0800 runtime[4880:1603707] 方法 : privateDoSomething --->类型v16@0:8
2017-06-22 11:03:30.575459+0800 runtime[4880:1603707] 方法 : doSomething --->类型v16@0:8
2017-06-22 11:03:30.575552+0800 runtime[4880:1603707] 方法 : dic --->类型@16@0:8
2017-06-22 11:03:30.575645+0800 runtime[4880:1603707] 方法 : setDic: --->类型v24@0:8@16
2017-06-22 11:03:30.575759+0800 runtime[4880:1603707] 方法 : .cxx_destruct --->类型v16@0:8
2017-06-22 11:03:30.575856+0800 runtime[4880:1603707] 方法 : setName: --->类型v24@0:8@16
2017-06-22 11:03:30.576947+0800 runtime[4880:1603707] 方法 : name --->类型@16@0:8
2017-06-22 11:03:30.577344+0800 runtime[4880:1603707] 方法 : array --->类型@16@0:8
2017-06-22 11:03:30.577455+0800 runtime[4880:1603707] 方法 : setArray: --->类型v24@0:8@16
2017-06-22 11:03:30.577550+0800 runtime[4880:1603707] 方法 : height --->类型f16@0:8
2017-06-22 11:03:30.580284+0800 runtime[4880:1603707] 方法 : setHeight: --->类型v20@0:8f16
2017-06-22 11:03:30.580562+0800 runtime[4880:1603707] 方法 : weight --->类型f16@0:8
2017-06-22 11:03:30.580663+0800 runtime[4880:1603707] 方法 : setWeight: --->类型v20@0:8f16
2017-06-22 11:03:30.580757+0800 runtime[4880:1603707] 方法 : age --->类型i16@0:8
2017-06-22 11:03:30.582048+0800 runtime[4880:1603707] 方法 : setAge: --->类型v20@0:8i16
2017-06-22 11:03:30.582122+0800 runtime[4880:1603707] 方法 : integer --->类型q16@0:8
2017-06-22 11:03:30.582185+0800 runtime[4880:1603707] 方法 : setInteger: --->类型v24@0:8q16
2017-06-22 11:03:30.582453+0800 runtime[4880:1603707] 方法 : setSex: --->类型v20@0:8i16
2017-06-22 11:03:30.582519+0800 runtime[4880:1603707] 方法 : sex --->类型i16@0:8
4.对私有属性修改

上面讲到获取类的属性,我们可以利用这个在外部修改类的私有属性,因为私有属性我们在外部是不能直接访问私有属性的。
用kvo

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

推荐阅读更多精彩内容

  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,170评论 0 7
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 727评论 0 2
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,541评论 33 466
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 913评论 0 6