Objective-C对象模型

xiaoxiao2021-02-28  93

网上好多英文版的。看的累。自己写个中文的。 一个箭头方向代表指向, 虚线代表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

参考文章列表:

参考文章1 iOS动态性(一) 一行代码实现iOS序列化与反序列化(runtime)

转载请注明原文地址: https://www.6miu.com/read-45874.html

最新回复(0)