Runtime —— 从应用场景说起

根据平时遇到的情况,通过查资料和自己的理解,对Runtime黑科技进行一次个人的学习总结???。

什么是Runtime

Runtime又叫做运行时,是底层C语言的API,是iOS内部核心之一。它是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。这就说明我们不止需要编译器,还需要一个运行时系统来处理这些工作。

iOS本身又一个Runtime库,里面有很多函数可以直接调用,但是在使用时一定要记得导入头文件 #import<objc/runtime.h>

发送消息

OC中调用方法的本质是,发送消息。

对象发送消息,即调用objc_msgSend方法,其原理是:对象根据方法编号SEL去映射查找对应的方法实现。

[p message];// 无参数
// 底层运行时会被编译器转化为:
objc_msgSend(p, selector)

[p message:(id)arg...];// 有参数
// 底层运行时会被编译器转化为:
objc_msgSend(p, selector, arg1, arg2, ...)

获取属性/方法/成员变量/协议等列表

就我所遇到过的需求有两种情况,是要拿到当前类中的每个属性的。其一就是字典转模型,所谓字典转模型,就是进行网络请求时,拿到服务器返回给你的数据(json/xml等格式),此时你要使用该数据需先将其转成集合类型,然后将集合转成模型集合(即为字典转模型),这个有很多写的很好的第三方库,如JSONModelMJExtension等,可以去GitHub上下载看源码原理。其二就是对当前类的所有属性统一设置,我所在项目的封装了三层网络请求,其中网络请求参数是根据一个LXProject类来设置的,所以提供project单例方法,下面根据代码来说明需求和实现方法。

LXProject.m

// 单例方法
+ (instancetype)project 
{
    static LXProject *_instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[LXProject alloc] init];
        
    });
    return _instance;
}

如下面网络请求,请求参数根据LXProject的属性来设置

// 网络请求参数根据LXProject的属性来设置
LXProject *project = [LXProject project];
project.key = self.productName;
project.productStatus = self.productStatus;
project.page = @(self.page);
project.size = @10;
    
[LXHttpClientList request_ProductListProject:project andBlock:^(id data, NSError *error) {
    if (data) {// 成功码为success返回data,否则上一层封装做了处理
        // 拿到网络请求的数据,此处进行字典转模型
    }
    // error有值表示网络请求失败返回错误码,上一层封装做了处理
}];

这时候存在一个问题就是,当该请求未释放的时候,那么再进行一次新的请求时,上次请求设置的key/productStatus/page/size是有值的。所以每次请求创建LXProject对象,设置参数前,都需要清空LXProject对象的属性。

改进的LXProject.m

+ (instancetype)project 
{
    static LXProject *_instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[LXProject alloc] init];
        
    });
    
    // 清空project对象的属性
    NSArray *names = [LXProject getProperties];// #import "NSObject+Extension.h" 使用分类
    for (NSString *name in names) {
        [_instance setValue:nil forKey:name];
    }
    return _instance;
}

NSObject+Extension.m

//返回当前类的所有属性
+ (NSArray *)getProperties 
{
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList(self, &count);
    NSMutableArray *mArray = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        const char *cName = property_getName(property);
        NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
        [mArray addObject:name];
    }
    
    return mArray.copy;
}

附上所有Runtime获取的列表:

- (void)userRuntimeGetSomeList 
{
  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]);
    }
}

关联对象

在项目种我们时常要使用某个系统类,然而系统类过于局限性,我们要添加某个属性。这时候想到的解决方法有:一、直接继承系统类;二、创建分类,Runtime关联对象动态添加属性。

OC中的Category无法向既有的类添加属性, 但是可以使用Runtime的关联对象(associated objects)来实现,所以我们都称呼Runtime为黑科技?,因为它能实现常理不能实现的东西。

/**
 设置关联对象
 @param object 给谁设置关联对象
 @param key 关联对象唯一的key,获取时会用到
 @param value 关联对象
 @param policy 关联策略,有以下几种策略
*/
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

