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 的调用。

(待续)


原创粉丝点击