共计 10222 个字符,预计需要花费 26 分钟才能阅读完成。
runtime 简介
因为 objective- c 是一门动静语言,也就是说只有编译器是不够的,还须要一个运行时零碎(runtime system)来执行编译后的代码。这是整个 objective- c 运行框架的一块基石。runtime 简称运行时。其中最次要的就是音讯机制。对于编译期语言,会在编译的时候决定调用哪个函数。对于 OC 的函数,是动静调用的,在编译的时候并不能决定真正调用哪个函数,只有在运行时才会依据函数的名称找到对应的函数来调用。
runtime 的作用
Objc 在三种层面上与 Runtime 零碎进行交互:1. 通过 Objective-C 源代码
2. 通过 Foundation 框架的 NSObject 类定义的办法
3. 通过对 Runtime 库函数的间接调用
runtime 源码
苹果和 GNU 各自保护一个开源的 runtime 版本,这两个版本之间都在致力的保持一致。
都是运行时的头文件,其中次要应用的函数定义在 message.h 和 runtime.h 这两个文件中。
通过 Foundation 框架的 NSObject 类定义的办法
Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类是个例外,它是个形象超类)
-class
办法返回对象的类;-isKindOfClass:
和-isMemberOfClass:
办法查看对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者以后类的成员变量);-respondsToSelector:
查看对象是否响应指定的音讯;-conformsToProtocol:
查看对象是否实现了指定协定类的办法;-methodForSelector:
返回指定办法实现的地址。
通过对 Runtime 库函数的间接调用
Runtime 零碎是具备公共接口的动静共享库。头文件寄存于 /usr/include/objc 目录下,这意味着咱们应用时只须要引入 objc/Runtime.h
头文件即可。
许多函数能够让你应用纯 C 代码来实现 Objc 中同样的性能。除非是写一些 Objc 与其余语言的桥接或是底层的 debug 工作,你在写 Objc 代码时个别不会用到这些 C 语言函数。对于公共接口都有哪些,前面会讲到。我将会参考苹果官网的 API 文档。
Runtime 的术语的数据结构
SEL
它是 selector
在 Objc 中的示意(Swift 中是 Selector 类)。selector 是办法选择器,其实作用就和名字一样,日常生活中,咱们通过人名分别谁是谁,留神 Objc 在雷同的类中不会有命名雷同的两个办法。selector 对办法名进行包装,以便找到对应的办法实现。它的数据结构是:
typedef struct objc_selector *SEL;
咱们能够看出它是个映射到办法的 C 字符串,你能够通过 Objc 编译器器命令@selector()
或者 Runtime 零碎的 sel_registerName
函数来获取一个 SEL
类型的办法选择器。
id
id 是一个参数类型,它是指向某个类的实例的指针。定义如下:
typedef struct objc_object *id;
struct objc_object {Class isa;};
以上定义,看到 objc_object
构造体蕴含一个 isa 指针,依据 isa 指针就能够找到对象所属的类。
Class
typedef struct objc_class *Class;
Class
其实是指向 objc_class
构造体的指针。objc_class
的数据结构如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
从 objc_class
能够看到,一个运行时类中关联了它的父类指针、类名、成员变量、办法、缓存以及从属的协定。
其中 objc_ivar_list
和 objc_method_list
别离是成员变量列表和办法列表:
// 成员变量列表
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
// 办法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
Method
Method 代表类中某个办法的类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
objc_method
存储了办法名,办法类型和办法实现:
- 办法名类型为
SEL
- 办法类型
method_types
是个 char 指针,存储办法的参数类型和返回值类型 method_imp
指向了办法的实现,实质是一个函数指针
Ivar
Ivar
是示意成员变量的类型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
其中 ivar_offset
是基地址偏移字节
IMP
IMP 在 objc.h 中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发动一个 ObjC 音讯之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP
这个函数指针就指向了这个办法的实现。
如果失去了执行某个实例某个办法的入口,咱们就能够绕开消息传递阶段,间接执行办法,这在前面 Cache
中会提到。
你会发现 IMP
指向的办法与 objc_msgSend
函数类型雷同,参数都蕴含 id
和 SEL
类型。每个办法名都对应一个 SEL
类型的办法选择器,而每个实例对象中的 SEL
对应的办法实现必定是惟一的,通过一组 id
和 SEL
参数就能确定惟一的办法实现地址。
而一个确定的办法也只有惟一的一组 id
和 SEL
参数。
Cache
Cache 定义如下:
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache 为办法调用的性能进行优化,每当实例对象接管到一个音讯时,它不会间接在 isa 指针指向的类的办法列表中遍历查找可能响应的办法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。
Runtime 零碎会把被调用的办法存到 Cache 中,如果一个办法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先拜访 Cache 一样。
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;// 这个更罕用
能够通过class_copyPropertyList
和 protocol_copyPropertyList
办法获取类和协定中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
#import <Foundation/Foundation.h>
@interface Person : NSObject
/** 姓名 */
@property (strong, nonatomic) NSString *name;
/** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
@end
以上是一个 Person 类,有 3 个属性。让咱们用上述办法获取类的运行时属性。
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
NSLog(@"%d", outCount);
for (NSInteger i = 0; i < outCount; i++) {NSString *name = @(property_getName(properties[i]));
NSString *attributes = @(property_getAttributes(properties[i]));
NSLog(@"%@--------%@", name, attributes);
}
音讯
一些 Runtime 术语讲完了,接下来就要说到音讯了。领会苹果官网文档中的 messages aren’t bound to method implementations until Runtime。音讯直到运行时才会与办法实现进行绑定。
这里要分明一点,objc_msgSend
办法看清来如同返回了数据,其实objc_msgSend
从不返回数据,而是你的办法在运行时实现被调用后才会返回数据。上面具体叙述音讯发送的步骤(如下图):
深刻代码了解 instance、class object、metaclass
通过上图能够看出,一个实例对象 `struct objc_object` 的 isa 指针指向它的 `struct objc_class` 类对象,类对象的 isa 指针指向它的元类;`super_class` 指针指向了父类的 ` 类对象 `,而 ` 元类 ` 的 `super_class` 指针指向了父类的 ` 元类 `。
runtime 的利用
发送音讯
办法调用的实质,就是让对象发送音讯。objc\_msgSend, 只有对象能力发送音讯,因而以 objc 结尾。应用音讯机制前提,必须导入 #import <objc/message.h>
音讯机制简略应用:
// 创立 person 对象
Person *p = [[Person alloc] init];
// 调用对象办法
[p eat];
// 实质:让对象发送音讯
objc_msgSend(p, @selector(eat));
// 调用类办法的形式:两种
// 第一种通过类名调用
[Person eat];
// 第二种通过类对象调用
[[Person class] eat];
// 用类名调用类办法,底层会主动把类名转换成类对象调用
// 实质:让类对象发送音讯
objc_msgSend([Person class], @selector(eat));
咱们能够通过 clang 来查看代码生成的 CPP 代码。例如:clang 将 oc main.m 文件转成 c ++ main\_cpp 文件代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_cpp.cpp
替换办法
替换两个办法的实现个别写在类的 load 办法外面,因为 load 办法会在程序运行前加载一次,而 initialize 办法会在类或者子类在 第一次应用的时候调用,当有分类的时候会调用屡次。
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 需要:给 imageNamed 办法提供性能,每次加载图片就判断下图片是否加载胜利。// 步骤一:先搞个分类,定义一个能加载图片并且能打印的办法 + (instancetype)imageWithName:(NSString *)name;
// 步骤二:替换 imageNamed 和 imageWithName 的实现,就能调用 imageWithName,间接调用 imageWithName 的实现。UIImage *image = [UIImage imageNamed:@"123"];
}
@end
@implementation UIImage (Image)
// 加载分类到内存的时候调用
+ (void)load
{
// 替换办法
// 获取 imageWithName 办法地址
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
// 获取 imageWithName 办法地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
// 替换办法地址,相当于替换实现形式
method_exchangeImplementations(imageWithName, imageName);
}
// 不能在分类中重写零碎办法 imageNamed,因为会把零碎的性能给笼罩掉,而且分类中不能调用 super.
// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
// 这里调用 imageWithName,相当于调用 imageName
UIImage *image = [self imageWithName:name];
if (image == nil) {NSLog(@"加载空的图片");
}
return image;
}
@end
类 / 对象的关联对象
应用形式一:给分类增加属性
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 给零碎 NSObject 类动静增加属性 name
NSObject *objc = [[NSObject alloc] init];
objc.name = @"小码哥";
NSLog(@"%@",objc.name);
}
@end
// 定义关联的 key
static const char *key = "name";
@implementation NSObject (Property)
- (NSString *)name
{
// 依据关联的 key,获取关联的值。return objc_getAssociatedObject(self, key);
}
- (void)setName:(NSString *)name
{
// 第一个参数:给哪个对象增加关联
// 第二个参数:关联的 key,通过这个 key 获取
// 第三个参数:关联的 value
// 第四个参数: 关联的策略
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
应用形式二:给对象增加关联对象。
/**
* 删除点击
* @param recId 购物车 ID
*/
- (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId
{UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@""message:@" 确认要删除这个宝贝 "delegate:self cancelButtonTitle:@" 勾销 "otherButtonTitles:@" 确定 ", nil];
// 传递多参数
objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
alert.tag = [recId intValue];
[alert show];
}
/**
* 确定删除操作
*/
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {if (buttonIndex == 1) {NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id");
NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id");
NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag];
}
}
动静增加办法
简略应用:
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *p = [[Person alloc] init];
// 默认 person,没有实现 eat 办法,能够通过 performSelector 调用,然而会报错。// 动静增加办法就不会报错
[p performSelector:@selector(eat)];
}
@end
@implementation Person
// void(*)()
// 默认办法都有两个隐式参数,void eat(id self,SEL sel)
{NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的办法,会调用这个办法解决, 并且会把对应的办法列表传过来.
// 刚好能够用来判断,未实现的办法是不是咱们想要动静增加的办法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{if (sel == @selector(eat)) {
// 动静增加 eat 办法
// 第一个参数:给哪个类增加办法
// 第二个参数:增加办法的办法编号
// 第三个参数:增加办法的函数实现(函数地址)// 第四个参数:函数的类型,(返回值 + 参数类型) v:void @: 对象 ->self : 示意 SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
字典转模型 KVC 实现
// Ivar: 成员变量 以下划线结尾
// Property: 属性
+ (instancetype)modelWithDict:(NSDictionary *)dict
{id objc = [[self alloc] init];
// runtime: 依据模型中属性, 去字典中取出对应的 value 给模型属性赋值
// 1. 获取模型中所有成员变量 key
// 获取哪个类的成员变量
// count: 成员变量个数
unsigned int count = 0;
// 获取成员变量数组
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 获取成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量类型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 获取 key
NSString *key = [ivarName substringFromIndex:1];
// 去字典中查找对应 value
// key:user value:NSDictionary
id value = dict[key];
// 二级转换: 判断下 value 是否是字典, 如果是, 字典转换层对应的模型
// 并且是自定义对象才须要转换
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典转换成模型 userDict => User 模型
// 转换成哪个模型
// 获取类
Class modelClass = NSClassFromString(ivarType);
value = [modelClass modelWithDict:value];
}
// 给模型中属性赋值
if (value) {;}
}
return objc;
}
+ load 和 + initialize 原理解说
+load 总结
- load 办法调用在 main 之前,并且不须要咱们初始化,程序启动就会把所有文件加载
- 主类的调用优先于分类,分类的调动优先于以后类优先于分类
- 主类和分类的调用程序跟编译程序无关
- 分类之间加载,也就是平级之前加载取决于编译程序,谁先编译就先加载谁
注意事项
1. 咱们发现。load 的加载比 main 还要早,所以如果咱们再 load 办法外面做了耗时的操作,那么肯定会影响程序的启动工夫,所以在 load 外面肯定不要写耗时的代码。
2. 不要在 load 外面取加载对象,因为咱们再 load 调用的时候基本就不确定咱们的对象是否曾经初始化了,所以不要去做对象的初始化
调用程序延长(category)
分类中的同名办法,源码中是依照逆序加载的,也就是说后编译的分类办法会笼罩后面所有的同名的办法,分类还有一个个性就是,不论把申明写在主类还是分类,只有分类中实现了就能够找到。
+ initialize
+initialize 实质为 objc/_msgSend,如果子类没有实现 initialize 则会去父类查找,如果分类中实现,那么会笼罩主类,和 runtime 音讯转发逻辑一样
initialize 总结
1.initialize 会在类第一次接管到音讯的时候调用
2. 先调用父类的 initialize,而后调用子类。
3.initialize 是通过 objc_msgSend 调用的
4. 如果子类没有实现 initialize,会调用父类的 initialize(父类可能被调用屡次)
5. 如果分类实现了 initialize,会笼罩本类的 initialize 办法