OC底层原理二十一:内存平移 & Mothod Swizzlin
本节介绍:
-
内存平移
1.1类对象
调用实例方法
1.2指针
平移读取属性
1.3*(void **)
的作用
1.4栈
的内存排布
1.5结构体
压栈
1.6 打印案例 -
Mothod Swizzling
方法交换
2.1Mothod 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
-
打印结果:
image.png
Q: 实例化对象
image.pngperosn
调用sayHello
,这个没问题,我们都知道。
但为什么通过cls类对象
的地址,强转
为id
对象,也可以调用sayHello
?输入时
还有代码补充提示
:
- 要回答这个问题。我们先回顾一下
类
和对象
的结构:
image.png
如果想深入了解
类
和对象
的结构
,可以看OC底层原理 学习大纲中的alloc篇章
和类的结构
篇章
- 调用流程如下:
-
对象
中没有方法
,对象调用方法
,实际上是指针
指向类对象
,让类对象
调用相应方法
。 - 我们直接
读取类地址
,指针
指向类对象
,让其调用
相应方法
。是一样的效果。
1.2 指针
平移读取属性
-
HTPerosn
新增name
属性,并在sayHello
中打印name
属性。测试代码:
@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
- 打印结果:
未命名.pngQ:
person
对象调用sayHello
打印name
为null
是正常的,为什么强转
的ht
对象调用sayHello
打印name
却是ViewController
?
此处有几个知识点,可以拓展一下:
-
*(void **)
的作用 -
结构体
压栈 -
栈
的内存排布
1.3 *(void **)的作用
如上面案例中,person
的内存地址为:0x600001208530
-
(void**)
代表的是指向指针
的指针
。 读取是地址值
加上*
进行解引用
:*(void **)
,可读取
到地址中存放的内容
:
image.png
1.4 栈的内存排布
栈
在内存中是遵循FIFO
(First Input First Output
先进先出)原则,是从高地址
往低地址
压栈(进栈
)。
image.png
先进栈
的在高地址
,后进栈
的在低地址
。低地址
元素先出栈
。
1.5 结构体压栈
结构体
入栈顺序是相反
的:
- 定义一个含2个
NSNumber
元素的HT_Number结构体
,在person
后面加入断点
,观察内存排布:
typedef struct {
NSNumber * a;
NSNumber * b;
} HT_Number;
HT_Number num = {@(10),@(20)};
HTPerson * person = [HTPerson new];
image.png
-
内存地址排布如下:
image.png -
可以看到
结构体内部
,a元素
原本在b元素
前面,但却比b
元素晚入栈
。
(打印中a元素(int 10)
的内存地址
比b元素(int 20)
的内存地址低
,表示a
元素在b
元素后面入栈
)
结论: 结构体
内部入栈顺序
与常规入栈顺序相反
,后面
的元素先入栈
1.6 打印案例
依旧以上面案例为例,加入打印代码(从self
到person
的内存排布
):
- 测试代码:
@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
- 打印结果如下:
- 打印顺序为:
self
->_cmd
->superclass
->self
->cls
->ht
补充:
函数内部定义的局部变量
和数组
,都存放在栈区
; (比如每个函数都有的(id self
,SEL _cmd
))
- 注释掉
打印内存排序的代码
,打开终端
,cd
到当前控制器
的文件夹
,将当前文件
编译成cpp
文件(替换ViewController.m
为自己文件名):
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
- 打开编译后的
ViewController.cpp
文件,找到viewDidLoad
函数内部(如下):
- 只有
局部变量
和self
调用的函数参数
可入栈
我们理一下顺序:
- 函数参数(self,_cmd入栈):
self为:self
_cmd为:sel_registerName("viewDidLoad")
- 编译器生成的
__rw_objc_super对象
(局部变量入栈)
(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}
- 生成的cls(局部变量入栈)
- 生成的ht (局部变量入栈)
- 所以内存地址从
高位
到低位
的顺序为:
self
->_cmd
->->__rw_objc_super对象
->cls
->ht
- 我们查看
__rw_objc_super
结构如下:
image.png
- 可见
__rw_objc_super
是结构体
。
根据入参,我们知道object
传入的是self
,superClass
传入的是ViewController
。
而根据上面1.5 结构体压栈
我们分析的:结构体内部元素
,是后面
元素先插入
内存栈中。
所以__rw_objc_super
内部的内存顺序为:ViewController
->self
最终内存顺序为: self
-> _cmd
-> ViewController
-> self
-> cls
-> ht
通过这个案例,我们能清晰的知道:
栈中存放哪些内容
?
局部变量(手动生成和编译器自动生成的)、函数参数(self,_cmd等)
压栈顺序
常规是从高地址
到低地址
入栈,只有结构体内部
是低地址
到高地址
入栈
(ps: 个人觉得,只记住高地址
到低地址
入栈就行,把结构体整体
当做一个对象
,对象内部处理
好了,再插入
栈中,就可以了。)
栈的连续性
栈内地址
是连续
的,所以我们可以通过地址平移
的方式,来读取
前后元素
。
(早期编程,有这种直接通过内存地址
读取信息
,再转换
成自己的对象
格式的。但是这样很不稳定
,容易发生越界
和强转失败
的情况。现在不提倡
,但是可
作为研究手段
)
2. Method Swizzling方法交换
2.1 Mothod Swizzling
图解
Method Swizzling
就是用于方法交换
的(交换SEL
和IMP
绑定关系)。
- 测试代码:
HTRuntimeTool
类:负责方法交换的具体操作
HTPerson
类: 继承自NSObject
的类,拥有personFunc
方法
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 != 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
- 测试代码中,我们将
HTPeroson对象
的personFunc
函数与HTStudent对象
的studentFunc
函数交换绑定关系
。 -
student对象
调用studentFunc
函数时,实现的是HTPeroson
的personFunc
方法。打印结果如下:
image.png【注意】这是
错误示例
,不应该与父类
交换方法,请参考下面坑点3
2.2 坑点1:必须只执行一次
因为交换第二次时,原方法
又会指回原实现
。所以我们使用dispatch_once
来保证仅执行一次
2.3 坑点2:必须提前准备
交换操作
必须提前完成,不然会产生调用混乱
,执行错误会造成crash
或其他业务bug
。
我们可以在+load
方法或+initialize
方法内完成交换操作,保障
交换操作的提前准备
。
-
+load
方法:将懒加载类
变成非懒加载类
,在程序启动前
就完成相应操作。会影响
程序启动时长
。不建议使用。 -
+initialize
方法:系统动态加入
到NSObject
的方法,所有继承
自NSObject
的类,都拥有该方法。
在类
首次被调用
时,首先会执行+initialize
方法。所以既做到了懒加载
,不提前占用资源
。也满足了提前准备
的要求。
(关于initialize
的相关介绍,可以查看3. 分类的加载 ->methodizeClass)
2.4 坑点3:不可交换父类方法
-
上面案例,粗看没啥问题,但是当我们创建
image.pngHTPerson对象
,调用personFunc
函数时,crash
了:
-
崩溃信息
告诉我们:HTPerson
类没有studentFunc
方法,导致崩溃
。
原因:
我们进行方法交换时,
HTStudent
中没有找到personFunc
方法,所以会沿着继承链
往上找,在父类HTPerson
中找到了personFunc
方法。所以我们将HTStudent
的studentFunc
方法与HTPerson
的personFunc
方法进行了交换。当使用子类
HTStudent
实例对象进行调用时,一切都正常。但是当使用父类HTPerson
进行调用时,就会找不到交换后的studentFunc
方法,导致崩溃。
-
解决方法:
将影响范围
,限制
在当前类
中,可借助父类
的IMP
实现,但不可
让交换主体
变成父类
。 -
具体操作:
方法交换前,先尝试给自己添加待交换
的方法
,再将父类
的IMP
指给swizzle
。
保障交换cls
是当前对象
,不会找到父类
或继承链上层
。 -
合格代码:
#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
分析:
- 分别读取
oriMethod
和swiMethod
.
(此时的oriMethod
实现可能来自于继承链
上的某个类
。)
被交换的函数
(swiMethod
)必须实现
(比如: 你想将HTStudent对象
的studentFunc
与HTPerson
的personFunc
进行交换,你至少得实现studentFunc
方法啊)
- 检查
oriMethod
是否存在
。
(不存在表示整个继承链
都没有oriSel
的实现)
如果不存在,为了避免crash
,我们手动添加
一个IMP
(内容是个空的Block
)
- 尝试给
cls
添加oriSel
方法。4.1 添加成功:
表示之前cls没有oriSel
方法。
addMethod
时,我们已将oriSel
的IMP
实现,并指向了swiMethod
,此时只需将swizzledSel
的IMP
实现,指向oriMethod
即可。
(class_replaceMethod: 是替换
,覆盖
的意思。等于重绑定
了swizzledSel
的sel
和imp
关系)4.2 添加失败:
表示之前cls已存在oriSel
方法。
需要将oriSel
和swizzledSel
的SEL
和IMP
进行交换
(method_exchangeImplementations: 是交换
的意思,等于将oriMethod
和swiMethod
的SEL
和IMP
的绑定关
系进行交叉互换
。
(oriSel -> swiMethod,swizzledSel -> oriMethod)