iOS 底层原理 面试IOS开发知识点

OC底层原理二十一:内存平移 & Mothod Swizzlin

2020-11-04  本文已影响0人  markhetao

OC底层原理 学习大纲

本节介绍:

  1. 内存平移
    1.1 类对象调用实例方法
    1.2 指针平移读取属性
    1.3 *(void **)的作用
    1.4 内存排布
    1.5 结构体压栈
    1.6 打印案例
  2. Mothod Swizzling方法交换
    2.1 Mothod Swizzling图解
    2.2 坑点1:必须只执行一次
    2.3 坑点2:必须提前准备
    2.4 坑点3:不可交换父类方法

1. 内存平移

1.1 类对象调用实例方法

@interface HTPerson : NSObject
- (void)sayHello;
@end
@implementation HTPerson
- (void)sayHello { NSLog(@"%s: 你好", __func__); }
@end

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
   // 指针读取类地址,强转为对象,调用sayHello。
    Class cls = [HTPerson class];
    void * ht = &cls;
    [(__bridge id)ht sayHello];
    
    // 实例化对象,调用sayHello
    HTPerson * person = [HTPerson new];
    [person sayHello];
    
}

@end

Q: 实例化对象perosn调用sayHello,这个没问题,我们都知道。
但为什么通过cls类对象的地址,强转id对象,也可以调用sayHello? 输入时还有代码补充提示

image.png

如果想深入了解对象结构,可以看OC底层原理 学习大纲中的alloc篇章类的结构篇章

image.png

1.2 指针平移读取属性

@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
@end

@implementation HTPerson
- (void)sayHello { NSLog(@"%s: 你好,%@", __func__, self.name); }
@end

@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
        
    Class cls = [HTPerson class];
    void * ht = &cls;
    [(__bridge id)ht sayHello]; // name打印: <ViewController: 0x7f91f5d05180>
        
    HTPerson * person = [HTPerson new];
    [person sayHello]; // name打印: null
    
}
@end
image.png

Q:person对象调用sayHello打印namenull是正常的,为什么强转ht对象调用sayHello打印name却是ViewController

未命名.png

此处有几个知识点,可以拓展一下:

  1. *(void **)的作用
  2. 结构体压栈
  3. 内存排布

1.3 *(void **)的作用

如上面案例中,person的内存地址为:0x600001208530

1.4 栈的内存排布

在内存中是遵循FIFOFirst Input First Output先进先出)原则,是从高地址低地址压栈(进栈)。

先进栈的在高地址后进栈的在低地址低地址元素先出栈

image.png

1.5 结构体压栈

结构体入栈顺序是相反的:

typedef struct {
        NSNumber * a;
        NSNumber * b;
    } HT_Number;
    
    HT_Number num = {@(10),@(20)};
    HTPerson * person = [HTPerson new];
image.png

结论: 结构体内部入栈顺序与常规入栈顺序相反后面的元素先入栈

1.6 打印案例

依旧以上面案例为例,加入打印代码(从selfperson内存排布):

@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
@end

@implementation HTPerson
- (void)sayHello { NSLog(@"%s: 你好,%@", __func__, self.name); }
@end

@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
        
    Class cls = [HTPerson class];
    void * ht = &cls;
    [(__bridge id)ht sayHello]; // name打印: <ViewController: 0x7f91f5d05180>

    HTPerson * person = [HTPerson new];
    person.name = @"ht";
    [person sayHello]; // name打印: ht
    
   // 打印内存元素排序(从self到person的内存排布)
    void * sp = (void *)&self;
    void * end = (void *)&person;
    long count = (sp - end) / 0x8;

    for (long i = 0; i < count; i++) {
        void * address = sp - i * 0x8;
        if (i == 1) { // 因为i==1时,是_cmd,不能转对象类型,所以用char *
            NSLog(@"%p : %s", address, *(char **)address); 
        } else {
            NSLog(@"%p : %@", address, *(void **)address);
        }
    }
    
}
@end
image.png

补充:
函数内部定义的局部变量数组,都存放在栈区; (比如每个函数都有的(id self, SEL _cmd))

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
image.png

