iOS RunTime的原理和使用

RunTime顾名思义运行时,就是系统在运行的时候的一些机制,最主要的是消息机制。对于C语言,函数的调用会在编译时决定调用哪个函数,编译完成后,会按顺序执行,无二义性。OC的调用成为消息转发,编译时不能决定调哪个函数,只有在真正运行的时候通过函数名找到对应的函数调用,属于动态调用的过程。

  • 什么是runtime?

我们写的oc代码在运行的时候,会被转化为runtime的C代码执行。

e.g.:[obj doSomething];方法被转化为objc_msgSend(obj, @selector(doSomething));

OC中的obj都继承自NSObject,类的实例在Runtime中的结构如下:

/// 描述类中的一个方法

typedef struct objc_method *Method;

/// 实例变量

typedef struct objc_ivar *Ivar;

/// 类别

typedef struct objc_category *Category;

/// 描述类中的属性

typedef struct objc_property *objc_property_t;

类在runtime中的表示:

//类在runtime中的表示
struct objc_class {
    Class isa; //指针
    //实例的isa指向类对象,类对象的isa指向元类
    
#if !__OBJC2__
    Class super_class;    //指向父类
    const char *name;     //类名
    long version;         //当前版本
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;             //成员变量列表
    struct objc_method_list **methodLists;    //方法列表
    struct objc_cache *cache;                 //缓存
    struct objc_protocol_list *protocols;     //协议列表
#endif
    
} OBJC2_UNAVAILABLE;
  • runtime的使用:
1、获取类中的属性方法列表等

我们可以通过runtime的一些方法获取属性列表、成员变量列表、方法列表、遵循的协议列表

unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
    Method method = methodList[i];
    NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
    Ivar myIvar = ivarList[i];
    const char *ivarName = ivar_getName(myIvar);
    NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
    Protocol *myProtocal = protocolList[i];
    const char *protocolName = protocol_getName(myProtocal);
    NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
2、方法调用过程(在运行时)
  1. 如果是实例对象调用实例方法,会在实例的isa指针指向的对象(类对象)操作。
  2. 如果是类对象调用类方法,会在类对象的isa指针指向的对象(元类/基类)操作。
    具体操作:
  • 首先,在相应操作对象的缓存方法列表中查找要调用的方法,如果找到,转向相应实现,并执行。
  • 如果未找到,在相应操作对象的方法列表中查找,如果找到,转向相应实现,并执行。
  • 如果未找到,在操作对象的父类指针所指向的对象中操作1,2步。
  • 以此类推,如果到根类还未找到,转向拦截调用方法。
  • 如果未实现拦截调用,程序报错。
3、拦截调用

上面说到了,如果未找到方法,则转向拦截调用。
那什么是拦截调用呢?
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会重写NSObject类中的四个方法来做处理:

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

重写了拦截方法并返回YES,我们要采取什么措施补救呢?
可以通过传递的SEL类型的selector动态添加方法。

举个栗子:
obj 对象隐式调用一个不存在的方法doSomething
[obj performSelector:@selector(doSomething) withObject:nil];

然后在obj内部的拦截方法动态添加方法

void runAddMethod(id obj, SEL _cmd, NSString *str) {
    NSLog(@"add C IMP");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if ([NSStringFromSelector(sel) isEqualToString:@"doSomething"]) {
        class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
    }
    return YES;
}

class_addMethod的四个参数解释

  • Class cls 给哪个类添加方法,本例中是self。
  • SEL name 添加的方法,本例中是重写的拦截调用传进来的selector。
  • IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
  • “v@:*”方法的签名,代表有一个参数的方法,下面还会讲到。
5.关联对象

如果你想用系统类,在不满足需求情况下,你需要额外添加属性,多数会使用继承,然后定义属性。总是继承不是太麻烦,有了runtime,你不用费那大劲了。接下来,就来看看runtime是如何动态添加属性,获取属性的。

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

objc_setAssociatedObject的四个参数:

  • id object给谁设置关联对象。
  • const void *key关联对象唯一的key,获取时会用到。
  • id value关联对象。
  • objc_AssociationPolicy关联策略,有以下几种策略:
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403 
}```

objc_getAssociatedObject的两个参数:

- id object获取谁的关联对象。
- 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代表当前调用方法的地址。
    }```

看到这里估计大家有些疲累了,别急,马上就结束了!

6.交换方法

就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
我们来试一下 定义一个UIViewController的category

/**
 load方法会在类第一次加载的时候被调用
 调用的时间比较靠前,适合在这个方法里做方法交换
 */
+(void)load{
    //方法交换应该被保证,在程序中只会执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //获得viewController的生命周期方法的selector
        SEL systemSel = @selector(viewWillAppear:);
        //自己实现的将要被交换的方法的selector
        SEL customeSel = @selector(custome_viewWillAppear:);
        //两个方法的Method
        Method systemMethod = class_getInstanceMethod([self class], systemSel);
        Method customeMethod = class_getInstanceMethod([self class], customeSel);
        
        //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
        BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(customeMethod), method_getTypeEncoding(customeMethod));
        if (isAdd) {
            //如果成功,说明类中不存在这个方法的实现
            //将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, customeSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
        }else{
            //否则,交换两个方法的实现
            method_exchangeImplementations(systemMethod, customeMethod);
        }
    });
}
- (void)custome_viewWillAppear:(BOOL)animated{
    //这时候调用自己,看起来像是死循环
    //但是其实自己的实现已经被替换了
    [self custome_viewWillAppear:animated];//这里 是去执行系统的viewWillApper:方法
    NSLog(@"custome");
}```

写个通用的方法供外部调
  • (void)changeMethodWithTarget:(id)obj fromMethod:(SEL)fromMethod toMethod:(SEL)toMethod
    {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    Method m1 = class_getInstanceMethod([obj class], fromMethod);
    Method m2 = class_getInstanceMethod([obj class], toMethod);
    BOOL isAdd = class_addMethod([obj class], fromMethod, method_getImplementation(m2), method_getTypeEncoding(m2));
    if (isAdd) {
    class_replaceMethod([obj class], fromMethod, method_getImplementation(m2), method_getTypeEncoding(m2));
    }else{
    method_exchangeImplementations(m1, m2);
    }
    });
    }```

以上是runtime的基本原理何使用讲解,有什么不对或不足的地方,希望大家多多指教!
下一章:RunLoop与多线程的原理和使用

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

推荐阅读更多精彩内容