1.前言
上一节已经介绍的Runtime的基础知识,讲解了常用的几个结构体和方法,这一节我们进入实战,看看Runtime在我的实际开发中到底能做什么,可以做什么。以下通个几个具体的应用场景,和大家探讨一下,希望通过实际的应用能加深对Runtime的理解并能在项目中熟练的使用。
2.Runtime的应用场景
2.1 UIView添加渐变色
- 在这之前我们首先来熟悉一下下面几个知识点:
// 设置关联对象
/*
参数说明:
object:添加关联的对象
key:用于存储被关联对象的key
value:被关联的对象
policy:内存管理方式
*/
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
// 获取关联对象
/*
参数说明:
object:添加关联的对象
key:通过存储时的key获取被关联对象
*/
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
// 移除关联对象
objc_removeAssociatedObjects(id _Nonnull object)
- 关于内存管理的方式我这里就不多说了,盗用网上的一张表,出处来自这里,能很好的说明各个枚举对应的内存管理方式。
| 内存策略 | 属性修饰 | 描述 |
|---|---|---|
| OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。 |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | @property (nonatomic, strong) 指定一个关联对象的强引用,不能被原子化使用。 |
| OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一个关联对象的copy引用,不能被原子化使用。 |
| OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一个关联对象的强引用,能被原子化使用。 |
| OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一个关联对象的copy引用,能被原子化使用。 |
- 准备好了么?现在我们要进入正真的实战演练了。在开始做之前我们仔细的想一下,我们目前用到的UIView能够添加渐变色么?好像不能吧,在
iOS系统中只有一个CAGradientLayer可以设置渐变色吧。但是CAGradientLayer有一个问题,是不能响应事件的,那我们如何给普通的UIView上添加一个CAGradientLayer呢? - 首先创建
UIView+Gradient这个分类,在分类中我们可以给分类设置属性和方法
@interface UIView (Gradient)
// CAGradientLayer的渐变色需要这几个参数
@property (nonatomic, strong) NSArray *colors;
@property (nonatomic, strong) NSArray *locations;
@property (nonatomic, assign) CGPoint startPoint;
@property (nonatomic, assign) CGPoint endPoint;
// 创建一个渐变的UIView
+ (UIView *)gradientViewWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;
// 设置CAGradientLayer的参数
- (void)setGrandientBackgroundWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;
@end
- 在
UIView内部其实是没有CAGradientLayer的,难道我们要给UIView创建这么一个?其实大可不必,因为每个UIView都有一个自带的layer对象,我们可以通过替换的方式把CALayer换成CAGradientLayer,通过下面的方法:
// default is [CALayer class]. Used when creating the underlying layer for the view.
@property(class, nonatomic, readonly) Class layerClass;
在UIView+Gradient.m文件中添加下面这个方法,这样UIView的layer实际上是CAGradientLayer,这样我们就可以在创建的时候给这个CAGradientLayer设置渐变色了。
+ (Class)layerClass {
return [CAGradientLayer class];
}
- 实现创建渐变的
UIView对象的类方法和设置渐变颜色的相关属性
+ (UIView *)gradientViewWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
UIView *view = [[self alloc] init];
[view setGrandientBackgroundWithColors:colors locations:locations startPoint:startPoint endPoint:endPoint];
return view;
}
- (void)setGrandientBackgroundWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
NSMutableArray *tempColors = [NSMutableArray new];
for (UIColor *color in colors) {
[tempColors addObject:(__bridge id)color.CGColor];
}
// 在分类中添加属性,其实就是实现相应的setter和getter方法
self.colors = tempColors;
self.locations = locations;
self.startPoint = startPoint;
self.endPoint = endPoint;
}
- (void)setColors:(NSArray *)colors {
// 给self设置一个颜色数组的关联对象,当上面调用setter方法的时候,保存关联对象,同时设置渐变色
objc_setAssociatedObject(self, @selector(colors), colors, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 这里需要强制转换成CAGradientLayer,因为上面的类方法已经指定返回的是CAGradientLayer
CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
gLayer.colors = colors;
}
- (NSArray *)colors {
// 通过_cmd这个key,其实就是@selector(colors),把上面保存的颜色数组取出来
return objc_getAssociatedObject(self, _cmd);
}
// 下面的方式跟上面颜色数组一样
- (void)setLocations:(NSArray *)locations {
objc_setAssociatedObject(self, @selector(locations), locations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
gLayer.locations = locations;
}
- (NSArray *)locations {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setStartPoint:(CGPoint)startPoint {
objc_setAssociatedObject(self, @selector(startPoint), [NSValue valueWithCGPoint:startPoint], OBJC_ASSOCIATION_ASSIGN);
CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
gLayer.startPoint = startPoint;
}
- (CGPoint)startPoint {
NSValue *pointValue = objc_getAssociatedObject(self, _cmd);
return pointValue.CGPointValue;
}
- (void)setEndPoint:(CGPoint)endPoint {
objc_setAssociatedObject(self, @selector(endPoint), [NSValue valueWithCGPoint:endPoint], OBJC_ASSOCIATION_ASSIGN);
CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
gLayer.endPoint = endPoint;
}
- (CGPoint)endPoint {
NSValue *pointValue = objc_getAssociatedObject(self, _cmd);
return pointValue.CGPointValue;
}
- 这样我们的分类就弄好了,可以随意创建一个渐变色的
view出来,无需在view上面再添加一个CAGradientLayer,下面是创建后的效果
UIView *gradientView = [UIView gradientViewWithColors:@[[UIColor redColor], [UIColor greenColor]] locations:@[@0, @1] startPoint:CGPointMake(0, 0.5) endPoint:CGPointMake(1, 0.5)];
gradientView.frame = CGRectMake(10, 100, self.view.bounds.size.width - 20, 200);
[self.view addSubview:gradientView];
gradientView.backgroundColor = [UIColor yellowColor];
2.2消息转发
首先我们先理解OC中消息的机制,OC底层传递消息的函数常用的有两个,objc_msgSend(void /* id self, SEL op, ... */ )这是是调用自己的方法,objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )是调用父类的方法。
当我们使用[obj sayHello],编译器会转换成objc_msgSend(obj, sayHello)进行消息发送,Runtime内部的流程大概是这样的。
- 首先通过
obj的isa指针找到它的class; - 在
class的cache内部查找,看是否有之前调用过,如果有就直接找到缓存的函数执行,如果没有就去class的method list查找; - 如果在
class中没有找到sayHello,继续通过superclass,找到父类,在父类的method list里查找; - 如果找到就执行函数对应的
IMP(找不到的情况后面会进入消息转发流程); 如果在class内找不到,沿着继承树也找不到,就会进入消息转发流程,在消息转发失败之前,系统提供了最后三次机会让我们可以拦截,对没有实现的方法进行处理。 - 动态方法解析
- 备用接收者
- 完整的消息转发
2.2.1动态方法解析
当OC运行时没有找到SEL对应的的IMP,就会调用+resolveInstanceMethod或者+resolveClassMethod让你有机会在这两个地方进行拦截处理,可以在这两个方法内部动态的给当前的SEL指定一个IMP执行函数,这样就能避免崩溃。
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector: @selector(sayHello:) withObject:@"哈哈"];
}
// 这个方法能拦截未实现的方法,动态的给当前对象添加一个IMP执行函数
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHello:)) {
// "v@:@"前面的章节已经有说明了
class_addMethod(self, sel, (IMP)mySayHello, "v@:@");
// 如果上面动态的添加了方法,这里返回YES或者是NO都无所谓
return YES;
}
return [super resolveInstanceMethod:sel];
}
void mySayHello(id self, SEL _cmd, id obj) {
NSLog(@"sel:%@, obj:%@", NSStringFromSelector( _cmd), obj);
}
// 打印结果
2021-10-21 11:51:34.232107+0800 abffff[20246:412330] sel:sayHello:, obj:哈哈
通过上面的动态解析,给到当前控制添加了执行函数,等下一次调用[self performSelector: @selector(sayHello:) withObject:@"哈哈"]的时候,由于class内部能通过@selector(sayHello:)这个SEL找到(IMP)mySayHello去直接执行,就不会调用+resolveInstanceMethod了。
2.2.2备用接收者
当+resolveInstanceMethod或者+resolveClassMethod方法中没有动态的给对象添加方法,无论这两个方法返回的是YES还是NO,目标对象都会调用-forwardingTargetForSelector,这个方法里我们就有机会把当前未实现的方法转发给其他对象。
// 定义一个Person类实现sayHello方法
@interface Person: NSObject
@end
@implementation Person
- (void)sayHello:(NSString *)hi {
NSLog(@"person sayHello:%@", hi);
}
@end
// viewController中实现这个方法,转发给备用对象Person去处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello:)) {
// 自己没实现的方法,让Peron去接收这个消息
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}
// 打印结果如下
2021-10-21 15:05:05.101455+0800 abffff[35193:518268] person sayHello:哈哈
2.2.3完整的消息转发
如果在上面两步中消息还没得到处理,就会进入完整的消息转发机制了。首先会发送- (NSMethodSignature *)methodSignatureForSelector:(**SEL**)aSelector这个方法需求返回对未实现的SEL的函数签名,如果返回nil,程序就会出现经典的unrecognized selector send to ...,其实是Runtime调用了-doesNotRecognizeSelector:;如果返回一个函数签名,Runtime就会内部创建一个NSInvocation,同时调用- (void)forwardInvocation:(NSInvocation *)anInvocation把创建的NSInvocation带过来,在这个方法里,我们就可以通过invokeWithTarget把消息转发给指定的对象了。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 其实这里返回NO和YES都是可以的
return NO;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello:)) {
// v@:@参考前面章节
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if ([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
} else {
return [self doesNotRecognizeSelector:sel];
}
}
以上就是消息转发的三个过程,消息转发流程参考出处这里。第三步中NSInvocation和NSMethodSignature也可以用于给指定的对象发送消息,相比performSelector能传递多个参数。
2.3字典模型相互转换
在iOS开发中常用的字典模型互相转换的两个框架是MJExtension和YYModel两个框架,其实内部都是运用Runtime,把字典对应的key和对象的属性名对应赋值,核心方法在NSObject分类里,下面我们模仿这两个框架去实现一个字典模型互换的功能,加强自己对Runtime的熟练度。
- (instancetype)iyc_modelWithDict:(NSDictionary *)dict {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList(self.class, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = propertyList[i];
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
NSString *propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];
// 其实下面的类型判断MJExtension内部是直接if else一个个类型判断的,这里我们只是为了说明propertyAttributes各种字段对应的类型是什么
// propertyAttributes用逗号分割
NSArray *attributes = [propertyAttributes componentsSeparatedByString:@","];
// 第一个字符串为属性的类型
NSString *propertyTypeStr = attributes[0];
if ([propertyTypeStr isEqualToString:@"T@\"NSString\""]) {
// 字符串
if (dict[propertyName] != nil) {
[self setValue:dict[propertyName] forKey:propertyName];
}
} else if ([propertyTypeStr isEqualToString:@"Tq"]) {
// 整形
if (dict[propertyName] != nil) {
[self setValue:dict[propertyName] forKey:propertyName];
}
} else if ([propertyTypeStr isEqualToString:@"TB"]) {
// BOOL
if (dict[propertyName] != nil) {
[self setValue:dict[propertyName] forKey:propertyName];
}
} else if ([propertyTypeStr isEqualToString:@"Tf"]) {
// 浮点型
if (dict[propertyName] != nil) {
[self setValue:dict[propertyName] forKey:propertyName];
}
} else if ([propertyTypeStr isEqualToString:@"Td"]) {
// double
if (dict[propertyName] != nil) {
[self setValue:dict[propertyName] forKey:propertyName];
}
}
NSLog(@"propertyName:%@, propertyAttributes:%@", propertyName, propertyAttributes);
}
free(propertyList);
return self;
}
下面我们定一个一个Person类,看我们自己定义的方法是否可用。
@interface Person: NSObject
@property (nonatomic, copy) NSString *name; //!< 姓名
@property (nonatomic, assign) NSInteger age; //!< 年龄
@property (nonatomic, assign) BOOL isMarried; //!< 是否已婚
@property (nonatomic, assign) float weight; //!< 体重
@property (nonatomic, assign) double money; //!< 钱
@end
@implementation Person
- (void)sayHello:(NSString *)hi {
NSLog(@"person sayHello:%@", hi);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary *d3 = @{@"name": @"Sony",
@"age": @"42",
@"isMarried": @1,
@"weight": @"133.4",
@"money": @"99442.44"};
Person *p = [[Person alloc] init];
[p iyc_modelWithDict:d3];
NSLog(@"p.name=%@\np.age=%zd\np.isMarried=%d\np.weight=%f\np.money=%f", p.name, p.age, p.isMarried, p.weight, p.money);
}
// 打印信息如下
2021-10-22 15:09:59.431060+0800 abffff[5299:1068425] propertyName:name, propertyAttributes:T@"NSString",C,N,V_name
2021-10-22 15:09:59.431273+0800 abffff[5299:1068425] propertyName:age, propertyAttributes:Tq,N,V_age
2021-10-22 15:09:59.431401+0800 abffff[5299:1068425] propertyName:isMarried, propertyAttributes:TB,N,V_isMarried
2021-10-22 15:09:59.431582+0800 abffff[5299:1068425] propertyName:weight, propertyAttributes:Tf,N,V_weight
2021-10-22 15:09:59.431739+0800 abffff[5299:1068425] propertyName:money, propertyAttributes:Td,N,V_money
2021-10-22 15:09:59.431876+0800 abffff[5299:1068425] p.name=Sony
p.age=42
p.isMarried=1
p.weight=133.399994
p.money=99442.440000
从打印信息看出我们自定义的字典转模型已经成功了;但是形如T@"NSString",C,N,V_name具体是什么意思呢,我这里贴出官网的链接,这里详细说明了各个特性字符串代表的意思。
- 特性字符串以,分割;
- 特性第一个是属性的类型,第二个是内存管理方式,第三个是原子性,第四个是属性的名字;
就拿
T@"NSString",C,N,V_name说明,该属性是NSString,修饰符是copy,原子性是nonatomic,属性名是name,以前自己开发的时候对这些字符串代表的意思不是很明确,现在通过官网的这张表格,就一清二楚了。
2.4 iOS开发中的钩子
钩子,英文叫hook,来源于Windows程序开发。之前一听说钩子,感觉很高大上,不知道是什么玩意。在iOS中钩子的意思,就是在消息传递的过程中,插入一个功能,改变消息或者延迟消息的执行顺序,让在执行某个功能或者方法之前,优先调用另外一个方法,然后再回到原来的执行路线上。用下面这张图就能很好的解释了。
在iOS中要想实现上面这张图,只有通过
Method Swizzle,正常情况下我们一个SEL对应一个IMP,如下图。
我们可以用method_exchangeImplementations来交换两个SEL指向的IMP
废话不多说,还是上代码更加直接一点。我们已模拟神策的无痕埋点这个功能,在viewController的生命周期里把当前页面的埋点参数进行上传给后台。可以想一下,如果我们的APP内部有100个页面,每个viewController有四五个生命周期函数,难道我们要一个个的周期函数内部添加我们的埋点上传参数么?这里就可以通过hook进行处理。新建viewController的分类UIViewController+Hook。
@implementation UIViewController (Hook)
+ (void)load {
[super load];
// 获取两个SEL对应的IMP,然后交换。
Method m1 = class_getInstanceMethod(self, @selector(viewDidLoad));
Method m2 = class_getInstanceMethod(self, @selector(iyc_viewDidLoad));
method_exchangeImplementations(m1, m2);
}
- (void)iyc_viewDidLoad {
/**
这里可以添加一下功能,比如埋点上传,我们就不用到每个页面的viewDidLoad去手动添加埋点,
因为所有的控制器调用viewDidLoad,内部都会经过这里,就可以统一在这里处理
*/
[self uploadTrackWithParams:@{@"pageName": @"首页"}];
// 这里调用iyc_viewDidLoad,因为iyc_viewDidLoad指向的是交换前的IMP,也就是原来的viewDidLoad方法
[self iyc_viewDidLoad];
}
- (void)uploadTrackWithParams:(NSDictionary *)params {
NSLog(@"开始上传埋点了");
}
这样每次调用viewDidLoad的时候,其实是先执行iyc_viewDidLoad,在这个方法内部我们就可以把我们需要上传的参数传给后台,然后在方法的最后调用iyc_viewDidLoad,因为这时候iyc_viewDidLoad这个SEL指向的是交换过的viewDidLoad的IMP,也就是原来的执行方法,回到原来的执行线路上来。这样我们就在不改变原有代码的情况下给每个控制器的生命周期函数hook上了一个埋点上传的功能。
3.结尾
到此关于Runtime的基础和基本使用就写到这里了,期间也参考了很多别人的博客,在此感谢。其实Runtime的应用远不止这些,这里只想通过几个简单的Tip来让自己记住Runtime的使用,也希望大家能通过这几个小功能,能加深对Runtime的理解和运用,如果有点用处,帮我点一点星星,Thanks。