美文网首页
Runtime基本应用

Runtime基本应用

作者: Queen_BJ | 来源:发表于2020-09-16 15:11 被阅读0次

1、方法交换(Method-Swizzling黑魔法)

  + (void)load
{
    Method test = class_getInstanceMethod(self, @selector(test));

    Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

    method_exchangeImplementations(test, otherTest);
}

应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息。
再例如更换全局UILabel默认字体,可以通过Method Swizzling替换UILabel初始方法来修改等

因为Method Swizzling的实现模式比较固定,所以将其抽象为一个分类,可以直接方便调用

#import <Foundation/Foundation.h>

@interface NSObject (Swizzling)

+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector;
@end

#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzling)

+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                        bySwizzledSelector:(SEL)swizzledSelector{

    Class class = [self class];
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzleMethod = class_getInstanceMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }
}
@end
  • class_addMethod:会覆盖父类的方法实现,但不会取代本类中已存在的实现。如果本类中包含一个同名的实现,则函数返回NO。这里为源SEL添加IMP是为了避免源SEL没有实现IMP的情况。若添加成功则说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP;若添加失败则说明源SEL已经有IMP了,直接将两个SEL的IMP交换就可以了

  • class_replaceMethod
    该函数的行为分为两种:如果类中不存在name指定的方法,则类似于clss_addMethod函数一样会添加方法;
    如果类中已存在name指定的方法,则类似于method_setImplementation一样代替原方法的实现。

实现第一个场景:跟踪程序每个ViewController展示给用户的次数,可以通过Method Swizzling替换ViewDidAppear初始方法。

创建一个UIViewController的分类,重写自定义的ViewDidAppear方法,并在其+load方法中实现ViewDidAppear方法的交换。

#import <UIKit/UIKit.h>
@interface UIViewController (Swizzling)
@end

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"

@implementation UIViewController (Swizzling)
+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [UIViewController methodSwizzlingWithOriginalSelector:@selector(viewDidAppear:)
                                           bySwizzledSelector:@selector(my_ViewDidAppear:)];

    });
    }
-(void) my_ViewDidAppear:(BOOL)animated{
    [self my_ViewDidAppear:animated];
    NSLog(@"===== %@ viewDidAppear=====",[self class]);
}
@end

注意:

  • Swizzling应该总在+load中执行。
  • 在OC中,Runtime会在类初始加载时调用+load方法,在类第一次被调用时实现+initialize方法。由于Method Swizzling会影响到类的全局状态,所以要尽量避免在并发处理中出现竞争情况。+load方法能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。
  • 要使用dispatch_once执行方法交换
    方法交换要求线程安全,而且保证在任何情况下只能交换一次。
//例如:image 的方法交换

#import "UIImage+swizzing.h"

 @implementation UIImage (swizzing)

+ (void)load{

    NSLog(@"imae laod");

    Method oldMethod = class_getClassMethod(self, @selector(imageNamed:));

    Method newMethod = class_getClassMethod(self, @selector(sw_imageWithName:));

  method_exchangeImplementations(oldMethod, newMethod);

}

+ (UIImage *)sw_imageWithName:(NSString *)imageName{

     // 这里调用sw_imageWithName,相当于调用imageName

    UIImage *image = [UIImage sw_imageWithName:imageName];

    if (!image) {

        NSLog(@"imgae nil");

    }

    NSLog(@"new method");

    return image;

}

@end

2、给分类增加属性

开发中常需要在不改变某个类的前提下为其添加一个新的属性,尤其是为系统的类添加新的属性,这个时候就可以利用Runtime的关联对象(Associated Objects)来为分类添加新的属性了。

关联对象是Runtime的一个特性,Runtime中定义了三个将任何键值在Runtime关联到对象上的函数:

  • objc_setAssociatedObject 设置关联对象:
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

  • objc_getAssociatedObject 获取关联对象
    id objc_getAssociatedObject(id object, const void *key)

  • objc_removeAssociatedObjects移除某个对象的所有关联对象,此方法不常用。

为UIImage类添加一个downLoadURL的属性。

#import <UIKit/UIKit.h>

@interface UIImage (downLoadURL)
@property (nonatomic, strong) NSString *downLoadURL;
@end

#import "UIImage+downLoadURL.h"
#import <objc/runtime.h>
@implementation UIImage (downLoadURL)

