你真的了解 Objective-C 中的load 方法么?

dmsp7785 8年前
   <h2>写在前面</h2>    <blockquote>     <p>文章的标题与其说是问各位读者,不如说是问笔者自己:<strong>我</strong>真的了解 <code>+ load</code> 方法么?</p>    </blockquote>    <p><code>+ load</code> 作为 Objective-C 中的一个方法,与其它方法有很大的不同。它只是一个<strong>在整个文件被加载到运行时,在 <code>main</code> 函数调用之前被 ObjC 运行时调用的钩子方法</strong>。其中关键字有这么几个:</p>    <ul>     <li>文件刚加载</li>     <li><code>main</code> 函数之前</li>     <li>钩子方法</li>    </ul>    <p>我在阅读 ObjC 源代码之前,曾经一度感觉自己对 <code>+ load</code> 方法的作用非常了解,直到看了源代码中的实现,才知道以前的以为,只是自己的以为罢了。</p>    <p>这篇文章会假设你知道:</p>    <ul>     <li>使用过 <code>+ load</code> 方法</li>     <li>知道 <code>+ load</code> 方法的调用顺序(文章中会简单介绍)</li>    </ul>    <p>在这篇文章中并不会用大篇幅介绍 <code>+ load</code> 方法的作用其实也没几个作用,关注点主要在以下两个问题上:</p>    <ul>     <li><code>+ load</code> 方法是如何被调用的</li>     <li><code>+ load</code> 方法为什么会有这种调用顺序</li>    </ul>    <h2>load 方法的调用栈</h2>    <p>首先来通过 <code>load</code> 方法的调用栈,分析一下它到底是如何被调用的。</p>    <p>下面是程序的全部代码:</p>    <pre>  <code class="language-objectivec">// main.m  #import <Foundation/Foundation.h>    @interface XXObject : NSObject @end    @implementation XXObject    + (void)load {      NSLog(@"XXObject load");  }    @end    int main(int argc, const char * argv[]) {        @autoreleasepool { }      return 0;  }  </code></pre>    <p>代码总共只实现了一个 <code>XXObject</code> 的 <code>+ load</code> 方法,主函数中也没有任何的东西:</p>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/3d3f69f6041b04fcde745e5c9492946d.jpg"></p>    <p>虽然在主函数中什么方法都没有调用,但是运行之后,依然打印了 <code>XXObject load</code> 字符串,也就是说调用了 <code>+ load</code> 方法。</p>    <h3>使用符号断点</h3>    <p>使用 Xcode 添加一个符号断点 <code>+[XXObject load]</code>:</p>    <blockquote>     <p>注意这里 <code>+</code> 和 <code>[</code> 之间没有空格</p>    </blockquote>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/c1cecaa122470f718109bbcd69d5a54a.jpg"></p>    <blockquote>     <p>为什么要加一个符号断点呢?因为这样看起来比较高级。</p>    </blockquote>    <p>重新运行程序。这时,代码会停在 <code>NSLog(@"XXObject load");</code> 这一行的实现上:</p>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/b73a8640ea538847fb0b88fd1a4826fd.jpg"></p>    <p>左侧的调用栈很清楚的告诉我们,哪些方法被调用了:</p>    <pre>  <code class="language-objectivec">0  +[XXObject load]    1  call_class_loads()    2  call_load_methods    3  load_images    4  dyld::notifySingle(dyld_image_states, ImageLoader const*)    11 _dyld_start    </code></pre>    <blockquote>     <p><a href="/misc/goto?guid=4959672153123714825">dyld</a> 是 the dynamic link editor 的缩写,它是苹果的<em>动态链接器</em>。</p>     <p>在系统内核做好程序准备工作之后,交由 dyld 负责余下的工作。本文不会对其进行解释</p>    </blockquote>    <p>每当有新的镜像加载之后,都会执行 <code>3 load_images</code> 方法进行回调,这里的回调是在整个运行时初始化时 <code>_objc_init</code> 注册的(会在之后的文章中具体介绍):</p>    <pre>  <code class="language-objectivec">dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);    </code></pre>    <p>有新的镜像被加载到 runtime 时,调用 <code>load_images</code> 方法,并传入最新镜像的信息列表 <code>infoList</code>:</p>    <pre>  <code class="language-objectivec">const char *    load_images(enum dyld_image_states state, uint32_t infoCount,                const struct dyld_image_info infoList[])  {      bool found;        found = false;      for (uint32_t i = 0; i < infoCount; i++) {          if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {              found = true;              break;          }      }      if (!found) return nil;        recursive_mutex_locker_t lock(loadMethodLock);        {          rwlock_writer_t lock2(runtimeLock);          found = load_images_nolock(state, infoCount, infoList);      }        if (found) {          call_load_methods();      }        return nil;  }  </code></pre>    <h3>什么是镜像</h3>    <p>这里就会遇到一个问题:镜像到底是什么,我们用一个断点打印出所有加载的镜像:</p>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/c1f5b45ae5ab3be9d7a47fd48f88b79a.jpg"></p>    <p>从控制台输出的结果大概就是这样的,我们可以看到镜像并不是一个 Objective-C 的代码文件,它应该是一个 target 的编译产物。</p>    <pre>  <code class="language-objectivec">...  (const dyld_image_info) $52 = {    imageLoadAddress = 0x00007fff8a144000    imageFilePath = 0x00007fff8a144168 "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"    imageFileModDate = 1452737802  }  (const dyld_image_info) $53 = {    imageLoadAddress = 0x00007fff946d9000    imageFilePath = 0x00007fff946d9480 "/usr/lib/liblangid.dylib"    imageFileModDate = 1452737618  }  (const dyld_image_info) $54 = {    imageLoadAddress = 0x00007fff88016000    imageFilePath = 0x00007fff88016d40 "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation"    imageFileModDate = 1452737917  }  (const dyld_image_info) $55 = {    imageLoadAddress = 0x0000000100000000    imageFilePath = 0x00007fff5fbff8f0 "/Users/apple/Library/Developer/Xcode/DerivedData/objc-dibgivkseuawonexgbqssmdszazo/Build/Products/Debug/debug-objc"    imageFileModDate = 0  }  </code></pre>    <p>这里面有很多的动态链接库,还有一些苹果为我们提供的框架,比如 Foundation、 CoreServices 等等,都是在这个 <code>load_images</code> 中加载进来的,而这些 <code>imageFilePath</code> 都是对应的<strong>二进制文件</strong>的地址。</p>    <p>但是如果进入最下面的这个目录,会发现它是一个<strong>可执行文件</strong>,它的运行结果与 Xcode 中的运行结果相同:</p>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/25a2b5e5966c63a8d90aefbd93602227.jpg"></p>    <h3>准备 + load 方法</h3>    <p>我们重新回到 <code>load_images</code> 方法,如果在扫描镜像的过程中发现了 <code>+ load</code> 符号:</p>    <pre>  <code class="language-objectivec">for (uint32_t i = 0; i < infoCount; i++) {        if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {          found = true;          break;      }  }  </code></pre>    <p>就会进入 <code>load_images_nolock</code> 来查找 <code>load</code> 方法:</p>    <pre>  <code class="language-objectivec">bool load_images_nolock(enum dyld_image_states state,uint32_t infoCount,                       const struct dyld_image_info infoList[])  {      bool found = NO;      uint32_t i;        i = infoCount;      while (i--) {          const headerType *mhdr = (headerType*)infoList[i].imageLoadAddress;          if (!hasLoadMethods(mhdr)) continue;            prepare_load_methods(mhdr);          found = YES;      }        return found;  }  </code></pre>    <p>调用 <code>prepare_load_methods</code> 对 <code>load</code> 方法的调用进行准备(将需要调用 <code>load</code> 方法的类添加到一个列表中,后面的小节中会介绍):</p>    <pre>  <code class="language-objectivec">void prepare_load_methods(const headerType *mhdr)    {      size_t count, i;        runtimeLock.assertWriting();        classref_t *classlist =           _getObjc2NonlazyClassList(mhdr, &count);      for (i = 0; i < count; i++) {          schedule_class_load(remapClass(classlist[i]));      }        category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);      for (i = 0; i < count; i++) {          category_t *cat = categorylist[i];          Class cls = remapClass(cat->cls);          if (!cls) continue;  // category for ignored weak-linked class          realizeClass(cls);          assert(cls->ISA()->isRealized());          add_category_to_loadable_list(cat);      }  }  </code></pre>    <p>通过 <code>_getObjc2NonlazyClassList</code> 获取所有的类的列表之后,会通过 <code>remapClass</code> 获取类对应的指针,然后调用 <code>schedule_class_load</code> <strong>递归地安排当前类和没有调用 <code>+ load</code> 父类</strong>进入列表。</p>    <pre>  <code class="language-objectivec">static void schedule_class_load(Class cls)    {      if (!cls) return;      assert(cls->isRealized());        if (cls->data()->flags & RW_LOADED) return;        schedule_class_load(cls->superclass);        add_class_to_loadable_list(cls);      cls->setInfo(RW_LOADED);   }  </code></pre>    <p>在执行 <code>add_class_to_loadable_list(cls)</code> 将当前类加入加载列表之前,会<strong>先把父类加入待加载的列表</strong>,保证父类在子类前调用 <code>load</code> 方法。</p>    <h3>调用 + load 方法</h3>    <p>在将镜像加载到运行时、对 <code>load</code> 方法的准备就绪之后,执行 <code>call_load_methods</code>,开始调用 <code>load</code> 方法:</p>    <pre>  <code class="language-objectivec">void call_load_methods(void)    {      ...        do {          while (loadable_classes_used > 0) {              call_class_loads();          }            more_categories = call_category_loads();        } while (loadable_classes_used > 0  ||  more_categories);        ...  }  </code></pre>    <p>方法的调用流程大概是这样的:</p>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/4444289be23257d2443ce5e51dab6922.png"></p>    <p>其中 <code>call_class_loads</code> 会从一个待加载的类列表 <code>loadable_classes</code> 中寻找对应的类,然后找到 <code>@selector(load)</code> 的实现并执行。</p>    <pre>  <code class="language-objectivec">static void call_class_loads(void)    {      int i;        struct loadable_class *classes = loadable_classes;      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_method_t load_method = (load_method_t)classes[i].method;          if (!cls) continue;            (*load_method)(cls, SEL_load);      }        if (classes) free(classes);  }  </code></pre>    <p>这行 <code>(*load_method)(cls, SEL_load)</code> 代码就会调用 <code>+[XXObject load]</code> 方法。</p>    <blockquote>     <p>我们会在下面介绍 <code>loadable_classes</code> 列表是如何管理的。</p>    </blockquote>    <p>到现在,我们回答了第一个问题:</p>    <p>Q:<strong><code>load</code> 方法是如何被调用的?</strong></p>    <p>A:当 Objective-C 运行时初始化的时候,会通过 <code>dyld_register_image_state_change_handler</code> 在每次有新的镜像加入<em>运行时</em>的时候,进行回调。执行 <code>load_images</code> 将所有包含 <code>load</code> 方法的文件加入列表 <code>loadable_classes</code> ,然后从这个列表中找到对应的 <code>load</code> 方法的实现,调用 <code>load</code> 方法。</p>    <h2>加载的管理</h2>    <p>ObjC 对于加载的管理,主要使用了两个列表,分别是 <code>loadable_classes</code> 和 <code>loadable_categories</code>。</p>    <p>方法的调用过程也分为两个部分,准备 <code>load</code> 方法和调用 <code>load</code> 方法,我更觉得这两个部分比较像生产者与消费者:</p>    <p><img alt="你真的了解 Objective-C 中的load 方法么?" src="https://simg.open-open.com/show/93bcae636bb324010d415ae7fcd8a567.png"></p>    <p><code>add_class_to_loadable_list</code> 方法负责将类加入 <code>loadable_classes</code> 集合,而 <code>call_class_loads</code> 负责消费集合中的元素。</p>    <p>而对于分类来说,其模型也是类似的,只不过使用了另一个列表 <code>loadable_categories</code>。</p>    <h3>“生产” loadable_class</h3>    <p>在调用</p>    <p><code>load_images -> load_images_nolock -> prepare_load_methods -> schedule_class_load -> add_class_to_loadable_list</code> 的时候会将未加载的类添加到 <code>loadable_classes</code> 数组中:</p>    <pre>  <code class="language-objectivec">void add_class_to_loadable_list(Class cls)    {      IMP method;        loadMethodLock.assertLocked();        method = cls->getLoadMethod();      if (!method) return;        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_classes[loadable_classes_used].cls = cls;      loadable_classes[loadable_classes_used].method = method;      loadable_classes_used++;  }  </code></pre>    <p>方法刚被调用时:</p>    <ol>     <li>会从 <code>class</code> 中获取 <code>load</code> 方法: <code>method = cls->getLoadMethod();</code></li>     <li>判断当前 <code>loadable_classes</code> 这个数组是否已经被全部占用了:<code>loadable_classes_used == loadable_classes_allocated</code></li>     <li>在当前数组的基础上扩大数组的大小:<code>realloc</code></li>     <li>把传入的 <code>class</code> 以及对应的方法的实现加到列表中</li>    </ol>    <p>另外一个用于保存分类的列表 <code>loadable_categories</code> 也有一个类似的方法 <code>add_category_to_loadable_list</code>。</p>    <pre>  <code class="language-objectivec">void add_category_to_loadable_list(Category cat)    {      IMP method;        loadMethodLock.assertLocked();        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));      }        loadable_categories[loadable_categories_used].cat = cat;      loadable_categories[loadable_categories_used].method = method;      loadable_categories_used++;  }  </code></pre>    <p>实现几乎与 <code>add_class_to_loadable_list</code> 完全相同。</p>    <p>到这里我们完成了对 <code>loadable_classes</code> 以及 <code>loadable_categories</code> 的提供,下面会开始消耗列表中的元素。</p>    <h3>“消费” loadable_class</h3>    <p>调用 <code>load</code> 方法的过程就是“消费” <code>loadable_classes</code> 的过程,</p>    <p><code>load_images -> call_load_methods -> call_class_loads</code> 会从 <code>loadable_classes</code> 中取出对应类和方法,执行 <code>load</code>。</p>    <pre>  <code class="language-objectivec">void call_load_methods(void)    {      static bool loading = NO;      bool more_categories;        loadMethodLock.assertLocked();        if (loading) return;      loading = YES;        void *pool = objc_autoreleasePoolPush();        do {          while (loadable_classes_used > 0) {              call_class_loads();          }            more_categories = call_category_loads();        } while (loadable_classes_used > 0  ||  more_categories);        objc_autoreleasePoolPop(pool);        loading = NO;  }  </code></pre>    <p>上述方法对所有在 <code>loadable_classes</code> 以及 <code>loadable_categories</code> 中的类以及分类执行 <code>load</code>方法。</p>    <pre>  <code class="language-objectivec">do {        while (loadable_classes_used > 0) {          call_class_loads();      }        more_categories = call_category_loads();    } while (loadable_classes_used > 0  ||  more_categories);  </code></pre>    <p>调用顺序如下:</p>    <ol>     <li>不停调用类的 <code>+ load</code> 方法,直到 <code>loadable_classes</code> 为空</li>     <li>调用<strong>一次</strong> <code>call_category_loads</code> 加载分类</li>     <li>如果有 <code>loadable_classes</code> 或者更多的分类,继续调用 <code>load</code> 方法</li>    </ol>    <p>相比于类 <code>load</code> 方法的调用,分类中 <code>load</code> 方法的调用就有些复杂了:</p>    <pre>  <code class="language-objectivec">static bool call_category_loads(void)    {      int i, shift;      bool new_categories_added = NO;      // 1. 获取当前可以加载的分类列表      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;        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()) {              // 2. 如果当前类是可加载的 `cls  &&  cls->isLoadable()` 就会调用分类的 load 方法              (*load_method)(cls, SEL_load);              cats[i].cat = nil;          }      }        // 3. 将所有加载过的分类移除 `loadable_categories` 列表      shift = 0;      for (i = 0; i < used; i++) {          if (cats[i].cat) {              cats[i-shift] = cats[i];          } else {              shift++;          }      }      used -= shift;        // 4. 为 `loadable_categories` 重新分配内存,并重新设置它的值      new_categories_added = (loadable_categories_used > 0);      for (i = 0; i < loadable_categories_used; i++) {          if (used == allocated) {              allocated = allocated*2 + 16;              cats = (struct loadable_category *)                  realloc(cats, allocated *                                    sizeof(struct loadable_category));          }          cats[used++] = loadable_categories[i];      }        if (loadable_categories) free(loadable_categories);        if (used) {          loadable_categories = cats;          loadable_categories_used = used;          loadable_categories_allocated = allocated;      } else {          if (cats) free(cats);          loadable_categories = nil;          loadable_categories_used = 0;          loadable_categories_allocated = 0;      }        return new_categories_added;  }  </code></pre>    <p>这个方法有些长,我们来分步解释方法的作用:</p>    <ol>     <li>获取当前可以加载的分类列表</li>     <li>如果当前类是可加载的 <code>cls && cls->isLoadable()</code> 就会调用分类的 <code>load</code> 方法</li>     <li>将所有加载过的分类移除 <code>loadable_categories</code> 列表</li>     <li>为 <code>loadable_categories</code> 重新分配内存,并重新设置它的值</li>    </ol>    <h2>调用的顺序</h2>    <p>你过去可能会听说过,对于 <code>load</code> 方法的调用顺序有两条规则:</p>    <ol>     <li>父类先于子类调用</li>     <li>类先于分类调用</li>    </ol>    <p>这种现象是非常符合我们的直觉的,我们来分析一下这种现象出现的原因。</p>    <p>第一条规则是由于 <code>schedule_class_load</code> 有如下的实现:</p>    <pre>  <code class="language-objectivec">static void schedule_class_load(Class cls)    {      if (!cls) return;      assert(cls->isRealized());        if (cls->data()->flags & RW_LOADED) return;        schedule_class_load(cls->superclass);        add_class_to_loadable_list(cls);      cls->setInfo(RW_LOADED);   }  </code></pre>    <p>这里通过这行代码 <code>schedule_class_load(cls->superclass)</code> 总是能够保证没有调用 <code>load</code> 方法的父类先于子类加入 <code>loadable_classes</code> 数组,从而确保其调用顺序的正确性。</p>    <p>类与分类中 <code>load</code> 方法的调用顺序主要在 <code>call_load_methods</code> 中实现:</p>    <pre>  <code class="language-objectivec">do {        while (loadable_classes_used > 0) {          call_class_loads();      }        more_categories = call_category_loads();    } while (loadable_classes_used > 0  ||  more_categories);  </code></pre>    <p>上面的 <code>do while</code> 语句能够在一定程度上确保,类的 <code>load</code> 方法会先于分类调用。但是这里不能完全保证调用顺序的正确。</p>    <p>如果<strong>分类的镜像在类的镜像之前加载到运行时</strong>,上面的代码就没法保证顺序的正确了,所以,我们还需要在 <code>call_category_loads</code> 中判断类是否已经加载到内存中(调用 <code>load</code> 方法):</p>    <pre>  <code class="language-objectivec">if (cls  &&  cls->isLoadable()) {        (*load_method)(cls, SEL_load);      cats[i].cat = nil;  }  </code></pre>    <p>这里,检查了类是否存在并且是否可以加载,如果都为真,那么就可以调用分类的 load 方法了。</p>    <h2>load 的应用</h2>    <p><code>load</code> 可以说我们在日常开发中可以接触到的调用时间<strong>最靠前的方法</strong>,在主函数运行之前,<code>load</code> 方法就会调用。</p>    <p>由于它的调用不是<em>惰性</em>的,且其只会在程序调用期间调用一次,最最重要的是,如果在类与分类中都实现了 <code>load</code> 方法,它们都会被调用,不像其它的在分类中实现的方法会被覆盖,这就使 <code>load</code> 方法成为了<a href="/misc/goto?guid=4958988058459808734">方法调剂</a>的绝佳时机。</p>    <p>但是由于 <code>load</code> 方法的运行时间过早,所以这里可能不是一个理想的环境,因为<strong>某些类可能需要在在其它类之前加载</strong>,但是这是我们无法保证的。不过在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的。</p>    <h2>参考资料</h2>    <ul>     <li><a href="/misc/goto?guid=4959672153243540679">NSObject +load and +initialize - What do they do?</a></li>     <li><a href="/misc/goto?guid=4958988058459808734">Method Swizzling</a></li>     <li><a href="/misc/goto?guid=4959672153336498851">Objective-C Class Loading and Initialization</a></li>    </ul>    <p> 来源:<a href="/misc/goto?guid=4959672153421674250">Draveness</a></p>