iOS之Runtime(二)

作者: 践行者_Leng | 来源:发表于2019-08-13 22:46 被阅读4次

    一:@@@《基础篇》@@@

    二:@@@《应用篇》@@@

    本篇将会结合Rutime其动态特性,总结Rutime的具体应用实例。Runtime在开发中的应用大致分为以下几个方面:


    image.png

    一、动态方法交换:Method Swizzling

    实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能。使用到关键方法如下:

    // 获取类方法的Mthod
    Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
    // 获取实例对象方法的Mthod
    Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    // 交换两个方法的实现
    void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
    

    1. 动态方法交换示例

    代码示例:在视图控制中,定义两个实例方法 playGFA 与 playGFB,然后执行交换

    // 导入运行时文件
    #import <objc/runtime.h>
    
    //  调用方法
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self startExchangeFunAction];
    }
    
    -(void)startExchangeFunAction{
        
        // 说明: 获取类方法 class_getClassMethod
        //   1. 获取实例方法A B
        Method funA=class_getInstanceMethod([self class], @selector(playGFA));
        Method funB=class_getInstanceMethod([self class], @selector(playGFB));
        
        // 2. 交换方法A和B
        method_exchangeImplementations(funA, funB);
        
        // 3. 调用两个方法
        [self playGFA];
        [self playGFB];
    }
    -(void)playGFA{
        NSLog(@"快到碗里来!");
    }
    -(void)playGFB{
        NSLog(@"来了,老弟(❤)");
    }
    
    最终运行结果:
    
    TestModel[90052:13941169] 来了,老弟(❤)
    TestModel[90052:13941169] 快到碗里来!
    

    2. 拦截并替换系统方法

    拦截并替换系统方法的示例:实现不同机型上的字体都按照比例适配,我们可以拦截系统 UIFont 的 systemFontOfSize: 方法,具体操作如下:

    在当前工程中添加UIFont的分类:UIFont +CustomFont ,并在其中添用以替换的方法。

    // UIFont+CustomFont.h 头文件
    #import <UIKit/UIKit.h>
    @interface UIFont (CustomFont)
    @end
    
    
    // UIFont+CustomFont.m 头文件
    #import "UIFont+CustomFont.h"
    #import "objc/runtime.h"   // 导入运行时文件
    
    @implementation UIFont (CustomFont)
    
    +(void)load{
        Method systemFontMethod = class_getClassMethod([UIFont class], @selector(systemFontOfSize:));
        Method replaceFontMethod = class_getClassMethod([UIFont class], @selector(replaceSystemFontFun:));
        method_exchangeImplementations(systemFontMethod, replaceFontMethod);
    }
    
    +(UIFont *)replaceSystemFontFun:(CGFloat)fontSize{
        
        CGFloat screenWValue=[UIScreen mainScreen].bounds.size.width;
        CGFloat ratioValue=screenWValue/375.0;
        
        return [UIFont replaceSystemFontFun:fontSize*ratioValue];
    }
    
    @end
      
      
      
    运用一下:
    
    切换不同的模拟器,观察在不同机型上文字的大小:
      
     - (void)viewDidLoad {
        [super viewDidLoad];
       
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 40)];
        
        label.text = @"一句话,一辈子";
        label.font = [UIFont systemFontOfSize:20];
        label.textColor=[UIColor whiteColor];
        label.backgroundColor=[UIColor darkGrayColor];
        [self.view addSubview:label];
     
    }
    

    二、实现分类添加新属性

    给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上,下面是相关的三个方法:

    /**
     1.给对象设置关联属性
     @param object 需要设置关联属性的对象,即给哪个对象关联属性
     @param key 关联属性对应的key,可通过key获取这个属性,
     @param value 给关联属性设置的值
     @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
     OBJC_ASSOCIATION_ASSIGN             @property(assign)。
     OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
     OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
     OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
     OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
     */
    void objc_setAssociatedObject(id _Nonnull object,
                                  const void * _Nonnull key,
                                  id _Nullable value,
                                  objc_AssociationPolicy policy)
    /**
     2.通过key获取关联的属性
     @param object 从哪个对象中获取关联属性
     @param key 关联属性对应的key
     @return 返回关联属性的值
     */
    id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                          const void * _Nonnull key)
    /**
     3.移除对象所关联的属性
     @param object 移除某个对象的所有关联属性
     */
    void objc_removeAssociatedObjects(id _Nonnull object)
    

    注意:key 与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key。

    一个代码示例:为 UIImage 增加一个分类:UIImage+AddUrl,并为其设置关联属性urlString(图片网络链接属性),相关代码如下:

    // #import "UIImage+AddUrl.h" // 头文件中
    
    #import <UIKit/UIKit.h>
    @interface UIImage (AddUrl)
    
    // 添加的属性urlString
    @property (nonatomic,copy)NSString *urlString;
    
    // 移除关联的属性操作
    -(void)removeAssociatedObjectAction;
    
    @end
     
      
     
    // #import "UIImage+AddUrl.m" // 实现文件中
    #import "UIImage+AddUrl.h"
    #import <objc/runtime.h>
      
    @implementation UIImage (AddUrl)
    
    // Setting方法
    -(void)setUrlString:(NSString *)urlString{
        // 设置关联的属性
        objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    // Getting方法
    -(NSString *)urlString{
        // 获取关联的属性
        return objc_getAssociatedObject(self, @selector(urlString));
    }
    
    // 移除关联的属性操作
    -(void)removeAssociatedObjectAction{
        objc_removeAssociatedObjects(self);
    }
    
    @end
      
      
      
    在控制器中导入 #import "UIImage+AddUrl.h"
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self addCategoryPropertyAction];
    }
    
    // 使用分类中的属性
    -(void)addCategoryPropertyAction{
    
        UIImage *imageObj=[[UIImage alloc]init];
        imageObj.urlString=@"https://www.baidu.com";
        NSLog(@"图片的地址是: %@",imageObj.urlString);
      
        [imageObj removeAssociatedObjectAction];  // 移除关联属性操作
        NSLog(@"移除关联后的属性值:%@",imageObj.urlString);
    }
      
    最终运行结果:
    
    
    TestModel[95333:14775645] 图片的地址是: https://www.baidu.com
    TestModel[95333:14775645] 移除关联后的属性值:(null)
    

    三、获取类的详细信息

    1. 获取属性列表( class_copyPropertyList )

    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(@"下标是:(%d): 属性名:%@",i,[NSString stringWithUTF8String:propertyName]);
    }
    free(propertyList);
    

    2. 获取所有成员变量( class_copyIvarList )

    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (int i= 0; i<count; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSLog(@"下标是:(%d): 成员变量名:%@", i, [NSString stringWithUTF8String:ivarName]);
    }
    free(ivarList);
    

    3.获取所有方法( class_copyMethodList )

    unsigned int count;
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL mthodName = method_getName(method);
        NSLog(@"下标是:(%d) 方法名是:%@",i,NSStringFromSelector(mthodName));
    }
    free(methodList);
    

    4.获取当前遵循的所有协议( class_copyProtocolList )

    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (int i=0; i<count; i++) {
        Protocol *protocal = protocolList[i];
        const char *protocolName = protocol_getName(protocal);
        NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
    }
    free(propertyList);
    

    注意:C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏

    四、解决同一方法高频率调用的效率问题

    Runtime 源码中的 IMP作为函数指针指向方法的实现。通过它,我们可以绕开发送消息的过程来提高函数调用的效率。当我们需要持续大量重复调用某个方法的时候,会十分有用,具体代码示例如下:

    void (*setter)(id, SEL, BOOL);
    int i;
    
    setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
    for ( i = 0 ; i < 1000 ; i++ ){
       setter(targetList[i], @selector(setFilled:), YES);
    }
        
    

    五、方法动态解析与消息转发

    该部分可以参考基础篇中内容,这里不再重复赘述,只是大概做出一些总结。

    1.动态方法解析:动态添加方法

    Runtime足够强大,能够让我们在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:
    场景1:动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
    场景2:利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法。方法动态解析主要用到的方法如下:

    //OC方法:
    //类方法未找到时调起,可于此添加类方法实现
    + (BOOL)resolveClassMethod:(SEL)sel
    
    //实例方法未找到时调起,可于此添加实例方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel
    
    //Runtime方法:
    /**
     运行时方法:向指定类中添加特定方法实现的操作
     @param cls 被添加方法的类
     @param name selector方法名
     @param imp 指向实现方法的函数指针
     @param types imp函数实现的返回值与参数类型
     @return 添加方法是否成功
     */
    BOOL class_addMethod(Class _Nullable cls,
                         SEL _Nonnull name,
                         IMP _Nonnull imp,
                         const char * _Nullable types)
    

    六、动态操作属性

    1.动态修改属性变量

    现在假设这样一个情况:我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName,这样的操作我们就可以使用Runtime可以动态修改对象属性。

    基本思路:首先使用Runtime获取Peson对象的所有属性,找到nickName,然后使用ivar的方法修改其值。具体的代码示例如下:

    Person *ps = [[Person alloc] init];
    NSLog(@"昵称是: %@",[ps valueForKey:@"nickName"]); //null
    
    //第一步:遍历对象的所有属性
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([ps class], &count);
    for (int i= 0; i<count; i++) {
        //第二步:获取每个属性名
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSString *propertyName = [NSString stringWithUTF8String:ivarName];
        if ([propertyName isEqualToString:@"_nickName"]) {
          
            //第三步:匹配到对应的属性,然后修改;注意属性带有下划线
            object_setIvar(ps, ivar, @"降龙十八掌");
        }
    }
    NSLog(@"昵称是: %@",[ps valueForKey:@"nickName"]); 
    

    总结:此过程类似 KVC 的取值和赋值

    2.实现 NSCoding 的自动归档和解档

    归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。

    归档操作主要涉及两个方法:encodeObject 和 decodeObjectForKey,现在,我们可以利用Runtime来改进它们,关键的代码示例如下:

    // 原理:使用Runtime动态获取所有属性
    // 解档操作
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        self = [super init];
        if (self) {
            unsigned int count = 0;
            // 获取类中的成员变量
            Ivar *ivarList = class_copyIvarList([self class], &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivarList[i];
                const char *ivarName = ivar_getName(ivar);
                NSString *key = [NSString stringWithUTF8String:ivarName];
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
            free(ivarList); // 释放指针
        }
        return self;
    }
    
    
    // 归档操作
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        unsigned int count = 0;
        
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (NSInteger i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivarList); // 释放指针
    }
    

    下面是有关归档的测试代码:

    // --测试归档
    Person *ps = [[Person alloc] init];
    ps.name = @"乔峰";
    ps.age  = 23;
    NSString *temp = NSTemporaryDirectory();
    NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
    [NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];
    
    // --测试解档
    NSString *temp = NSTemporaryDirectory();
    NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
    Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
    NSLog(@"名字:%@,年龄:%ld",person.name,person.age); 
    
    

    3. 实现字典与模型的转换

    更新中😁...

    相关文章

      网友评论

        本文标题:iOS之Runtime(二)

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