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
(完)
相关文章推荐
- ObjC如何通过runtime修改Ivar的内存管理方式(二)
- ObjC如何通过runtime修改Ivar的内存管理方式(一)
- 如何通过命令行方式修改XWindows的分辨率和刷新频率
- 如何通过修改快捷方式目标属性加载插件
- 如何通过注册表修改文件关联方式
- 如何通过修改注册表的方式解锁被锁定的用户
- Windows和Linux下,如何在不关闭浏览器的方式下快速生效hosts修改
- Windows7下.dll系统图标因打开方式而被修改后如何修复
- 如何通过使用注册项 (.reg) 文件添加、修改或删除注册表子项和值
- c#如何通过https方式调用java写的WebServices
- 如何修改MSDE的登录方式及SA密码[原创]
- iis 如何修改网站的默认浏览方式
- 实现域内用户能够通过网页web方式修改与用户密码
- 如何通过修改文件添加用户到sudoers上
- 如何通过网页方式将jar包上传到nexus?
- 如何通过HTTPS(SSL加密)方式访问webservice
- 【Win7中如何通过修改注册表将IE设置为默认浏览器】
- C++ builder 通过WMI方式修改DNS
- QTableView中的文本如何修改对齐方式
- WPF DataGrid 自动生成行号的方法(通过修改RowHeaderTemplate的方式)