iOS-底层探索13:懒加载类与非懒加载类(类的加载中)

iOS 底层探索 文章汇总

目录


一、前言

上一篇文章iOS dyld和objc的关联分析(类的加载上)中我们分析了dyldobjc的关联关系,但是类中的方法、属性、协议什么时候添加到类中的,rwe什么时候产生的这些问题依然没有解决。那么这篇文章我们就继续往下探索。

二、继续探索_read_images方法

这里重点分析_read_images方法中处理non-lazy classes的代码
通过上篇文章的分析我们从macho文件中读取到类的地址名字,接下来处理类中的ro、rw、rwe等数据。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...
    // +load handled by prepare_load_methods()

    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) {
        classref_t const *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            addClassTableEntry(cls);

            if (cls->isSwiftStable()) {
                if (cls->swiftMetadataInitializer()) {
                    _objc_fatal("Swift class %s with a metadata initializer "
                                "is not allowed to be non-lazy",
                                cls->nameForLogging());
                }
                // fixme also disallow relocatable classes
                // We can't disallow all Swift classes because of
                // classes like Swift.__EmptyArrayStorage
            }
            realizeClassWithoutSwift(cls, nil);
        }
    }

    ts.log("IMAGE TIMES: realize non-lazy classes");
...
}

重点方法:realizeClassWithoutSwift

三、realizeClassWithoutSwift方法分析

注意:realizeClassWithoutSwift方法调用的前提是这个类是非懒加载类

/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    class_rw_t *rw;
    Class supercls;
    Class metacls;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    ASSERT(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }
...

    递归调用realizeClassWithoutSwift方法实现父类及元类
    // cls 信息 -> 父类 -> 元类 : cls LGPerson
    supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
...

    // Attach categories - 分类 
    methodizeClass(cls, previously);

    return cls;
}

realizeClassWithoutSwift方法注释如下:

  • 在类cls上执行首次初始化,
  • 包括分配它的读写数据。
  • 不执行任何快速端初始化。
  • 返回类的真实类结构。
  • 锁定:runtimeLock必须由调用者写锁

技巧:在realizeClassWithoutSwift方法中加入如下代码并打上断点即可只研究我们自定义的类

clean memory:加载后不会发生更改的内存
dirty memory:加载进程运行时会发生更改的内存。类结构一经使用就会变成dirty memory ,因为运行时会向它写入新的数据(eg:方法缓存)。

roread only-只读的,保留类的原始数据
rwread write-外界通过rw查找方法,首先判断是否存在rwe,有就从rwe中寻找方法,没有就从ro中寻找方法。
rwe:2020新增内容,由于rw中的数据和ro存在重复造成了内存浪费,而且只有运行时动态修改类的信息才会造成两者数据存在差异。为了节省内存从rw中分离出会变化的rwe,创建rwe时首先会copy一份ro中的数据,然后在修改或新增数据。因此使用了Runtime修改了的类才有rwe数据。

关于ro、rw、rwe的官方介绍:WWDC 2020 :Advancements in the Objective-C runtime

Class in memory
优化前的内存结构
优化后的内存结构


四、methodizeClass方法分析

realizeClassWithoutSwift方法中设置了ro、rw并在最后调用了methodizeClass方法,由此我们猜想methodizeClass方法中设置了rwe数据。

