# 17.iOS应用启动（四）：分类的加载

## 前言

我们在[12.类的实现与初始化](/wiki/ios/di-ceng/12.-lei-de-shi-xian-yu-chu-shi-hua.md)中实现类 `realizeClass` 的流程最后一步发现是处理分类 `methodizeClass`。本文就将以此为入口，深入探索 ***什么是分类***。

## 一、 methodizeClass

处理`方法列表`、`协议列表`、`属性列表`，添加`分类`。

> 重要部分见注释：

![methodizeClass01](/files/-Mg-WlI-b2mr8w1h7dHv)

![methodizeClass02](/files/-Mg-WlI3vNuyxSc8bU3b)

### 1.1 attachToClass 将分类添加到类

![attachToClass](/files/-Mg-WlI4tuAg1ckkY9PL)

> 继续调用 `attachCategories`，通过初步阅读源码，发现此处为核心步骤，下面进行具体分析

## 二、 attachCategories

> 这里对源码中两处重要的注释做翻译：

* 添加分类的方法列表、属性、协议到类
* 最早加载的分类在最顶部
* 只有很少的类在运行时会有超过`64`个分类
* 这里使用一个小容量的`栈`
* 分类必须按照合适的顺序添加，这里会从前向后读取，从后向前创建本地缓存，并调用 `attachLists`。所以最后的顺序是对的。
*

```cpp
// 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, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
。。。
    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    // 创建 rwe
    auto rwe = cls->data()->extAllocIfNeeded();

    // 循环添加方法、属性、协议
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
```

### 2.1 流程

* extAllocIfNeeded 创建 rwe
* 循环添加方法、属性、协议

## 三、 分类加载的时机

我们直到类添加分类的核心逻辑是 `attachCategories`。我们全局搜索一下 `attachCategories`。

***attachToClass***

![](/files/-Mg-WlI9g9pN8Wsk_HRZ)

***load\_categories\_nolock***

![](/files/-Mg-WlICj1ZtIHjChsCo)

有上面两处进行了调用。

### 3.1 测试代码准备

***自定义类***

```
@interface RYCat : NSObject

@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) NSInteger age;

- (void)yeah;
- (void)yeah2;
- (void)yeah3;

@end
```

***分类***

```
@interface RYCat (CateA)

- (void)funcA;
- (void)funcA2;
- (void)funcA3;

@end
```

***main函数***

```
RYCat *cat = [RYCat alloc];
[cat funcA];
```

### 3.1 添加断点及日志

![attachToClass](/files/-Mg-WlIFMNjKEuf_psRH)

![attachCategories](/files/-Mg-WlIGYFT6E37IatcR)

![realizeClassWithoutSwift](/files/-Mg-WlIH3NljFDoGP41g)

![load\_categories\_nolock](/files/-Mg-WlIIJWJgNyurkL4B)

### 3.2 场景一： 分类有Load✅ 主类有Load✅ （非懒加载）

Log如下：

```cpp
_read_images -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
load_categories_nolock -- RYCat -- CateA
attachCategories -- RYCat -- CateA
attachCategories -- RYCat -- CateA
+[RYCat load]
+[RYCat(CateA) load]
-[RYCat(CateA) funcA]
Program ended with exit code: 0
```

***流程：***

* \_read\_images
  * realizeClassWithoutSwift
    * attachToClass
      * load\_categories\_nolock
        * attachCategories

### 3.3 场景二： 分类有Load✅ 主类无Load❌ （非懒加载）

Log如下：

```cpp
_read_images -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
+[RYCat(CateA) load]
-[RYCat(CateA) funcA]
Program ended with exit code: 0
```

***流程：***

* \_read\_images
  * realizeClassWithoutSwift
    * attachToClass

> 注意到：***并没有调用 `attachCategories`***。

#### 非懒加载

![非懒加载](/files/-Mg-WlIKVFL50-jPqDXl)

虽然主类未实现+load方法，但是分类实现了，那么主类也是非懒加载的。

### 3.4 场景三： 分类无Load❌ 主类有Load✅ （非懒加载）

Log如下：

```cpp
_read_images -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
+[RYCat load]
-[RYCat(CateA) funcA]
Program ended with exit code: 0
```

***流程：***

* \_read\_images
  * realizeClassWithoutSwift
    * attachToClass

> 注意到：和 ***分类有Load✅ 主类无Load❌*** 一样，***没有调用 `attachCategories`***。

### 3.5 场景四： 分类无Load❌ 主类无Load错❌ （懒加载）

![懒加载](/files/-Mg-WlIMcbSsNLoHGW_Q)

Log如下：

```
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
realizeClassWithoutSwift -- RYCat
attachToClass -- RYCat
Program ended with exit code: 0
```

> 注意到：和 ***3.3和3.4*** 一样，***没有调用 `attachCategories`***。

### 思考： 为什么没有调用 `attachCategories`

`编译器`优化了，直接把分类的数据合并到了主类中。

## 四、 多分类 `+load` 执行顺序

***如果有多个分类，执行的顺序是什么样的呢？***

如图创建两个分类，分别实现 `+load` 方法。思考一下执行的顺序是什么样的呢？为什么呢？

![12](/files/-MgUhWKhJFTREG_47Iu7)

### 4.1 顺序

按顺序输出如下：

* `+[RYCat load]`
* `+[RYCat(CateA) load]`
* `+[RYCat(CateB) load]`

我们先看下编译顺序：

![13](/files/-MgUhWKiHnoIfSqWdb4G)

调整一下编译顺序看有没有影响：

![14](/files/-MgUhWKjWbhasNysJoso)

调整编译顺序后输出如下：

* `+[RYCat load]`
* `+[RYCat(CateB) load]`
* `+[RYCat(CateA) load]`

### 4.2 结论

多分类的情况下，`+load` 的执行顺序是和编译的顺序有关的。

## 五、 多分类同名方法执行顺序

如果多个分类由同名的方法，又会是怎样的顺序呢？

***在两个分类中声明实现同名的\*\*\*\* \*\*\*\*`funcA`***

![15](/files/-MgUhWKk7JmM3cEL1sTD)

> 在 [多分类 `+load` 执行顺序](https://ryukiedev.gitbook.io/wiki/ios/di-ceng/pages/-Mg-Wkn2gCL1YAJt5uyg#四-多分类-load-执行顺序) 中我们知道，编译的顺序影响了各个分类的表现，这里我们也进行一下区分

### 5.1 CateA 比 CateB 先编译

* +\[RYCat load]
* +\[RYCat(CateA) load]
* +\[RYCat(CateB) load]
* -\[RYCat(CateB) funcA]

### 5.2 CateB 比 CateA 先编译

* +\[RYCat load]
* +\[RYCat(CateA) load]
* +\[RYCat(CateB) load]
* -\[RYCat(CateA) funcA]

### 5.3 在主类中也声明并实现 funcA （CateB 比 CateA 先编译）

* +\[RYCat load]
* +\[RYCat(CateA) load]
* +\[RYCat(CateB) load]
* -\[RYCat(CateA) funcA]

### 5.4 总结

可见：多个分类拥有`同名方法`时，`先编译的分类的方法`会被`后编译的分类`覆盖掉。如果主类中也有同名方法也会被覆盖掉。

## 思考

***对启动速度的影响***

在 ***3.2*** 主类分类都有实现 `Load方法` 的时候可见整个流程流程会复杂一下，所以减少这种场景的出现，是有助于提升应用启动速度。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryukiedev.gitbook.io/wiki/ios/di-ceng/17.ios-ying-yong-qi-dong-si-fen-lei-de-jia-zai.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
