今天下定决心写一篇runtime的文章来记录一下runtime的用法吧。刚开始觉得runtime很高大上很深奥的样子,其实,理解使用之后还是这个样子。在写之前也看多几篇网上的文章,有些写的很好,可以缺乏源码,就只有个别说明,感觉模拟两可,有些只是说到一部分,成分残缺一样,不过还是自己亲自写一篇好,起码我真能理解到这里。
首先先说一下runtime的基本概念。借网上的一段话。
Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错
)。
只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
接下来说一下,runtime里面提供给我们的api有哪些,这些api我基本都会举例子来去用一下它的
主意:
C语言是一门静态语言,声明的方法必须要有实现,才能调用,不然连编译都不通过
然后实现myFunction方法之后,就能变易通过了
OC语言只有声明,没有实现也是可以的
但是运行之后就会崩掉,因为找不到该方法的实现
在动态添加方法(1)–源码用到这个方法
参数说明:
BOOL: 返回值,yes-------方法添加成功 no--------方法添加失败
Class cls: 将要给添加方法的类,传的类型 [类名 class]
SEL name: 将要添加的方法名,传的类型 @selector(方法名)
IMP imp:实现这个方法的函数 ,传的类型 1,C语言写法:(IMP)方法名 2,OC的写法:class_getMethodImplementation(self,@selector(方法名:))
class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
注意:const char *types:表示我们要添加的方法的返回值和参数
“v@”:v:是添加方法无返回值 @表示是id(也就是要添加的类) :表示添加的方法类型 @表示:参数类型
这个方法用于动态运行的时候为分类添加一个方法。
在方法的交换(同一个类里面)–源码有用到这个方法
class_getClassMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
class_getInstanceMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
在方法的交换(同一个类里面)–源码有用到这个方法
method_exchangeImplementations(<#Method _Nonnull m1#>, <#Method _Nonnull m2#>)
在万能控制器跳转–源码有改方法的应用
就是@property申明的属性
class_copyPropertyList(<#Class _Nullable __unsafe_unretained cls#>, <#unsigned int * _Nullable outCount#>)
在实现字典转模型的自动转换–源码有用过这个方法
(包括属性)
返回类的所有属性和变量(包括在@interface大括号中声明的变量)
class_copyIvarList(<#Class _Nullable __unsafe_unretained cls#>, <#unsigned int * _Nullable outCount#>)
在万能控制器跳转–源码有改方法的应用
objc_getClass(<#const char * _Nonnull name#>)
相当于把一个类,转换成一个对象,这个类相当于这个对象对底层的基类。
在万能控制器跳转–源码有改方法的应用
objc_allocateClassPair(<#Class _Nullable __unsafe_unretained superclass#>, <#const char * _Nonnull name#>, <#size_t extraBytes#>)
class_addIvar(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
在Runtime Ivar的使用–源码里面有用到这个方法
const char *ivar_getName(Ivar v);
const char *ivar_getTypeEncoding(Ivar v)
在Runtime Ivar的使用–源码里面有用到这个方法
Ivar class_getInstanceVariable(Class cls, const char *name)
Ivar class_getClassVariable(Class cls, const char *name)
在Runtime Ivar的使用–源码里面有用到这个方法
id object_getIvar(id obj, Ivar ivar)
在Runtime Ivar的使用–源码里面有用到这个方法
void object_setIvar(id obj, Ivar ivar, id value)
在Runtime Ivar的使用–源码里面有用到这个方法
objc_registerClassPair(<#Class _Nonnull __unsafe_unretained cls#>)
下面的方法是针对Property的runtime方法,就相当于在runtime里面创一个下面图片的属性
特性相关编码
属性的特性字符串 以 T@encode(type) 开头, 以 V实例变量名称 结尾,中间以特性编码填充,通过property_getAttributes即可查看
特性编码 具体含义
R readonly
C copy
& retain
N nonatomic
G(name) getter=(name)
S(name) setter=(name)
D @dynamic
W weak
P 用于垃圾回收机制
在RunTime Property的使用–源码里面有用到这个
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
objc_property_t class_getProperty(Class cls, const char *name)
在RunTime Property的使用–源码里面有用到这个方法
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
在RunTime Property的使用–源码里面有用到这个方法
const char *property_getName(objc_property_t property)
在RunTime Property的使用–源码里面有用到这个方法
const char *property_getAttributes(objc_property_t property)
objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount)
char *property_copyAttributeValue(objc_property_t property, const char *attributeName)
RunTime获取类的对象方法里面有用到这个方法 - 源码
不过获取不了类里面的类方法
下面将会把上面runtime提供的方法结合开发的需求来用一下它。
为分类动态添加属性–源码
一般来说,分类只能添加方法,不能添加属性的,但是如果你想非要添加的话可以通过runtime方法来去添加。
动态添加方法(1)–源码
一般项目中都有这样的一个需求,我们需要把项目中的一下信息归档起来保存在本地。然后每次项目启动的时候都会解档出来,用一个单例来获取点出属性来获取解档出来的属性,能在整个项目里面去使用。在模型里面我们需要遵守NSCodeing协议。实现两个方法,如下的代码:
-(void)setName:(NSString *)name{
NSUserDefaults *def = [NSUserDefaults standardUserDefaults];
[def setObject:name forKey:@"UserName"];
}
-(NSString*)name{
NSUserDefaults *def = [NSUserDefaults standardUserDefaults];
return [def objectForKey:@"UserName"];
}
如果项目里面只有一两个属性的话,工作量不是很大,但是如果属性有100个的话,写起来就很费劲,如果哪一天加一个哪一天加一个的话,就觉得很繁琐。
所以我们可以用到runtime里面动态添加方法来去解决这个问题。
在模型的.h增加属性之后,.m里面将属性声明为@dynamic,即不使用自动生成的get和set方法。
在消息转发机制中,对象若收到无法解读的消息,首先会调用所属类的类方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
参数selector就是未知的选择子(方法)。例如,将name声明为@dynamic后,执行[[Passport sharedPassport] setName:@“Jack”]时,会给Passport类发送一个setName:的消息,Passport收到消息后将其转换为函数调用,但是在Passport类中并没有setName:方法,因此会调用类的resolveInstanceMethod:方法,并将setName:作为参数传入。注意到resolveInstanceMethod:有Bool类型的返回值,表示这个类是否能新增一个实例方法来处理此选择子。因此我们要在resolveInstanceMethod:中完成未实现的set和get操作:
上面的代码,先通过选择子的前缀判断是set方法还是get方法,然后通过class_addMethod()方法动态给Passport类添加对应的set或get方法。
下面将实现autoDictionarySetter和autoDictionaryGetter方法:
person本来没有这个方法,运行的时候动态添加
假如在.h里面声明两个属性
在.m里面中添加@dynamic
使用
结果
动态添加方法(2)–源码
动态添加方法(2)是另外的一个简单的例子来的,主要讲了,程序编译的时候,人类是没有回答的方法的,当程序运行,点击按键的时候才动态添加把人类的回答的放大添加进去运行时里面,然后执行它,输出结果。
方法的交换(同一个类里面)–源码
上面的链接demol里面,我用UIImage的类里面的imageNamed原生的方法替换成自己写的方法。然后再程序启动的load里面进行替换。那么以后使用imageNamed的时候,系统就会替换成我自己写的In_imageName方法。
假如有下面的一个需求:当用户点击按键的时候,我们想记录一下用户点击按键的次数,其实我们可以对点击按键的方法做一个分类来去继承它的基础上去写的,但是用runtime方法的替换实现起来的效果更好,我们替换了系统的方法之后,可以在自己的方法里面写每当点击按键的时候累加一下次数的逻辑。
方法的替换(不同类的方法)–源码
也有这样的一个需求,有些公司是买人家的SDK来用,然后发现SDK某个方法里面有BUG,但是他是静态文件来的,我们修改不了,只知道提供给我们的方法,我们只能够自己写一个类,创建一个方法,在方法里面实现我们想实现的逻辑,然后去替换它。这样也需要用到方法的替换。
其实上面的例子,替换跟交换的用法是一样的,只是自定义的方法在同一个类还是不同的类,叫法不一样而已,用的都是runtime提供给我们相同的api来去调用的。
动态修改属性的值–源码
runtime虽然有动态修改属性的值的写法,但是我在开发中没有遇到需求用过它的,它的主要原理是在代码编译的时候,假如person类有一个属性的值name 是A,然后运行的使用编译 person类的属性列表,拿到name之后进行修改。然后输出结果
unsigned int count = 0;
Ivar *ivar = class_copyIvarList([self.person class], &count);
for (int i = 0; i
万能控制器跳转–源码
有这样的一个需求,在一个电商的项目里面,假如做双十一的活动,后台推送了一条消息过来,前端App收到消息,点击消息跳到对应的控制器里面,消息里面有一个字典里面有个 要跳转控制器的名称是一个字符串来的,然后字典中还带了需要传进控制器里面的参数,这时候就会用到runtime里面提供的api进行转换了。
其实下面的整一串代码就是想后台传一个字典过来里面保存 homeViewController 字符串转换成横控制器的类 用来跳转控制器
还需要把参数传递到home控制器里面,但是在传递之前要判断home控制器是否存在这些属性,存在才传进去,不存在就不存,不然会崩掉
当然,自己写的项目当然知道肯定存在这个属性的,但是防止后台的人写错字符串的名称,一句话为了安全。所以才有这么多的操作。
核心代码:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 延时,等待所有控件加载完
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self test];
});
}
- (void)test{
// 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数
NSDictionary *userInfo = @{
@"class": @"HomeViewController",
@"property": @{
@"ID": @"123",
@"type": @"12"
}
};
[self push:userInfo];
}
/**
* 跳转界面
*/
- (void)push:(NSDictionary *)params{
// 类名
NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
//把OC的字符串转换成成C语言能处理的字符
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
// 把字串名称转换成一个类
Class newClass = objc_getClass(className);//利用参数名返回一个实例对象
if (!newClass)
{
// 创建一个类
Class superClass = [NSObject class];
newClass = objc_allocateClassPair(superClass, className, 0);//相当于把一个类,转换成一个对象,这个类相当于这个对象对底层的基类。
// 注册你创建的这个类
objc_registerClassPair(newClass);
}
// 创建对象
id instance = [[newClass alloc] init];
NSDictionary *propertys = params[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// 检测这个对象是否存在该属性
if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
// 利用kvc赋值
[instance setValue:obj forKey:key];
}
}];
// 跳转到对应的控制器
[self.navigationController pushViewController:instance animated:YES];
}
/**
* 检测对象是否存在该属性
*/
- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName{
unsigned int outCount, i;
//class_copyPropertyList 是C语言的api 用来获取协议 或者 类的属性
// 获取对象里的属性列表 objc_property_t这个是C语言 存储数据类的数据(存:字符串,数组,数字都可以) OC语言的字符串是NSArray
objc_property_t * properties = class_copyPropertyList([instance class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property =properties[i];//C语言的字符串数据
// 属性名转成字符串 把C语言的字符串数据 转换成OC的字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判断该属性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);//释放内存 C语言的Api
return YES;
}
}
free(properties);//释放内存 C语言的Api
return NO;
}
@end
实现字典转模型的自动转换–源码
字典转模型就是用到runtime里面的 获取类中的成员属性列表方法,然后把里面的成员属性,存到模型里面。
// 字典转模型
+ (instancetype)objectWithDict:(NSDictionary *)dict
{
// 创建对应模型对象
id objc = [[self alloc] init];
unsigned int count = 0;
// 1.获取成员属性数组
Ivar *ivarList = class_copyIvarList(self, &count);
// 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
for (int i = 0; i < count; i++) {
// 2.1 获取成员属性
Ivar ivar = ivarList[i];
// 2.2 获取成员属性名 C -> OC 字符串
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 2.3 _成员属性名 => 字典key
NSString *key = [ivarName substringFromIndex:1];
// 2.4 去字典中取出对应value给模型属性赋值
id value = dict[key];
// 获取成员属性类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 二级转换,字典中还有字典,也需要把对应字典转换成模型
//
// 判断下value,是不是字典
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { // 是字典对象,并且属性名对应类型是自定义类型
// user User
// 处理类型字符串 @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 自定义对象,并且值是字典
// value:user字典 -> User模型
// 获取模型(user)类对象
Class modalClass = NSClassFromString(ivarType);
// 字典转模型
if (modalClass) {
// 字典转模型 user
value = [modalClass objectWithDict:value];
}
// 字典,user
// NSLog(@"%@",key);
}
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel objectWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
// 2.5 KVC字典转模型
if (value) {
[objc setValue:value forKey:key];
}
}
// 返回对象
return objc;
}
实现NSCoding的自动归档和解档–源码
归档解档的应用也是有点像上面保存用户信息一样的,当需要保存新的值的时候,就不用重复的去增加属性。
- (void)encodeWithCoder:(NSCoder *)encoder{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i
假如:我们在某些需求上面没有找到解决办法,我们可以看看系统里面有没有我们可以用方法进行调用,就是调用系统没有提供给我们的私有方法,调用前面遍历系统的所有对象方法。
2022.08.08开发公司的app的时候是基于苹果自带的播放器appleMusic链接播放的,公司app需要判断appleMusic是否被删除,不然就没有办法播放。目前没有办法判断出appleMusic是否被删除,但是我在MPMusicPlayerController里面发现有个方法用来连接appleMusic,如果连接失败会返回“com.apple.Music”,我打算用他来判断是否下载有下载appleMusic。
首先通过runtime便利MPMusicPlayerController的对象方法
于是就去尝试调用系统的私有方法,结果有返回