Objective-C Class Ivar Layout 探索

原文出自博客Objective-C Class Ivar Layout 探索
当我们定义一个类的实例变量的时候,可以指定其修饰符:

@interface Sark : NSObject {
  __strong id _gayFriend; // 无修饰符的对象默认会加 __strong
  __weak id _girlFriend;
  __unsafe_unretained id _company;
}
@end

这使得 ivar (instance variable) 可以像属性一样在 ARC 下进行正确的引用计数管理。

那么问题来了,假如这个类是动态生成的:

Class class = objc_allocateClassPair(NSObject.class, "Sark", 0);
class_addIvar(class, "_gayFriend", sizeof(id), log2(sizeof(id)), @encode(id));
class_addIvar(class, "_girlFriend", sizeof(id), log2(sizeof(id)), @encode(id));
class_addIvar(class, "_company", sizeof(id), log2(sizeof(id)), @encode(id));
objc_registerClassPair(class);

该如何像上面一样来添加 ivar 的属性修饰符呢?

刨根问底了一下,发现 ivar 的修饰信息存放在了 Class 的 Ivar Layout 中:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout; // <- 记录了哪些是 strong 的 ivar

    const char * name;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout; // <- 记录了哪些是 weak 的 ivar
    const property_list_t *baseProperties;
};

ivarLayout 和 weakIvarLayout 分别记录了哪些 ivar 是 strong 或是 weak,都未记录的就是基本类型和 __unsafe_unretained 的对象类型。

这两个值可以通过 runtime 提供的几个 API 来访问:

const uint8_t *class_getIvarLayout(Class cls)
const uint8_t *class_getWeakIvarLayout(Class cls)
void class_setIvarLayout(Class cls, const uint8_t *layout)
void class_setWeakIvarLayout(Class cls, const uint8_t *layout)

但我们几乎没可能用到这几个 API,IvarLayout 的值由 runtime 确定,没必要关心它的存在,但为了解决上述问题,我们试着破解了 IvarLayout 的编码方式。

举个例子说明,若类定义为:

@interface Foo : NSObject {
    __strong id ivar0;
    __weak id ivar1;
    __weak id ivar2;
}
@end

则储存 strong ivar 的 ivarLayout 的值为 0x012000

储存 weak ivar 的 weakIvarLayout 的值为 0x1200

一个 uint8_t 在 16 进制下是两位,所以编码的值每两位一对儿,以上面的 ivarLayout 为例:

前两位 01 表示有 0 个非 strong 对象和 1 个 strong 对象
之后两位 20 表示有 2 个非 strong 对象和 0 个 strong 对象
最后两位 00 为结束符,就像 cstring 的 \0 一样
同理,上面的 weakIvarLayout:

前两位 12 表示有 1 个非 weak 对象和接下来连续 2 个 weak 对象
00 结束符
这样,用两个 layout 编码值就可以排查出一个 ivar 是属于 strong 还是 weak 的,若都没有找到,就说明这个对象是 unsafe_unretained.

做个练习,若类定义为:

@interface Bar : NSObject {
    __weak id ivar0;
    __strong id ivar1;
    __unsafe_unretained id ivar2;
    __weak id ivar3;
    __strong id ivar4;
}
@end

则储存 strong ivar 的 ivarLayout 的值为 0x012100

储存 weak ivar 的 weakIvarLayout 的值为 0x01211000

于是乎将 class 的创建代码增加了两个 ivarLayout 值的设置:

Class class = objc_allocateClassPair(NSObject.class, "Sark", 0);
class_addIvar(class, "_gayFriend", sizeof(id), log2(sizeof(id)), @encode(id));
class_addIvar(class, "_girlFriend", sizeof(id), log2(sizeof(id)), @encode(id));
class_addIvar(class, "_company", sizeof(id), log2(sizeof(id)), @encode(id));
class_setIvarLayout(class, (const uint8_t *)"\x01\x12"); // <--- new
class_setWeakIvarLayout(class, (const uint8_t *)"\x11\x10"); // <--- new
objc_registerClassPair(class);

本以为解决了这个问题,但是 runtime 继续打脸,strong 和 weak 的内存管理并没有生效,继续研究发现, class 的 flags 中有一个标记位记录这个类是否 ARC,正常编译的类,且标识了 -fobjc-arc flag 时,这个标记位为 1,而动态创建的类并没有设置它。所以只能继续黑魔法,运行时把这个标记位设置上,探索过程不赘述了,实现如下:

static void fixup_class_arc(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;
        } *ro;
    } *objcRWClass = (typeof(objcRWClass))(objcClass->bits & FAST_DATA_MASK);
#define RO_IS_ARR 1<<7    
    objcRWClass->ro->flags |= RO_IS_ARR;
}

把这个 fixup 放在 objc_registerClassPair(class); 之后,这个动态的类终于可以像静态编译的类一样操作 ivar 了,可以测试一下:

id sark = [class new];
Ivar weakIvar = class_getInstanceVariable(class, "_girlFriend");
Ivar strongIvar = class_getInstanceVariable(class, "_gayFriend");
{
    id girl = [NSObject new];
    id boy = [NSObject new];
    object_setIvar(sark, weakIvar, girl);
    object_setIvar(sark, strongIvar, boy);
} // ARC 在这里会释放大括号内的 girl,boy
// 输出:weakIvar 为 nil,strongIvar 有值
NSLog(@"%@, %@", object_getIvar(sark, weakIvar), object_getIvar(sark, strongIvar));
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容