我们理一下顺序:

  1. 函数参数(self,_cmd入栈):
    self为:self
    _cmd为:sel_registerName("viewDidLoad")
  2. 编译器生成的__rw_objc_super对象(局部变量入栈)
    (__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}
  3. 生成的cls(局部变量入栈)
  4. 生成的ht (局部变量入栈)
  • 所以内存地址从高位低位的顺序为:
    self -> _cmd ->-> __rw_objc_super对象->cls->ht
  • 可见__rw_objc_super结构体
    根据入参,我们知道object传入的是selfsuperClass传入的是ViewController
    而根据上面1.5 结构体压栈我们分析的:结构体内部元素,是后面元素先插入内存栈中
    所以__rw_objc_super内部的内存顺序为: ViewController -> self

最终内存顺序为: self -> _cmd -> ViewController -> self -> cls -> ht

通过这个案例,我们能清晰的知道:

    1. 栈中存放哪些内容
      局部变量(手动生成和编译器自动生成的)、函数参数(self,_cmd等)
    1. 压栈顺序
      常规是从高地址低地址入栈,只有结构体内部低地址高地址入栈
      (ps: 个人觉得,只记住高地址低地址入栈就行,把结构体整体当做一个对象,对象内部处理好了,再插入栈中,就可以了。)
    1. 栈的连续性
      栈内地址连续的,所以我们可以通过地址平移的方式,来读取前后元素
      (早期编程,有这种直接通过内存地址读取信息,再转换成自己的对象格式的。但是这样很不稳定,容易发生越界强转失败的情况。现在不提倡,但是作为研究手段)

2. Method Swizzling方法交换

2.1 Mothod Swizzling图解

Method Swizzling就是用于方法交换的(交换SELIMP绑定关系)。

image.png
#import <objc/runtime.h>

//MARK: - HTRuntimeTool
@interface HTRuntimeTool : NSObject
+ (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel;
@end

@implementation HTRuntimeTool

+ (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel {
    NSAssert(cls != nil, @"传入的交换类不能为空!");
    // 【这是错误实例,下面坑点3讲解】
    Method oriMethod = class_getInstanceMethod(cls, oriSel);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSel);
    method_exchangeImplementations(oriMethod, swiMethod);
}

@end

//MARK: - HTPerson
@interface HTPerson : NSObject
- (void)personFunc;
@end

@implementation HTPerson

- (void)personFunc { NSLog(@"HTPerson实例方法: %s", __func__); }
@end

//MARK: - HTStudent
@interface HTStudent : HTPerson
- (void)studentFunc;
@end

@implementation HTStudent

//+ (void)load {
//    static dispatch_once_t onceToken;
//    dispatch_once(&onceToken, ^{
//        [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)];
//    });
//}

// 避免影响启动时长,方法交换放在initialize中实现
+ (void)initialize
{
    if (self == [HTStudent class]) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)];
        });
    }
}

- (void)studentFunc {
    [self studentFunc];
    NSLog(@"HTStudent实例方法: %s", __func__);
}

@end

//MARK: -ViewController
@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    HTStudent * student = [HTStudent new];
    [student studentFunc];
    
}
@end

【注意】这是错误示例,不应该与父类交换方法,请参考下面坑点3

image.png

2.2 坑点1:必须只执行一次

因为交换第二次时,原方法又会指回原实现。所以我们使用dispatch_once来保证仅执行一次

2.3 坑点2:必须提前准备

交换操作必须提前完成,不然会产生调用混乱,执行错误会造成crash或其他业务bug

我们可以在+load方法或+initialize方法内完成交换操作,保障交换操作的提前准备

2.4 坑点3:不可交换父类方法

原因:

  • 我们进行方法交换时,HTStudent中没有找到personFunc方法,所以会沿着继承链往上找,在父类HTPerson中找到了personFunc方法。所以我们将HTStudentstudentFunc方法与HTPersonpersonFunc方法进行了交换。

  • 当使用子类HTStudent实例对象进行调用时,一切都正常。但是当使用父类HTPerson进行调用时,就会找不到交换后的studentFunc方法,导致崩溃。

#import <objc/runtime.h>

