@whiteiOS
2017-04-10T11:08:46.000000Z
字数 8870
阅读 693
Objective-c
id objc = [[NSObject alloc] init];
id objc = [NSObject alloc];objc = [objc init];
clang -rewrite-objc main.m,然后就会发现文件夹下生产了main.cpp文件,里面代码非常多,使用@autoreleasepool关键词进行搜索,就会发现上面的两句代码已经被转换成了runtime语句

//参数一:谁发送消息//参数二:发送什么消息//参数三:发送消息传递的参数,以此类推objc_msgSend(id self, SEL op, ...)
注意:
1.使用消息机制,必须导入#import <objc/message.h>
并且设置Build Setting -> 搜索msg -> 设置属性为No,否则没有参数提示
2.只有对象才能发送消息,就算是类名调用类方法,其本质也是将类名转换成类对象,因此以objc开头
- (void)eat;,一个类方法+ (void)run:(NSInteger)meter;①创建Person对象
OC语句
Person *p = [[Person alloc] init];
runtime
/***************简单写法***************/Person *p = objc_msgSend([Person class], @selector(alloc));p = objc_msgSend(p, @selector(init));/***************底层写法***************///objc_getClass() : 根据传入的字符串获取某个类,注意传入的C语言字符串,所以前面不加@//sel_registerName() : 根据传入的方法名注册方法编号,根据方法编号才能找到对应的方法实现Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));p = objc_msgSend(p, sel_registerName("init"));
②调用对象方法eat
OC语句
[p eat];
runtime
objc_msgSend(p, @selector(eat));
③调用类方法run
OC语句
[Person run:20];
runtime
objc_msgSend([Person class], @selector(run:), 20);
调用私有方法[p eat];来使用的话就会报错,但是使用runtime语句objc_msgSend(p, @selector(eat));依旧能正常调用比如[p eat];
1. 对象通过isa指针找到自己所属的类
2. runtime会通过sel_registerName("eat")将eat方法注册成方法编号
3. 根据方法编号去查找对应方法(对象方法存储在类对象的方法列表中,类方法存储在元类的方法列表中)
4. 找到的只是最终函数的实现地址,最后根绝地址去方法区调用对应函数的实现

