一. 什么是runtime
runtime
是由C 和C++ 汇编 实现的一套API,为OC语言加入了面向对象
,运行时
的功能。runtime
是指将数据类型的确定由编译时
推迟到了运行时
。例如 extension - category 的区别:
- extension可以添加实例变量,而category是无法添加实例变量。 因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。
- extension在编译期决议(就是类的一部分),category在运行期决议。 extension在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,extension伴随类的产生而产生,亦随之一起消亡。
- extension一般用来隐藏类的私有信息,无法直接为系统的类扩展,但可以先创建系统类的子类再添加extension。
- extension和category都可以添加属性,但是category的属性不能生成成员变量和getter、setter方法的实现
- 平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代 码,
runtime
是Object-C
的幕后工作者
二. ro & rw
问题一:
能否向编译后的得到的类中添加成员变量?能否向运行时创建的类中添加成员变量?
- 不能向编译后的得到的类中添加成员变量。
- 我们编译好的成员变量存储在
ro -> ivarList
中,一旦编译完成,内存机构就完全确定无法修改。
- 运行时创建的类只要还没有注册到内存可以添加成员变量变量
问题二:
为什么运行时创建的类注册到内存中就无法修改了呢?
我们先代码测试下。
- 先添加成员变量, 再将类注册到内存
- 先将类注册到内存,再添加成员变量
objc_registerClassPair
和class_addIvar
底层到底干了什么?
/***********************************************************************
* objc_registerClassPair
* fixme
* Locking: acquires runtimeLock
**********************************************************************/
void objc_registerClassPair(Class cls)
{
...
// Clear "under construction" bit, set "done constructing" bit
cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
// Add to named class table.
addNamedClass(cls, cls->data()->ro->name);
}
objc_registerClassPair
会将flags
设置为RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING
状态
/***********************************************************************
* class_addIvar
* Adds an ivar to a class.
* Locking: acquires runtimeLock
**********************************************************************/
BOOL
class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)
{
...
// Can only add ivars to in-construction classes.
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}
...
return YES;
}
flags
只有为in-construction
, 才能添加成员变量。
问题3:
如何动态添加属性呢?
定义一个property,在编译期间,编译器会生成实例变量
,getter方法
、setter方法
,这些方法是通过自动合成(autosynthesize)的方式生成并添加到类中。那么我们动态添加属性就要添加实例变量
,getter方法
、setter方法
。
void akSetter(NSString *value){
printf("%s/n",__func__);
}
NSString *akName(){
printf("%s/n",__func__);
return @"master NB";
}
void ak_class_addProperty(Class targetClass , const char *propertyName){
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
objc_property_attribute_t backingivar = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String }; //variable name
objc_property_attribute_t attrs[] = {type, ownership0, ownership,backingivar};
class_addProperty(targetClass, propertyName, attrs, 4);
}
void ak_printerProperty(Class targetClass){
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 动态创建类
Class AKPerson = objc_allocateClassPair([NSObject class], "AKPerson", 0);
// 2.1 添加property - rw
ak_class_addProperty(AKPerson, "subject");
// 2.2 添加成员变量 1<<aligment ivar - ro - ivarlist
class_addIvar(AKPerson, "subject", sizeof(NSString *), log2(sizeof(NSString *)), "@");
// 2.3 添加setter + getter 方法
class_addMethod(AKPerson, @selector(setSubject:), (IMP)akSetter, "v@:@");
class_addMethod(AKPerson, @selector(subject), (IMP)akName, "@@:");
// 3. 注册到内存
objc_registerClassPair(AKPerson);
// 4. 开始使用
id person = [AKPerson alloc];
[person setValue:@"iOS" forKey:@"subject"];
NSLog(@"%@",[person valueForKey:@"subject"]);
}
return 0;
}
- 一些相关api的注释:
/**
* 创建类对
*superClass: 父类,传Nil会创建一个新的根类
*name: 类名
*extraBytes: 0
*return:返回新类,创建失败返回Nil,如果类名已经存在,则创建失败
objc_allocateClassPair(<#Class _Nullable __unsafe_unretained superclass#>, <#const char * _Nonnull name#>, <#size_t extraBytes#>)
*/
/**
*添加成员变量
*
*cls 往哪个类添加
*name 添加的名字
*size 大小
*alignment 对齐处理方式
*types 签名
*
*这个函数只能在objc_allocateClassPair和objc_registerClassPair之前调用。不支持向现有类添加一个实例变量。
*这个类不能是元类。不支持在元类中添加一个实例变量。
*实例变量的最小对齐为1 << align。实例变量的最小对齐依赖于ivar的类型和机器架构。对于任何指针类型的变量,请通过log2(sizeof(pointer_type))。
class_addIvar(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
*/
/**
*往内存注册类
*
* cls 要注册的类
*
* objc_registerClassPair(<#Class _Nonnull __unsafe_unretained cls#>)
*/
/**
*往类里面添加方法
*
*cls 要添加方法的类
*sel 方法编号
*imp 函数实现指针
*types 签名
*
*class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
*/
/**
*往类里面添加属性
*
*cls 要添加属性的类
*name 属性名字
*attributes 属性的属性数组。
*attriCount 属性中属性的数量。
*
*class_addProperty(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#const objc_property_attribute_t * _Nullable attributes#>, <#unsigned int attributeCount#>)
三. 方法的本质是什么? SEL/IMP分别是什么?两者有什么联系?
- 方法的本质:发送消息 , 消息会有以下几个流程
- 快速查找:
objc_msgSend
~ cache_t 缓存消息 - 慢速查找L:
lookUpImpOrForward
递归自己和父类 - 查找不到消息:
resolveInstanceMethod
动态方法解析 - 消息快速转发:
forwardingTargetForSelector
- 消息慢速转发:
methodSignatureForSelector
&forwardInvocation
- sel 是方法编号 ~ 在read_images 期间就编译进入了内存
- imp 是函数实现指针 ,找imp 就是找函数的过程
- sel 相当于书本的目录 tittle,imp 相当于书本的⻚码 查找具体的函数就是想看这本书里面具体篇章的内容
- 我们首先知道想看什么 ~ tittle (sel)
- 根据目录对应的⻚码 (imp)
- 翻到具体的内容
四. isKindOfClass & isMemberOfClass
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// 题目1
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[AKPerson class] isKindOfClass:[AKPerson class]];
BOOL re4 = [(id)[AKPerson class] isMemberOfClass:[AKPerson class]];
NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n", re1, re2, re3, re4);
// 题目2
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[AKPerson alloc] isKindOfClass:[AKPerson class]];
BOOL re8 = [(id)[AKPerson alloc] isMemberOfClass:[AKPerson class]];
NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n", re5, re6, re7, re8);
}
return 0;
}
先探索一下isKindOfClass
和 isMemberOfClass
的源码
1. + isKindOfClass
+ (Class)class {
return self;
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- 这是一个类似于
for (int i = 0; i < 3; i ++)
的for循环
object_getClass
得到当前类对象的元类
,初始化tcls
- 只要tcls有值就可以继续循环,当tcls为nil循环结束
- 取得tcls的父类作为tcls的新值,继续下次循环
isKindOfClass
是循环不断获取self的isa指针以及超类的isa指针指向和cls做对比。
结论一:`+isKindOfClass 是元类及其元类的父类 vs cls
2. + isMemberOfClass
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
object_getClass
得到当前类对象的元类
,和类本身cls
进行比较- 相较于
+isKindOfClass
少了父类的比较,因此+isMemberOfClass
为YES时可以得到+isKindOfClass
为YES
结论二:+isMemberOfClass是元类 vs cls
3. - isKindOfClass
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
结论3:-isKindOfClass是类本身及其父类 vs cls
4. - isMemberOfClass
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
结论4:-isMemberOfClass是拿实例对象的类(即当前类)vs cls作比较
5. 分析
- isa 走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
- 继承关系(实现):子类 -> 父类 -> NSObject -> nil。
- 根元类的父类为NSObject。
结合 isa走位图 分析题目1:
NSObject的元类(根元类)
与NSObject类
不相等,NSObject元类的父类(就是NSObject类)
与`NSObject类``相等——YESNSObject的元类(根元类)
与NSObject类
不相等——NOAKPerson的元类
与AKPerson类
不相等,AKPerson元类的父类(根元类)
与AKPerson类
不相等——NOAKPerson的元类
与AKPerson类
不相等——NO
题目二:
NSObject类
与NSObject类
相等——YESNSObject类
与NSObject类
相等——YESAKPerson类
与AKPerson类
相等——YESAKPerson类
与AKPerson类
相等——YES
isKindOfClass 侧重于是不是一个类型或者款式;isMemberOfClass 侧重于是不是它的成员,更加固定死了。
五. [self class] & [super class]
AKStudent
是AKPerson
的子类, 主程序初始化AKStudent
会打印什么?
#import "AKStudent.h"
@implementation AKStudent
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"[self class] = %@", NSStringFromClass([self class]));
NSLog(@"[super class] = %@", NSStringFromClass([super class]));
}
return self;
}
@end
1. [self class]
[self class]
就是发送消息objc_msgSend
,消息接受者是 self,方法编号:class。
objc_msgSend(void /* id self, SEL op, ... */ )
2. [super class]
通过汇编和源码发现:[super class] 本质就是使用objc_msgSendSuper
向objc_super
发送消息,消息接受者是 self,方法编号:class。
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
我们可以用伪代码重写[super class]
objc_msgSend
走的是消息查找的流程,会递归查找class方法;objc_msgSendSuper
直接跳过 self 的查找,从objc_super结构体
开始查找,更加效率。
六. 内存偏移
题目1
程序能否运行吗?是否正常输出?为什么?
@interface AKPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)doSomething;
@end
@implementation AKPerson
- (void)doSomething {
NSLog(@"%s", __func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [AKPerson class];
void *obj= &cls;
[(__bridge id)obj doSomething];
AKPerson *p = [AKPerson alloc];
[p doSomething];
}
- 指针1:
指针p
指向实例对象
的首地址(实例对象的首地址是isa
) - 指针2:
实例对象的isa
指向类对象cls
- 指针3:
cls类对象
指向AKPerson
- 指针4:
obj
指向类对象cls
题目2
修改doSomething
方法实现,会打印什么?
- (void)doSomething {
NSLog(@"%s——————%@", __func__, self.name);
}
为什么会打印viewController呢?
那么我们就来看一下viewDidLoad里面总共有哪些局部变量,再贴一下代码
- (void)viewDidLoad {
// [super viewDidLoad];
struct __rw_objc_super arg = {
(id)self,
(id)class_getSuperclass(objc_getClass("ViewController"))
};
objc_msgSendSuper(arg, @selector(viewDidLoad));
id cls = [AKPerson class];
void *obj= &cls;
[(__bridge id)obj doSomething];
}
self.name相当于self->_name,因为_name是isa后面紧接着的成员变量,而_name是一个指针,占8个字节大小,因此self->_name实际上得到的就是从self所指向的内存地址往高地址偏移8个字节(跨过isa的大小)后的内存地址,指向一段8字节大小的内存空间,从而获得person对象的成员变量_name。
self.name所拿到的变量,就是图中cls下面的那8个字节,也就是当前方法的消息接受者self(ViewController实例对象),因此打印的结果是<ViewController: 0x7fce43e08aa0>。
函数的栈空间简介
对于arm64架构来说,栈空间的作用:
- 存放被调用函数其内部所定义的局部变量的。
- 局部变量的存放顺序,是根据定义的先后顺序,从函数栈底开始,一个一个排列,最先定义的局部变量位于栈底(高地址)。
int a = 1;
int b = 2;
int c = 3;
int d = 4;
NSLog(@"\na = %p\nb = %p\nc = %p\nd = %p\n",&a,&b,&c,&d);
a = 0x7ffeefbfea8c
b = 0x7ffeefbfea88
c = 0x7ffeefbfea84
d = 0x7ffeefbfea80。