赞
踩
当我们需要访问一个.m中的私有属性的时候,需要通过class_getInstanceVariable
和object_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:女
上一篇文章中消息转发时运用到,当方法不存在时通过动态添加方法避免崩溃。
通过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];
}
可以将系统的方法转换成自定义的方法,在一些特殊的场景;在真实项目开发中,会全局统一某控件样式,以导航栏返回按钮为例,通常项目中只需要显示返回箭头不需要显示文字。
这里我们仍可以通过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
方法是在类被加载的时候调用的,一定会被调用。在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法造成覆盖。[self swizzling_viewDidAppear];
可能导致无限循环但是并没有,因为在swizzling的过程中,swizzling_viewDidAppear已经被重新指向UIViewController 的原始实现viewWillAppear:,但是如果我们在这个方法中调用viewWillAppear:则会导致无限循环。网上有很多开源库, 比如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:男
热更新、热修复就是利用OC的消息机制消息发送
,动态解析
,消息转发
。就是在消息转发
阶段动态的添加了方法的实现,以达到热修复的目的。
JSPatch
是一个开源项目,虽然苹果禁止使用热更新但是还是有很多值得参考的技术
埋点是现在很多App中都需要用到的,这个问题可能每个人都能处理,但是怎样来减少埋点所带来的侵入性。
举个例子:当我们需要统计进入/离开某个界面是我们可以在当前这个页面的viewDidAppear
、viewDidDisappear
方法向服务器上报数据,但是如果统计界面越来越多这么做不合适了。这个时候就可以通过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"]}]};
格式查找数据进行埋点处理Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。