当前位置:   article > 正文

iOS-runtime应用场景_ios runtime应用

ios runtime应用

1、私有变量

当我们需要访问一个.m中的私有属性的时候,需要通过class_getInstanceVariableobject_getIvar来获取我们想要的属性;
object_setIvar 来更改私有属性。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    NSLog(@"%@", person.p_description);
    // 获取所有属性
    unsigned  int count = 0;
    Ivar *ivarList = class_copyIvarList([Person class], &count);
    for (int i = 0; i < count; i++){
        Ivar var = ivarList[i];
        const char *memberAddress = ivar_getName(var);
        const char *memberType = ivar_getTypeEncoding(var);
        NSLog(@"获取属性: %s ; type = %s",memberAddress,memberType);
    }
    
    // 访问私有属性
    Ivar ivar = class_getInstanceVariable([Person class], "_gender");
    id gender =  object_getIvar(person, ivar);
    NSLog(@"初始私有:%@",gender);
    
    // 修改私有属性
    object_setIvar(person, ivar, @"女");
    NSLog(@"%@", person.p_description);
}

打印结果
2020-08-24 14:20:03.504731+0800 ModuleProject[8849:237982] name:乔峰,age:18,gender:2020-08-24 14:20:03.504942+0800 ModuleProject[8849:237982] 获取属性: _age ; type = i
2020-08-24 14:20:03.505055+0800 ModuleProject[8849:237982] 获取属性: _name ; type = @"NSString"
2020-08-24 14:20:03.505157+0800 ModuleProject[8849:237982] 获取属性: _gender ; type = @"NSString"
2020-08-24 14:20:03.505260+0800 ModuleProject[8849:237982] 初始私有:男
2020-08-24 14:20:03.505367+0800 ModuleProject[8849:237982] name:乔峰,age:18,gender:

2、动态添加方法

上一篇文章中消息转发时运用到,当方法不存在时通过动态添加方法避免崩溃。

3、分类添加属性

通过runtime动态添加属性,可以给系统类添加自定义属性。比如UIButton增加点击范围。
主要用到objc_setAssociatedObject添加属性,objc_getAssociatedObject获取属性。

// UIButton+Area.h
@interface UIButton (Area)
// 设置点击范围
- (void)setEnlargeEdgeWithTop:(CGFloat) top right:(CGFloat) right bottom:(CGFloat) bottom left:(CGFloat) left;
@end

// UIButton+Area.m
// 设置点击范围
- (void) setEnlargeEdgeWithTop:(CGFloat) top right:(CGFloat) right bottom:(CGFloat) bottom left:(CGFloat) left
{
    objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

// 获取点击范围
- (CGRect) enlargedRect
{
    NSNumber* topEdge = objc_getAssociatedObject(self, &topNameKey);
    NSNumber* rightEdge = objc_getAssociatedObject(self, &rightNameKey);
    NSNumber* bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
    NSNumber* leftEdge = objc_getAssociatedObject(self, &leftNameKey);
    if (topEdge && rightEdge && bottomEdge && leftEdge)
    {
        return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
                          self.bounds.origin.y - topEdge.floatValue,
                          self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    }
    else
    {
        return self.bounds;
    }
}

// 拦截点击->重置范围
- (UIView*) hitTest:(CGPoint) point withEvent:(UIEvent*) event
{
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds))
    {
        return [super hitTest:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? self : nil;
}

// 使用
- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *areaButton = [[UIButton alloc] init];
    [areaButton setEnlargeEdgeWithTop:10 right:10 bottom:10 left:10];
}

4、Method swizzling(方法交换)

