iOS原理分析之从源码看load与initialize方法(一)

简介: iOS原理分析之从源码看load与initialize方法

一、引言


   在iOS开发中,NSObject类是万事万物的基类,其在Objective-C的整理类架构中非常重要,其中有两个很有名的方法:load方法与initialize方法。


+ (void)load;

+ (void)initialize;

说起这两个方法,你的第一反应一定是觉得太老套了,这两个方法的调用时机及作用几乎成为了iOS面试的必考题。其本身调用时机也非常简单:


1. load方法在pre-main阶段被调用,每个类都会调用且只会调用一次。


2. initialize方法在类或子类第一次进行方法调用前会调用。


上面的两点说明本身是正确的,但是除此之外,还有许多问题值得我们深究,例如:


1. 子类与父类的load方法的调用顺序是怎样的?


2. 类与分类的load方法调用顺序是怎样的?


3. 子类未实现load方法,会调用父类的么?


4. 当有多个分类都实现了load方法时,会怎么样?


5. 每个类的load方法的调用顺序是怎样的?


6. 父类与子类的initialize的方法调用顺序是怎样的?


7. 子类实现initialize方法后,还会调用父类的initialize方法么?


8. 多个分类都实现了initialize方法后,会怎么样?


9. ...


如上所提到的问题,你现在都能给出明确的答案么?其实,load与initialize方法本身还有许多非常有意思的特点,本篇博客,我们将结合Objective-C源码,对这两个方法的实现原理做深入的分析,相信,如果你对load与initialize还不够了解,不能完全明白上面所提出的问题,那么本篇博客将会使其收获满满。无论在以后的面试中,还是工作中使用到load和initialize方法时,都可能帮助你从源码上理解其执行原理。


二、实践出真知 - 先看load方法


   在开始分析之前,我们首先可以先创建一个测试工程,对load方法的执行时机先做一个简单的测试。首先,我们创建一个Xcode的命令行程序工程,在其中创建一些类、子类和分类,方便我们测试,目录结构如下图所示:


image.png


其中,MyObjectOne和MyObjectTwo都是继承自NSObject的类,MySubObjectOne是MyObjectOne的子类,MySubObjectTwo是MyObjectTwo的子类,同时我们还创建了3个分类,在类中实现load方法,并做打印处理,如下:


+ (void)load {

   NSLog(@"load:%@", [self className]);

}

同样,类似的也在分类中做实现:


+ (void)load {

   NSLog(@"load-category:%@", [self className]);

}

最后我们在main函数中添加一个Log:


int main(int argc, const char * argv[]) {

   @autoreleasepool {

       NSLog(@"Main");

   }

   return 0;

}

运行工程,打印结果如下:


2021-02-18 14:33:46.773294+0800 KCObjc[21400:23090040] load:MyObjectOne

2021-02-18 14:33:46.773867+0800 KCObjc[21400:23090040] load:MySubObjectOne

2021-02-18 14:33:46.773959+0800 KCObjc[21400:23090040] load:MyObjectTwo

2021-02-18 14:33:46.774008+0800 KCObjc[21400:23090040] load:MySubObjectTwo

2021-02-18 14:33:46.774052+0800 KCObjc[21400:23090040] load-category:MyObjectTwo

2021-02-18 14:33:46.774090+0800 KCObjc[21400:23090040] load-category:MyObjectOne

2021-02-18 14:33:46.774127+0800 KCObjc[21400:23090040] load-category:MyObjectOne

2021-02-18 14:33:46.774231+0800 KCObjc[21400:23090040] Main

从打印结果可以看出,load方法在main方法开始之前被调用,执行顺序上来说,先调用类的load方法,再调用分类的load方法,从父子类的关系上看来,先调用父类的load方法,再调用子类的load方法。


   下面,我们就从源码上来分析下,系统如此调用load方法,是源自于什么样的奥妙。


三、从源码分析load方法的调用


   要深入的研究load方法,我们首先需要从Objective-C的初始化函数说起:


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();

   runtime_init();

   exception_init();

   cache_init();

   _imp_implementationWithBlock_init();


   // 其他的我们都不需要关注,只需要关注这行代码

   _dyld_objc_notify_register(&map_images, load_images, unmap_image);


#if __OBJC2__

   didCallDyldNotifyRegister = true;

