ObjC如何通过runtime修改Ivar的内存管理方式(三)

5 篇文章 0 订阅
3 篇文章 0 订阅
本文介绍了如何通过Objective-C runtime在运行时修改成员变量(ivar)的内存管理方式,详细阐述了setter和getter的替换,以及如何自动生成Ivar Layout以适应不同情况,最终解决在iOS 8.x上UIScrollView delegate属性导致的崩溃问题。
摘要由CSDN通过智能技术生成

第二次尝试

到了这里,我们已经完全搞清楚了 oc 是如何管理assignweak对象的了,如果你有兴趣也可以去自己尝试破解strong的实现机制,原理一样。接下来我们决定开始对 MCAssignToWeak 进行第二次修改的尝试,这一次,我们需要加入对 delegate 属性的 setter 和 getter 的替换,使之调用正确的方法存取成员变量。

@implementation MCAssignToWeak (fixup)
+ (void)load {...}
- (void)fixup_setDelegate:(id)delegate {
    Ivar ivar = class_getInstanceVariable([self class], "_delegate");
    object_setIvar(self, ivar, delegate);
    [self fixup_setDelegate:delegate]; // 最后调用原实现
}
- (id)fixup_delegate {
    id del = [self fixup_delegate];
    del = objc_loadWeak(&del);
    return del;
}
@end

我们之所以在 fixup_setDelegate: 方法里,调用了 object_setIvar 而不是 objc_storeWeak 方法来设置弱引用到 _delegate,是因为 object_setIvar 里面需要先获取 Ivar 的 offset,然后将加上了偏移后的地址传入到 objc_storeWeak方法,同时 object_setIvar 还可以根据内存修饰符来调用与之相符的内存管理方法,这样写不仅能适应我们当前的assignweak的需要,还可以满足以后其他类型之间互转的需要:

static ALWAYS_INLINE 
void _object_setIvar(id obj, Ivar ivar, id value, bool assumeStrong)
{
    if (!obj  ||  !ivar  ||  obj->isTaggedPointer()) return;

    ptrdiff_t offset;
    objc_ivar_memory_management_t memoryManagement;
    _class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);

    if (memoryManagement == objc_ivar_memoryUnknown) {
        if (assumeStrong) memoryManagement = objc_ivar_memoryStrong;
        else memoryManagement = objc_ivar_memoryUnretained;
    }

    id *location = (id *)((char *)obj + offset);

    switch (memoryManagement) {
    case objc_ivar_memoryWeak:       objc_storeWeak(location, value); break;
    case objc_ivar_memoryStrong:     objc_storeStrong(location, value); break;
    case objc_ivar_memoryUnretained: *location = value; break;
    case objc_ivar_memoryUnknown:    _objc_fatal("impossible");
    }
}

同理 fixup_delegate 也可以使用object_getIvar 方法来获取 Ivar,这里我们先简单调用 objc_loadWeak。看到这里,你可能会问,如果 setter 和 getter 被重写,对应的并不是与 property 同名的 Ivar,那怎么办呢?遇到这种情况需要通过解析汇编代码确定 setter 和 getter 操作的内存地址,然后利用 runtime 方法获取目标类所有的 Ivar 信息比对即可得知 Ivar 的名称。

现在我们修改一下之前的 _fixupAssignDelegate方法,在方法的最后增加代码:

static void _fixupSelector(Class cls, SEL origSel, SEL fixSel) {
    Method setter = class_getInstanceMethod(cls, origSel);
    Method fixSetter = class_getInstanceMethod(cls, fixSel);
    BOOL success = class_addMethod(cls, origSel,
                                   method_getImplementation(fixSetter),
                                   method_getTypeEncoding(fixSetter));
    if (success) {
        class_replaceMethod(cls, fixSel,
                            method_getImplementation(setter),
                            method_getTypeEncoding(setter));
    } else {
        method_exchangeImplementations(setter, fixSetter);
    }
}
static void _fixupAssginDelegate(Class class) {
    ...
    // swizzling setter finally
    _fixupSelector(origCls, @selector(setDelegate:), @selector(fixup_setDelegate:));
    _fixupSelector(origCls, @selector(delegate), @selector(fixup_delegate));
}

重新运行我们的 demo,当 delegate 定义为assign的时候, 我们通过 log 可以观察到,delegate对象在第二次调用 Notify 前已经被正确置为 nil:

2017-07-21 19:16:31.157609+0800 demo[38605:16165704] ===== notify NSObject
2017-07-21 19:16:31.157691+0800 demo[38605:16165704] ===== notify (null)

通过代码生成 Ivar Layout

到了这里,我们已经非常地接近目标了,能够通过修改内存修饰符在运行时改变成员变量的内存管理方式。但是在上面的例子里,对 IvarLayout 和 WeakIvarLayout的重新赋值都是需要我们提前计算好并且 hardcode 到代码里面的。如果需要修改的目标类发生了变化,或者在不同的版本上成员变量的数量和内存修饰符不一样,例如添加了新的成员变量、或是简单地调整了成员变量的定义顺序,就会导致代码里 hardcode 的 layout 值失效需要重新计算。为了避免频繁改动代码,我们的方案应当更智能更自动化,通过代码自动生成的方式来确定 Ivar Layout。

