Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。学会使用Runtime api ,在iOS开发中实现更多的骚操作。
Runtime介绍
Runtime
其实有两个版本: “modern
” 和 “legacy
”。我们现在用的 Objective-C 2.0
采用的是现行 (Modern
) 版的 Runtime
系统,只能运行在 iOS
和 macOS 10.5
之后的 64
位程序中。而 macOS
较老的32
位程序仍采用 Objective-C 1
中的(早期)Legacy
版本的 Runtime
系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC
并不能直接编译为汇编语言,而是要先转写为纯C
语言再进行编译和汇编的操作,从OC
到C
语言的过渡就是由runtime来实现的。然而我们使用OC
进行面向对象开发,而C
语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
阅读使用官方Api,解决项目各种骚需求。
Runtime应用
Runtime
提供了很多api,功能强大,应用场景非常多,下面主要介绍3中应用场景。
- 关联对象(Objective-C Associated Objects)给分类增加属性;
- 方法魔法(Method Swizzling)方法替换;
- 实现字典和模型的自动转换;
关联对象(Objective-C Associated Objects)给分类增加属性
我们都是知道分类(category)中是不能自定义属性和变量的。但通过Runtime
的关联对象方法,就可以给分类添加属性。
关联对象Runtime提供饿了下面几个api:
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
参数解释
id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略
内存管理策略枚举
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
想要使用 runtime 的 api 需要引入#import "objc/runtime.h"
头文件。
下面实现一个 UIViewController
的 Category
添加自定义属性NSString * newTitle
。
ViewController+Property.h
@interface ViewController (Property)
@property (nonatomic , strong) NSString *newTitle;
@end
--------------------------------------------------------------------------
ViewController+Property.m
#import "ViewController+Property.h"
#import "objc/runtime.h"
static char kNewTitleKey;
@implementation ViewController (Property)
- (void)setNewTitle:(NSString *)newTitle {
objc_setAssociatedObject(self, &kNewTitleKey, newTitle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString*)newTitle {
return objc_getAssociatedObject(self, &kNewTitleKey);
}
@end
viewController 中的操作
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self testCategoryProperty];
}
- (void)testCategoryProperty {
self.newTitle = @"FK";
NSLog(@"%@",self.newTitle);
NSLog(@"运行成功");
}
如果不使用runtime中的关联api对newTitle进行关联,运行后会闪退,并显示如下报错:
Category 属性取值赋值报错.png
关联成功后,newTitle
setter
getter
方法使用正常。
成功为分类设置自定义属性。
方法魔法(Method Swizzling)方法替换
曾经在了解 +(void)load
方法时,也有了解过一下 Method Swizzling
,今天针对方法魔法进行实践操作。
实现方法替换的核心:
//方法实现替换api
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
下面实现一个替换 ViewController
的 viewDidLoad
方法的例子。
@implementation ViewController
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(FKViewdidLoad);
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"viewDidLoad");
}
- (void)FKViewdidLoad {
NSLog(@"FKViewdidLoad");
}
@end
最后的打印结果
犯法替换结果.png
viewDidLoad
和 FKViewdidLoad
两个方法替换成功。
此处值得注意的是 swizzling
应该只在+load
中完成。 在 Objective-C
的运行时中,每个类有两个方法都会自动调用。+load
是在一个类被初始装载时调用,+initialize
是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。
swizzling
应该只在dispatch_once
中完成,由于swizzling
改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch
的 dispatch_once
满足了所需要的需求,并且应该被当做使用swizzling
的初始化单例方法的标准。
实现字典和模型的自动转换
原理描述:用runtime
提供的函数遍历Model
自身所有属性,如果属性在json
中有对应的值,则将其赋值。 核心api:
//遍历类型中所有属性
class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
在 NSObject
中添加方法:
@implementation NSObject (RuntimeModel)
- (void)initWithDict:(NSDictionary *)dict {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
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];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);
//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
@end
viewController
中的操作
- (void)viewDidLoad {
[super viewDidLoad];
[self.runtimeM initWithDict:@{@"time" : @"5点" , @"name" : @"FK" , @"sex" : @"Man"}];
NSLog(@"%@ - %@ - %@",self.runtimeM.time , self.runtimeM.name , self.runtimeM.sex);
}
打印结果如下:
屏幕快照 2018-09-09 下午5.52.33.png
成功将字典类型转换为模型类型。
拓展
使用 class_copyPropertyList
遍历方法,可以遍历对象中所有属性,并且可以把所有属性制空。
总结
到这里,本文想介绍的 runtime
3个应用场景就介绍完了。本文已贴出Demo中核心代码,如果想阅读完整代码,可以下载Demo查看。
网友评论