ObjC如何通过runtime修改Ivar的内存管理方式(二)
来源:互联网 发布:广州知豆电动车官网 编辑:程序博客网 时间:2024/06/15 16:31
是否能够在运行时修改 Ivar Layout?
虽然我们已经破译了 oc runtime 如何存储变量的内存修饰符的秘密,但是我们是否能够在运行时通过修改 Ivar Layout 的方式来改变变量的内存管理方式呢?例如 assgin
变为 weak
?仔细推敲Objective-C Class Ivar Layout 探索的细节后,我们不难得出一个简单直接的办法——调用 class_setIvarLayout 和 class_setWeakIvarLayout 重新设置 Ivar Layout 不就达成目标了么?看起来简单可行,我们新建了一个测试类 MCAssignToWeak 来模拟 UIScrollView 的场景:
@interface MCAssignToWeak : NSObject@property (nonatomic, strong) id s1;@property (nonatomic, assign) id delegate;@property (nonatomic, weak) id w1;- (void)notifyDelegate;@end// MCAssignToWeak.m@implementation MCAssignToWeak- (void)notifyDelegate { NSLog(@"===== notify %@", [self.delegate class]);}- (void)setDelegate:(id)delegate { _delegate = delegate; NSLog(@"===== setDelegate:");}@end
并将里面的 delegate 属性从assign
设置为weak
,直接 hardcode 在纸上算好的 ivarLayout 和 weakIvarLayout 的新值赋给 MCAssignToWeak,调用后立马被 runtime 无情地打了脸。
*** Can't set ivar layout for already-registered class 'MCAssignToWeak'
无奈之下, 我们只好回过头来翻出 class_setIvarLayout 的源码看一下:
/************************************************************************ class_setIvarLayout* Changes the class's ivar layout.* nil layout means no unscanned ivars* The class must be under construction....**********************************************************************/voidclass_setIvarLayout(Class cls, const uint8_t *layout){ ... // Can only change layout of in-construction classes. // note: if modifications to post-construction classes were // allowed, there would be a race below (us vs. concurrent object_setIvar) if (!(cls->data()->flags & RW_CONSTRUCTING)) { _objc_inform("*** Can't set ivar layout for already-registered " "class '%s'", cls->nameForLogging()); return; } ...}
注释里明确说了 The class must be under construnction, 而我们看到的那行 log 则来自于第 15 行的 if 判断失败。我们只好继续在源代码里搜索使用RW_CONSTRUCTING
的地方,接着就找到了下面代码:
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta){ ... cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING; meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING; ...}
原来只有调用了 objc_initializeClassPair 的类才会有这个RW_CONSTRUCTING
的标志位,而这意味着只有在运行时由开发者动态添加的类在 objc_registerClassPair 调用之前才能修改 Ivar Layout,一旦调用了 objc_registerClassPair 就意味着这个类已经对修改关闭,不再接受任何对 Ivar 的修改了,而那些编译时就已确定的类根本就没有任何机会修改 Ivar Layout。回想Objective-C Class Ivar Layout 探索里,大神需要解决的问题确实是如何为一个动态添加的类添加 weak
属性的 Ivar,和我们所处的场景不一样。难道我们探索了这么久最终还是走进了一条根本行不通的死胡同?
幸亏我们有 runtime 的源代码,让我们知道这个标志位的定义以及作用。我们尝试在调用 class_setIvarLayout 之前,将这个类的 flags 加上RW_CONSTRUCTING
标志,调用完成后再重置。因为设置 flags 需要使用到 runtime 源码内关于 object_class、class_data_bits_t 以及 class_rw_t 的结构体定义,于是我们偷懒地在大神的代码基础上进行再加工,那些我们暂时还不需要知道细节的指针一律使用了void *
:
static void _fixupAssginDelegate(Class class) { struct { Class isa; Class superclass; struct { void *_buckets;#if __LP64__ uint32_t _mask; uint32_t _occupied;#else uint16_t _mask; uint16_t _occupied;#endif } cache; uintptr_t bits; } *objcClass = (__bridge typeof(objcClass))class;#if !__LP64__#define FAST_DATA_MASK 0xfffffffcUL#else#define FAST_DATA_MASK 0x00007ffffffffff8UL#endif struct { uint32_t flags; uint32_t version; struct { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize;#ifdef __LP64__ uint32_t reserved;#endif const uint8_t *ivarLayout; const char *name; void *baseMethodList; void *baseProtocols; void *ivars; const uint8_t *weakIvarLayout; } *ro; } *objcRWClass = (typeof(objcRWClass))(objcClass->bits & FAST_DATA_MASK);#define RW_CONSTRUCTING (1<<26) objcRWClass->flags |= RW_CONSTRUCTING; // delegate从assign变为weak,需要将weakIvarLayout从\x21修改为\x12 uint8_t *weakIvarLayout = (uint8_t *)calloc(3, 1); *weakIvarLayout = 0x21; *(weakIvarLayout+1) = 0x12; class_setWeakIvarLayout(class, weakIvarLayout); // 完成后清除标志位 objcRWClass->flags &= ~RW_CONSTRUCTING;}
一次失败的尝试
既然我们已经有了如何修复的假设,接下来就需要验证我们的假设是不是正确的。这段代码应该放在哪里执行呢?我们知道 runtime 在启动的时候会依次调用所有类以及所有分类的+ (void)load
方法,我们为了展示 UIScrollView 这种没有源码的系统类应该如何进行修改,特意为 MCAssignToWeak 创建了一个新的分类 fixup,然后在这个分类重写+ (void)load
方法:
@interface MCAssignToWeak (fixup)@end@implementation MCAssignToWeak (fixup)+ (void)load { _fixupAssginDelegate(self);}@end
为了验证我们的代码是否真的将delegate对象从assign
变为了weak
,我们还需要下面的验证代码:
int main(int argc, const char * argv[]) { @autoreleasepool { MCAssignToWeak *atw = [MCAssignToWeak new]; { NSObject *proxy = [NSObject new]; atw.delegate = proxy; [atw notifyDelegate]; // 这里不会崩溃 } // 如果delegate仍然是assign,那这里会崩溃 [atw notifyDelegate]; } return 0;}
运行之后,我们期望的输出应该是这样的:
2017-07-21 11:06:31.157609+0800 demo[38605:16165704] ===== notify NSObject2017-07-21 11:06:31.157691+0800 demo[38605:16165704] ===== notify (null)
但事与愿违,执行程序后崩溃在第二个 notifyDelegate 处,看起来 delegate 对象依然是个野指针。这是为什么呢?仔细推敲assign
或着说_unsafe_unretained
的实现原理,这个修饰符会在编译时告诉编译器赋值和取值的时候,不需要运行时做任何内存管理,直接操作内存地址即可,这些操作可以直接在编译时确定,无需再依赖运行时。所以编译器插入的 setter 里面,对_delegate = delegate
会直接转化为指针拷贝( getter 同理),这样就算我们在运行时动态修改了 _delegate 的 layout 也无济于事,因为代码早就确定了。难道我们又走进了死胡同吗?
继续深入
既然编译器生成的 getter 和 setter不能用,那我们就自己写一套吧。在这之前我们需要搞清楚编译器如何为一个weak对象生成 getter 和 setter。还是 MCAssignToWeak,我们先来看一下 delegate 是assign
的时侯 setter 的汇编代码:
; 附上oc代码方便对照; @property (nonatomic, assign) id delegate;; - (void)setDelegate:(id)delegate {; _delegate = delegate;; }demo`::-[MCAssignToWeak setDelegate:](id): ... 0x100001af4 <+36>: movq -0x18(%rbp), %rdx 0x100001af8 <+40>: movq -0x8(%rbp), %rsi 0x100001afc <+44>: movq 0x21ad(%rip), %rdi ; MCAssignToWeak._delegate 0x100001b03 <+51>: movq %rdx, (%rsi,%rdi) ...
编者注:这是模拟器运行的 x86_64 汇编,AT&T 的汇编语法。ARM 与 AT&T 不同,但原理都一样。如果你对 ARM 汇编有兴趣,可以参考iOS汇编教程:理解ARM。我们这里就以模拟器来做为分析样本了。
我们为了节省篇幅,省略了获取 self 引用的过程,几乎所有的对象方法都有这一段。跳过这里来到第 8 行到第 11 行,这就是我们要找的_delegate = delegate
所对应的汇编代码。那这四行都做了什么呢:
0x100001af4 <+36>: movq -0x18(%rbp), %rdx ; $rbp-0x18里存放delegate的地址 0x100001af8 <+40>: movq -0x8(%rbp), %rsi ; $rbp-0x8里存放self对象的起始地址 0x100001afc <+44>: movq 0x21ad(%rip), %rdi ; $rip-0x21ad里存放_delegate相对于self的偏移 0x100001b03 <+51>: movq %rdx, (%rsi,%rdi) ; $rsi+rdi = $rdx => _delegate = delegate
这四句代码印证了我们的推断,对于一个标记为assign
的成员变量来说,setter 就是直接进行指针拷贝。那么我们再来看看如果 delegate 是weak
的时候是什么样子:
debug-objc`::-[MCAssignToWeak setDelegate:](id): ... 0x100001a74 <+36>: movq -0x18(%rbp), %rsi ; delegate 0x100001a78 <+40>: movq -0x8(%rbp), %rdx ; self 0x100001a7c <+44>: movq 0x2235(%rip), %rdi ; offset 0x100001a83 <+51>: addq %rdi, %rdx ; $rdx = self + offset 0x100001a86 <+54>: movq %rdx, %rdi ; $rdi = $rdx 0x100001a89 <+57>: callq 0x100002952 ; symbol stub for: objc_storeWeak ...
和assign
的汇编差不多,唯一不同的是assign
的时候,直接进行了指针拷贝,而weak
则调用了 objc_storeWeak 方法去拷贝指针。这是因为对于弱引用对象,赋值的时候需要首先在 runtime 全局维护的一张弱引用表中更新记录,维持正确的引用关系,最后才会进行指针拷贝,这一系列操作都要加锁保证线程安全,所以它的代码看起来很长很复杂。objc_storeWeak 也可以在源代码中找到,我们忽略那些对我们完成目标没有直接关系的代码,直接看指针拷贝的那段代码即可:
template <HaveOld haveOld, HaveNew haveNew, CrashIfDeallocating crashIfDeallocating>static id storeWeak(id *location, objc_object *newObj) { ... // Assign new value, if any. if (haveNew) { newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating); // weak_register_no_lock returns nil if weak store should be rejected // Set is-weakly-referenced bit in refcount table. if (newObj && !newObj->isTaggedPointer()) { newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. *location = (id)newObj; } ...}
通过第 18 行我们最终确认,在更新弱引用记录表后,最后和assign
一样也会进行指针拷贝。我们可以由此得出推论,对于任意一个 setter,我们都可以通过替换它的 setter 方法来完成对Ivar变量的内存管理方式的修改。幸运的是,runtime 将 objc_storeWeak 方法公开了出来, 我们只要替换原有 setter 后,先调用原 setter 实现,再调用 objc_storeWeak 方法,即可将 setter 变为一个可以操作weak
变量的方法。同理,getter 也可以通过方法替换的方式来完成对 objc_loadWeak 的调用。
(待续)
- ObjC如何通过runtime修改Ivar的内存管理方式(二)
- ObjC如何通过runtime修改Ivar的内存管理方式(一)
- ObjC如何通过runtime修改Ivar的内存管理方式(三)
- ObjC Runtime(二):配置调试环境
- objc的runtime
- ObjC: 内存管理
- objc使用什么机制管理对象内存(内存管理方式有哪些)
- ObjC内存管理推荐写法
- ObjC第三节:内存管理
- Objc内存管理之MRC
- Objc内存管理之ARC
- 精品 CF与OBJC在ARC下的内存管理。
- ObjC Runtime
- Objc Runtime
- Objc Runtime
- objc runtime
- Runtime方法的使用—Method、Ivar、Property篇
- Swift/Objc的Runtime(运行时)机制
- 中国大陆开源镜像站汇总
- JDK安装
- 【腾讯TMQ】一种Android端Web多进程情况下支持Web自动化测试的方法
- CoordinatorLayout CollapsingToolbarLayout 与滚动的处理
- Android 序列化 Parcelable VS Serializable
- ObjC如何通过runtime修改Ivar的内存管理方式(二)
- 使用groovy进行大文件外排序
- Linux vmstat命令实战详解
- 深究js(三)——变量
- 关于react this.setState is not a function 的报错问题解决
- 解决问题:NODE_ENV 不是内部或外部命令,也不是可运行的程序,或者批处理文件
- springboot自学资料网址
- 【数据库】Oracle查询优化改写 技巧与案例 思维导图
- @Autowired与@Resource的区别