class_ro_t里面 IvarLayout 和 weakIvarLayout 通常是在编译时生成的,如果在运行时将一个变量的内存 Layout 变更,可能需要同时更新 ivarLayout 和 weakIvarLayout 的值。我们在上面的章节说过,Ivar Layout 为了节省内存占用对内存修饰符进行了压缩,所以我们在修改前,需要先将它还原成非压缩的格式,修改完成后再压缩回 Ivar Layout。我们设计了一个简单的 char 数组 ivarInfos,用来表示每个成员变量的内存类型,其长度与成员变量的总数相当,数组的每一个 char 与 ivar_list 里面每一个成员变量一一对应,它有 3 个可能的值(’S’、’W’、’A’),分别对应着strongweak、以及_unsafe_unretained类型。我们通过遍历 ivarLayout 和 weakIvarLayout 来重建 Layout 信息,重建逻辑与 runtime 中 isScanned 方法的逻辑一样,结合我们上面的章节所讲的 Ivar Layout 的编码细节,我们首先找到需要修改的成员变量在 ivar_list 中的位置:

uint32_t ivarPos = 0;
for (_mcc_ivar_list_t::iterator it = ivarList->begin(); it != ivarList->end(); ++it, ++ivarPos) {
    if (it->name  &&  0 == strcmp("_delegate", it->name)) {
        ivar = &*it; break;
    }
}

然后通过调用 _constructIvarInfos 函数来重建 Layout 信息:

static void _inferLayoutInfo(const uint8_t *layout, char *ivar_info, char type) {
    if (!layout || !ivar_info) {
        return;
    }
    ptrdiff_t index = 0; uint8_t byte;
    while ((byte = *layout++)) {
        unsigned skips = (byte >> 4);
        unsigned scans = (byte & 0x0F);
        index += skips;
        for (ptrdiff_t i = index; i < index+scans; ++i) {
            *(ivar_info+i) = type;
        }
        index = index+scans;
    }
}
static char *_constructIvarInfos(Class cls, _mcc_ivar_list_t *ivar_list) {
    if (!cls || !ivar_list) {
        return NULL;
    }
    uint32_t ivarCount = ivar_list->count;
    char *ivarInfo = (char *)calloc(ivarCount+1, sizeof(char));
    memset(ivarInfo, 'A', ivarCount);
    const uint8_t *ivarLayout = class_getIvarLayout(cls);
    _inferLayoutInfo(ivarLayout, ivarInfo, 'S');
    const uint8_t *weakLayout = class_getWeakIvarLayout(cls);
    _inferLayoutInfo(weakLayout, ivarInfo, 'W');
    return ivarInfo;
}

重建后的 ivarInfo 列表,对 ivar_list 中每一个成员变量的内存属性进行了标注。这样可以直接修改 ivarInfo 列表,将成员变量的内存属性从一种类型变更为另一种类型,修改完成后,调用 _fixupIvarLayout 方法重新创建 ivarLayout 和 weakIvarLayout,这是 _inferLayoutInfo 方法的逆向逻辑。因为 _fixupIvarLayout 代码逻辑比较复杂,就不在这里贴出来了,如果有兴趣可以直接查看demo的源代码


写在最后

到了这里,方案3已经初具雏形。我们基于此解决了 8.x 系统上 UIScrollView 的 delegate 属性被声明为assign所带来的崩溃。 虽然它看起来很简单佷暴力,既不像方案1那样需要开发者在业务代码里添加或修改任何代码,也不像方案2那样需要对 dealloc 方法做全局 hook 会带来其他的风险,但和任何方案一样,方案3也受到一些先决条件的限制:

  • 修改必须要在 runtime 初始化完成之后立即执行,一旦app已经开始创建你需要修改的类的对象后,再修改 Ivar Layout 会造成不可预知的后果。与 method swizzling 的推荐做法一样,在 + (void) load 方法里面执行是最稳妥最简单的。
  • 修改前必须要知道所修改的变量名。这个看似简单的前提条件,在实际操作中通常会耗费一些时间才能得到。以 UITableView 为例,它从 UIScrollView 继承而来,在 8.x 系统上都有一个名为@property (nonatomic, assign) id delegate的属性,但是仔细分析 UITableView 的变量列表发现其实它并没有定义与 delegate 对应的 _delegate,而是它的父类 UIScrollView 有一个名为 _delegate 的变量。那么实际修改的对象从 UITableView 变成了 UIScrollView。由于 property 定义的多样性以及 setter 和 getter 实现的灵活性,导致寻找到正确的 Ivar Name 在有些特殊场景下变成了一个比较费时费力的操作。

虽然存在着上述这些局限性,方案3相比其它两种方案,依然有着不可忽视的优势:

成员变量的内存管理方式可以在编译确定后重新定义

这一点为各种热修复方案提供了巨大的操作空间,例如一个不慎被程序员指定错误的内存管理方式,可以在运行时被重新修复,不需要重新发版。至于其他可能的应用场景,还需要靠我们天马行空的想象力一起来发掘。

最后可能你会疑问,property 的 type encodings,有一个 ‘W’ 的类型标识来表明这个属性是不是weak的,我们既然修改了成员变量的内存管理方式,从assign变成了weak,那我们是否需要添加这个标识到 UIScrollView 和 UITableView 的 delegate 呢?这个问题就作为本文的习题留给大家自己思考吧,如果有疑问请联系我:dechaos@163.com

(完)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值