#endif

}

_objc_init函数定义在objc-os.mm文件中,这个函数用来做Objective-C程序的初始化,由引导程序进行调用,其调用实际会非常的早,并且是操作系统引导程序复杂调用驱动,对开发者无感。在_objc_init函数中,会进行环境的初始化,runtime的初始化以及缓存的初始化等等操作,其中很重要的一步操作是执行_dyld_objc_notify_register函数,这个函数会调用load_images函数来进行镜像的加载。


   load方法的调用,其实就是类加载过程中的一步,首先,我们先来看一个load_images函数的实现:


void

load_images(const char *path __unused, const struct mach_header *mh)

{

   if (!didInitialAttachCategories && didCallDyldNotifyRegister) {

       didInitialAttachCategories = true;

       loadAllCategories();

   }


   // Return without taking locks if there are no +load methods here.

   if (!hasLoadMethods((const headerType *)mh)) return;


   recursive_mutex_locker_t lock(loadMethodLock);


   // Discover load methods

   {

       mutex_locker_t lock2(runtimeLock);

       prepare_load_methods((const headerType *)mh);

   }


   // Call +load methods (without runtimeLock - re-entrant)

   call_load_methods();

}

滤掉其中我们不关心的部分,与load方法调用相关的核心如下:


void

load_images(const char *path __unused, const struct mach_header *mh)

{

   // 镜像中没有load方法,直接返回

   if (!hasLoadMethods((const headerType *)mh)) return;

   {

       // 准备load方法

       prepare_load_methods((const headerType *)mh);

   }

   // 进行load方法的调用

   call_load_methods();

}

最核心的部分在于load方法的准备与laod方法的调用,我们一步一步看,先来看load方法的准备(我们去掉了无关紧要的部分):


void prepare_load_methods(const headerType *mhdr)

{

   size_t count, i;

   // 获取所有类 组成列表

   classref_t const *classlist =

       _getObjc2NonlazyClassList(mhdr, &count);

   for (i = 0; i < count; i++) {

       // 将所有类的load方法进行整理

       schedule_class_load(remapClass(classlist[i]));

   }

   // 获取所有的分类 组成列表

   category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);

   for (i = 0; i < count; i++) {

       category_t *cat = categorylist[i];

       // 将分类的load方法进行整理

       add_category_to_loadable_list(cat);

   }

}

看到这里,我们基本就有头绪了,load方法的调用顺序,基本可以确定是由整理过程所决定的,并且我们可以发现,类的load方法整理与分类的load方法整理是互相独立的,因此也可以推断其调用的时机也是独立的。首先我们先来看类的load方法整理函数schedule_class_load(去掉无关代码后):


static void schedule_class_load(Class cls)

{

   // 类不存在或者已经加载过load,则return

   if (!cls) return;

   if (cls->data()->flags & RW_LOADED) return;


   // 保证加载顺序,递归进行父类加载

   schedule_class_load(cls->superclass);

   // 将当前类的load方法加载进load方法列表中

   add_class_to_loadable_list(cls);

   // 将当前类设置为已经加载过laod

   cls->setInfo(RW_LOADED);

}

可以看到,schedule_class_load函数中使用了递归的方式演着继承链逐层向上,保证在加载load方法时,先加载父类,再加载子类。add_class_to_loadable_list是核心的load方法整理函数,如下(去掉了无关代码):


void add_class_to_loadable_list(Class cls)

{

   IMP method;

   // 读取类中的load方法

   method = cls->getLoadMethod();

   if (!method) return; // 类中没有实现load方法,直接返回

   // 构建存储列表及扩容逻辑

   if (loadable_classes_used == loadable_classes_allocated) {

       loadable_classes_allocated = loadable_classes_allocated*2 + 16;

       loadable_classes = (struct loadable_class *)

           realloc(loadable_classes,

                             loadable_classes_allocated *

                             sizeof(struct loadable_class));

   }

   // 向列表中添加 loadable_class 结构体,这个结构体中存储了类与对应的laod方法

   loadable_classes[loadable_classes_used].cls = cls;

   loadable_classes[loadable_classes_used].method = method;

   // 标记列表index的指针移动

   loadable_classes_used++;

}

loadable_clas结构体的定义如下:


struct loadable_class {

   Class cls;  // may be nil

