Runtime

77___ 2019-04-11 18:04:44 1235

什么是Runtime?

  • 我们写的代码在程序运行过程中都会被转化成runtime的C代码执行

[target doSomething]
转化为:
objc_msgSend(target, @selector(doSomething))

  • OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例就是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。

  • 相关的定义:

// 描述类中的一个方法
typedef struct objc_method *Method;
// 实例变量
typedef struct objc_ivar *Ivar;
// 类别Category
typedef struct objc_category *Category;
// 类中声明的属性
typedef struct objc_property *objc_property_t;
  • 类在runtime中的表示

/类在runtime中的表示
struct objc_class {
    / 实例的isa指向类对象,类对象的isa指向元类
    Class 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;
/* Use `Class` instead of `struct objc_class *` */

获取列表

有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的Key和模型对象的属性名字不匹配)
我们可以通过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]);
}

在Xcode上跑一下看看输出吧,

  • 导入头文件#import <objc/runtime.h>

  • 写几个属性,成员变量,方法和协议

方法调用

让我们看一下方法调用在运行时的过程(参照前文类在runtime中的表示)

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

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

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

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

  3. 如果没找到,去父类指针所指向的对象中执行1,2.
    以此类推,如果一直到根类还没找到,转向拦截调用。

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

重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
如果想调用已经重写过的方法的父类的实现,只需使用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,我们要怎么处理呢?
有一个办法是根据传进来的SEL类型的selector动态添加一个方法。

首先从外部隐式调用一个不存在的方法:

/ 隐式调用方法
[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@:*");
    }
    return YES;
}

其中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关联策略,有以下几种策略:

enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403 
};

如果你熟悉OC,看名字应该知道这几种策略的意思了吧。

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方法中的代码,反之亦然。
下面是一个数组越界的runtime实现:

#import "NSArray+Overstep.h"
#import <objc/runtime.h>

@implementation NSArray (Overstep)

+ (void)load {
    / 利用GCD只执行一次,防止多线程问题
    static dispatch_once_t onceToken;
    / 调用原方法以及新方法进行交换,处理崩溃问题。
    dispatch_once(&onceToken, ^{
        / 获得不可变数组objectAtIndex的selector
        SEL A_sel = @selector(objectAtIndex:);
        / 自己实现的将要被交换的方法的selector
        SEL B_sel = @selector(kj_objectAtIndex:);
        / 两个方法的Method
        Method A_Method = class_getInstanceMethod(objc_getClass("__NSArrayI"), A_sel);
        / 自己实现的将要被交换的方法的selector
        Method B_Method = class_getInstanceMethod(objc_getClass("__NSArrayI"), B_sel);

        / 首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
        BOOL isAdd = class_addMethod(self, A_sel, method_getImplementation(B_Method), method_getTypeEncoding(B_Method));
        if (isAdd) {
            / 如果成功,说明类中不存在这个方法的实现
            / 将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, B_sel, method_getImplementation(A_Method), method_getTypeEncoding(A_Method));
        }else{
            / 否则,交换两个方法的实现
            method_exchangeImplementations(A_Method, B_Method);
        }
    });   
}
- (id)kj_objectAtIndex:(NSUInteger)index{
    if (index < self.count) {
        / 这时候调用自己,看起来像是死循环
        / 但是其实自己的实现已经被替换了
        return [self kj_objectAtIndex:index];
    }
    return nil/ 越界返回为nil
}
@end

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

作者:77___
链接:/www0686jianshu0686com/p/55af84bf937b

今日头条回应广电总局要求整改:推5举措落实整改要求 人才租赁住房REITs破冰 助力地方人才住房建设 “严书记”出事71天后 广安市委书记换人 中期选举临近特朗普不耐烦 美官员抓紧密会塔利班 马戏团长无运输证被判十年 改无罪后巡演排到十一 男子因家庭纠纷掐死妻子后跳楼自杀 留下3岁孩子 斯皮尔伯格的新电影“头号玩家”或驱动AR设备销售 权健亚冠末轮将大规模轮换 技战术问题仍需时间 美媒:美军B52避飞朝鲜半岛 避免破坏美朝会晤气氛 中甲-申鑫送乌龙大礼 埃弗拉建功卓尔2-1继续领跑 津巴布韦大选:穆加贝挺反对派 拒支持前盟友连任 中国移动宣布2018年将建世界规模最大5G试验网
不止比特币期货 Cboe交易所酝酿加密货币衍生品 天津市委市政府为女排发贺电 盛赞精神尤为宝贵 江西一干部工作时间干“滴滴” 累计接单105次 北京和新疆正追求张兆旭 两因素左右是否留沪 堵远放徐西盛和郭普校晋升空军中将 月球到底属于谁? 国际法律有待完善 全国政协委员俞敏洪:公立学校老师不应该私自代课 司法部长:法律职业资格考试实施办法有望4月出台 南极温室收获首批新鲜蔬菜:无土壤无自然光照 亚冠不败被淘汰?恒大胜率太低 创纪录却笑不出来 湖南:锻造干部硬作风 啃下脱贫硬骨头 华为:上半年实现销售收入3257亿元人民币 同比增15…
李春江:冠军主帅最基本的素养就是得能拿冠军 美国阿拉巴马州一家烧烤店发生枪击事件 致1死3伤 吉林书记巴音朝鲁:开放是吉林的短板也是潜力所在 广东一养殖场万条中华鲟全部死亡 警方已介入(图) XCOQ爱客汇评:美元强势新高 黄金暴跌破位千三 里克尔梅:现在球员心不在足球上 他们更关心发型 3秒上篮成就险胜!3人20+鱼腩靠这胜少队魂之队 欧冠连胜第一人!拜仁老帅欧冠12连胜神迹压名帅 网约车安全事故频发 如何才能保证乘车安全? 手机挂机软件一小时5元 白手起家创业好项目 马云说未来赚钱的行业 如何创业白手起家 AG亚游集团