网上好多英文版的。看的累。自己写个中文的。
一个箭头方向代表指向, 虚线代表isa指向,实线代表父类指向
注意几个细节
所有元类的isa 都指向根元类,根元类指向他自个
注意到根元类的父类是根类。。。就连起来了
根类的父类是nil ,所以找不到就nil了
类的成员变量 这些是在rutime 源码里的,所以方法列表什么的我都没找到。
(Child) $0 = {
Father = (_father = 0)
_child = 0
}
我打印的结果也不太一样
无法给类动态增加成员变量的原因:
因为对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化。所以无法在运行时动态给对象增加成员变量。
可以给分类添加方法的原因:
相对的,对象的方法定义都保存在类的可变区域中。 Objective-C 1.0 中,我们可以看到方法的定义列表是一个名为 methodLists的指针的指针(如下图所示)。通过修改该指针指向的指针的值,就可以实现动态地为某一个类增加成员方法。这也是Category实现的原理。同时也说明了为什么Category只可为对象增加成员方法,却不能增加成员变量。
注意点:
需要特别说明一下,通过objc_setAssociatedObject 和 objc_getAssociatedObject方法可以变相地给对象增加成员变量,但由于实现机制不一样,所以并不是真正改变了对象的内存结构。
这段内容简直贯穿OC的整个使用。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
**struct objc_method_list **methodLists**;
**struct objc_cache *cache**;
struct objc_protocol_list *protocols;
#endif
};
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
objc_method_list
本质是一个有 objc_method
元素的可变长度的数组。一个 objc_method
结构体中有函数名,也就是SEL,有表示函数类型的字符串 (见 Type Encoding) ,以及函数的实现IMP。
从这些定义中可以看出发送一条消息也就 objc_msgSend 做了什么事。举 objc_msgSend(obj, foo) 这个例子来说:
1、首先,通过 obj 的 isa 指针找到它的 class ;
2、在 class 的 method list 找 foo ;
3、如果 class 中没到 foo,继续往它的 superclass 中找 ;
4、一旦找到 foo 这个函数,就去执行它的实现IMP .
但这种实现有个问题,效率低。但一个 class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次 objc_method_list
并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是 objc_class
中另一个重要成员 objc_cache
做的事情 - 再找到 foo 之后,把 foo 的 method_name
作为 key ,method_imp
作为 value 给存起来。当再次收到 foo 消息的时候,可以直接在 cache 里找到,避免去遍历 objc_method_list.
动态方法解析和转发
在上面的例子中,如果 foo 没有找到会发生什么?通常情况下,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:
1.Method resolution
2.Fast forwarding
3.Normal forwarding
贴一段消息转发的代码
@interface TestClass : NSObject
@end
#import "TestClass.h"
#import <objc/runtime.h>
#import <objc/message.h>
@interface SecondClass : NSObject
- (void)noThisMethod:(NSString *)value;
@end
@implementation SecondClass
- (void)noThisMethod:(NSString *)value {
NSLog(@"SecondClass中的方法实现 ===== %@", value);
}
@end
@implementation TestClass
//运行时方法拦截
- (void)dynamicAddMethod: (NSString *) value {
NSLog(@"调用不存在的方法 ===== 参数:%@", value);
}
/**
没有找到SEL的IML实现时会执行下方的方法
@param sel 当前对象调用并且找不到IML的SEL
@return 找到其他的执行方法,并返回yes
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO; //当返回NO时,会接着执行forwordingTargetForSelector:方法,
[TestClass addMethod:[self class] method:sel method:@selector(dynamicAddMethod:)];
return YES;
}
/**
将当前对象不存在的SEL传给其他存在该SEL的对象
@param aSelector 当前类中不存在的SEL
@return 存在该SEL的对象
*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
// return self;
return nil;
return [SecondClass new]; //让SecondClass中相应的SEL去执行该方法
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
//消息获得函数的参数和返回值类型
//如果 -methodSignatureForSelector: 返回 nil ,Runtime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如 //查找父类的方法签名
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if(signature == nil) {
signature = [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
//果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。
return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SecondClass * forwardClass = [SecondClass new];
SEL sel = invocation.selector;
if ([forwardClass respondsToSelector:sel]) {
[invocation invokeWithTarget:forwardClass];
} else {
[self doesNotRecognizeSelector:sel];
}
}
+ (void)addMethod:(Class)class method:(SEL)methodSel method:(SEL)methodSelImpl {
Method method = class_getInstanceMethod(class, methodSelImpl);
IMP methodIMP = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(class, methodSel, methodIMP, types);
}
@end
这个是消息转发的三次机会
第一次是
resolveInstanceMethod
这时候可以写个方法交换
这样会重发一次消息,因为方法交换了,所以就不会失败了第二次是快速转发
forwardingTargetForSelector
可以把消息转发
给别的对象。第三次是普通转发,首先要在这个
methodSignatureForSelector
方法中返回NSMethodSignature
不过没返回就挂了,如果返回了就会继续走forwardInvocation
这个方法 然后在这个方法里面做处理。
图片增加一下说服力
这是上面使用的源码
然后讲到KVO
当一个观察者为一个对象的属性注册时,被观察对象的isa指针会被修改,指向一个中间类,而不是真正的类。
因此,isa指针的值不一定反映实例的实际类。绝不应依靠isa指针来确定类的成员资格。 相反,您应该使用类方法来确定对象实例的类。
这段没看太懂。比较晕
我找了一段源码:
iOS_模拟KVO的底层实现、手动实现KVO(附源码)
这里就分析别人的源码吧
可以看到项目结构
修改isa指针,就是把当前对象指向一个新类、 Person’s isa -> Person_KVO
使当前对象的isa 指向新的派生类(Person_KVO),就会调用派生类的set方法
因为所有的对象都继承自NSObject,给NSObject 添加 (NSObject+KVO)
分类就可以增加新的方法
- (void)ml_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
//修改isa指针,就是把`当前对象`指向一个`新类`
// Person's isa -> Person_KVO
// Class object_setClass(id obj, Class cls)
//使当前对象的isa指向新的派生类(Person_KVO),就会调用派生类的set方法
object_setClass(self, [Person_KVO class]);
//给对象绑定观察者对象
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//然后在控制器调用
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
_person = person;
//手写KVO
[person ml_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}
注意 这个self 是控制器,控制器在这里充当观察者
这段内容是指当调用这个方法的时候,就把Person 类的isa指针指向Person_KVO
//使当前对象的isa指向新的派生类(Person_KVO),就会调用派生类的set方法
object_setClass(self, [Person_KVO class]);
所以set方法被重写
再看Person_KVO 类实现
-(void)setAge:(int)age{
[super setAge:age]; //修改了子类的值后,父类的值也修改了
//调用KVO
//获取观察者
id observer = objc_getAssociatedObject(self, @"observer");
//调用观察者的方法
//就是验证手写KVO的方法 observer其实这个VC中的self
[observer ml_observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}
所以只要子类Personset值修改了,父类person 也值也修改了。
然后调用观察者 这个观察者是 vc控制器
所以就调用vc控制器的方法,
[observer ml_observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
自然 控制器就收到了这个消息。
总结:
所谓的观察属性的实时变化,就是重写isa类,让其指向他的子类。
子类重写需要观察的属性值的set方法,当外界改变a.b = c 这样的
b的属性值时,会调用子类set方法,子类就会告知观察者数值变化了
看了一下他的源码
觉得最后personKVO还引用了控制器的方法,不太好,这样有种循环引用的
感觉。我改了一下他的代码,具体改的地方是增加一个代理。让控制器去实现这个协议就好。
#import <Foundation/Foundation.h>
#import "Person.h"
@protocol observeValueDelegate <NSObject>
- (void)ml_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context;
@end
@interface Person_KVO : Person
@property(nonatomic, weak) id<observeValueDelegate> delegate;
@end
#import "Person_KVO.h"
#import <objc/runtime.h>
@implementation Person_KVO
-(void)setAge:(int)age{
[super setAge:age]; //修改了子类的值后,父类的值也修改了
[self observeChange:@"age"];
}
- (void)setName:(NSString *)name {
[super setName:name];
[self observeChange:@"name"];
}
- (void)observeChange:(NSString *)keypath {
//调用KVO
//获取观察者
id observer = objc_getAssociatedObject(self, @"observer");
self.delegate = observer;
SEL selector = @selector(ml_observeValueForKeyPath:ofObject: change: context:);
if ([self.delegate respondsToSelector:selector]) {
//调用观察者的方法
//就是验证手写KVO的方法 observer其实这个VC中的self
[self.delegate ml_observeValueForKeyPath:keypath ofObject:self change:nil context:nil];
}
}
@end
这样就好很多viewcontroll也不需要这个方法了。
继续往下看
runtime的黑魔法
method_setImplementation
设置 1 个方法的实现
IMP method_setImplementation(Method m, IMP imp)
m代表方法 imp 代表实例,就是给实例添加一个方法
method_exchangeImplementations
,当需要交换 2 个方法的实现时使用。
原理
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
//应该就是拿到实例1 在拿到实例2 给实例2 方法1 给实例1 方法2
class_replaceMethod
, 当需要替换的方法可能有不存在的情况时,可以考虑使用该方法。
class_replaceMethod
在苹果的文档(如下图所示)中能看到,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用class_addMethod
来为该类增加一个新方法,也因为如此,class_replaceMethod
在调用时需要传入types参数
我在aop里面有应用这些
关键代码
上面是交换实例方法,下面是交换类方法的
CG_INLINE void
ReplaceMethod(Class _class, SEL _originSelector, SEL _newSelector) {
Method oriMethod = class_getInstanceMethod(_class, _originSelector);//类的实例方法
Method newMethod = class_getInstanceMethod(_class, _newSelector);
BOOL isAddedMethod = class_addMethod(_class, _originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (isAddedMethod) {
class_replaceMethod(_class, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, newMethod);
}
}
CG_INLINE void
ReplaceCLASSMethod(Class _class, SEL _originSelector, SEL _newSelector) {
Method oriMethod = class_getClassMethod(_class, _originSelector);//类方法
Method newMethod = class_getClassMethod(_class, _newSelector);
method_exchangeImplementations(oriMethod, newMethod);
}
应用的时候 首先是实例方法
获取 控制器的viewappear时机,可以给viewappear添加需要的代码,
#import "UIViewController+swizzlingLogCurrentVC.h"
@implementation UIViewController (swizzlingLogCurrentVC)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ReplaceMethod([self class], @selector(viewWillAppear:), @selector(yyb_viewWillAppear:));
});
}
- (void)yyb_viewWillAppear:(BOOL)animated{
[self yyb_viewWillAppear:animated];
// NSLog(@"%@ viewWillAppear",[self class]);
}
@end
然后是类方法 这是把image 从内存读取换成从文件读写其他细节就不讲了
#import <UIKit/UIKit.h>
@interface UIImage (swizzlingImageName)
@property (nonatomic, copy) NSString *imageName;
@end
#import "UIImage+swizzlingImageName.h"
#import <objc/runtime.h>
@implementation UIImage (swizzlingImageName)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ReplaceCLASSMethod([self class], @selector(imageNamed:), @selector(yyb_imageNamed:));
ReplaceCLASSMethod([self class], @selector(imageWithContentsOfFile:), @selector(yyb_imageWithContentsOfFile:));
});
}
+ (nullable UIImage *)yyb_imageNamed:(NSString *)name
{
UIImage *image = [UIImage yyb_imageNamed:name];
image.imageName = name;
return image;
}
+ (nullable UIImage *)yyb_imageWithContentsOfFile:(NSString *)path
{
UIImage *image = [UIImage yyb_imageWithContentsOfFile:path];
NSURL *urlPath = [NSURL fileURLWithPath:path];
NSString *imageName = [[urlPath.lastPathComponent componentsSeparatedByString:@"."] firstObject];
image.imageName = imageName;
return image;
}
- (NSString *)imageName
{
return objc_getAssociatedObject(self, _cmd);//_cmd相当于当前方法的指针,类似self
}
- (void)setImageName:(NSString *)imageName
{
objc_setAssociatedObject(self, @selector(imageName), imageName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
参考文章列表: