OC底层原理(11)-部分知识点总结(随记)

441 阅读10分钟

一、runtime

runtime 是由C、C++、汇编实现的一套API,为OC语言加入了面向对象,运行时的功能。

运行时(runtime)是指将数据类型的确定由编译时推迟到了运行时。如,extension-category 的区别

平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,‘Runtime’是‘Objcect-c’的幕后工作者。

二、方法的本质,sel是什么?IMP是什么?两者之间的关系是什么?

方法的本质:发送消息,消息会有以下几个流程

1、快速查找(objc_msgSend)~cache_t 缓存消息

2、慢速查找~递归自己->父类 ~lookUpImpOrForward

3、查找不到消息:动态方法解析~resolveInstanceMethod

4、消息快速转发~forwardingTargetForSelector

5、消息慢速转发~methodSignatureForSelector & forwardInvocation

sel是方法编号~在read_images期间就编译进入了内存

imp就是我们函数实现指针,找imp就是找函数的过程

sel就相当于书本的目录 title

imp 就是书本的页码

查找具体的函数就是想看这本书里面具体篇章的内容

三、能否想编译后的得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?

1:不能向编译后的得到的类中增加实例变量 2:只要类没有注册到内存还是可以添加

原因:我们编译好的实例变量存储的位置在ro,一旦编译完成,内存结构就完全确定了就无法修改

四、isKindOfClass 和 isMemberOfClass

五、[self class] 与 [super class]

有图可知[self class] 与 [super class] 这两个打印出来的结构都是一样,为什么没有将父类打印出来呢?依次分析

[[self class]]:对于这个的打印结果估计都不会意外,还是将寻找流程贴出来

如果obj存在就直接返回了isa,isa就是关联类。

[super class]:这时就可以通过汇编的方式Debug->Debug Workflow -> Always Show Disassemble ,来查看调用流程

上面的@“class”我们知道,可以猜到是[self class],下面接着又有个 objc_msgSendSuper,这个估计就是[super class]时,调用的接口了

然后去找一下 objc_msgSendSuper 的源码

这里传递了两个参数,SEL,这里知道,这时去看一下 objc_super

仔细看一下注释就会发现 objc_super 主要有两个变量,然后就可以模拟一下这个环境

打印结果一模一样

[[self class]] 与 [super class] 有什么区别呢?

[[self class]]走的是objc_msgsend,从自己去递归,消息接受者是 self,

[super class] 走的是objc_msgSendSuper,就会跳过自己的查找流程,直接去父类查找,消息接受者还是self。

在 LGSutdent 与父类中并没有去实现 class 方法,所以调用的 class方法其实调用的是 NSObject 里面class 方法

只是objc_msgSendSuper会更快直接跳过self的查找 在这个过程中的研究对象是 LGSutdent ,所以打印出来就是LGSutdent。

六、weak原理

weak 修饰弱引用,在释放时能够自动置空,用来打破循环引用。

接下来通过汇编找到weak源码分析weak的实现流程 Debug->Debug Workflow -> Always Show Disassemble

然后点进去

添加一个段点,就会直接到 objc_initWeak 源码

接着往下看,看这个api storeWeak 点进去看源码

找一段关键的代码,点进去看 weak_register_no_lock ,传了一个弱引用表

这里有个通过weak_entry_for_referent拿到一个entry,拿到之后进行拼接,看下这个接口append_referrer

进入之后就会进行内部持有,如果超容就进行扩容,然后在进行其他处理 画个图

如果没有这个表,就进行创建数组

然后进行扩容并插入到weak_table里

总结:Runtime 是如何实现weak的,为什么可以自动置 nil

1:通过Side Table 找到我们的 weak_table

2:weak_talbe 根据referent 找到或者创建 weak_entry_t

3:然后 append_referrer(entry,referrer)将我的新弱引用的对象加进去entry

4:最后weak_entry_insert 把entry加入到我们的weak_table

说明,weak并没有对内存进行管理,只是记录保存,weak进行释放的时候,拿到散列表weak_table,通过objc找到相应局里面entry数组,把它从整个数组中移调,什么时候释放呢,就是进行析构的时候。跟一下这个过程

释放当前对象

这里开始清除,传入的是一个objc_object *,然后就在weak_table去查找到它,找到这个实体就置空以达到释放的效果

七、Method_Swizzling 坑点

runtime运用--避免数组越界时崩溃

首先我们先创建一个数组,4个元素,取一个会越界的下标

这时直接运行是肯定会崩溃的,要是想它不崩溃要怎么做呢,是不是需要拦截获取数组的方法呢,当获取越界是就给一个提示。接下来就操作一下,进行方法交换Swizzling,图形说明

代码实现

NSArray+LG.h

+ (void)load;

NSArray+LG.m

#import "NSArray+LG.h"
#import "LGRuntimeTool.h"
#import <objc/runtime.h>

@implementation NSArray (LG)
+ (void)load{
    //防止交换一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndex:) swizzledSEL:@selector(lg_objectAtIndex:)];
        
        [LGRuntimeTool lg_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(lg_objectAtIndexedSubscript:)];
    });
    
}

// 交换的方法

- (id)lg_objectAtIndex:(NSUInteger)index{
    if (index > self.count-1) {
        NSLog(@"取值越界了:%lu > %lu",(unsigned long)index, self.count-1);
        return nil;
    }
    return [self lg_objectAtIndex:index];
}

- (id)lg_objectAtIndexedSubscript:(NSUInteger)index {
    if (index > self.count-1) {
        NSLog(@"取值越界了:%lu > %lu",(unsigned long)index, self.count-1);
        return nil;
    }
    return [self lg_objectAtIndexedSubscript:index];
}

LGRuntimeTool.h

 交换方法

 @param cls 交换对象
 @param oriSEL 原始方法编号
 @param swizzledSEL 交换的方法编号
 */
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;

LGRuntimeTool.m

#import "LGRuntimeTool.h"
#import <objc/runtime.h>

@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
    
}

ViewController.m

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSArray *dataArray;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.dataArray = @[@"AA",@"BB",@"CC",@"DD"];
    [NSArray load];
    NSLog(@"%@",self.dataArray[4]);
    NSLog(@"%@",[self.dataArray objectAtIndex:4]);
    
}

打印

2020-04-11 13:25:13.124037+0800 005---Runtime应用[64014:5227823] 取值越界了:4 > 3
2020-04-11 13:25:13.124195+0800 005---Runtime应用[64014:5227823] (null)

2020-04-11 13:25:13.124037+0800 005---Runtime应用[64014:5227823] 取值越界了:4 > 3
2020-04-11 13:25:13.124195+0800 005---Runtime应用[64014:5227823] (null)

但是要是交换的方法可以多次执行会发生什么,验证一下就修改一下 NSArray+LG.m 里的 load 方法

然后在执行

崩溃了,所以要注意要这里有个坑点,执行方法交换的方法只能执行一次

然后还有一个坑点,通过子类调用一个子类没有父类有的方法,进行方法交换

ViewController.m

#import "ViewController.h"
#import "LGPerson.h"
#import "LGStudent.h"
#import "LGStudent+LG.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 黑魔法坑点二: 子类没有实现 - 父类实现
    LGStudent *s = [[LGStudent alloc] init];
    //此时调用的是 lg_studentInstanceMethod 
    [s personInstanceMethod];
    
}
@end

LGStudent.m/.h 里什么也没有

#import "LGStudent.h"

@implementation LGStudent

@end

LGStudent+LG.h


@interface LGStudent (LG)

@end

LGStudent+LG.m

#import "LGStudent+LG.h"
#import "LGRuntimeTool.h"
#import <objc/runtime.h>

@implementation LGStudent (LG)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp

- (void)lg_studentInstanceMethod{
//此时调用的是父类中的personInstanceMethod,因此做了方法交换
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}

@end

LGPerson.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

LGPerson.m

#import "LGPerson.h"

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}
+ (void)personClassMethod{
    NSLog(@"person类方法:%s",__func__);
}

@end

LGRuntimeTool.h

@interface LGRuntimeTool : NSObject
/**
 交换方法
 @param cls 交换对象
 @param oriSEL 原始方法编号
 @param swizzledSEL 交换的方法编号
 */
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;