   IMP method;

};

getLoadMetho函数的实现主要是从类中获取到load方法的实现,如下:


IMP

objc_class::getLoadMethod()

{

   // 获取方法列表

   const method_list_t *mlist;

   mlist = ISA()->data()->ro()->baseMethods();

   if (mlist) {

       // 遍历,找到load方法返回

       for (const auto& meth : *mlist) {

           const char *name = sel_cname(meth.name);

           if (0 == strcmp(name, "load")) {

               return meth.imp;

           }

       }

   }

   return nil;

}

现在,关于类的load方法的准备逻辑已经非常清晰了,最终会按照先父类后子类的顺序将所有类的load方法添加进名为loadable_classes的列表中,loadable_classes这个名字你要注意一下,后面我们还会遇到它。


   我们再来看分类的laod方法准备过程,其与我们上面介绍的类非常相似,add_category_to_loadable_list函数简化后如下:


void add_category_to_loadable_list(Category cat)

{

   IMP method;

   // 获取当前分类的load方法

   method = _category_getLoadMethod(cat);

   if (!method) return;

   // 列表创建与扩容逻辑

   if (loadable_categories_used == loadable_categories_allocated) {

       loadable_categories_allocated = loadable_categories_allocated*2 + 16;

       loadable_categories = (struct loadable_category *)

           realloc(loadable_categories,

                             loadable_categories_allocated *

                             sizeof(struct loadable_category));

   }

   // 将分类与load方法进行存储

   loadable_categories[loadable_categories_used].cat = cat;

   loadable_categories[loadable_categories_used].method = method;

   loadable_categories_used++;

}

可以看到,最终分类的load方法是存储在了loadable_categories列表中。


   准备好了load方法,我们再来分析下load方法的执行过程,call_load_methods函数的核心实现如下:


void call_load_methods(void)

{

   bool more_categories;

   do {

       // 先对 loadable_classes 进行遍历,loadable_classes_used这个字段可以理解为列表的元素个数

       while (loadable_classes_used > 0) {

           call_class_loads();

       }


       // 再对类别进行遍历调用

       more_categories = call_category_loads();

   

   } while (loadable_classes_used > 0  ||  more_categories);

}

call_class_loads函数实现简化后如下:


static void call_class_loads(void)

{

   int i;

   // loadable_classes列表

   struct loadable_class *classes = loadable_classes;

   // 需要执行load方法个数

   int used = loadable_classes_used;

   // 清理数据

   loadable_classes = nil;

   loadable_classes_allocated = 0;

   loadable_classes_used = 0;

   // 循环进行执行 循环的循序是从前到后

   for (i = 0; i < used; i++) {

       // 获取类

       Class cls = classes[i].cls;

       // 获取对应load方法

       load_method_t load_method = (load_method_t)classes[i].method;

       if (!cls) continue;

       // 执行load方法

       (*load_method)(cls, @selector(load));

   }

}

call_category_loads函数的实现要复杂一些,简化后如下:


static bool call_category_loads(void)

{

   int i, shift;

   bool new_categories_added = NO;

 

   // 获取loadable_categories分类load方法列表

   struct loadable_category *cats = loadable_categories;

   int used = loadable_categories_used;

   int allocated = loadable_categories_allocated;

   loadable_categories = nil;

   loadable_categories_allocated = 0;

   loadable_categories_used = 0;


   // 从前往后遍历进行load方法的调用

   for (i = 0; i < used; i++) {

       Category cat = cats[i].cat;

       load_method_t load_method = (load_method_t)cats[i].method;

       Class cls;

       if (!cat) continue;

       cls = _category_getClass(cat);

       if (cls  &&  cls->isLoadable()) {

           (*load_method)(cls, @selector(load));

           cats[i].cat = nil;

       }

   }

   return new_categories_added;

}

现在,我相信你已经对load方法为何类先调用,分类后调用,并且为何父类先调用,子类后调用。但是还有一点,我们不甚明了,即类之间或分类之间的调用顺序是怎么确定的,从源码中可以看到,类列表是通过_getObjc2NonlazyClassList函数获取的,同样分类的列表是通过_getObjc2NonlazyCategoryList函数获取的。这两个函数获取到的类或分类的顺序实际上是与类源文件的编译顺序有关的,如下图所示:

image.png

image.png



可以看到,打印的load方法的执行顺序与源代码的编译顺序是一直的。

目录
相关文章
|
12天前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
2月前
|
开发工具 Android开发 Swift
安卓与iOS开发环境对比分析
在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统无疑是主角。它们各自拥有独特的特点和优势,为开发者提供了不同的开发环境和工具。本文将深入浅出地探讨安卓和iOS开发环境的主要差异,包括开发工具、编程语言、用户界面设计、性能优化以及市场覆盖等方面,旨在帮助初学者更好地理解两大平台的开发特点,并为他们选择合适的开发路径提供参考。通过比较分析,我们将揭示不同环境下的开发实践,以及如何根据项目需求和目标受众来选择最合适的开发平台。
51 2
|
3月前
|
语音技术 开发工具 图形学
Unity与IOS⭐一、百度语音IOS版Demo调试方法
Unity与IOS⭐一、百度语音IOS版Demo调试方法
|
2月前
|
安全 Android开发 数据安全/隐私保护
探索安卓与iOS的安全性差异:技术深度分析与实践建议
本文旨在深入探讨并比较Android和iOS两大移动操作系统在安全性方面的不同之处。通过详细的技术分析,揭示两者在架构设计、权限管理、应用生态及更新机制等方面的安全特性。同时,针对这些差异提出针对性的实践建议,旨在为开发者和用户提供增强移动设备安全性的参考。
136 3
|
1月前
|
开发工具 Android开发 Swift
安卓与iOS开发环境的差异性分析
【10月更文挑战第8天】 本文旨在探讨Android和iOS两大移动操作系统在开发环境上的不同,包括开发语言、工具、平台特性等方面。通过对这些差异性的分析,帮助开发者更好地理解两大平台,以便在项目开发中做出更合适的技术选择。
|
2月前
|
安全 Linux Android开发
探索安卓与iOS的安全性差异:技术深度分析
本文深入探讨了安卓(Android)和iOS两个主流操作系统平台在安全性方面的不同之处。通过比较它们在架构设计、系统更新机制、应用程序生态和隐私保护策略等方面的差异,揭示了每个平台独特的安全优势及潜在风险。此外,文章还讨论了用户在使用这些设备时可以采取的一些最佳实践,以增强个人数据的安全。
|
3月前
|
Java 开发工具 Android开发
安卓与iOS开发环境对比分析
【8月更文挑战第20天】在移动应用开发的广阔天地中,Android和iOS两大平台各自占据着重要的位置。本文将深入探讨这两种操作系统的开发环境,从编程语言到开发工具,从用户界面设计到性能优化,以及市场趋势对开发者选择的影响。我们旨在为读者提供一个全面的比较视角,帮助理解不同平台的优势与挑战,并为那些站在选择十字路口的开发者提供有价值的参考信息。
|
2月前
|
IDE 开发工具 Android开发
安卓与iOS开发环境对比分析
本文将探讨安卓和iOS这两大移动操作系统在开发环境上的差异,从工具、语言、框架到生态系统等多个角度进行比较。我们将深入了解各自的优势和劣势,并尝试为开发者提供一些实用的建议,以帮助他们根据自己的需求选择最适合的开发平台。
47 1
|
3月前
|
开发框架 Android开发 Swift
安卓与iOS应用开发对比分析
【8月更文挑战第20天】在移动应用开发的广阔天地中,安卓和iOS两大平台各占半壁江山。本文将深入探讨这两大操作系统在开发环境、编程语言、用户界面设计、性能优化及市场分布等方面的差异和特点。通过比较分析,旨在为开发者提供一个宏观的视角,帮助他们根据项目需求和目标受众选择最合适的开发平台。同时,文章还将讨论跨平台开发框架的利与弊,以及它们如何影响着移动应用的开发趋势。
|
3月前
|
安全 搜索推荐 Android开发
安卓与iOS应用开发的对比分析
【8月更文挑战第20天】在移动应用开发领域,安卓和iOS两大平台各领风骚。本文通过深入探讨两者的开发环境、编程语言、用户界面设计、应用市场及分发机制等方面的差异,揭示了各自的优势和挑战。旨在为开发者提供决策支持,同时帮助理解为何某些应用可能优先选择在一个平台上发布。
45 2