先说点题外话,最近看自己的简书,快一个月没有更新了,其实在写去年的个人总结,只不过去年的经历太曲折,心路历程(我吐)太丰富,从元旦开始写了一个月还没写完。。。并且这篇自己的总结可能并不会发布出来,造成原来给自己下的持续写博客的任务可能完成不了,但是一月底之前加上本篇应该会有两篇关于技术的文章会整理发出来,权当弥补了自己的懒惰,一篇总结写了一个月我也是醉了。
其实我的简书也没什么人看啦,但是一直以来有看 ibireme 和 巧神 的博客,追着看下来,我觉得有些时候这些东西不是写给别人看的,也是现在的自己在帮助以后的自己在总结、在记录,是自己对自己说的话,balabala,开始正题。
前言
OC 的运行时机制(Runtime)这个词接触过 iOS 开发的人或多或少应该都听说过,有的人已经能够熟练掌握它的真谛,有的对其依然感觉神秘无比。诚然,即使不掌握 OC 的运行时机制并不影响开发者开发出优秀的 iOS 应用,但是了解运行时机制能够帮助开发者更加透彻的了解 OC 背后的故事,揭开高级语言壳子下面的东西。
举个例子:我们都知道 OC 是一门动态语言,消息机制甚至有一段时间变成 iOS 程序员面试必问面试题,那么要答好这道题的真正本质,如果不了解运行时是比较困难的。
本篇博客不会就运行时机制进行详细的探讨,作者本人也不具备这样的能力,关于详细介绍运行时的文章,我推荐 南峰子 大神的博客,他的博客较为系统、详细的对 OC 运行时机制进行了介绍,我原来也是通过他的博客开始慢慢了解 OC 的运行时机制的。本篇博客主要是结合我的一些工作、学习经历,总结运行时在我个人开发中的一些应用,也会随用随总结。
应用场景
动态添加属性
我们都知道,分类(Category)是 OC 的一个重要的语法,它可以帮助开发者在类的外部扩充方法,保持原类的规模,方法在需要的时候分块声明和实现,然而 Category 并不能帮助我们方便的给原来的类扩充属性,如果我们在分类中如下添加属性:
@interface NSObject (DZTest)
@property (nonatomic, strong) NSString* test;
@end
会出现如下错误:
错误截图而利用 objc/runtime.h 库中的一些函数可以帮助我们在分类添加属性,具体如下:
#import "NSObject+DZTest.h"
#import <objc/runtime.h>
#define DZTestDemoKey "test"
@implementation NSObject (DZTest)
- (void)setTest:(NSString*)test
{
//将属性同对象关联
objc_setAssociatedObject(self, DZTestDemoKey, test, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString*)test
{
//对出 key 关联的对象属性
return objc_getAssociatedObject(self, DZTestDemoKey);
}
@end
这种应用场景在 SDWebImage 框架中多次出现,SDWebImage 框架大量使用了分类来给 UIKit 框架的种类扩充方法,而在有些情况下又不得不通过分类给原类扩充属性来达到框架的设计目的,比如:我们可以在UIImageView+WebCache.m
中找到这样的使用场景:
- (void)setShowActivityIndicatorView:(BOOL)show{
objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, [NSNumber numberWithBool:show], OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)showActivityIndicatorView{
return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
- (void)setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}
- (int)getIndicatorStyle{
return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}
动态检测模型属性
使用 KVC 进行字典转模型的时候,我们一般都会这样写:
- (instancetype)initWithDict:(NSDictionary*)dict
{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
这样写会有一个缺陷,就是当 server 返回的 json 数据比转成字典后属性比我们定义的模型属性多的时候,KVC 转换的时候会报undefined key
错误,我们可以重写
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{}
改写原来方法默认的报异常的实现方式,这样就不会报错误。
其实,除这个方法进行字典转模型以外,我们可以使用运行时的一些方法动态的获取模型中的属性列表:
- (NSArray *)getProperties
{
unsigned int count;
objc_property_t *properties = class_copyPropertyList(self.class, &count);
NSMutableArray *array = [NSMutableArray array];
for (int i =0; i< count ; i++) {
objc_property_t pro = properties[i];
const char *name = property_getName(pro);
NSString *property = [[NSString alloc] initWithUTF8String:name];
[array addObject:property];
}
return array;
}
然后根据获得属性列表进行有针对性的赋值,达到字典转模型的目的:
+(instancetype)channelWithDict:(NSDictionary *)dict
{
HMChannel *channel = [[HMChannel alloc] init];
NSArray *array = [channel getProperties];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
//
NSString *key = obj;
if (dict[key]) {
[channel setValue:dict[key] forKey:key];
}
}];
return channel;
}
这个方法也在 MJExtension 框架有所涉及,NSObject+MJProperty.m
中的+ (NSMutableArray *)properties
方法使用了这种方式获取了属性列表。
方法的交换(拦截)
因为 OC 是动态的语言,方法的调用并不是在编译阶段完全定死的,而是在运行的时候以发送消息的形式进行调用,所以这里就引出了一个比较受争议的做法,就是通过运行时的 api 对方法的调用进行交换,也就是所谓的 method swizzling。我们通过下面的例子演示一下,将系统的[UIImage imageNamed:]
的方法与我们自己实现的imageWithName:
方法进行交换:
#import "UIImage+Extension.h"
#import <objc/runtime.h>
@implementation UIImage (Extension)
+ (void)load{
//当我们的类加载到内存时候调用,在整个程序运行期间,只会调用一次
//1.系统的方法
Method orginalMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
//2.得到自己写的方法
Method myMethod = class_getClassMethod([UIImage class], @selector(imageWithName:));
//将系统方法和我们的方法交换
method_exchangeImplementations(orginalMethod, myMethod);
}
+ (UIImage *)imageWithName:(NSString *)name{
name=@"2.png";
//注意调用的方法
return [self imageWithName:name];
}
@end
这个地方需要注意的是:在自己写的需要交换的方法中,由于方法已经被交换了,不能在自己写的方法中使用imageNamed方法,否则会造成死循环。
修改对象中的私有属性
工作中可能会遇到一种情况,你的项目是一个维护有些时日的老项目,当有的时候实现一个功能需要修改基类中的一些属性,然而基类在定义的时候并未考虑到这个属性需要修改,所以在类扩展中进行的定义,没有暴露出来,由于考虑到基类在多个地方被使用,无法评估修改基类对于项目其他地方的影响,这个时候需要用运行时的机制对私有属性进行获取和修改:
DZOldNavigationController* nav = (DZOldNavigationController*)self.navigationController;
unsigned int count = 0;
// 得到类中的 Ivar 数组
Ivar* members = class_copyIvarList([DZOldNavigationController class], &count);
// 遍历数组,获得需要的 Ivar
for (int i = 0; i < count; i++) {
Ivar var = members[i];
// 获取 Ivar 对应的属性名称
const char *memberName = ivar_getName(var);
// 获取对应的 Ivar
if (strcmp(memberName, "_screenShotsList") == 0) {
NSMutableArray* arr = [NSMutableArray array];
// 获取 Ivar 对应的值
arr = object_getIvar(nav, var);
[arr removeObjectAtIndex:arr.count - 1];
// 修改 Ivar 的值
object_setIvar(nav, var, arr);
break;
}
// const char *memberType = ivar_getTypeEncoding(var);
// NSLog(@"%s----%s", memberName, memberType);
}
这个例子中就在没有破坏类结构的前提下修改了DZOldNavigationController
中的私有属性_screenShotsList
的值。
网友评论