运行时机制
runtime是一套比较底层的纯C语言的API, 属于C语言库, 包含了很多底层的C语言API。
在我们平时编写的iOS代码中, 最终都是转成了runtime的C语言代码。
所谓运行时,也就是在编译时是不存在的,只是在运行过程中才去确定对象的类型、方法等。利用Runtime机制可以在程序运行时动态修改类、对象中的所有属性、方法等。
还记得我们在网络请求数据处理时,调用了-setValuesForKeysWithDictionary:方法来设置模型的值。这里什么原理呢?为什么能这么做?其实就是通过Runtime机制来完成的,内部会遍历模型类的所有属性名,然后设置与key对应的属性名的值。
我们在使用运行时的地方,都需要包含头文件:#import <objc/runtime.h>。如果是Swift就不需要包含头文件,就可以直接使用了。
runtime基础
1. 获取对象所有属性名
利用运行时获取对象的所有属性名是可以的,但是变量名获取就得用另外的方法了。我们可以通过class_copyPropertyList方法获取所有的属性名称。
- 第一个参数:类
- 第二个参数:存放属性个数的地址
下面我们通过一个Person类来学习,这里的方法没有写成扩展,只是为了简化,将获取属性名的方法直接作为类的实例方法:
Objective-C版
@interface Person : NSObject{
NSString *_variableString;
}
// 默认会是什么呢?
@property (nonatomic, copy) NSString *name;
// 默认是strong类型
@property (nonatomic, strong) NSMutableArray *array;
// 获取所有的属性名
- (NSArray *)allProperties;
@end
下面主要是写如何获取类的所有属性名的方法。注意,这里的objc_property_t是一个结构体指针objc_property *,因此我们声明的properties就是二维指针。在使用完成后,我们一定要记得释放内存,否则会造成内存泄露。这里是使用的是C语言的API,因此我们也需要使用C语言的释放内存的方法free。
@implementation Person
typedef struct objc_property *objc_property_t;
- (NSArray *)allProperties {
unsigned int count;
// 获取类的所有属性
// 如果没有属性,则count为0,properties为nil
objc_property_t *properties = class_copyPropertyList([self class], &count);
NSMutableArray *propertiesArray = [NSMutableArray arrayWithCapacity:count];
for (NSUInteger i = 0; i < count; i++) {
// 获取属性名称
const char *propertyName = property_getName(properties[i]);
NSString *name = [NSString stringWithUTF8String:propertyName];
[propertiesArray addObject:name];
}
// 注意,这里properties是一个数组指针,是C的语法,
// 我们需要使用free函数来释放内存,否则会造成内存泄露
free(properties);
return propertiesArray;
}
来测试一下,我们的方法是否正确获取到了呢?看下面的打印结果就明白了吧
Person *p = [[Person alloc] init];
p.name = @"Lili";
size_t size = class_getInstanceSize(p.class);
NSLog(@"size=%ld", size);
for (NSString *propertyName in p.allProperties) {
NSLog(@"%@", propertyName);
}
// 打印结果:
// 2016-04-19 11:37:24.589 LSRuntimeOCDemo[1554:108843] size=32
// 2016-04-19 11:37:24.590 LSRuntimeOCDemo[1554:108843] name
// 2016-04-19 11:37:24.590 LSRuntimeOCDemo[1554:108843] array
Swift版
对于Swift版,使用C语言的指针就不容易了,因为Swift希望尽可能减少C语言的指针的直接使用,因此在Swift中已经提供了相应的结构体封装了C语言的指针。但是看起来好复杂,使用起来好麻烦。看看Swift版的获取类的属性名称如何做:
class Person: NSObject {
var name: String = ""
var hasBMW = false
override init() {
super.init()
}
func allProperties() ->[String] {
// 这个类型可以使用CUnsignedInt,对应Swift中的UInt32
var count : UInt32 = 0
let properties = class_copyPropertyList(Person.self, &count)
///定义一个元素为字符串的数组
var propertyNames: [String] = []
// Swift中类型是严格检查的,必须转换成同一类型
for var i in 0..<Int(count) {
// UnsafeMutablePointer<objc_property_t>是可变指针,因此properties就是类似数组一样,可以通过下标获取
let property = properties[i]
let name = property_getName(property)
// 这里还得转换成字符串
let strName = String.fromCString(name)
propertyNames.append(strName!)
}
// 不要忘记释放内存,否则C语言的指针很容易成野指针的
free(properties)
return propertyNames
}
}
关于Swift中如何C语言的指针问题,这里不细说,如果需要了解,请查阅相关文章。
测试一下是否获取正确:
let p = Person()
p.name = "Lili"
// 打印结果:["name", "hasBMW"],说明成功
print( p.allProperties() )
2.获取对象的所有属性名和属性值
对于获取对象的所有属性名,在上面的-allProperties方法已经可以拿到了,但是并没有处理获取属性值,下面的方法就是可以获取属性名和属性值,将属性名作为key,属性值作为value。
Object-C版
- (NSDictionary *)allPropertyNamesAndValues {
NSMutableDictionary *resultDict = [NSMutableDictionary dictionary];
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
const char *name = property_getName(property);
// 得到属性名
NSString *propertyName = [NSString stringWithUTF8String:name];
// 获取属性值
id propertyValue = [self valueForKey:propertyName];
if (propertyValue && propertyValue != nil) {
///如果值为空则不添加进字典
[resultDict setObject:propertyValue forKey:propertyName];
}
}
// 记得释放
free(properties);
return resultDict;
}
测试一下
// 此方法返回的只有属性值不为空的属性
NSDictionary *dict = p.allPropertyNamesAndValues;
for (NSString *propertyName in dict.allKeys) {
NSLog(@"propertyName: %@ propertyValue: %@",
propertyName,
dict[propertyName]);
}
输出结果:
2016-04-19 12:30:49.367 LSRuntimeOCDemo[1983:145600] propertyName: name propertyValue: Lili
Siwft版
func allPropertyNamesAndValues() ->[String: AnyObject] {
var count: UInt32 = 0
let properties = class_copyPropertyList(Person.self, &count)
var resultDict: [String: AnyObject] = [:]
for var i in 0..<Int(count) {
let property = properties[i]
// 取得属性名
let name = property_getName(property)
if let propertyName = String.fromCString(name) {
// 取得属性值
if let propertyValue = self.valueForKey(propertyName) {
///属性不为空的添加在字典中
resultDict[propertyName] = propertyValue
}
}
}
return resultDict
}
测试一下:
let dict = p.allPropertyNamesAndValues()
for (propertyName, propertyValue) in dict {
print("propertyName: (\(propertyName)), propertyValue: (\(propertyValue))")
}
打印结果:
propertyName: (hasBMW), propertyValue: (0)
propertyName: (name), propertyValue: (Lili)
3. 获取对象的所有方法名
通过class_copyMethodList方法就可以获取所有的方法。
- 第一个参数:类
- 第二个参数:存放方法个数的地址
Object-C版
- (void)allMethods {
unsigned int outCount = 0;
Method *methods = class_copyMethodList([self class], &outCount);
for (int i = 0; i < outCount; ++i) {
Method method = methods[i];
// 获取方法名称,但是类型是一个SEL选择器类型
SEL methodSEL = method_getName(method);
// 需要获取C字符串
const char *name = sel_getName(methodSEL);
// 将方法名转换成OC字符串
NSString *methodName = [NSString stringWithUTF8String:name];
// 获取方法的参数列表
int arguments = method_getNumberOfArguments(method);
NSLog(@"方法名:%@, 参数个数:%d", methodName, arguments);
}
// 记得释放
free(methods);
}
测试
///获取所有的方法名
[p allMethods];
打印结果:
2016-04-19 13:33:49.999 LSRuntimeOCDemo[2174:175322] 方法名:allProperties, 参数个数:2
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:allPropertyNamesAndValues, 参数个数:2
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:allMethods, 参数个数:2
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:setArray:, 参数个数:3
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:.cxx_destruct, 参数个数:2
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:name, 参数个数:2
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:array, 参数个数:2
2016-04-19 13:33:50.000 LSRuntimeOCDemo[2174:175322] 方法名:setName:, 参数个数:3
Swift版
func allMethods() {
var count : UInt32 = 0
let methods = class_copyMethodList(Person.self,&count)
for var i in 0..<Int(count) {
let method = methods[i]
let sel = method_getName(method)
// 获取方法的参数列表
let argument = method_getNumberOfArguments(method)
print("name: \(sel), arguemtns: \(argument)")
}
}
测试一下:
///获取所有方法名
p.allMethods()
打印结果:
name: hasBMW, arguemtns: 2
name: setHasBMW:, arguemtns: 3
name: allProperties, arguemtns: 2
name: allPropertyNamesAndValues, arguemtns: 2
name: allMethods, arguemtns: 2
name: name, arguemtns: 2
name: .cxx_destruct, arguemtns: 0
name: init, arguemtns: 2
name: setName:, arguemtns: 3
4. 获取对象的成员变量名称
要获取对象的成员变量,可以通过class_copyIvarList方法来获取,通过ivar_getName来获取成员变量的名称。对于属性,会自动生成一个成员变量。使用方法与前面类似。
- 第一个参数:类
- 第二个参数:存放变量个数的地址
Object-C版
- (NSArray *)allMemberVariables {
unsigned int count = 0;
///Ivar 变量
///获取成员变量的数组 (指针)
Ivar *ivars = class_copyIvarList([self class], &count);
NSMutableArray *results = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < count; ++i) {
///获取每个成员变量
Ivar variable = ivars[i];
///获取成员变量的字符名称
const char *name = ivar_getName(variable);
///将名称转为NSString
NSString *varName = [NSString stringWithUTF8String:name];
[results addObject:varName];
}
free(ivars);
return results;
}
测试:
NSLog(@"%@",[p allMemberVariables]);
打印结果:
2016-04-19 14:09:50.781 LSRuntimeOCDemo[2555:199104] (
"_variableString",
"_name",
"_array"
)
Swift版
Swift的成员变量名与属性名是一样的,不会生成下划线的成员变量名,这一点与Oc是有区别的。
func allMemberVariables() ->[String] {
var count:UInt32 = 0
let ivars = class_copyIvarList(Person.self, &count)
var result: [String] = []
for var i = 0; i < Int(count); ++i {
let ivar = ivars[i]
let name = ivar_getName(ivar)
if let varName = String.fromCString(name) {
result.append(varName)
}
}
return result
}
测试:
///获取成员变量
print(p.allMemberVariables())
打印结果:说明Swift的属性不会自动加下划线,属性名就是变量名:
["name", "hasBMW"]
5. 运行时发消息
Object-C版
Person *p = [[Person alloc] init];
p.name = @"Lili";
objc_msgSend(p, @selector(allMethods));
这样就相当于手动调用[p allMethods];。但是编译器会抱错,问题提示期望的参数为0,但是实际上有两个参数。解决办法是,关闭严格检查:
如果,此时报错
objc_msgSend()报错Too many arguments to function call ,expected 0,have2
那么,请Build Setting-->搜索 Enable Strict Checking of objc_msgSend Calls 改为 NO
Swift版
抱歉,Swift中没有此类方法。
6. Category扩展”属性”
iOS的category是不能扩展存储属性的,但是我们可以通过运行时关联来扩展“属性”。
Object-C 版
假设扩展下面的“属性”:
// 由于扩展不能扩展属性,因此我们这里在实现文件中需要利用运行时实现。
#import <objc/runtime.h>
typedef void(^LSCallBack)();
@interface NSObject (Property)
@property (nonatomic, copy) LSCallBack callback;
@end
在实现文件中,我们用一个静态变量作为key:
const void *s_LSCallbackKey = "s_LSCallbackKey";
- (void)setCallback:(LSCallBack)callback {
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, s_LSCallbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (HYBCallBack)callback {
return objc_getAssociatedObject(self, s_LSCallbackKey);
}
测试:
p.callback=^(){
NSLog(@"aaaa");
};
p.callback();
其实就是通过objc_getAssociatedObject取得关联的值,通过objc_setAssociatedObject设置关联。
Swift版
Swift版的要想扩展闭包,就比OC版的要复杂得多了。这里只是例子,写了一个简单的存储属性扩展。
let s_LSFullnameKey = "s_LSFullnameKey"
extension Person {
var fullName: String? {
get { return objc_getAssociatedObject(self, s_LSFullnameKey) as? String }
set {
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, s_LSFullnameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
总结
在开发中,我们比较常用的是使用关联属性的方式来扩展我们的“属性”,以便在开发中简单代码。我们在开发中使用关联属性扩展所有响应事件、将代理转换成block版等。比如,我们可以将所有继承于UIControl的控件,都拥有block版的点击响应,那么我们就可以给UIControl扩展一个TouchUp、TouchDown、TouchOut的block等。
对于动态获取属性的名称、属性值使用较多的地方一般是在使用第三方库中,比如MJExtension等。这些三方库都是通过这种方式将Model转换成字典,或者将字典转换成Model
另外
1.交换方法
开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
// 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
// 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
// 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
@implementation UIImage (image)
// 加载分类到内存的时候调用
+(void)load
{
// 交换方法
// 获取imageWithName方法地址
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
// 获取imageWithName方法地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
// 交换方法地址,相当于交换实现方式
method_exchangeImplementations(imageWithName, imageName);
}
// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
// 既能加载图片又能打印
+(instancetype)imageWithName:(NSString *)name
{
// 这里调用imageWithName,相当于调用imageName
UIImage *image = [self imageWithName:name];
if (image == nil) {
NSLog(@"加载空的图片");
}
return image;
}
测试:
UIImage *image = [UIImage imageNamed:@"123"];
打印结果:
2016-04-19 16:26:45.317 LSRuntimeOCDemo[3394:299037] 加载空的图片
2.动态添加方法
开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
简单使用
例如:
Person *p = [[Person alloc] init];
// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
// 动态添加方法就不会报错
[p performSelector:@selector(eat)];
实现:
#import "Person.h"
@implementation Person
// void(*)()
// 默认方法都有两个隐式参数,
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 动态添加eat方法
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
测试:
Person *p = [[Person alloc] init];
// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
// 动态添加方法就不会报错
[p performSelector:@selector(eat)];
打印:
2016-04-19 16:36:50.275 LSRuntimeOCDemo[3429:304239] <Person: 0x7f8620d9dd00> eat
3.字典转模型
设计模型:字典转模型的第一步
模型属性,通常需要跟字典中的key一一对应
问题:一个一个的生成模型属性,很慢?
需求:能不能自动根据一个字典,生成对应的属性。
解决:提供一个分类,专门根据字典生成对应的属性字符串。
@implementation NSObject (Log)
+(void)resolveDict:(NSDictionary *)dict{
// 拼接属性字符串代码
NSMutableString *strM = [NSMutableString string];
// 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 类型经常变,抽出来
NSString *type;
if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
type = @"NSString";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
type = @"NSArray";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
type = @"int";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){ type = @"NSDictionary";
}
// 属性字符串
NSString *str;
if ([type containsString:@"NS"]) {
str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
}else{
str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
}
// 每生成属性字符串,就自动换行。
[strM appendFormat:@"\n%@\n",str];
}];
// 把拼接好的字符串打印出来,就好了。
NSLog(@"%@",strM);
}
@end
字典转模型的方式一:KVC
@implementation Status
+(instancetype)statusWithDict:(NSDictionary *)dict
{
Status *status = [[self alloc] init];
[status setValuesForKeysWithDictionary:dict];
return status;
}
@end
KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。
如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 报key找不到的错。
分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖, 就能继续使用KVC,字典转模型了。
-(void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}
字典转模型的方式二:Runtime
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
#import "NSObject+Model.h"
@implementation NSObject (Model)
+(instancetype)modelWithDict:(NSDictionary *)dict
{
// 思路:遍历模型中所有属性-》使用运行时
// 0.创建对应的对象
id objc = [[self alloc] init];
// 1.利用runtime给对象中的成员属性赋值
// class_copyIvarList:获取类中的所有成员属性
// Ivar:成员属性的意思
// 第一个参数:表示获取哪个类中的成员属性
// 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
// 返回值Ivar :指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
// 类似下面这种写法
// Ivar ivar;
// Ivar ivar1;
// Ivar ivar2;
// // 定义一个ivar的数组a
// Ivar a[] = {ivar,ivar1,ivar2};
//
// // 用一个Ivar 指针指向数组第一个元素
// Ivar ivarList = a;
//
// // 根据指针访问数组第一个元素
// ivarList[0];
unsigned int count;
// 获取类中的所有成员属性
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员属性
Ivar ivar = ivarList[i];
// 获取成员属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key
// 从第一个角标开始截取
NSString *key = [name substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
// 判断下value是否是字典
if ([value isKindOfClass:[NSDictionary class]]) {
// 字典转模型
// 获取模型的类对象,调用modelWithDict
// 模型的类名已知,就是成员属性的类型
// 获取成员属性类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 生成的是这种@"@\"User\"" 类型 -》 @"User" 在OC字符串中 \" -> ",\是转义的意思,不占用字符
// 裁剪类型字符串
NSRange range = [type rangeOfString:@"\""];
type = [type substringFromIndex:range.location + range.length];
range = [type rangeOfString:@"\""];
// 裁剪到哪个角标,不包括当前角标
type = [type substringToIndex:range.location];
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(type);
if (modelClass) { // 有对应的模型才需要转
// 把字典转模型
value = [modelClass modelWithDict:value];
}
}
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
if (value) { // 有值,才需要给模型的属性赋值
// 利用KVC给模型中的属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
-(NSDictionary *)arrayContainModelClass{
NSDictionary *dic=@{@"aa":@"sta"};
return dic;
}
这个方法主要是确定三级转换时,用哪个类接收
- 二级转换时,如果用类去接收字典,那么需要重写setter方法
测试:
// 解析Plist文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
// 获取字典数组NSArray *dictArr = statusDict[@"statuses"];
// 自动生成模型的属性字符串//
[NSObject resolveDict:dictArr[0][@"user"]];
_statuses = [NSMutableArray array];
// 遍历字典数组
for (NSDictionary *dict in dictArr) {
Status *status = [Status modelWithDict:dict];
[_statuses addObject:status];
}
// 测试数据
NSLog(@"%@ %@",_statuses,[_statuses[0] user]);
}
网友评论