Objective-C对象模型

这里写图片描述

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值