//MARK: - HTRuntimeTool
@interface HTRuntimeTool : NSObject
+ (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel;
@end

@implementation HTRuntimeTool

+ (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel {
   
    NSAssert(cls, @"传入的交换类不能为空");
    
    // 1. 分别读取`oriMethod`和`swiMethod`. (此时的oriMethod实现可能来自于`继承链`上的`某个类`。)
    Method oriMethod = class_getInstanceMethod(cls, oriSel);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSel);
    
    // 2. 被交换的函数必须实现 (想要交换的函数都没实现,就完全没有意义了)
    NSString * str = [NSString stringWithFormat:@"被交换的函数:[%@]没有实现",NSStringFromSelector(swizzledSel)];
    NSAssert(swiMethod, str);
    
    // 3. 检查oriMethod是否存在。(不存在表示整个继承链都没有`oriSel`的实现)
    if (!oriMethod) {
        // 不存在时,为了避免crash,我们手动添加一个空的Block IMP
        class_addMethod(cls, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^{ NSLog(@"[%@]创建新对象%@",NSStringFromClass(cls) ,NSStringFromSelector(oriSel)); }));
    }
    
    // 4. 尝试给cls添加`oriSel`方法。
    BOOL addMethod = class_addMethod(cls, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    // 4.1 添加成功, 表示之前cls没有`oriSel`方法。
    if (addMethod) {
        // `addMethod`时,我们已将`oriSel`的imp实现,并指向了swiMethod,
        //  所以此时,只需要将`swizzledSel`的imp实现,指向oriMethod即可。
        //  class_replaceMethod 是替换,覆盖的意思。等于重写绑定了`swizzledSel`的sel和imp关系
        class_replaceMethod(cls, swizzledSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }
    // 4.2 添加失败,表示之前cls已经存在`oriSel`方法。
    else {
        
        // 需要将`oriSel`和`swizzledSel`的Imp进行交换
        // method_exchangeImplementations 是交换的意思,等于将`oriMethod`和`swiMethod`的sel和imp的绑定关系进行交叉互换。
        //(oriSel -> swiMethod, swizzledSel -> oriMethod)
        method_exchangeImplementations(oriMethod, swiMethod);
        
    }
}

@end

//MARK: - HTPerson
@interface HTPerson : NSObject
- (void)personFunc;
@end

@implementation HTPerson
- (void)personFunc { NSLog(@"HTPerson实例方法: %s", __func__); }
@end

//MARK: - HTStudent
@interface HTStudent : HTPerson
- (void)studentFunc;
@end

@implementation HTStudent

//+ (void)load {
//    static dispatch_once_t onceToken;
//    dispatch_once(&onceToken, ^{
//        [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)];
//    });
//}

// 避免影响启动时长,方法交换放在initialize中实现
+ (void)initialize
{
    if (self == [HTStudent class]) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)];
        });
    }
}

- (void)studentFunc {
    // 当我们将studentFunc的实现与personFunc的实现互换后,此处就不是递归调用自己了。
    [self studentFunc];
    NSLog(@"HTStudent实例方法: %s", __func__);
}

@end

//MARK: -ViewController
@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    HTStudent * student = [HTStudent new];
    [student studentFunc];
    [student personFunc];
    
    HTPerson * person = [HTPerson new];
    [person personFunc];
    
}
@end

ht_methodSwizzilingWithClass分析:

    1. 分别读取oriMethodswiMethod.
      (此时的oriMethod实现可能来自于继承链上的某个类。)
    1. 被交换的函数(swiMethod)必须实现
      (比如: 你想将HTStudent对象studentFuncHTPersonpersonFunc进行交换,你至少得实现studentFunc方法啊)
    1. 检查oriMethod是否存在
      (不存在表示 整个继承链都没有oriSel的实现)
      如果不存在,为了避免crash,我们手动添加一个IMP(内容是个空的Block)
    1. 尝试给cls添加oriSel方法。

4.1 添加成功:
表示之前cls没有oriSel方法。
addMethod时,我们已将oriSelIMP实现,并指向了swiMethod,此时只需将swizzledSelIMP实现,指向oriMethod即可。
(class_replaceMethod:替换覆盖的意思。等于重绑定swizzledSelselimp关系)

4.2 添加失败:
表示之前cls已存在oriSel方法。
需要将oriSelswizzledSelSELIMP进行交换
(method_exchangeImplementations:交换的意思,等于将oriMethodswiMethodSELIMP绑定关系进行交叉互换
(oriSel -> swiMethod,swizzledSel -> oriMethod)

上一篇下一篇

猜你喜欢

热点阅读