可以将系统的方法转换成自定义的方法,在一些特殊的场景;在真实项目开发中,会全局统一某控件样式,以导航栏返回按钮为例,通常项目中只需要显示返回箭头不需要显示文字。
这里我们仍可以通过Runtime Method Swizzling来实现该需求:
给UIViewController添加类别,然后在load方法里面用Method Swzilling方法替换 交换ViewDidAppear

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzling_viewDidAppear);
        //原有方法
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        //替换原有方法的新方法
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        // 先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况(这种情况是原方法继承自父类,而本身并没有实现)
        BOOL didAddMethod = class_addMethod(class,originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            //添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
            class_replaceMethod(class,swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            //添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

 - (void)swizzling_viewDidAppear {
    self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc]
    initWithTitle:@""
    style:UIBarButtonItemStylePlain
    target:self
    action:nil];
    [self swizzling_viewDidAppear];
}

这里会有几个疑问

  • 为什么方法交换调用在+load方法中
    +load方法是在类被加载的时候调用的,一定会被调用。在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法造成覆盖。
  • 为什么方法交换要在dispatch_once中执行
    方法交换应该要线程安全,而且保证只交换一次,防止再次调用又将方法交换回来。 最常用的解决方案是在+load方法中使用dispatch_once来保证交换是安全的。
  • 总是调用方法的原始实现
    看起来[self swizzling_viewDidAppear]; 可能导致无限循环但是并没有,因为在swizzling的过程中,swizzling_viewDidAppear已经被重新指向UIViewController 的原始实现viewWillAppear:,但是如果我们在这个方法中调用viewWillAppear:则会导致无限循环。

5、json转model

网上有很多开源库, 比如YYModel, MJExtension等等, 都可用于json解析,底层的实现原理莫过于此。

// NSObject+Json2Model

// 获取所有属性
+(void)allProperties:(NSMutableArray *)keys aClass:(Class)aClass
{
    u_int count = 0;
    
    //到达系统类停止获取(可能有问题)
    if ([NSStringFromClass(aClass) isEqualToString:@"NSObject"]) {
        return;
    }
    
    //获取类的所有属性列表
    objc_property_t *properties = class_copyPropertyList(aClass, &count);
    for (int i=0; i<count; i++) {
        objc_property_t property = properties[i];
        
        //获取类属性名称
        const char *propertyCString = property_getName(property);
        NSString *propertyName = [NSString stringWithCString:propertyCString encoding:NSUTF8StringEncoding];
        [keys addObject:propertyName];
    }
    free(properties);
    
    //循环获取父类的属性列表
    Class superClass = class_getSuperclass(aClass);
    [self allProperties:keys aClass:superClass];
}