LGRuntimeTool.m

#import "LGRuntimeTool.h"
#import <objc/runtime.h>

@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}
    
}

@end

打印结果:

如果此时,我在用父类调用自己的方法

运行结果:

就会闪退,因为子类LGStudent 调用父类方法的时候,用了一个方法交换,将父类的方法换成了 LGStudent 在自己这个类添加的一个方法。于是在父类调用自己的方法是,由于子类将方法换了,所以就会找到交换之后方法,但是那个方法又是在子类的,LGPerson 自己又没实现,所以运行时就会找不到这个方法,而崩溃。

为了避免这个问题,此时我们就要在 LGRuntimeTool.m 类里做一下改动

解决办法:

1.会先尝试给自己添加要交换的方法 2.然后再将父类的IMP给Swizzle

LGRuntimeTool.m

#import "LGRuntimeTool.h"
#import <objc/runtime.h>

@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}


+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL 
    //oriSEL:personInstanceMethod
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 尝试添加 向LGStudent 的 oriSEL(personInstanceMethod)添加 swiMethod(lg_studentInstanceMethod)方法
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    
    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
         //如果自己没有 personInstanceMethod ,并且添加 lg_studentInstanceMethod 实现成功时,
         //就对 swizzledSEL(lg_studentInstanceMethod)替换为 oriMethod(personInstanceMethod)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
@end

LGStudent+LG.m

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

运行结果:

这里能不崩溃的原因就是,只对LGStudent 调用的方法做了方法的添加和覆盖,没有对LGPerson 的方法做改动

要是子类LGStudent 调用了一个子类和父类都没有实现的方法呢要怎么避免崩溃呢

就相当于只在子类有个申明

代码: LGStudent.h

@interface LGStudent : LGPerson
- (void)helloword;
@end

LGStudent+LG.m

#import "LGStudent+LG.h"
#import "LGRuntimeTool.h"
#import <objc/runtime.h>

@implementation LGStudent (LG)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

- (void)lg_studentInstanceMethod{
    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}

ViewController.m

#import "ViewController.h"
#import "LGPerson.h"
#import "LGStudent.h"
#import "LGStudent+LG.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    LGStudent *s = [[LGStudent alloc] init];
    [s helloword];
 
}

打印结果:出现了死循环

是因为,在对LGStudent 添加完方法后,然后要进行替换的方法没有做实现,所以是会出问题的就造成了递归问题。

为了避免这个问题,这是我们在做替换的时候要做一个判断,判断要替换的方法是否有实现IMP,判断实现不存在时,就对没有实现的方法(SEL),添加一个实现(IMP)

实现:

在 LGRuntimeTool.m 中重新写一个方法

+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

运行结果:

八、内存偏移

先给出一段代码 LGPerson.h

@interface LGPerson : NSObject
- (void)saySomething;
@end

LGPerson.m

#import "LGPerson.h"

@implementation LGPerson
- (void)saySomething{
    NSLog(@"NB %s - %@",__func__,self.age);
}
@end

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    id pcls = [LGPerson class];
    void *pp= &pcls;
    [(__bridge id)pp saySomething];
    
    // p -> LGPerson 实例对象
    LGPerson *p = [LGPerson alloc];
    [p saySomething];
    
    NSLog(@"面试题");
}

这时会打印什么呢?

两个结果一模一样,这个要怎么解释呢,画一幅图

因为isa偏移一下就是指向 LGPerson 类,与pCls指向 是一样的,然后pp 与 p 都是指向 LGPerson 类 的指针,所以说都能掉 saySomething。

接下来再多打印一点其他,self.name

LGPerson.m

#import "LGPerson.h"

@implementation LGPerson
- (void)saySomething{
    NSLog(@"NB %s - %@",__func__, self.name);
}
@end

打印结果:

很神奇为什么打印的结果是这样的呢?

栈的当前结构如图,当前pp指向ISA,当访问name时,类型为NSString,8字节,所以就会向下偏移,8个字节就到了VC。这就是传说中的野指针,才会打印出这个结果。