您的位置:首页 > 移动开发 > Objective-C

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

2017-07-24 19:37 295 查看

第二次尝试

到了这里,我们已经完全搞清楚了 oc 是如何管理
assign
weak
对象的了,如果你有兴趣也可以去自己尝试破解
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 还可以根据内存修饰符来调用与之相符的内存管理方法,这样写不仅能适应我们当前的
assign
weak
的需要,还可以满足以后其他类型之间互转的需要:

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’),分别对应着
strong
weak
、以及
_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

(完)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息