Objc runtime 初始化过程分析

Bootstrap initialization
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

dyld自举引导调用_objc_init,这个函数很清晰:
首先会进行各单位初始化,如环境变量初始化、tls (thread local storage)线程本地缓存初始化、静态构造器初始化、锁初始化、异常处理初始化;
其次要保存映射所有的镜像文件(map_images)、保存加载这些镜像文件函数指针(load_images)、保存取消映射的处理函数指针(unmap_images)。

Map images

这里map_images会调用map_images_nolock,这个函数主要会做两件事:

Selector initialization

一是遍历当前image中所有的class并计算出使用到的所有libobjc库中的sel的数量,然后调用sel_init对所有sel进行一次初始化selector table注册 (Initialize selector tables and register selectors used internally.);

Read images

二是读入该镜像文件(_read_images),这个函数会处理当前镜像文件的头部信息。源码注释如下:

Perform initial processing of the headers 
in the linked list beginning with headerList. 

该过程又包含若干步骤:

  1. 获取镜像文件中的类列表,遍历列表进行类读取(调用readClass);
  2. 遍历注册所有的selector名字(调用__sel_registerName);
  3. 遍历读取协议列表(调用_getObjc2ProtocolList, readProtocol);
  4. 遍历实例化运行时类结构(调用realizeClass)。
    以上是Read images的主要步骤,省略了一些打印和悬垂指针的判断条件处理等等。下面具体分析各个主要函数做的事情:
Read class
* readClass
* Read a class and metaclass as written by a compiler.
* Returns the new class pointer. This could be: 
* - cls
* - nil  (cls has a missing weak-linked superclass)
* - something else (space for this class was reserved by a future class)

源码函数说明注释如上,该函数会初始化类结构体的class_rw_t类型的data (rw)并强制转换成该结构体的class_ro_t类型的数据ro;同时把rw赋给本类。也就是说初始化读取时,本类的rorw是相同的。核心代码如下:

class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro;
memcpy(newCls, cls, sizeof(objc_class));
rw->ro = (class_ro_t *)newCls->data();
newCls->setData(rw);
Realize class
* realizeClass
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Returns the real class structure for the class. 

函数说明注释中解释得很清楚,这个函数主要是用于执行类的第一次初始化,其中包括了给rw数据分配内存空间,同时构造这个类的结构体。
从函数内部源码看到,具体的执行步骤有:

  1. 初始化类结构体的基本变量,如ro(基本只读数据), rw(可读写数据), supercls(超类), metacls(元类), isMeta(元类标志位)等;
const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;
  1. ro, rw分配内存地址;
ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }

    isMeta = ro->flags & RO_META;
  1. 确定rw的版本;
rw->version = isMeta ? 7 : 0;  // old runtime went up to 6
  1. 对超类和元类分别进行realize class
// Realize superclass and metaclass, if they aren't already.
    // This needs to be done after RW_REALIZED is set above, for root classes.
    // This needs to be done after class index is chosen, for root metaclasses.
    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));
  1. 初始化本类的isa并关联超类;
// Update superclass and metaclass in case of remapping
    cls->superclass = supercls;
    cls->initClassIsa(metacls);
  1. 继承超类的原始变量并初始化本类的变量内存布局;
//static void reconcileInstanceVariables
//(Class cls, Class supercls, const class_ro_t*& ro) 
// Reconcile instance variable offsets / layout.
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

这里reconcileInstanceVariables发挥的作用就是确定本类的内存布局。
具体来说,继承于NSObject的类在不同版本的iOS下应该是不会因为系统增加了基类变量而导致子类的内存布局出问题的。

比如说1.0的系统NSObject中有n个变量,他们的内存布局是从0-n的一个范围;子类如果继承于NSObject并增加了m个自定义的变量,理论上这m个变量应该是追加在0-n的末尾,变成0-(n+m)

如果下个版本的NSObject增加了x个变量,基类的内存布局变成了0-(n+x),如果runtime不进行相应的布局调整,那么子类自定义的m个变量可能就会覆盖掉系统新增的那x个变量,造成内存覆盖的问题。

所以这个函数的作用,就是根据基类的变量内存布局,进行相应的调整。内部会调用moveIvars来进行变量在内存上移动。

  1. 关联超类的继承链;
// Connect this class to its superclass's subclass lists
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }
  1. 关联类别列表。
// Attach categories
    methodizeClass(cls);
Methodize class

进一步阅读这个函数,源码注释如下:

* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.

该函数会对属性列表、方法列表和协议列表做一些排序上的调整;然后会把外部的类别整合进来,关键代码如下:

Attach categories
// Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);

查看attachCategories,有如下注释:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)

即该函数会将类别中的方法列表,属性和协议列表分别都加入本类中,并假定了类别列表加载的顺序是根据类别文件的加载顺序。
注意其中加载方法列表的代码如下:

rw->methods.attachLists(mlists, mcount);
Attach category method lists

attachLists的关键实现如下:

array()->count = newCount;
memmove(array()->lists + addedCount, 
        array()->lists, oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, 
       addedLists, addedCount * sizeof(array()->lists[0]));

可以看到本类在加载类别中的方法列表时会先使用memmove将本类的原始方法所占内存向后移动类别方法所需的空间偏移,然后再用memcpy将类别方法复制到本类的方法列表的头部空间位置。这一点的运行时表现就是当我们使用类别复写了类的一个已有方法时,优先会调用的是类别复写的方法而不是已有方法。此时如果想在类别存在同名方法的前提下依然调用原方法,则可以利用运行时获取方法列表,从列表尾部往回遍历,找到第一个SEL相同的方法指针并执行即可。

注意到,这个runtime初始化过程不包含任何类的initialization,而仅仅是做了运行时类结构体的一些准备工作。这是因为类的initialization (_class_initialize)会发生在对某个类或类实例第一次发送消息时完成,也就是在_class_lookupMethodAndLoadCache3 -> lookUpImpOrForward中完成,同时会发送+initialize消息给复写方法。这是消息发送流程,在此不具体说明。

至此,Objc runtime初始化的一些关键步骤就分析完了,还有一些细节部分没有覆盖,有时间会继续进行完善。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,087评论 1 32
  • runtime 介绍 Objective-C 是一门动态性比较强的编程语言,跟 C、C++ 等语言有着很大的不同,...
    666真666阅读 891评论 0 3
  • 填满的 填不饱的是记忆深处 打断的 打不散的是深情款款 挥洒自如的是 娓娓道来的拥抱之后 的别离
    云舞阅读 233评论 0 0
  • 干画技法、宝虹细纹300g、高光运用留白液。 1.构图,将几块明显的肉画出细节。 2.留白液将高光画出来。 3.橙...
    可爱的小猪琳琳阅读 1,050评论 5 20