需求:给imageNamed方法添加功能,加载图片的时候能判断是否加载成功
在没有学到交换方法之前,要实现上述需求应该会自定义类,继承自UIImage,然后重写imageNamed方法
//自定义类CXImage,继承自UIImage,重写imageNamed+ (UIImage *)imageNamed:(NSString *)name{//原有功能:加载图片UIImage *image = [super imageNamed:name];//扩展功能:判断图片是否加载成功if (image) {NSLog(@"加载成功");} else {NSLog(@"加载失败");}return image;}
但是上面的方式有弊端:每次使用都要#import "CXImage",而且如果项目中已经有N多地方用到了[UIImage imageNamed:];,要一一替换的话太过麻烦
使用交换方法的基本步骤:
① 步骤1:先创建分类UIImage+Image,自己定义一个能加载图片并判断是否加载成功的方法
+ (UIImage *)cx_imageNamed:(NSString *)imageName{//原有功能:加载图片UIImage *image = [UIImage imageNamed:imageNamed];//扩展功能:判断图片是否加载成功if (image) {NSLog(@"加载成功");} else {NSLog(@"加载失败");}return image;}
② 步骤2:当调用imageNamed方法时会自动调用cx_imageNamed方法,也就是交换两个方法的实现
//交换方法只需要设置一次//加载这个分类时调用+ (void)load{//1.先把两个方法取出来//方法都是从类里面寻找,所以以class开头//第一个参数:哪个类//第二个参数:哪个方法Method imageNamedMethod = class_getclassMethod(self, @selector(imageNamed:));Method cx_imageNamedMethod = class_getclassMethod(self, @selector(cx_imageNamed:));//2.方法交换method_exchangeImplementations(imageNamedMethod, cx_imageNamedMethod);}
③ 注意:因为此时imageNamed和cx_imageNamed方法已经交换实现了,所以在cx_imageNamed方法中不能再使用imageNamed,那样会引起死循环,要改成cx_imageNamed
+ (UIImage *)cx_imageNamed:(NSString *)imageName{//原有功能:加载图片UIImage *image = [UIImage cx_imageNamed:imageNamed];//扩展功能:判断图片是否加载成功if (image) {NSLog(@"加载成功");} else {NSLog(@"加载失败");}return image;}
imageNamed方式时会来到cx_imageNamed方法内,且不需要#import "UIImage+Image",原有的代码不需要更改performSelector,什么时候使用? -> 动态添加方法的时候使用 -> 怎么动态添加方法 -> runtime -> 为什么要动态添加方法① 创建一个Person类,里面没有任何方法,然后在外界直接调用某个方法,如果这时运行程序就会崩
Person *p = [[Person alloc] init];//performSelector:动态添加方法[p performSelector:@selector(eat)];
② 在Person类中动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel,要在该方法中动态添加(如果是未实现的类方法,就会调用+ (BOOL)resolveClassMethod:(SEL)sel)
+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == NSSelectorFromString(@"eat")) {//动态添加方法}return [super resolveInstanceMethod:sel];}
动态添加方法为class_addMethod,里面有四个参数

不知道如何下手,可以查看官方文档,写的很清楚

//函数的实现//默认一个方法都会包含两个隐式参数-self和_cmdvoid myMethodIMP(id self, SEL _cmd){//implementation...}//动态添加方法class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");//最后一个参数-函数的类型可以通过官方文档中`Type Encodings`查到,v表示void,@表示对象,:表示SEL
void eat(id self, SEL _cmd){NSLog(@"实现了eat%@--%@", self, NSStringFromSelector(_cmd));}+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == NSSelectorFromString(@"eat")) {class_addMethod(self, sel, (IMP)eat, "v@:");return YES;}return [super resolveInstanceMethod:sel];}
外界调用
[p performSelector:@selector(run:) withObject:@10];
动态添加
void run(id self, SEL _cmd, NSNumber *meter){NSLog(@"跑了%@米", meter);}+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == NSSelectorFromString(@"run:")) {class_addMethod(self, sel, (IMP)run, "v@:@");return YES;}return [super resolveInstanceMethod:sel];}
objc_setAssociatedObject和objc_getAssociatedObjectNSObject+Extension.h
@interface NSObject (Extension)@property NSString *name;@end
NSObject+Extension.m
@implementation NSObject (Extension)- (void)setName:(NSString *)name{//参数一:给谁添加属性//参数二:属性名称//参数三:属性值//参数四:保存策略objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSString *)name{return objc_getAssociatedObject(self, @"name");}@end
ViewController.m
- (void)viewDidLoad {[super viewDidLoad];NSObject *objc = [[NSObject alloc] init];objc.name = @"123";NSLog(@"%@", objc.name);}

+ (instancetype)itemWithDictionary:(NSDictionary *)dict{StatusItem *item = [[StatusItem alloc] init];//KVC:把字典中所有值给模型的属性赋值[item setValuesForKeysWithDictionary:dict];return item;}
setValuesForKeysWithDictionary方法内部会遍历字典中所有key,然后去模型中查找有没有对应的属性,找到后进行赋值,相当于下面代码:
+ (instancetype)itemWithDictionary:(NSDictionary *)dict{StatusItem *item = [[StatusItem alloc] init];//KVC:把字典中所有值给模型的属性赋值// [item setValuesForKeysWithDictionary:dict];[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {[item setValue:value forKey:key];}];return item;}
[item setValue:value forKey:key];内部又做了什么?比如[item setValue:@"笑cry" forKey:@"text"];:
[self setText:@"笑cry"];text = @"笑cry";_text = @"笑cry";setValue:forUndefinedKey:错误(比如随便把一个属性注释掉,运行时就会报错)所以利用KVC进行字典转模型要求模型中的属性必须与字典中的key一一对应,但是很多时候,我们只会用到字典中的一部分值,不一定要全部取出来,当模型属性和字典中key不对应的时候,再利用KVC进行字典转模型就会报错了。如果不让系统报错,可以重写报错的方法
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
//字典转模型+ (instancetype)modelWithDict:(NSDictionary *)dict{id objc = [[self alloc] init];//1.获取模型中的所有成员变量//参数一:获取哪个类的成员变量//参数二:成员变量个数//返回类型:Ivar *,是一个数组unsigned int count = 0;Ivar *ivarList = class_copyIvarList(self, &count);//遍历所有成员变量for (int i = 0, i < count, i++) {//取出成员变量Ivar ivar = ivarList[i];//获取成员变量的名字NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];//成员变量名字是带下划线的,要把下划线去掉才是正确的keyNSString *key = [ivarName substringFromIndex:1];//2.根据key去字典中取出对应的valueid value = dict[key];//3.用value给模型属性赋值if (value) {[objc setValue:value forKey:key];}}return objc;}
为什么第一步是获取模型中的所有成员变量而不是属性?
利用runtime既可以获取类中的所有成员变量,也可以获取所有属性(class_copyPropertyList)。
如果类中存在成员变量:
{
int _age;
}
@property (nonatomic, strong) NSString *source;
这时候使用获取属性的方法class_copyPropertyList是拿不到_age的,只能拿到source属性。
如果使用获取成员变量方法的话,既能拿到_age,又能拿到_source,所以直接获取所有成员变量的方法更为稳妥。
//字典转模型+ (instancetype)modelWithDict:(NSDictionary *)dict{id objc = [[self alloc] init];//1.获取模型中的所有成员变量//参数一:获取哪个类的成员变量//参数二:成员变量个数//返回类型:Ivar *,是一个数组unsigned int count = 0;Ivar *ivarList = class_copyIvarList(self, &count);//遍历所有成员变量for (int i = 0, i < count, i++) {//取出成员变量Ivar ivar = ivarList[i];//获取成员变量的名字NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];//获取成员变量的类型NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// @\"User\" -> UserivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];//成员变量名字是带下划线的,要把下划线去掉才是正确的keyNSString *key = [ivarName substringFromIndex:1];//2.根据key去字典中取出对应的valueid value = dict[key];//二级转换:判断value是否是字典//并且是自定义对象才需要转换if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {// 获取类Class modelClass = NSClassFromString(ivarType);value = [modelClass modelWithDict:value];}//3.用value给模型属性赋值if (value) {[objc setValue:value forKey:key];}}return objc;}