// json -> model
+(instancetype)json2Model:(NSDictionary *)dict
{
    //用来存放所有属性
    NSMutableArray *keys = [[NSMutableArray alloc] init];
    
    //获取当前类及所有父类属性
    [self allProperties:keys aClass:[self class]];
    
    //创建当前模型对象
    id model = [[[self class] alloc] init];
    
    //遍历属性并赋值
    for (NSString *key in keys) {
        if ([dict valueForKey:key]) {
            // 也可通过关联对象设置: objc_setAssociatedObject
            [model setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return model;
}

// 数组类型数据
+ (NSMutableArray *) getListArray:(NSArray *)array
{
    if (array == nil) {
        return [NSMutableArray array];
    }
    NSMutableArray *dataArray = [[NSMutableArray alloc] init];
    for(NSDictionary *dic in array)
    {
        [dataArray addObject: [self json2Model:dic]];
    }
    return dataArray;
}


// 实际应用
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // json -> model
    NSDictionary *dict = @{@"name":@"杨过",@"age":@20,@"gender":@"男"};
    Person *p = [Person json2Model:dict];
    NSLog(@"%@", p.p_description);
    
    // arrar -> model
    NSArray *arr = @[@{@"name":@"乔峰",@"age":@40,@"gender":@"男"}, @{@"name":@"郭靖",@"age":@28,@"gender":@"男"}, @{@"name":@"张无忌",@"age":@18,@"gender":@"男"}];
    NSArray *personArray = [Person getListArray:arr];
    NSLog(@"%@", personArray);
    
    for (int i = 0; i < personArray.count; i++) {
        
        Person *person = personArray[i];
        NSLog(@"%@", person.p_description);
        
    }
}

打印结果
2020-08-25 10:33:50.825494+0800 ModuleProject[2921:97024] name:杨过,age:20,gender:2020-08-25 10:33:50.835247+0800 ModuleProject[2921:97024] (
    "<Person: 0x600001d20f60>",
    "<Person: 0x600001d21300>",
    "<Person: 0x600001d21e20>"
)
2020-08-25 10:33:50.835384+0800 ModuleProject[2921:97024] name:乔峰,age:40,gender:2020-08-25 10:33:50.835512+0800 ModuleProject[2921:97024] name:郭靖,age:28,gender:2020-08-25 10:33:50.835616+0800 ModuleProject[2921:97024] name:张无忌,age:18,gender:

6、热更新

热更新、热修复就是利用OC的消息机制消息发送动态解析消息转发。就是在消息转发阶段动态的添加了方法的实现,以达到热修复的目的。
JSPatch是一个开源项目,虽然苹果禁止使用热更新但是还是有很多值得参考的技术

7、埋点

埋点是现在很多App中都需要用到的,这个问题可能每个人都能处理,但是怎样来减少埋点所带来的侵入性。
举个例子:当我们需要统计进入/离开某个界面是我们可以在当前这个页面的viewDidAppearviewDidDisappear方法向服务器上报数据,但是如果统计界面越来越多这么做不合适了。这个时候就可以通过runtime进行统一处理了。

@implementation UIViewController (Swizzling)

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzling_viewDidAppear);
        [self swizzling:originalSelector swizzled:swizzledSelector];
        
        SEL originalSelector1 = @selector(viewDidDisappear:);
        SEL swizzledSelector1 = @selector(swizzling_viewDidDisappear);
        [self swizzling:originalSelector1 swizzled:swizzledSelector1];
    });
}

 + (void)swizzling:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Class class = [self class];
    //原有方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    //替换原有方法的新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    // 先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况(这种情况是原方法继承自父类,而本身并没有实现)
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        //添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
        class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        //添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

 + (void)swizzling_viewDidAppear {
    [self swizzling_viewDidAppear];
    // 埋点信息:控制器:传递的参数
    NSDictionary *infoDic = @{@"AViewController":@"A控制器"};
    // 获取当前控制器对应的参数
    NSString *name = infoDic[NSStringFromClass([self class])];
    // 标记同一控制器的进入/离开:同一控制器在进入离开应该相同
    NSString *ID = @"id";

    // 上传服务器的数据
    NSMutableDictionary *logInfoDic = [NSMutableDictionary dictionary];
    [logInfoDic setObject:name forKey:@"name"];
    [logInfoDic setObject:ID forKey:@"id"];
    // 进入埋点
    [logInfoDic setObject:@"in" forKey:@"type"];
    // logInfoDic上传到服务器
}

 + (void)swizzling_viewDidDisappear {
    [self swizzling_viewDidDisappear];
    // 埋点信息:控制器:传递的参数
    NSDictionary *infoDic = @{@"AViewController":@"A控制器"};
    // 获取当前控制器对应的参数
    NSString *name = infoDic[NSStringFromClass([self class])];
    // 标记同一控制器的进入/离开:同一控制器在进入离开应该相同
    NSString *ID = @"id";

    // 上传服务器的数据
    NSMutableDictionary *logInfoDic = [NSMutableDictionary dictionary];
    [logInfoDic setObject:name forKey:@"name"];
    [logInfoDic setObject:ID forKey:@"id"];
    // 离开埋点
    [logInfoDic setObject:@"out" forKey:@"type"];
    // logInfoDic上传到服务器
}

通过Method swizzling就可以很好的解决界面进入/离开统计。更深层次的话我们可以进行点击事件的统计,这里只做简单介绍。

  • 创建UIControl分类
  • -(void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;替换成自定义的函数(void)sendSwizzleAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
  • 在该函数内结合target(AViewController)action(selector)根据@{@"AViewController":@[@{@"selector":@[@"name",@"type"]}]};格式查找数据进行埋点处理
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
  

闽ICP备14008679号