-(void)setDownLoadURL:(NSString *)downLoadURL{
    objc_setAssociatedObject(self, @selector(downLoadURL), downLoadURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)downLoadURL{
    return objc_getAssociatedObject(self, @selector(downLoadURL));
}
@end

有名的第三方库LTNavigationBar在实现滑动界面时导航栏显隐功能时也使用了Associated Objects,为UINavigation类添加了一个UIView类的属性overlay,使功能实现起来更加简便。大家可以阅读一下源码。

3、实现字典和模型的自动转换

优秀的JSON转模型第三方库JSONModel、YYModel等都利用runtime对属性进行获取,赋值等操作

阅读YYModel的源码可以看出,YY大神对NSObject的内容进行了又一次封装,添加了许多描述内容。其中YYClassInfo是对Class进行了再次封装,而YYClassIvarInfo、YYClassMethodInfo、YYClPropertyInfo分别是对Class的Ivar、Method和property进行了封装和描述。在提取Class的相关信息时都运用了Runtime。

源码中提取Class的Ivar的相关代码:
unsigned int ivarCount = 0;
    Ivar *ivars = class_copyIvarList(cls, &ivarCount);
    if (ivars) {
        NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
        _ivarInfos = ivarInfos;
        for (unsigned int i = 0; i < ivarCount; i++) {
            YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
            if (info.name) ivarInfos[info.name] = info;
        }
        free(ivars);
    }

为了方便大家更好的理解字典转模型,我粗略的写了一个极简陋版的转模型方案供大家理解其思路,更优秀完美的思路建议大家阅读优秀第三方框架的源码。

@interface NSObject (ZCModel)
+(instancetype) setModelWithDict:(NSDictionary*)dict;
@end

@implementation NSObject (ZCModel)
+(instancetype) setModelWithDict:(NSDictionary*)dict{

    Class cls = [self class];
    id Model = [[self alloc]init];

    unsigned int count;
    //提取Class的property列表
    objc_property_t *property_t = class_copyPropertyList(cls, &count);
    //遍历列表,对每个property分别处理
    for (int i =0; i< count; i++) {
        objc_property_t property = property_t[i];
        NSString *key = [NSString stringWithUTF8String:property_getName(property)];

        id value = dict[key];
        if (!value) continue;
        //提取property的attribute信息
        NSString *attribure = [NSString stringWithUTF8String:property_getAttributes(property)];
        //从attribute信息中提取其class的信息
        if ([attribure hasPrefix:@"T@"]) {
            NSRange range =  [attribure rangeOfString:@"\","];
            NSString *typeString = [attribure substringWithRange:NSMakeRange(3, range.location  - 3)];

            NSLog(@"the property class is %@",typeString);
            //对非NS开头的class处理为嵌套的model,对model进行递归,转为模型
            if (![typeString hasPrefix:@"NS"]) {

                Class modelClass = NSClassFromString(typeString);
                value = [modelClass setModelWithDict:value];
            }
        }
        //将字典中的值设置给模型
        [Model setValue:value forKeyPath:key];
    }

    free(property_t);
    return Model;
}
@end

4、实现NSCoding的自动归档和自动解档

实现<NSCoding>协议

//获取所有属性

- (NSArray *)propertyOfSelf{

     unsigned int count = 0;

    Ivar *ivarList = class_copyIvarList([self class], &count);

    NSMutableArray *propertyArr = [NSMutableArray array];

    for (int i = 0; i<count; i++) {

        //获取成员属性

        Ivar ivar = ivarList[i];

        //属性名

        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

         //去掉属性名前面的_

        NSString *key = [name substringFromIndex:1];

        [propertyArr addObject:key];

    }

     return [propertyArr copy];

}

//归档
- (void)encodeWithCoder:(NSCoder *)aCoder{

    NSArray *propertyNames = [self propertyOfSelf];

    for (NSString *properName in propertyNames) {

        id value = [self valueForKey:properName];//KVC

        [aCoder encodeObject:value forKey:properName];

    }
}

//解档

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super init];

    if (self) {

        NSArray *propertyNames = [self propertyOfSelf];

        for (NSString *properName in propertyNames) {

            id value = [coder decodeObjectForKey:properName];

            [self setValue:value forKey:properName];//KVC

        }

   }

    return self;
}

//使用
- (IBAction)arch:(id)sender {

    Student *pp = [Student new];

    pp.name = @"xiaoming";

    pp.age = @"11";

    NSLog(@"Name:%@--%@",pp.name,pp.age);

 NSData *data = [NSKeyedArchiver archivedDataWithRootObject:pp];

    [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"sss"];

}
- (IBAction)unarch:(id)sender {

 NSData *data = [[NSUserDefaults standardUserDefaults]objectForKey:@"sss"];

  Student *stu = [NSKeyedUnarchiver unarchiveObjectWithData:data];

   NSLog(@"Name:%@--%@",stu.name,stu.age);
}

5、消息转发(热更新)解决Bug(JSPatch)

JSPatch是个优秀开源项目,在项目里引入极小的引擎文件就可以使用JavaScript调用任何OC的原生接口,替换任意的OC原生方法。
目前主要用于下发JS脚本替换原生OC代码,实时修复线上bug,更多详情可以阅读JSPatch技术文档。
JSPatch能做到通过JS调用和改写OC方法最根本的原因是OC是动态语言,OC上所有方法的调用和类的生成都通过OC Runtime在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法。
所以JSPatch的基本原理是:JS传递字符串给OC,OC通过Runtime接口调用和替换OC方法。
在JSPatch实现方法替换上,通过Selector调用方法时,会从methodList链表里找到对应的Method进行调用,这个methodList上的元素是可以动态替换的,可以把某个Selector对应的函数指针IMP替换成新的,也可以拿到已有的某个Selector对应的函数指针IMP,让另一个Selector跟它对应,Runtime提供了相应的方法实现这些。
现在苹果审核严格可能使用热更新后审核被拒

6、防止按钮多次点击runtime

参考别人简书详细实现

相关文章

网友评论

      本文标题:Runtime基本应用

      本文链接:https://www.haomeiwen.com/subject/yomaghtx.html