在开始之前,先了解下实例对象、类和元类的关系图:
关系图
图中的实线表示superClass的指向,虚线表示isa指针的指向,也就是元类的指向。
由此得到结论:
所有类的超类(superClass)最终指向的其实是NSObject(包括根元类),而NSObject的超类是nil,也就是没有超类;
所有类的元类最终都指向一个根元类,而根元类的isa指针指向自己,根元类的superClass指向NSObject.
接下来,我们直接用runtime相关的知识来做一些小的demo,生产环境中的应用要复杂的多,这里只是展示一些思路,利用runtime的便利,我们能够做到下面一些事情:
- 万能控制器跳转
- 更改系统控件的样式(或者系统类的属性)
- 字典转模型
- 自定归档解档
- 文字字体自适应
- 处理按钮重复点击事件
- UITableView, UICollectionView空数据占位图
- 多重代理
- 多继承
- 模型转字典
1.万能跳转控制器
要实现万能控制器跳转,需要事先约定好一些必要参数,并利用runtime来动态解析,达到控制器跳转的目的。大体思路是这样的:
- 在参数中指定要跳转的控制器名称,以及该控制器必须的参数
- 动态解析实例化控制器,并利用KVC设值,然后跳转
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface LFViewControllerManager : NSObject
+(void)showViewControllerWithParams:(NSDictionary *)params;
@end
#import "LFViewControllerManager.h"
#import "objc/runtime.h"
@implementation LFViewControllerManager
+(void)showViewControllerWithParams:(NSDictionary *)params {
// 获取控制器名称
NSString *controllerStr = params[@"controller"];
const char *controllerClassName = [controllerStr cStringUsingEncoding:NSASCIIStringEncoding];
// 获取控制器类
Class controllerClass = objc_getClass(controllerClassName);
if (!controllerClass) { // 如果控制器没有在runtime中注册,则注册一个新的控制器,或者直接抛出异常,这里我们直接返回
NSString *msg = [NSString stringWithFormat:@"类 %@ 没有注册", controllerStr];
NSAssert(false, msg);
return;
// Class superClass = [NSObject class];
// controllerClass = objc_allocateClassPair(superClass, controllerClassName, 0); // 新建一个类
// objc_registerClassPair(controllerClass); // 注册类
}
// 创建控制器对象
id object = [[controllerClass alloc] init];
if (![object isKindOfClass:[UIViewController class]]) {
NSString *msg = [NSString stringWithFormat:@"%@ 不是控制器类型", controllerStr];
NSAssert(false, msg);
return;
}
// 检查参数是否是对应的属性
NSDictionary *inParams = params[@"params"];
[inParams enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL * stop) {
if ([LFViewControllerManager isProperty:key existsInObject:object]) {
[object setValue:obj forKey:key];
}
}];
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *presentingViewController = [LFViewControllerManager getPresentingViewControllerInViewController:rootVC];
if ([presentingViewController navigationController]) {
[presentingViewController.navigationController pushViewController:object animated:YES];
} else {
[presentingViewController presentViewController:object animated:YES completion:nil];
}
}
// 检测实例对象中是否存在某个属性
+(BOOL)isProperty:(NSString *)propertyName existsInObject:(id)object {
unsigned int count;
objc_property_t *propetyList = class_copyPropertyList([object class], &count);
for (int i=0; i<count; i++) {
objc_property_t property = propetyList[i];
const char *_propertyName = property_getName(property);
NSString *propertyNameStr = [NSString stringWithCString:_propertyName encoding:NSUTF8StringEncoding];
if ([propertyName isEqualToString:propertyNameStr]) {
free(propetyList);
return YES;
}
}
free(propetyList);
return NO;
}
// 获取当前处于顶端的控制器
+(UIViewController *)getPresentingViewControllerInViewController:(UIViewController *)vc {
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [LFViewControllerManager getPresentingViewControllerInViewController:[nav topViewController]];
} else if ([vc isKindOfClass:[UITabBarController class]]) {
UITabBarController *tab = (UITabBarController *)vc;
return [LFViewControllerManager getPresentingViewControllerInViewController:[tab selectedViewController]];
} else if ([vc presentedViewController]) {
return [LFViewControllerManager getPresentingViewControllerInViewController:[vc presentedViewController]];
} else {
return vc;
}
}
@end
-(void)controllerManagerTest {
NSDictionary *params = @{
@"controller": @"NextViewController",
@"params": @{
@"name": @"john",
@"age": @"10"
}
};
[LFViewControllerManager showViewControllerWithParams:params];
}
这里只是简单的做了一个跳转的控制,当然,这里可以做的更细致一些。
2.更改系统控件的样式
比如最常见的是修改UITextField的样式,一种方式是利用NSAttributeString来修改:
NSDictionary *attrs = @{
NSForegroundColorAttributeName: [UIColor blackColor],
NSFontAttributeName: [UIFont systemFontOfSize:20]
}
NSAttributedString *placeHolderAttr = [[NSAttributedString alloc] initWithString:@"请输入用户名" attributes:attrs];
[demoField setAttributedPlaceholder: placeHolderAttr];
或者,我们可以利用runtime来找到UITextField对应的属性,直接利用KVC设值:
首先,我们来打印下UITextField的所有成员变量,顺便打印下属性:
-(void)getPropertyAndIVarsOfUITextField {
unsigned int count;
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i=0; i<count; i++) {
Ivar ivar = ivars[i];
const char *cName = ivar_getName(ivar);
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
NSLog(@"%@", name);
}
objc_property_t *properties = class_copyPropertyList([UITextField class], &count);
for (int i=0; i<count; i++) {
objc_property_t property = properties[i];
const char *cPropertyName = property_getName(property);
NSString *name = [NSString stringWithCString:cPropertyName encoding:NSUTF8StringEncoding];
NSLog(@"%@", name);
}
}
在打印结果中,我们搜一下"placeholder",发现有一个变量,相信你一眼就知道她是谁:
2019-08-11 10:00:44.375298+0800 Runtime[317:13489] _placeholderLabel
好了,下面,我们直接用KVC来修改她的字体和颜色:
demoField.placeholder = @"请输入用户名";
[demoField setValue:[UIColor blueColor] forKeyPath:@"_placeholderLabel.textColor"];
[demoField setValue:[UIFont systemFontOfSize:20] forKeyPath:@"_placeholderLabel.font"];
3. 字典转模型
既然runtime可获取属性列表,属性类型,我们就可以runtime结合KVC来从字典创建模型实例,比如有Student模型,他包含一些基本的属性和嵌套的模型address,以及模型数组courses:
#import <Foundation/Foundation.h>
#import "NSObject+JSONToModel.h"
#import "Address.h"
#import "Course.h"
@interface Student : NSObject <JSONToModelProtocol>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *uid;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) Address *address;
@property (nonatomic, strong) NSArray *courses;
@end
#import "Student.h"
@implementation Student
+(NSDictionary<NSString *,id> *)specialPropertyInfo {
return @{
@"uid": @"id",
@"courses": [Course class]
};
}
@end
这里,我们和其他JSON解析框架一样,让模型类遵从我们的协议并提供一下特殊键的转换,以及模型数组中的模型类型。
#import <Foundation/Foundation.h>
// 课程模型
@interface Course : NSObject
@property (nonatomic, copy) NSString *name; // 课程名称
@property (nonatomic, copy) NSString *desc; // 课程介绍
@end
#import <Foundation/Foundation.h>
// 地址模型
@interface Address : NSObject
@property (nonatomic, copy) NSString *country;
@property (nonatomic, copy) NSString *province;
@property (nonatomic, copy) NSString *city;
@end
下面,我们给NSObject添加一个类别,在这个类别中处理字典转模型的逻辑,这样,所有继承自NSObject的模型都可以使用这个功能:
@protocol JSONToModelProtocol <NSObject>
+(NSDictionary<NSString *, id> *)specialPropertyInfo;
@end
@interface NSObject (JSONToModel)
+(instancetype)modelWithDictionary:(NSDictionary *)dictionary;
@end
#import "NSObject+JSONToModel.h"
#import "objc/runtime.h"
@implementation NSObject (JSONToModel)
+(instancetype)modelWithDictionary:(NSDictionary *)dictionary {
id object = [[self alloc] init];
// 获取对象的属性列表
unsigned int propertyCount;
objc_property_t *properties = class_copyPropertyList([self class], &propertyCount);
for (int i=0; i<propertyCount; i++) {
// 获取属性名称
objc_property_t property = properties[i];
const char *cPropertyName = property_getName(property);
NSString *propertyNameStr = [NSString stringWithCString:cPropertyName encoding:NSUTF8StringEncoding];
// 获取JSON中的属性值
id value = [dictionary valueForKey:propertyNameStr];
// 获取属性所属类名
NSString *propertyType;
unsigned int attrCount;
objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
for (int i=0; i<attrCount; i++) {
objc_property_attribute_t attr = attrs[i];
// attr中不会包含父类的属性,一般value都是空的,只有属性的类型type ‘T’才有值
switch (attr.name[0]) {
case 'T':{
if (attrs[i].value) {
propertyType = [NSString stringWithUTF8String:attrs[i].value];
// 去除转义字符: @\"NSString\" -> @NSString
propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 去掉@符号: @NSString -> NSString
propertyType = [propertyType stringByReplacingOccurrencesOfString:@"@" withString:@""];
}
}
break;
default:
break;
}
}
// 对特殊属性进行处理
NSDictionary *specialPropertiesInfo;
if ([self respondsToSelector:@selector(specialPropertyInfo)]) {
specialPropertiesInfo = [self performSelector:@selector(specialPropertyInfo) withObject:nil];
if (specialPropertiesInfo) {
// 1. 处理字典中的特殊键,比如"id"
id antherName = specialPropertiesInfo[propertyNameStr];
if (antherName && [antherName isKindOfClass:[NSString class]]) {
value = dictionary[antherName];
}
// 2. 模型嵌套模型
// value是字典类型,并且当前属性的类型不是系统自带类型
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType hasPrefix:@"NS"]) {
Class realClass = NSClassFromString(propertyType);
if (realClass) {
value = [realClass modelWithDictionary:value];
}
}
// 3. 模型嵌套数组
if ([value isKindOfClass:[NSArray class]] &&
([propertyType isEqualToString:@"NSArray"] ||
[propertyType isEqualToString:@"NSMutableArray"])) {
Class realClass = specialPropertiesInfo[propertyNameStr];
NSMutableArray *valueArr = [[NSMutableArray alloc] init];
for (NSDictionary *subValue in value) {
[valueArr addObject:[realClass modelWithDictionary:subValue]];
}
value = valueArr;
}
}
}
if (value) {
[object setValue:value forKey:propertyNameStr];
}
}
free(properties);
return object;
}
@end
上面的代码很简单,就是取出要转换的模型的属性,获取到他们的类型,然后一一设值,再就是模型嵌套和模型数组,也都是稍微转换以下即可。下面是json和测试代码:
student.json
:
{
"name": "John",
"id": "s2019090123",
"age": 23,
"address":{
"country": "中国",
"province": "江苏",
"city": "苏州"
},
"courses":[
{
"name": "英语",
"desc": "商务英语口语教学"
},
{
"name": "数学",
"desc": "数学模型与应用"
},
{
"name": "物理",
"desc": "基础物理导论"
}
]
}
-(void)JSONtoModel {
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"student" ofType:@"json"];
NSData *jsonData = [NSData dataWithContentsOfFile:filePath];
NSError *error;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:&error];
if(!error) {
Student *student = [Student modelWithDictionary:json];
NSLog(@"%@", student);
}
}
4. 自动归解档
自动归解档也是利用了runtime获取属性名称,结合KVC来实现的,代码很简单,直接上代码:
@interface NSObject (AutoEncodeDecode)
-(void)lj_decoderWithCoder:(NSCoder *)aDecoder;
-(void)lj_encodeWithCoder:(NSCoder *)aEncoder;
@end
#import "NSObject+AutoEncodeDecode.h"
#import "objc/runtime.h"
@implementation NSObject (AutoEncodeDecode)
-(void)lj_decoderWithCoder:(NSCoder *)aDecoder {
if (!aDecoder) {
return;
}
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (int i=0; i<count; i++) {
objc_property_t property = properties[i];
const char *cname = property_getName(property);
NSString *propertyName = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding];
id value = [aDecoder decodeObjectForKey:propertyName];
[self setValue:value forKey:propertyName];
}
free(properties);
}
-(void)lj_encodeWithCoder:(NSCoder *)aEncoder {
if (!aEncoder) {
return;
}
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (int i=0; i<count; i++) {
objc_property_t property = properties[i];
const char *cname = property_getName(property);
NSString *propertyName = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding];
id value = [self valueForKey:propertyName];
[aEncoder encodeObject:value forKey:propertyName];
}
free(properties);
}
@end
测试:比如有下面一个类:
@interface EncodeAndDecode : NSObject<NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSMutableArray *address;
@end
#import "EncodeAndDecode.h"
#import "NSObject+AutoEncodeDecode.h"
@implementation EncodeAndDecode
-(instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[self lj_decoderWithCoder:aDecoder];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder {
[self lj_encodeWithCoder:aCoder];
}
@end
-(void)autoEncodeAndDecoder {
EncodeAndDecode *demo = [[EncodeAndDecode alloc] init];
demo.name = @"demo";
demo.age = 20;
NSMutableArray *address = [[NSMutableArray alloc] init];
[address addObject:@"address 1"];
[address addObject:@"address 2"];
[address addObject:@"address 3"];
demo.address = address;
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"encodeDemo.archiver"];
BOOL result = [NSKeyedArchiver archiveRootObject:demo toFile:filePath];
if (result) {
NSLog(@"归档成功");
}
EncodeAndDecode *demoUnarchered = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"%@", demoUnarchered);
}
5. 文字字体自适应
由于屏幕尺寸大小不一,所以我们设置的字体在小屏幕上可能正好,但是到了大屏幕上 就显得略小了,这里,我们直接用method swizzle拦截UIFont的systemFontOfSize:方法,在这个方法里根据屏幕尺寸比例重新设置fontSize。常用的Method Swizzle方式到处都是了,这里我们用静态方法来实现,这么做也是有好处的,一方面可以避免方法重名,另一方面就是防止_cmd参数被篡改(有些开发者喜欢用分类挂载属性的时候用_cmd参数做key,而一般的Method Swizzle写法会篡改_cmd值,从而产生一些莫名其妙的问题)
#import "UIFont+AutoResize.h"
#import "objc/runtime.h"
typedef IMP *IMPPointer;
// swizzle函数声明
static UIFont* MethodSwizzle(id self, SEL _cmd, CGFloat fontSize);
// 声明一个函数指针用来保存原方法的函数地址
static UIFont* (*MethodOriginal)(id self, SEL _cmd, CGFloat fontSize);
static UIFont* MethodSwizzle(id self, SEL _cmd, CGFloat fontSize) {
CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
CGFloat scale = screenWidth / 375.0;
return MethodOriginal(self, _cmd, scale * fontSize);
}
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
// imp用来保存原方法的函数指针,方便在替换方法中调用原方法
IMP imp = NULL;
Method method = class_getClassMethod(class, original);
// 这里因为是类方法,所以我们要获取UIFont的元类,元类的实例方法就是类方法了
Class metaClass = object_getClass((id)class);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(metaClass, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation UIFont (AutoResize)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
+ (void)load {
// 我们用MethodOriginal来保存原方法的函数指针
[self swizzle:@selector(systemFontOfSize:) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
}
@end
这样,我们每次调用[UIFont systemFontOfSize:20]
的时候就能动态的根据屏幕宽度比例来设置字体大小了.
6. 处理按钮重复点击
一般处理按钮重复点击问题的解决方法都是根据两次相邻事件的时间间隔来判断要不要处理点击事件,当然你可以一个方法一个方法的去添加判断条件,不过,既然有了runtime,有了method swizzle,世界从此也就变得美好了,这俩货绝对是懒癌患者的福音。同样,我们用method swizzle拦截sendAction:to:forEvent:方法,结合category属性挂载来处理重复点击事件
@interface UIButton (ReTouchAvoid)
// 两次点击事件之间的间隔
@property (nonatomic, assign) NSTimeInterval eventInverval;
@end
static void *EventIntervalKey = &EventIntervalKey;
static void *LastEventDateKey = &LastEventDateKey;
@interface UIButton()
@property (nonatomic, assign) NSTimeInterval lastEvetDate;
@end
@implementation UIButton (ReTouchAvoid)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class clazz = [self class];
SEL originalSEL = @selector(sendAction:to:forEvent:);
SEL fakeSEL = @selector(fake_sendAction:to:forEvent:);
Method originalMethod = class_getInstanceMethod(clazz, originalSEL);
Method fakeMethod = class_getInstanceMethod(clazz, fakeSEL);
BOOL didAdded = class_addMethod(clazz,
originalSEL,
method_getImplementation(fakeMethod),
method_getTypeEncoding(fakeMethod));
if (didAdded) {
class_replaceMethod(clazz,
fakeSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, fakeMethod);
}
});
}
-(void)fake_sendAction:(SEL)selector to:(id)target forEvent:(UIEvent *)event {
if ([self eventInverval] <= 0) {
[self setEventInverval:0.5];
}
NSTimeInterval eventGap = [NSDate timeIntervalSinceReferenceDate] - [self lastEvetDate];
if (eventGap >= [self eventInverval]) {
[self setLastEvetDate:[NSDate timeIntervalSinceReferenceDate]];
[self fake_sendAction:selector to:target forEvent:event];
}
}
-(void)setEventInverval:(NSTimeInterval)eventInverval {
objc_setAssociatedObject(self, EventIntervalKey, @(eventInverval), OBJC_ASSOCIATION_RETAIN);
}
-(NSTimeInterval)eventInverval {
return [objc_getAssociatedObject(self, EventIntervalKey) doubleValue];
}
-(void)setLastEvetDate:(NSTimeInterval)lastEvetDate {
objc_setAssociatedObject(self, LastEventDateKey, @(lastEvetDate), OBJC_ASSOCIATION_RETAIN);
}
-(NSTimeInterval)lastEvetDate {
return [objc_getAssociatedObject(self, LastEventDateKey) doubleValue];
}
这里我们用了空指针地址的方式来做key,即确保了key值的唯一,又避免了之前说过的用_cmd参数做key的缺陷问题。
7. UITableView,UICollectionView占位图
我们希望在UITableView, UICollectionView数据为空的时候能展示一张图片,或者展示一个空数据的View之类的,展示了上面那么多例子之后,这个需求也是很简单的就可以实现了,同样,我们在用method swizzle 拦截下reloadData方法,然后获取到dataSource,看看是不是所有的section下的row行数都是0,依此来判断UITableView是否为空即可,并借机展示占位view.
static void *IsFirstLoadKey = &IsFirstLoadKey;
static void *PlaceHolderViewKey = &PlaceHolderViewKey;
@interface UITableView()
@property (nonatomic, assign) BOOL isFirstLoad;
@property (nonatomic, strong) UIView *placeHolderView;
@end
@implementation UITableView (PlaceHolder)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class clazz = [self class];
SEL originalSEL = @selector(reloadData);
SEL fakeSEL = @selector(fake_reloadData);
Method originalMethod = class_getInstanceMethod(clazz, originalSEL);
Method fakeMethod = class_getInstanceMethod(clazz, fakeSEL);
BOOL didAdded = class_addMethod(clazz,
originalSEL,
method_getImplementation(fakeMethod),
method_getTypeEncoding(fakeMethod));
if (didAdded) {
class_replaceMethod(clazz,
fakeSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, fakeMethod);
}
});
}
-(void)fake_reloadData {
if (!self.isFirstLoad) {
[self checkEmpty];
}
self.isFirstLoad = NO;
[self fake_reloadData];
}
-(void)checkEmpty {
BOOL isEmpty = YES;
id<UITableViewDataSource> dataSource = self.dataSource;
// 获取section数量
NSInteger sectionNumber = 1;
if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
sectionNumber = [dataSource numberOfSectionsInTableView:self];
}
// 检查每个section下面的row行数,有一个section下面行数>0就不为空
for (NSInteger i=0; i<sectionNumber; i++) {
if ([dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) {
NSInteger rowNumber = [dataSource tableView:self numberOfRowsInSection:i];
if (rowNumber > 0) {
isEmpty = NO;
break;
}
}
}
if (isEmpty) {
if (!self.placeHolderView) {
[self makePlaceHodlerView];
}
[self addSubview:self.placeHolderView];
} else {
[self.placeHolderView removeFromSuperview];
}
}
-(void)makePlaceHodlerView {
self.placeHolderView = [[UIView alloc] initWithFrame:self.bounds];
[self.placeHolderView setBackgroundColor:[UIColor redColor]];
}
-(void)setIsFirstLoad:(BOOL)isFirstLoad {
objc_setAssociatedObject(self, IsFirstLoadKey, @(isFirstLoad), OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)isFirstLoad {
return [objc_getAssociatedObject(self, IsFirstLoadKey) boolValue];
}
-(void)setPlaceHolderView:(UIView *)placeHolderView {
objc_setAssociatedObject(self, PlaceHolderViewKey, placeHolderView, OBJC_ASSOCIATION_RETAIN);
}
-(UIView *)placeHolderView {
return objc_getAssociatedObject(self, PlaceHolderViewKey);
}
@end
这里,我们只是展示了一张空白的UIView,做的更细致一些的话,比如有的应用没数据的时候,展示一个重新加载的按钮,这里也是可以的,只需要在UITableView分类上挂载一个Block,当placeHolderView上的刷新按钮点击的时候,调用这个Block就可以了,然后在使用UITableView的时候实现这个Block, 在其中重新走获取数据的流程就可以了。
8. 多重代理
一般的代理只能实现一对一的代理,要实现多重代理的话就需要用到消息转发机制。这里消息转发的原理不做重点解释,我们知道一个消息发出去之后,如果在method_list中没有找到的话,会走下面三个步骤:
- 动态方法解析
走下面两个方法之一,动态添加响应方法+(BOOL)resolveInstanceMethod:(SEL)sel +(BOOL)resolveClassMethod:(SEL)sel
- 消息重定向
走下面方法,将消息转发给另一个对象-(id)forwardingTargetForSelector:(SEL)aSelector
- 消息转发
先走第一个方法返回一个NSMethodSignature,再走第二个方法转发消息1. -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 2. -(void)forwardInvocation:(NSInvocation *)anInvocation
在第二步消息重定向那里也可以做消息转发,但这里只能返回一个对象,做不到多重代理,只能在第三步消息转发这里做文章。下面直接上代码:
- 代理协议以及被代理者
@protocol DoSomeThingDelegate <NSObject>
-(void)doSomething;
@end
@interface DoSomeThing : NSObject
@property (nonatomic, weak) id<DoSomeThingDelegate> delegate;
-(void)performDelegateMethod;
@end
@implementation DoSomeThing
-(void)performDelegateMethod {
if ([self.delegate respondsToSelector:@selector(doSomething)]) {
[self.delegate performSelector:@selector(doSomething)];
}
}
@end
- 两个代理人
@interface Delegate1 : NSObject <DoSomeThingDelegate>
@end
@implementation Delegate1
-(void)doSomething {
NSLog(@"delegate 1 work");
}
@end
@interface Delegate2 : NSObject <DoSomeThingDelegate>
@end
@implementation Delegate2
-(void)doSomething {
NSLog(@"delegate 2 work");
}
@end
- 代理管理者,最重要的就是这个管理者,负责将消息转发给所有注册的实际代理人
@interface MultDelegate : NSObject
-(void)addDelegate:(id<DoSomeThingDelegate>) delegate;
-(void)setDelegates:(NSArray<id<DoSomeThingDelegate>> *)delegates;
@end
@interface MultDelegate()
@property (nonatomic, strong) NSPointerArray *delegateArr;
@end
@implementation MultDelegate
// 添加一个代理人
-(void)addDelegate:(id<DoSomeThingDelegate>)delegate {
__block BOOL isExists = NO;
dispatch_apply(self.delegateArr.count, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
id target = [self.delegateArr pointerAtIndex:index];
if (target == (__bridge void *)delegate) {
isExists = YES;
}
});
if (!isExists) {
[self.delegateArr addPointer:(void *)delegate];
}
}
// 一次性添加所有的代理人
-(void)setDelegates:(NSArray<id<DoSomeThingDelegate>> *)delegates {
for (id<DoSomeThingDelegate> delegate in delegates) {
[self.delegateArr addPointer:(void *)delegate];
}
}
// 一般我们会在被代理人那里先用[delegate responseToSelector:]判断代理能不能响应代理方法
// 所以这里我们需要重写,如果我们管理的实际代理人能够响应,就代表我们也可以响应(实际上是通过消息转发给了实际代理人)
-(BOOL)respondsToSelector:(SEL)aSelector {
if ([super respondsToSelector:aSelector]) {
return YES;
}
for (id target in self.delegateArr) {
if ([target respondsToSelector:aSelector]) {
return YES;
}
}
return NO;
}
// 在这个方法里检查我们的代理人是否有对应的方法,如果有,就拿到方法签名并返回签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (signature) {
return signature;
}
for (id target in self.delegateArr) {
if (!target) {
continue;
}
if ((signature = [target methodSignatureForSelector:aSelector])) {
break;
}
}
return signature;
}
// 在这个方法里遍历所有实际代理人,并将消息转发给他们
-(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = anInvocation.selector;
BOOL responded = NO;
for (id target in self.delegateArr) {
if (target && [target respondsToSelector:selector]) {
[anInvocation invokeWithTarget:target];
responded = YES;
}
}
if (!responded) {
[self doesNotRecognizeSelector:selector];
}
}
-(NSPointerArray *)delegateArr {
if (!_delegateArr) {
_delegateArr = [NSPointerArray weakObjectsPointerArray];
}
return _delegateArr;
}
@end
测试代码:
-(void)multDelegateDemo {
MultDelegate *mulDelegate = [[MultDelegate alloc] init];
Delegate1 *delegate1 = [[Delegate1 alloc] init];
Delegate2 *delegate2 = [[Delegate2 alloc] init];
DoSomeThing *doSomething = [[DoSomeThing alloc] init];
[mulDelegate setDelegates:@[delegate1, delegate2]];
[mulDelegate addDelegate:delegate1];
doSomething.delegate = mulDelegate;
[doSomething performDelegateMethod];
}
2019-08-13 16:04:30.259258+0800 Runtime[357:53528] delegate 1 work
2019-08-13 16:04:30.259396+0800 Runtime[357:53528] delegate 2 work
可以看到两个代理人都执行了对应的代理方法。
9. 多继承
OC是没有多继承的,我们只能实现多继承的效果,这里还是要用到消息转发机制,在上面的例子中
- 消息重定向
走下面方法,将消息转发给另一个对象-(id)forwardingTargetForSelector:(SEL)aSelector
我们可以在这里判断不同的selector,然后分别返回不同的响应者就可以了
10. 模型转字典
模型转字典的方法和字典转模型正好是相反的,我们要遍历对象的所有属性,用KVC获取到属性对应的值,然后判断值的类型,如果是普通类型的话就直接添加到字典里,如果是自定义的model的话,就调用model的转换方法,如果是数组的话,再遍历数组,注意判断数组中item的类型,再去处理即可:
@interface NSObject (ModelToDict)
-(NSDictionary *)modelToDict;
@end
#import "NSObject+ModelToDict.h"
#import "objc/runtime.h"
// 需要忽略的属性
NSSet *ignoreProperties;
@implementation NSObject (ModelToDict)
-(NSDictionary *)modelToDict {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ignoreProperties = [NSSet setWithObjects:@"hash", @"superclass", @"description", @"debugDescription", nil];
});
NSMutableDictionary *resultDic = [[NSMutableDictionary alloc] init];
unsigned int propertyCount;
objc_property_t *properties = class_copyPropertyList([self class], &propertyCount);
for (int i=0; i<propertyCount; i++) {
objc_property_t property = properties[i];
// 获取属性名
const char *cName = property_getName(property);
NSString *propertyName = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
if ([ignoreProperties containsObject:propertyName]) {
continue;
}
id value = [self valueForKey:propertyName];
if (value) {
// 该property是嵌套的数组类型
if ([value isKindOfClass:[NSArray class]]) {
[resultDic setValue:[self getResultOfArr:value] forKey:propertyName];
// 该property是自定义的model
} else if ([value respondsToSelector:@selector(modelToDict)] &&
![NSStringFromClass([value class]) hasPrefix:@"NS"] &&
![NSStringFromClass([value class]) hasPrefix:@"__NS"]) {
[resultDic setValue:[value modelToDict] forKey:propertyName];
// 该property是其他的基本数据类型
} else {
[resultDic setValue:value forKey:propertyName];
}
} else {
[resultDic setValue:[NSNull null] forKey:propertyName];
}
}
return resultDic;
}
-(NSMutableArray *)getResultOfArr:(NSArray *)arr {
NSMutableArray *result = [[NSMutableArray alloc] init];
if (arr.count > 0) {
id firstItem = arr[0];
// 该数组里嵌套了其他的数组
if ([firstItem isKindOfClass:[NSArray class]]) {
for (id value in arr) {
[result addObject:[self getResultOfArr:value]];
}
// 该数组里装的是自定义的model
} else if ([firstItem respondsToSelector:@selector(modelToDict)] &&
![NSStringFromClass([firstItem class]) hasPrefix:@"NS"] &&
![NSStringFromClass([firstItem class]) hasPrefix:@"__NS"]) { // arr里面的项目是其他自定义的Model
for (id value in arr) {
[result addObject:[value modelToDict]];
}
// 数组中是其他的基本数据类型
} else {
[result addObjectsFromArray:arr];
}
}
return result;
}
@end
继续借用上面字典转模型时使用的几个model测试一下:
-(void)modelToDicDemo {
Student *student = [[Student alloc] init];
Course *course1 = [[Course alloc] init];
[course1 setName:@"数学"];
[course1 setDesc:@"高等数学理论教学"];
Course *course2 = [[Course alloc] init];
[course2 setName:@"英语"];
[course2 setDesc:@"商务英语口语教学"];
Address *address = [[Address alloc] init];
[address setCountry:@"中国"];
[address setProvince:@"山东"];
[address setCity:@"德州"];
NSMutableArray *courses = [[NSMutableArray alloc] init];
[courses addObject:course1];
[courses addObject:course2];
[student setName:@"John"];
[student setAge:19];
[student setUid:@"s2019091011"];
[student setAddress:address];
[student setCourses:courses];
NSDictionary *dic = [student modelToDict];
NSLog(@"%@", dic);
}
2019-08-13 18:19:17.089965+0800 Runtime[439:71840] {
address = {
city = "\U5fb7\U5dde";
country = "\U4e2d\U56fd";
province = "\U5c71\U4e1c";
};
age = 19;
courses = (
{
desc = "\U9ad8\U7b49\U6570\U5b66\U7406\U8bba\U6559\U5b66";
name = "\U6570\U5b66";
},
{
desc = "\U5546\U52a1\U82f1\U8bed\U53e3\U8bed\U6559\U5b66";
name = "\U82f1\U8bed";
}
);
name = John;
uid = s2019091011;
}
该实例只是一个简单粗糙的思路实现,实际转换过程中还有其他的判断和处理
网友评论