/**
 获取关联对象
 @param object 谁的关联对象
 @param key 根据这个唯一的key获取关联对象
*/
objc_getAssociatedObject(id object, const void *key)
//添加关联对象
//把getAssociatedObject方法的地址作为唯一的key
- (void)addAssociatedObject:(id)object{
    objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//获取关联对象
- (id)getAssociatedObject{
    // _cmd代表当前调用方法的地址
    return objc_getAssociatedObject(self, _cmd);
}

使用objc_removeAssociatedObjects可断开所有关联, 把对象恢复至原始状态

动态添加方法

使用场景一:如果一个类的方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用Runtime动态添加方法解决。

使用场景二:比如会员机制,一部分对象能够调用某方法,这个方法可以用Runtime动态添加。

/**
 动态添加方法
 @param cls 给哪个类添加方法
 @param name 添加的方法
 @param imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现
 @param types 表示返回值和参数
*/
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

上面最后一个参数表示返回值和参数,详情参见苹果API符号表:Type Encodings

@implementation Person
/**
 void(*)()
 默认方法都有两个隐式参数,
 默认一个方法都有两个参数,self,_cmd,隐式参数
 self:方法调用者
 _cmd:调用方法的编号
*/
void eat(id self,SEL sel)
{
  NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
 
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
 
<!--动态添加方法,首先实现这个resolveInstanceMethod-->
<!-- 调用时间:当调用了没有实现的方法没有实现就会调用-->
<!-- 作用:知道哪些方法没有实现,从而动态添加方法-->
<!-- sel:没有实现方法-->
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
 
  if (sel == @selector(eat)) {
    // 动态添加eat方法
    class_addMethod(self, @selector(eat), eat, "v@:");
  }
 
  return [super resolveInstanceMethod:sel];
}
@end

上面代码的resolveInstanceMethod是拦截实例方法的调用,还有类似的系统方法resolveClassMethod拦截类方法的调用。

method swizzling 交换方法

系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。此时能想到的方法有:一、继承系统类,重写方法;二、使用Runtime交换方法

使用场景:

  1. viewWillAppear:viewDidAppear:等生命周期上添加POA点(对 App 的用户行为进行追踪和分析),Blog
  2. 比如调用imageNamed时可以知道图片是否加载
  3. 在AFNetworking中也有应用,AFN中利用runtime将访问网络的方法做了替换,替换后可以监听网络连接状态
  4. 还有一些别的类似作用,比如我在网上看到的博文——使用runtime解决3D Touch导致UIImagePicker崩溃的问题

viewWillAppear:方法添加POA点

#import "UIViewController+swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (swizzling)

//load方法会在类第一次加载的时候被调用
+ (void)load{
    //方法交换应该被保证,在程序中只会执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        //获得viewController的生命周期方法的selector
        SEL systemSel = @selector(viewWillAppear:);
        //自己实现的将要被交换的方法的selector
        SEL swizzSel = @selector(swiz_viewWillAppear:);
        //两个方法的Method
        Method systemMethod = class_getInstanceMethod([self class], systemSel);
        Method swizzMethod = class_getInstanceMethod([self class], swizzSel);

        //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
        BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
        if (isAdd) {
            //如果成功,说明类中不存在这个方法的实现
            //将被交换方法的实现替换到这个并不存在的实现
            class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
        }else{
            //否则,交换两个方法的实现
            method_exchangeImplementations(systemMethod, swizzMethod);
        }

    });
}

- (void)swiz_viewWillAppear:(BOOL)animated{
    //这时候调用自己,看起来像是死循环
    //但是其实自己的实现已经被替换了
    [self swiz_viewWillAppear:animated];
    // POA...
    NSLog(@"method swizzle");
}

@end

此时在任何类中调用viewWillAppear:都会先打印 "method swizzle"

补充:Runtime里面的load与initialize方法

在苹果API中的介绍如下:

+initialize 
Initializes the class before it receives its first message. 
// 初始化类之前收到第一个消息,即收到第一个消息的时候调用

+load 
Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
// 当一个类或类调用添加到Objective-C运行;实现这个方法来加载后执行特定类的行为
// 即在类第一次加载的时候被调用

转载于:https://www.cnblogs.com/LongLJ/p/6289591.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值