/***********************************************************************
* methodizeClass
修复cls的方法列表、协议列表和属性列表。
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
// realizeClassWithoutSwift 尽管看到了 data() -> ro -> rw (rwe)
// ro - methodlist - 方法查找的时候 (二分查找) sel 排序
static void methodizeClass(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro();
    auto rwe = rw->ext();
    
    const char *mangledName  = cls->mangledName();
    const char *LGPersonName = "LGPerson";

    if (strcmp(mangledName, LGPersonName) == 0) {
        bool kc_isMeta = cls->isMetaClass();
        auto kc_rw = cls->data();
        auto kc_ro = kc_rw->ro();
        if (!kc_isMeta) {
            printf("%s: 这个是我要研究的 %s \n",__func__,LGPersonName);
        }
    }

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        if (rwe) rwe->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (rwe && proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (rwe && protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

#if DEBUG
    // Debug: sanity-check all SELs; log method list contents
    for (const auto& meth : rw->methods()) {
        if (PrintConnecting) {
            _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(meth.name));
        }
        ASSERT(sel_registerName(sel_getName(meth.name)) == meth.name); 
    }
#endif
}

方法排序的步骤:

prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                   bool baseMethods, bool methodsFromBundle)
{
...
        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
...
}
static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
...
    // Sort by selector address.
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
}
...

所以类中的方法列表是通过方法地址SEL排序的

虽然这里看到了rwe但是rwe却是NULL

因此methodizeClass方法中还是没有设置rwe


五、懒加载类与非懒加载类

  • 非懒加载类:实现了+load方法(会提前加载,load_images方法中会调用所有+load方法)
  • 懒加载类: 未实现+load方法

下面研究懒加载类在什么时候加载?

准备工作:

  1. 注释+load方法
//+ (void)load{
//
//}

2.添加断点

  1. realizeClassWithoutSwift方法处理
    因为要加载类就必然会调用realizeClassWithoutSwift方法,所以在该方法中也添加断点

运行代码

  1. 由于懒加载类先执行main中的代码再调用realizeClassWithoutSwift方法加载类,所以程序先在main函数中中断

继续调试

打印出堆栈信息

2.通过堆栈信息可知当调用LGPersonalloc方法后触发消息发送->消息转发->类的加载

现在我们知道了懒加载类的加载时机在于类的第一次消息发送


  1. 懒加载类和非懒加载类方法调用情况:

实际调用堆栈如下:

调用顺序为:map_images->_read_images->readClass->realizeClassWithoutSwift->methodizeClass

修改:


六、分类探索

由于在realizeClassWithoutSwift方法的最后调用了methodizeClass方法:

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
...
    // Attach categories - 分类 
    methodizeClass(cls, previously);

    return cls;
}

methodizeClass方法上面注释了Attach categories,所以我们先来分析分类的结构

准备如下代码:

通过clangmain.m生成cpp文件:
命令行代码如下:
cd main.m目录
clang -rewrite-objc main.m -o main2.cpp

通过生成的cpp文件我们可以发现LGPerson分类转换为了如下C++代码

其数据结构,也就是分类的本质为:_category_t

  • _category_t结构体中包含了两个_method_list_t一个是实例变量的方法列表,一个是的方法列表
  • 由于分类没有协议,所以protocols赋值为0,(cls赋值也为0,会在运行时赋上正确的值)
  • 由于结构体中没有成员变量列表,所以分类也不能添加成员变量

instance_methods、class_methods、properties对应的C++代码如下:

  • 可以发现instance_methods中并没有属性cate_name、cate_ageset、get方法。所以分类可以添加属性但是不会生成对应的set、get方法,只能通过Runtime动态添加set、get方法。

  • _method_list_t中的数据结构为method_t

    method_t

现在我们明白了分类的底层结构,那么分类是如何添加到类里面去的呢?


七、如何将分类中的方法添加到类中

由于之前我们猜想methodizeClass方法中设置了rwe数据,那么现在继续探索methodizeClass方法后面的代码,我们注意到有这样的代码:

    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

断点调试进入methodizeClass方法

methodizeClass

通过调试我们可以发现:

  • method_list_t *list中只有类本身的方法,还没添加分类中的方法
  • 虽然rweNULL但执行了prepareMethodLists方法将类中的方法进行了排序

继续调试我们可以发现if中的代码不会执行:

methodizeClass

然而进入attachToClass方法后里面也没执行if中的attachCategories方法

attachToClass

重点方法attachCategories

attachCategories中添加如下代码并加上断点:

if (strcmp(mangledName, LGPersonName) == 0) {
        bool kc_isMeta = cls->isMetaClass();
        auto kc_rw = cls->data();
        auto kc_ro = kc_rw->ro();
        if (!kc_isMeta) {
            printf("%s: 这个是我要研究的 %s \n",__func__,LGPersonName);
        }
    }

放开methodizeClass中的断点继续运行会发现会进入attachCategories方法

调用堆栈如下:

由此可得出以下结论:

  • 虽然实现类调用的methodizeClass->attachToClass后没调用attachCategories方法,但是load_images->loadAllCategories后会调用attachCategories方法。
  • attachCategories方法中进行了rwe初始化。
    继续运行:

这里可以看到分类在运行时会修改category_tname:由LGPerson改为LGA,并为cls赋值。

attachCategories方法中通过调用extAllocIfNeeded()生成了rwe,因此调用extAllocIfNeeded()的地方就会生成了rwe。全局搜索extAllocIfNeeded ()即可发现attachCategoriesaddMethodclass_addProtocol_class_addProperty等地方调用了extAllocIfNeeded()

如果类在有分类动态添加方法协议属性的情况下才会生成rwe

现在我们知道了什么添加分类,什么时候初始化rwe,但是什么时候将分类添加到类中不知道,后续文章将继续探索。


总结

  1. 非懒加载类在main函数之前加载(所以+load方法非必要不要实现)
  2. 懒加载类的加载推迟到类的第一次消息发送
  3. 分类中可以添加属性,但不会自动生成set、get方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容