面试系列:
- iOS面试全解1:基础/内存管理/Block/GCD
- iOS面试全解2:Runloop
- iOS面试全解3:Runtime
- iOS面试全解4:KVC、KVO、通知/推送/信号量、Delegate/Protocol、Singleton
一、概念:什么是Runtime?平时项目中有用过么?
- 简单来说:是一套底层的C语言API。
- 具体应用:能动态 “ 增/删/改/查 ” 一个类的 成员变量 和 方法.
1、能动态 创建 一个类,一个成员变量(属性),一个方法
2、能动态 修改 一个类,一个成员变量(属性),一个方法
3、能动态 删除 一个类,一个成员变量(属性),一个方法
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。通过此篇文章了解多态:iOS面试全解1:基础/内存管理/Block/GCD
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数,平时编写的OC代码,底层都是转换成了Runtime API进行调用:
比如类转成了runtime库里面的结构体等数据类型,方法转成了runtime库里面的C语言函数,平时调方法都是转成了objc_msgSend
函数 (就是OC的消息发送机制
)。在OC中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用“动态绑定机制”
,所以所要调用的方法直到运行期才能确定)。
项目中的使用:
1、
关联对象
: 给分类添加属性,AssociatedObject(提高性能,避免不使用的 继承类属性 初始化)
2、遍历类的所有成员变量
:修改textfield的占位文字颜色、字典转模型Model、自动归档解档
3、交换方法实现
:(交换系统的方法,埋点、防逆向hook)
4、利用消息转发机制
:解决 「方法找不到」的异常问题
......
什么是 isa ?
isa 指针(NONPOINTER_ISA)
isa:
是一个指向对象所属Class类型的指针。(nonPointer_isa)
对象的isa指针,用来表明对象所属的类类型和一些附加信息。
如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。于是,就像tagged pointer一样,对于isa指针,苹果同样进行了优化
。isa指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了 「引用计数」extra_rc
,是否有被weak引用标志位weakly_referenced
,是否有附加对象标志位has_assoc
等信息。(rc:reference counter 引用计数)
1、实例isa 指向类对象,类对象isa 指向元类对象(指向上)
2、在 arm64之前,isa指针就直接存储着 类对象/元类对象 的地址值;
在 arm64时,isa进行了优化,采取 共用体(union)的结构,共有64位,分开来存储了很多东西,其中有33位是存储 地址值,使用 &Mask 取出 地址值。
objc源码
//------- NSObject(实质) -------
@interface NSObject {
Class isa OBJC_ISA_AVAILABILITY;
}
//------- objc_class(继承)-------
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable 以前的缓存指针和虚函数表
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *加上自定义rr/alloc标志
//。。。
}
//------- isa指针(指向)-------
struct objc_object {
private:
isa_t isa;
public:
Class ISA(); // ISA() assumes this is NOT a tagged pointer object 假设这不是一个标记的指针对象
Class getIsa(); // getIsa() allows this to be a tagged pointer object 允许这是一个标记的指针对象
//。。。
}
//------- 联合体(定义)-------
union isa_t
{
isa_t() { } //构造函数1
isa_t(uintptr_t value) : bits(value) { } //构造函数2
Class cls; //成员1(占据64位内存空间)
uintptr_t bits; //成员2(占据64位内存空间)
#if SUPPORT_PACKED_ISA
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct { //成员3(占据64位内存空间:从低位到高位依次是nonpointer到extra_rc。成员后面的:表明了该成员占用几个bit。)
uintptr_t nonpointer : 1; //(低位)注意:标志位,表明isa_t *是否是一个真正的指针!!!
uintptr_t has_assoc : 1; // 关联对象
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1; //弱引用
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1; //引用计数 相关成员1
uintptr_t extra_rc : 19; //引用计数 相关成员2 (用19位来 记录对象的引用次数)
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
}
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//向该类所继承的父类对象
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
//成员变量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;//方法列表
//用于缓存调用过的方法
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
//协议链表用来存储声明遵守的正式协议
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
}
Runtime 实质是:方法和消息动态绑定的过程。
当对象收到消息时,消息函数首先根据该对象的isa指针找到该对象所对应的类的方法表,并从表中寻找 该消息对应的方法选标。如果找不到,objc_msgSend
将继续从父类中寻找,直到 NSObject 类。一 旦找到了方法选标, objc_msgSend 则以消息接收者对象为参数调用,调用该选标对应的方法实现。
这就是在运行时
系统中选择方法实现的方式。在面向对象编程中,一般称作方法和消息动态绑定的过程
。
为了加快消息的处理过程,运行时系统通常会将使用过的方法选标和方法实现的地址放入缓存中
。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。消息函数会首先检查消息接收者对象对应的类的缓存(理论上,如果一个方法被使用过一次,那么它很可能被再次使用)。如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加。
既然类也是一个对象,那么它一定是其他类的实例,那个类就是元类 (metaclass)
。元类是类(对象)的描述,就像类是普通实例的描述一样。特别的是,元类的方法列表是类方法(类响应的选择器)。当你发送消息给一个类(元类的实例),objc_msgSend()
会查找元类(已及它的父类)的方法列表并确定调用的方法。类方法是由类代表的元类所描述,如同实例方法是由实例对象对应的类(对象)来描述的。
注意:当一个消息发送给任何一个对象时,将会由对象的 isa 指针开始查找方法,接着沿着父类链向上去查找。
- 实例方法:被类定义,
- 类方法:被元类定义。
以下案例说明类的关系:
MyClass *myClass = [[MyClass alloc] init];
整理下相互间的关系:
• myClass 是实例对象
• MyClass 是类对象
• MyClass 的元类就是 NSObject
• NSObject 就是 Root class (class)
• NSObject 的 superclass 为 nil
• NSObject 的元类就是它自己
• NSObject 的元类的 superclass 就是 NSObject
你觉得元类是什么样的?到元类还会继续向下吗?不,元类其实是根类 (root class) 的元类的实例;这个根类的元类,实际上就是它自己。isa 链在这里结束,形成一个环形(实例->类->元类->父元类->根元类->根元类自己
)。
objc_method:Method
objc_selector:SEL
objc_category:Category
objc_cache: 类缓存
objc_class: 类对象
objc_object:实例
Root class:根类 object(人的认知)
meteClass:元类-人类
Superclass:父类 - 小明父亲、小华父亲......
Subclass: 子类- 小明、小华
找父类
子类的实例 -> 子类 -> 父类 -> 根类 -> nil
父类的实例 -> 父类 -> 根类 -> nil
根类的实例 -> 根类 -> nil
子类的元类 -> 父类的元类 -> 根类的元类 -> 根类的元类
找元类
子类 -> 子类的元类
父类 -> 父类的元类
根类 -> 根类的元类
终点是:根元类(NSObject
)
isa的关系图
二、基本源码解析
1、基本符号与标识:
SEL
:方法编号,类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。定义成 char *
IMP
:函数指针,保存了方法的地址
2、常用的头文件
#import
//包含对:类、成员变量、属性、方法的操作
#import
//包含消息机制
3.常用方法
class_copyIvarList() 返回一个指向类的成员变量数组的指针
class_copyPropertyList()返回一个指向类的属性数组的指针
注意:根据Apple官方runtime.h文档所示,上面两个方法返回的指针,在使用完毕之后必须free()。
-------------------------------------
ivar_getName() 获取成员变量名--> C类型的字符串
property_getName()获取属性名 --> C类型的字符串
-------------------------------------
typedef struct objc_method *Method;
class_getInstanceMethod()返回一个实例方法
class_getClassMethod() 返回一个类方法
method_exchangeImplementations()交换两个方法的实现
-------------------------------------
.......很多方法,在后面叙述!!
一种常见的办法是通过runtime.h中
objc_getAssociatedObject /
objc_setAssociatedObject 来访问和生成关联对象。
这两个方法可以让一个对象和另一个对象关联,就是说一个对象可以保持对另一个对象的引用,并获取那个对象。
4.Runtime 基础函数
method相关的函数也不是太多,下边简单罗列说明一下
Method class_getInstanceMethod(Class cls, SEL name) //(-)获取类中的 实例方法(减号方法):-(void)test;
Method class_getClassMethod(Class cls, SEL name) //(+)获取类中的 类方法 (加号方法):+(void)test;
IMP class_getMethodImplementation(Class cls, SEL name) //获取类中的方法实现
IMP class_getMethodImplementation_stret(Class cls, SEL name) //获取类中的方法的实现,该方法的返回值类型为struct
SEL method_getName(Method m) //获取Method中的SEL(方法的名称)
IMP method_getImplementation(Method m) //获取Method中的IMP(方法的指针)
IMP method_setImplementation(Method m, IMP imp) //设置Method的IMP (方法的指针)
// 消息发送:
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
-
方法执行的逻辑是:先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针, 经过异常错误处理后,最后跳到对应函数的实现。
对象对应类、方法的缓存、函数指针、异常处理、函数的实现。
object -> class -> IMP -> exception -> method
//例5:方法的调用本质是:消息机制,objc_msgSend(消息接收者, 消息名称);
[TZPerson addRun];
//同上
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TZPerson"), sel_registerName("addRun"));
//同上
objc_msgSend(objc_getClass("TZPerson"), sel_registerName("addRun"));
//同上
objc_msgSend(objc_getClass("TZPerson"), @selector(addRun));
三、方法的调用本质:消息转发机制
以下是方法调用的过程:
0、通过isa找到类:先去缓存表中找,如果没有,方法表中寻找
1、如果没有,就继续找父类方法缓存表,没有继续找父父类方法表,直到根类。
3、 是否有方法:有就去调用、没有去新增
4、是否有新增方法:有新增然后去调用、没有新增去转发
5、是否有消息转发:有取调用、没有就崩溃
// objc_msgSend:(类/实例, 方法名称) ;
// 本质是消息机制:(消息接收者, 消息名称)
在OC中方法调用的是通过Runtime实现的,Runtime进行方法调用本质上是发送消息,通过objc_msgSend()函数进行消息发送。
给实例发送一个消息:
- 判断receiver == nil
- 通过isa找到类
- 从类的缓存里查找IMP,去类的方法表中寻找
- 通过superclass去找父类
- 通过父类去找IMP
- 循环4,5
消息转发流程图
1.0、方法的实现
* 获取实例方法:class_getInstanceMethod(Class cls, SEL name)
* 设置类方法: class_getClassMethod(Class cls, SEL name)
* 获取方法指针:method_getImplementation(Method m)
* 设置方法指针:method_setImplementation(Method m, IMP imp) (方法可以重新指向)
* 设置对象的类:object_setClass(id obj, Class cls)
1、消息发送(方法调用的本质)
* 注册方法:sel_registerName(const char *str),sel_registerName("new")
* 消息发送:objc_msgSend(id self, SEL op, ...)
* 创建一类:objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
objc_msgSend(消息接受者,消息名称)
objc_msgSend(person, sel_registerName("walk"));
objc_msgSend(objc_getClass("TZPerson"), sel_registerName("addRun"));
Class TZCat = objc_allocateClassPair([NSObject class], "TZCat", 0);
2、动态方法解析
- 对象在收到无法解读的消息后:调用
+ (BOOL)resolveInstanceMethod:(SEL)sel
来动态为其 添加实例方法
,来处理该选择。 - 如果尚未实现的方法是类方法:调用
+ (BOOL)resolveClassMethod:(SEL)sel
来动态为其 添加类方法
//#pragma mark --- 实例方法 的转换:+ (BOOL)resolveInstanceMethod:(SEL)sel
//#pragma mark --- 实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"---resolveInstanceMethod:%s", __func__);
// 元类
// 实例对象、类对象、元类对象
//例6.2.1:无方法,构建一个方法,并去调用实现
if (sel == @selector(walk)) {
//创建实例方法(向实例发送消息,在类上添加方法)
Method runMethod = class_getInstanceMethod(self, @selector(run));
//方法指针
IMP runIMP = method_getImplementation(runMethod);
//描述方法参数的类型
const char* types = method_getTypeEncoding(runMethod);
NSLog(@"---(实例方法)types:%s", types); //v16@0:8 参数字节数16 @从第几个字节开始 :表示第8个
return class_addMethod(self, sel, runIMP, types);
}
//有方法,去调用实现,
return [super resolveInstanceMethod:sel];
}
//#pragma mark --- 类方法 的转换:+ (BOOL)resolveClassMethod:(SEL)sel
//#pragma mark --- 类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"---resolveClassMethod:%s", __func__);
//例6.2.2:(类方法)无方法,构建一个方法,并去调用实现
if (sel == @selector(walk)) {
//创建类方法(向类发送消息,在元类上添加方法)
Method runMethod = class_getInstanceMethod(object_getClass(self), @selector(run));
//方法指针
IMP runIMP = method_getImplementation(runMethod);
//描述方法参数的类型
const char* types = method_getTypeEncoding(runMethod);
NSLog(@"---(类方法)types:%s", types); //v16@0:8 参数字节数16 @从第几个字节开始 :表示第8个
return class_addMethod(object_getClass(self), sel, runIMP, types);
}
//有方法,去调用实现,
return [super resolveClassMethod:sel];
}
3、消息转发
挂载到其他类的方法上。
是否有消息转发:有就去调用、没有就崩溃。
既然已经问过了,没有新增方法,那就问问有没有别人能够帮忙处理一下:
\\实例方法 处理
- (id) forwardingTargetForSelector:(SEL)aSelector
- (NSMethodSignature* )methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
\\类方法 处理
+ (id) forwardingTargetForSelector:(SEL)aSelector
+ (NSMethodSignature* )methodSignatureForSelector:(SEL)aSelector
+ (void)forwardInvocation:(NSInvocation *)anInvocation
源码如下:
# ------------------------- (实例:消息转发) ---------------------------
# 7.1: 实例方法(方法挂载,挂载到其他类上)
# 方式一
- (id) forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(walk7)) {
return [TZDog new];
}
return [super forwardingTargetForSelector:aSelector];
}
# 方式二
# 方法名注册
- (NSMethodSignature* )methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(walk7)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
# 消息转发:实例方法
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//NSLog(@"%s", __func__);
//[anInvocation invokeWithTarget:[TZDog new]]; // 转发给 其他
// 转发给 自己
anInvocation.selector = @selector(run);
anInvocation.target = self;
[anInvocation invoke];
}
## ------------------------- (类:消息转发) ---------------------------
## 7.2:类方法 消息转发
## 方式一
+ (id) forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(walk7)) {
//return [TZDog new]; //可以转发到:类的实例方法
return [TZDog class];//可以转发到:类方法
}
return [super forwardingTargetForSelector:aSelector];
}
## 方式二
# 方法名注册
+ (NSMethodSignature* )methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(walk7)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
# 消息转发:类方法
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@" 转发给 其他= %s", __func__);
//[anInvocation invokeWithTarget:[TZDog new]]; //可以转发到:类的实例方法
//[anInvocation invokeWithTarget:[TZDog class]]; //可以转发到:类方法
// 转发给 自己
anInvocation.selector = @selector(run);
anInvocation.target = self;
[anInvocation invoke];
}
# ------------------------- 结束(消息转发) ---------------------------
三、Runtime 实现KVO
KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 People
时,KVO 机制动态创建一个对象people当前类的子类,并为这个新的子类重写了被观察属性 keyPath
的 setter
方法。setter 方法随后负责通知观察对象属性的改变状况。
Apple 使用了isa-swizzling
来实现 KVO 。当观察对象people时,KVO机制动态创建一个新的名为:NSKVONotifying_People
的新类,该类继承自对象People
的本类,且 KVO 为 NSKVONotifying_People 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
NSKVONotifying_People 类剖析
NSLog(@"self->isa: %@",self->isa);
NSLog(@"self class: %@",[self class]);
# 在建立KVO监听前,打印结果为:
self->isa: People
self class: People
# 在建立KVO监听之后,打印结果为:
self->isa: NSKVONotifying_People
self class: People
子类setter方法剖析
KVO 的键值观察通知依赖于 NSObject 的两个方法:
-
willChangeValueForKey:
被观察属性发生改变之前被调用,通知即将改变、 -
didChangeValueForKey:
被观察属性发生改变之后被调用,通知已经变更。
在存取数值的前后分别调用 这2 个方法,且重写观察属性的setter 方法这种继承方式的注入是在运行时
而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; # KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; # 调用父类的存取方法
[self didChangeValueForKey:@"name"]; # KVO 在调用存取方法之后总调用
}
四、Runtime的使用
1、
关联对象
: 给分类添加属性,AssociatedObject(提高性能,避免不使用的 继承类属性 初始化)
2、遍历类的所有成员变量
:修改textfield的占位文字颜色、字典转模型Model、自动归档解档
3、交换方法实现
:(交换系统的方法,埋点、防逆向hook)
4、利用消息转发机制
:解决 「方法找不到」的异常问题
......
1、实现NSCoding的自动归档和自动解档
原理描述:用 Runtime
提供的函数遍历Model
自身所有属性,并对属性进行encode
和decode
操作。
核心方法:在Model
的基类中重写方法:
引用:
1、NSCoding协议
2、 知识小结三:NSCoding理解
NSArchiver、NSUnarchiver、NSKeyedArchiver、NSKeyUnarchiver和NSPortCoder。NSCoder具体的子类统一称作:编码器类。
协议中只有两个方法,都是@require必须实现的方法
-(void)encodeWithCoder:(NSCoder *)aCoder
-(id)initWithCoder:(NSCoder *)aDecoder
# 解档(获取内容数据)
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
# 归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
2、实现字典和模型的自动转换( MJExtension )
原理描述:用Runtime
提供的函数遍历Model
自身所有属性,如果属性在json
中有对应的值,则将其赋值。
核心方法:在NSObject 的分类
中添加方法
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
#//(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];
}
}
return self;
}