近日看了一些 runtime 的资料,结合之前对 runtime 的认识,理解又深了一些。runtime 的使用网上资料很多,不再赘述,此处只分享一些我学习和使用过程中遇到的点。
一:成员变量包括实例变量和类变量,因为 iOS中无真正的类变量,所以,成员变量都是(就是)实例变量。
首先我想大家先搞懂一些基础知识,因为看 runtime 博客的时候会经常看到实例变量和成员变量,如果不清楚两者的关系,有可能会搞混淆,对于相关知识的理解就会很吃力。
java 里面默认情况下,成员变量是实例成员,在外部需要通过对象才能操作。如果用static修饰,就成为了静态成员,也称为类变量,无需通过对象就可以操作。(成员变量包括实例变量和类变量)
对于Object-C ,我查了相关资料说OC不支持真正的类变量。所以应该可以理解为 iOS 中成员变量都是实例变量。
好,接下来咱们聊 runtime。
二:一旦完成类定义,就不能再添加成员变量了
class_addProperty方法添加的属性,系统不会自动添加实例变量,即不会在class_copyIvarList内获取到该属性的成员变量。有别于在类定义时(category 里@property 修饰,系统也不会自动添加实例变量)直接用@property 修饰
在Objective-C提供的runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是文档中特别说明:
This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
意思是说,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
以下是代码验证:
.h文件
@interface SomeClass : NSObject {
NSString *age;
NSInteger weight;
}
@property (nonatomic, copy) NSString *aaa;
@end
.m 文件
@implementation SomeClass
-(id)init {
self = [super init];
//添加属性
[self addPropertyWithPropertyName:@"ccc" withValue:@"1234"];
unsigned int count1 = 0;
//获取属性列表
objc_property_t *pros = class_copyPropertyList([SomeClass class], &count1);
for (int i = 0; i < count1; i++) {
objc_property_t pro = pros[i];
NSString *proName = [NSString stringWithCString:property_getName(pro) encoding:NSUTF8StringEncoding];
//***打印1***
NSLog(@"proName:%@\n",proName);
}
free(pros);
unsigned int count = 0;
//获取成员变量列表
Ivar *members = class_copyIvarList([SomeClass class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = members[i];
NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
//***打印2***
NSLog(@"ivarName:%@\n",ivarName);
}
free(members);
return self;
}
-(void)addPropertyWithPropertyName:(NSString *)propertyName withValue:(id)value{
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([value class])] UTF8String] };
objc_property_attribute_t ownership = { "&", "N" };
objc_property_attribute_t backingivar = { "V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] };
objc_property_attribute_t attrs[] = { type, ownership, backingivar };
if (class_addProperty([self class], [propertyName UTF8String], attrs, 3)) {
//赋值
[self setValue:value forKey:propertyName];
NSLog(@"%@", [self valueForKey:propertyName]);
NSLog(@"创建属性Property成功");
}
}
打印1:
2017-02-15 18:21:43.934774 SomeThingTest[2715:953599] proName:ccc
2017-02-15 18:21:43.934788 SomeThingTest[2715:953599] proName:aaa
此处打印的是.h 里的aaa,运行时添加的属性ccc
打印2:
2017-02-15 18:21:43.934876 SomeThingTest[2715:953599] ivarName:age
2017-02-15 18:21:43.934897 SomeThingTest[2715:953599] ivarName:weight
2017-02-15 18:21:43.934910 SomeThingTest[2715:953599] ivarName:_aaa
此处打印的是.h 里的实例变量 age,weight 以及@property 修饰的 aaa,系统自动生成的实例变量_aaa.
三:Method Swizzling 实现友盟页面统计。我把下载链接放到下面了
应该大多数应用都有页面统计的要求。大多实现,1.最冗余的做法就是每个 VC都写上相关代码。2.好点的写个 Base 类,在该类中写上统计的相关代码,开发中让其他 VC 继承自改 Base类。
做法2肯定比做法1简洁了很多,但其实并不好维护, 比如项目进来个新人,或者交接时必须告知new 新的 ViewController时继承该 Base 类,而且有些同事要写一个 tableView 的界面,可能他直接创建了一个 继承自UITableViewController的 VC…
所以这个时候就该 Method Swizzling出场了,当然也许还有更好的方法。此处用 swizzling的原理就是用一个新的方法的实现和系统方法的实现作交换,这个新方法的实现包括系统方法的实现,同时还注入了一些自定义的行为,自定义行为就是我们想要做的操作。
首先 new 一个 category,UIViewController 的分类,此处我命名为UIViewController+Swizzling。
+ (void)load 方法是在运行期提前并且自动调用的方法。我们可以利用他们在类被使用前,做一些预处理工作。父类,子类,分类的 load 方法依次调用,在子类或者分类重写 load 方法不用调用[super load]; 在category 里除了+ (void)load 方法,重写类的同名方法会覆盖该类这个方法。
使用dispatch_once保证 method swizzling 只发生一次,避免手动调用+ (void)load交换方法实现后还原到最初状态。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self exchangeViewWillAppear ];
[self exchangeViewWillDisappear];
});
}
//交换 viewWillAppear
+ (void)exchangeViewWillAppear{
// 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
Method fromMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewWillAppear));
/**
* 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
* 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/
if (!class_addMethod([self class], @selector(swizzlingViewWillAppear), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
//交换 viewWillDisappear
+ (void)exchangeViewWillDisappear{
Method fromMethod = class_getInstanceMethod([self class], @selector(viewWillDisappear:));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewWillDisappear));
if (!class_addMethod([self class], @selector(swizzlingViewWillAppear), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
/*
我们自己实现的用来替换viewWillAppear的方法
*/
- (void)swizzlingViewWillAppear {
NSString *class = NSStringFromClass(self.class);//[NSString stringWithFormat:@"%@", self.class];
// 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![class containsString:@"UI"]){
NSLog(@"统计打点-beginLogPageView : %@", self.class);
[MobClick beginLogPageView:class];
}
//此处调的方法内部实现已替换成 viewWillAppear 方法
[self swizzlingViewWillAppear];
}
/*
我们自己实现的用来替换viewWillDisappear的方法
*/
- (void)swizzlingViewWillDisappear {
NSString *class = NSStringFromClass(self.class);//[NSString stringWithFormat:@"%@", self.class];
// 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![class containsString:@"UI"]){
NSLog(@"统计打点-endLogPageView : %@", self.class);
[MobClick endLogPageView:class];
}
//此处调的方法内部实现已替换成 viewWillDisappear 方法
[self swizzlingViewWillDisappear];
}