• iOS runtime的理解和应用


    项目中经常会有一些的功能模块用到runtime,最近也在学习它.对于要不要阅读runtime的源码,我觉得仅仅是处理正常的开发,那真的没有必要,只要把常用的一些函数看下和原理理解下就可以了.
    但是如果真能静下心好好阅读源码,真的能帮你更加深入理解objc本身以及经过高阶包装出来的那些特性。

    什么是runtime

    runtime就是运行时,每个语言都有它的runtime.通俗点讲就是程序运行时发生的事情.
    比如C语言,在编译的时候就决定了调用哪些函数,通过编译后就一步步执行下去,没有任何二义性,所以它是静态语言.
    而objc的函数调用则可以理解为发消息,在编译的时候完全不能决定哪个函数执行,只有在运行的时候才会根据函数名找到函数调用,所以在运行的时候它能动态地添加调换属性,函数.所以它是动态语言.
    动态和静态语言没有明显的界限,我感觉它们就是以runtime来区分的,看它在runtime时,有多灵活,那么它就有多动态.

    • 相关定义
    typedef struct objc_method *Method
    struct objc_method {
        SEL method_name;            
        char *method_types;
        IMP method_imp;
    } 
    

    SEL是char*,可以理解为函数的姓名.
    IMP就是函数指针,指向函数的实现.
    在objc_class中method list保存了一个SEL<>IMP的映射.所以通过SEL可以找到函数的实现

    typedef struct objc_ivar *Ivar;
    struct objc_ivar {
        char *ivar_name;
        char *ivar_type;
        int ivar_offset;
    #ifdef __LP64__
        int space;
    #endif
    }                                                            
    

    实例变量,跟某个对象关联,不能被静态方法使用,与之想对应的是类变量

    typedef struct objc_category *Category;
    struct objc_category {
        char *category_name;           
        char *class_name;
        struct objc_method_list *instance_methods;
        struct objc_method_list *class_methods;
        struct objc_protocol_list *protocols;
    }      
    

    Catagory可以动态地为已经存在的类添加新的行为。比如类方法,实例方法,协议.
    根据结构可知,不能添加属性,实例变量

    struct objc_method_list {
        struct objc_method_list *obsolete;
        int method_count;
        int space;
        struct objc_method method_list[1];
    }  
    
    struct objc_ivar_list {
        int ivar_count;
        int space;
        struct objc_ivar ivar_list[1];
    }                                                                    
    

    简单地理解为存有方法和实例变量的数组

    //类在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
    };
    
    struct objc_cache {
        unsigned int mask;
        unsigned int occupied;
        Method buckets[1];
    };
    

    objc_cache可以理解为存最近调用过的方法的数组,每次调用先访问它,提高效率

    runtime常用方法

    • 获取列表
      我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)
    class_copyPropertyList       //获取属性列表
    class_copyMethodList         //获取方法列表     
    class_copyIvarList           //获取成员变量列表
    class_copyProtocolList       //获取协议列表
    

    常见用于字典转模型的需求中:

    @interface LYUser : NSObject
    @property (nonatomic,strong)NSString *userId;
    @property (nonatomic,strong)NSString *userName;
    @property (nonatomic,strong)NSString *age;
    @end
    
    - (void)viewDidLoad {    
        [super viewDidLoad];    
        //利用runtime遍历一个类的全部成员变量
        NSDictionary *userDict = @{@"userId":@"1",@"userName":@"levi",@"age":@"20"};
        unsigned int count;
        LYUser *newUser = [LYUser new];
        objc_property_t *propertyList = class_copyPropertyList([LYUser class], &count);
        for (int i = 0; i < count; i++) {
            const char *propertyName = property_getName(propertyList[i]);
            NSString *key = [NSString stringWithUTF8String:propertyName];
            [newUser setValue:userDict[key] forKey:key];
        }
        NSLog(@"%@--%@--%@",newUser.userId,newUser.userName,newUser.age);     
    }  
    
    

    这只是最简单的转化,还要考虑容错,转换效率,现在有很多开源框架做的很不错.这是一些开源框架的性能对比:模型转换库评测结果

    • 交换方法
    class_getInstanceMethod() //类方法和实例方法存在不同的地方,所以两个不同的方法获得
    class_getClassMethod()    //以上两个函数传入返回Method类型
    method_exchangeImplementations    //()交换两个方法的实现
    

    这个用到的地方很多,可以大大减少我们的代码量,常用的有防错措施,统计打点,统一更新界面效果

    防错措施

    -(void)viewDidLoad
    {
        NSMutableArray *testArray = [NSMutableArray new];
        [testArray addObject:@"1"];
        NSString *a = nil;
        [testArray addObject:a];
        
        for (NSInteger i = 0; i < testArray.count; i++) {
            NSLog(@"%@",testArray[i]);
        }
    }
    
    @implementation NSMutableArray(ErrorLog)
    +(void)load
    {
        Method originAddMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(addObject:));
        Method newAddMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(el_addObject:));
        
        method_exchangeImplementations(originAddMethod, newAddMethod);
    }
    
    /*
     * 自己写的方法实现
     */
    -(void)el_addObject:(id)object
    {
        if (object != nil) {
            [self el_addObject:object];
        }
        else
        {
            //可以添加错误日志
            NSLog(@"数组添加nil");
        }
    }
    @end  
    
    

    统计打点

    和上面的实现方式一致.在对应类的Category的load方法里交换.

    //  统计页面出现
        Method originAddMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
        Method newAddMethod = class_getInstanceMethod([self class], @selector(el_ViewDidLoad));
        method_exchangeImplementations(originAddMethod, newAddMethod);
    
    //  统计Button点击
        Method originAddMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
        Method newAddMethod = class_getInstanceMethod([self class],@selector(el_sendAction:to:forEvent:)));
        
        method_exchangeImplementations(originAddMethod, newAddMethod);
    
    

    统一更新界面效果

    很多时候我们做项目都是先做逻辑,一些页面颜色,细节都是最后做.这就遇到了一些问题,可能只是改个cell右边箭头边距,placeholder默认颜色.如果一个个改过来又麻烦又有可能有疏漏,这个时候runtime就可以大显神通了.

    //这个就可以统一cell右边箭头格式,非常方便
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            
            SEL originalSelector = @selector(layoutSubviews);
            SEL swizzledSelector = @selector(swizzling_layoutSubviews);
            
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        });
    }
    
    //设置cell右边箭头
    - (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType {
        if (accessoryType == UITableViewCellAccessoryDisclosureIndicator) {
            UIImageView *accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"about_arrow_icon"]];
            accessoryView.centerY = self.centerY;
            accessoryView.right = self.width-16;
            self.accessoryView = accessoryView;
        } else if (accessoryType == UITableViewCellAccessoryNone) {
            self.accessoryView = nil;
        }
    }
    
    //设置cell右边箭头间距
    - (void)swizzling_layoutSubviews {
        [self swizzling_layoutSubviews];
        if (self.imageView.image) {
            self.imageView.origin = CGPointMake(16, self.imageView.origin.y);
            self.textLabel.origin = CGPointMake(CGRectGetMaxX(self.imageView.frame)+10, self.textLabel.origin.y);
        } else {
            self.textLabel.origin = CGPointMake(16, self.textLabel.origin.y);
        }
        self.textLabel.width = MIN(self.textLabel.width, 180);
        self.accessoryView.right = self.width-16;
    }  
    
    
    • 关联对象
    objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    
    objc_getAssociatedObject(id object, const void *key)  
    
    

    前面已经讲过,Category不能添加属性,通过关联对象就可以在运行时动态地添加属性.
    这可是神器,对于封装代码很有用,例如很常见的,textField限制长度.每个都在delegate里重复代码肯定不行.自己写个自定义textField,better,不过还是有点麻烦.而runtime就可以很优雅地解决问题.

    .h
    @interface UITextField (TextRange)
    @property (nonatomic, assign) NSInteger maxLength;   //每次限制的长度设置下就行了
    @end
    
    .m
    - (void)dealloc {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    
    - (void)setMaxLength:(NSInteger)maxLength {
        objc_setAssociatedObject(self, KTextFieldMaxLength, @(maxLength), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        [self textField_addTextDidChangeObserver];
    }
    
    - (NSInteger)maxLength {
        return [objc_getAssociatedObject(self, KTextFieldMaxLength) integerValue];
    }
    
    #pragma mark - Private method
    - (void)textField_addTextDidChangeObserver {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textField_textDidChange:) name:UITextFieldTextDidChangeNotification object:self];
    }
    
    #pragma mark - NSNotificationCenter action
    - (void)textField_textDidChange:(NSNotification *)notification {
        UITextField *textField = notification.object;
        NSString *text = textField.text;
        MYTitleInfo titleInfo = [text getInfoWithMaxLength:self.maxLength];
        if (titleInfo.length > self.maxLength) {
            UITextRange *selectedRange = [textField markedTextRange];
            UITextPosition *position = [textField positionFromPosition:selectedRange.start offset:0];
            if (!position) {
                UITextRange *textRange = textField.selectedTextRange;
                textField.text = [textField.text subStringWithMaxLength:self.maxLength];
                textField.selectedTextRange = textRange;
            }
        }
    }  
    
    

    以上就是关于runtime最常用的介绍,我还在学习当中,会不停地完善,和大家分享进步.
    最后给大家一个学习runtime的小技巧,毕竟看源码真的很枯燥,可以去github上输入import <objc/runtime.h>,就可以看到用到runtime的实例,使学习更有目标和动力.

  • 相关阅读:
    sublime使用及插件
    Unity 查找
    Unity 3D 的四种坐标系
    C#知识点<4>
    C#知识点<3>
    C#知识点<2>
    排序算法
    OOP的三大特性------封装、继承、多态
    C#常用函数
    C++-------------类和对象
  • 原文地址:https://www.cnblogs.com/stevenfukua/p/5578196.html
Copyright © 2020-2023  润新知