iOS中类的加载(下)以及分类的加载

532 阅读8分钟

引入

在类的加载中,我们知道类的加载是在realizeClassWithoutSwift函数中实现的,但是我们还有一些❓,

  • auto ro = (const class_ro_t *)cls->data();里面的data()又是从哪里获取的呢?

  • ❓我们知道有ro,rw,rwe,为什么rw要从ro中复制,为什么会有rwe,rwe又是什么时候赋值的?

  • ❓分类的本质是什么,分类的加载流程是怎么样子的,为什么要避免load方法的过度使用

ro、rw、rwe

realizeClassWithoutSwift中看到auto ro = (const class_ro_t *)cls->data();ro是从cls->data()中获取的,打印一下

截屏2021-07-18 下午2.53.11.png 截屏2021-07-18 下午2.53.35.png 看到里面是有值的,cls->data()中data()的定义如下:

    class_rw_t *data() const {

        return bits.data();

    }
    //点击bits.data()
    class_rw_t* data() const {

        return (class_rw_t *)(bits & FAST_DATA_MASK);

    }

cls->data(),是从macho中读取数据,最后返回的是一个指针类型,只要接收的数据类型中和之前类型里面的数据结构是一一对应的,就会进行赋值操作。所以auto ro = (const class_ro_t *)cls->data();后面加个断点,就可以打印出来具体内容信息。

  • ro ro属于clean memory,在编译即确定的内存空间,是从disk中读取的,只读,加载后不会改变内容的空间
  • rw rw属于dirty memory,rw属于运行时结构,可读可写,可以向类中添加属性、方法等。在运行时会改变的内存。
  • rwe rwe相当于类的额外信息,因为在实际使用过程中,只有很少的类会真正改变他们的内容,所以为了避免资源的消耗就有了rwe 运行时,如果需要动态向类中添加方法协议等,会创建rwe,并将ro的数据优先attache到rwe中,在读取时会优先返回rwe的数据,如果rwe没有被初始化,则返回ro的数据。 rw中包含ro和rwe,其中rw是dirty memory,ro是clean memory;为了让dirty memory占用更少的空间,把rw中可变的部分抽取出来为rwe; clean memory越多越好,dirty memory越少越好,因为iOS系统底层虚拟内存机制的原因,内存不足时会把一部分内存回收掉,后面需要再次使用时从硬盘中加载出来即swap机制,clean memory是可以从硬盘中重新加载出来的内存,iOS的macho文件动态库属于此类型;dirty memory是运行时产生的数据,这部分数据不能从硬盘中重新加载,所以必须一直占据内存,当系统物理内存紧张的时候,会回收掉clean memory内存,如果dirty memory过大,则直接会被回收掉,所以clean memory越多越好,dirty memory越少越好; 苹果对ro、rw、rwe进行这么细致的划分都是为了能够更好的区别dirty memory和 clean memory;

那么什么时候有rwe呢,从上面rwe的解释中我们知道,当我们添加分类的时候会有rwe,那么我们看下分类的数据是如何加载的?

分类的本质

示例代码:


//main
@interface Person (Stu)

+(void)sayHappy;

@end

@implementation Person (Stu)

+(void)sayHappy {

    NSLog(@"hahahhaha");

}
@end

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

    NSString * appDelegateClassName;

    @autoreleasepool {

        appDelegateClassName = NSStringFromClass([AppDelegate class]);

        [Person sayHappy];
    }

    return 0;

}
@end

我们写了一个Person的分类Person+Stu.h,然后来进行调用。使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m生成main.cpp 截屏2021-07-18 下午3.46.28.png 我们看下_category_t结构 截屏2021-07-18 下午3.47.17.png 里面有name、cls、instance_methods、class_methods、Protocols、properties。 我们说类的内部实现是不区分类方法和实例方法的,为什么分类中都列出来了呢,其实是因为分类没有元类,所以这个地方都列了出来。 我们给它添加两个属性:

@property (nonatomic, copy) NSString *stuName;

@property (nonatomic, copy) NSString *gender;

在查看main.cpp

截屏2021-07-18 下午3.55.51.png 我们看到没有对应的setter方法和getter方法实现。也没有带_的成员变量 所以一般不给分类添加属性,添加了也取不到,我们可以使用runtime关联对象的形式来给分类添加属性。

methodizeClass中看到源码if (rwe) rwe->methods.attachLists(&list, 1);,那么rwe什么时候赋值呢,点开看它的数据结构

    class_rw_ext_t *ext() const {

        return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);

    }
    class_rw_ext_t *extAllocIfNeeded() {

        auto v = get_ro_or_rwe();

        if (fastpath(v.is<class_rw_ext_t *>())) {

            return v.get<class_rw_ext_t *>(&ro_or_rw_ext);

        } else {

            return extAlloc(v.get<**const** class_ro_t *>(&ro_or_rw_ext));

        }

    }

大概是在extAllocIfNeeded的时候创建的,全局搜索看哪些地方有用到,我们看到在一些attachCategoriesdemangledNameclass_setVersionaddMethods_finishclass_addProtocol_class_addPropertyobjc_duplicateClass 动态处理的一些函数中用到。 attachCategories方法猜测与分类有关我们看下哪里有调用,可以看到在attachToClassload_categories_nolock中有调用。 那么我们来验证一下,创建一个分类,在main函数中调用 部分示例代码

//LGPerson.h
@interface LGPerson: NSObject

@property (nonatomic, copy) NSString *name;

- (void)saySomething;

@end
//LGPerson.m
@implementation LGPerson


+ (void)load{}

- (void)saySomething{

    NSLog(@"%s", __func__ );

}

//LGPerson+LGA.h
@interface LGPerson (LGA)

@property (nonatomic, copy) NSString *cateA_name;

@property (nonatomic, assign) int    *cateA_age;

- (void)saySomething;

- (void)cateA_instanceMethod1;

- (void)cateA_instanceMethod2;

+ (void)cateA_classMethod1;

+ (void)cateA_classMethod2;

@end

//LGPerson+LGA.m
@implementation LGPerson (LGA)


+ (void)load{}


- (void)saySomething{

    NSLog(@"%s", __func__ );

}

- (void)cateA_instanceMethod1{

    NSLog(@"%s", __func__ );

}

- (void)cateA_instanceMethod2{

    NSLog(@"%s", __func__ );

}

+ (void)cateA_classMethod1{

    NSLog(@"%s", __func__ );

}

+ (void)cateA_classMethod2{

    NSLog(@"%s", __func__);
}

@end

//main
LGPerson * person = [LGPerson alloc];
[person saySomething];
[LGPerson cateA_classMethod1];

分类中有load方法,类中也有load方法

我们以方法为例,探究这种情况是如何加载的。 在类的实现realizeClassWithoutSwift打一个断点

截屏2021-07-18 下午4.45.30.png

截屏2021-07-18 下午4.47.35.png 打印一下方法,一共有四个方法,都是主类的,没有分类的,说明分类的还没有加载出来, 方法调用路径依次是: _read_images非懒加载类->realizeClassWithoutSwift->methodizeClass->attachToClass->load_categories_nolock->attachCategories->attachLists

load_categories_nolock
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;

    auto processCatlist = [&](category_t * const *catlist) {
    for (unsigned i = 0; i < count; i++) {

            category_t *cat = catlist[i];

            Class cls = remapClass(cat->cls);

            locstamped_category_t lc{cat, hi};
           ...
         if (cat->instanceMethods ||  cat->protocols

                    ||  cat->instanceProperties)

                {

                    if (cls->isRealized()) {
                    //实例为非懒加载类,走这里
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);

                    } else {

                        objc::unattachedCategories.addForClass(lc, cls);

                    }

                }
                ...
                }
                processCatlist(hi->catlist(&count));
                processCatlist(hi->catlist2(&count));
                

auto processCatlist = [&](category_t * const *catlist) {}是一个block 由 processCatlist(hi->catlist(&count));的地方传入

截屏2021-07-18 下午5.45.15.png 可以看到category_t的结构和上面我们分析的对应,在c++分析中name还没有确定,所以显示的是Person,说明分类的值是在运行时赋值的。

attachCategories
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,

                 int flags)

{
    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;
...
   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) {//ATTACH_BUFSIZ = 64, 不走这里
                //方法排序
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__ );

                rwe->methods.attachLists(mlists, mcount);

                mcount = 0;

            }
            //走这里
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;

            fromBundle |= entry.hi->isBundle();

        }
        ...
        if (mcount > 0) {

        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,

                           NO, fromBundle, __func__ );

        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        ...

      }
...
}

mlists[ATTACH_BUFSIZ - ++mcount] = mlist;我们打印下mlist 和mlists ATTACH_BUFSIZ - ++mcount = 63 截屏2021-07-18 下午5.04.04.png

截屏2021-07-18 下午5.04.51.png 把本类的地址方法列表的地址放在了最后一个元素。

截屏2021-07-18 下午5.26.17.png mlists + ATTACH_BUFSIZ - mcount输出它的值,看到是一个二级指针,(method_list_t **) $1 = 0x00007ffeefbf5ae8

attachLists

我们看下attachLists

void attachLists(List* **const** * addedLists, uint32_t addedCount) {

        if (addedCount == 0) **return**;

        if (hasArray()) {

            // many lists -> many lists

            uint32_t oldCount = array()->count;

            uint32_t newCount = oldCount + addedCount;

            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));

            newArray->count = newCount;

            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)

                newArray->lists[i + addedCount] = array()->lists[i];

            for (unsigned i = 0; i < addedCount; i++)

                newArray->lists[i] = addedLists[i];

            free(array());

            setArray(newArray);

            validate();

        }

        else if (!list  &&  addedCount == 1) {

            // 0 lists -> 1 list

            list = addedLists[0];

            validate();

        } 

        else {

            // 1 list -> many lists

            Ptr<List> oldList = list;
           
            uint32_t oldCount = oldList ? 1 : 0;

            uint32_t newCount = oldCount + addedCount;

            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            //设置array()->count
            array()->count = newCount;
            //将oldlist放在array()->lists后面的位置
            if (oldList) array()->lists[addedCount] = oldList;

            for (unsigned i = 0; i < addedCount; i++)
               //为新添加的list赋值
                array()->lists[i] = addedLists[i];

            validate();

        }

    }

第一次是本类的加载,我们这个方法会走下面的// 1 list -> many lists 在array()->lists[i] = addedLists[i];打印一个断点,打印下addedLists[i] 截屏2021-07-18 下午5.36.32.png 断点向后移动,打印array->list[]中的值 截屏2021-07-18 下午5.39.38.png 我们看到array->list[i]中存的都是ptr,而ptr是一个指向method_list_t的指针。 从这一部分代码看出来,这个array的长度为2,里面放的是指向method_list_t的指针, 我们在多写一个分类,添加如下代码

//#import "LGPerson+LGB.h"
@interface LGPerson (LGB)

- (void)sayNothing;

@end
//#import "LGPerson+LGB.m"
@implementation LGPerson (LGB)

- (void)sayNothing {

    NSLog(@"sdfsf");

}
@end
//main
[person sayNothing];

截屏2021-07-18 下午6.00.47.png 打下入上面断点,打印newArray->lists[i]的值,是先把之前的主类和分类LGA的赋值在后面,我们打印下,[1]和[2]中的值,看下是否为主类和LGA中的方法。

截屏2021-07-18 下午6.02.28.png 然后再第一个位置,插入最后编译的分类,如下图我们打印LGB的方法 截屏2021-07-18 下午6.05.06.png 加载之后释放掉free(array());下次再有分类,则进入到// 1 list -> many lists,依次循环。

分类中没有load方法,类中有load方法

走如下方法调用 _read_images非懒加载类->realizeClassWithoutSwift->methodizeClass->attachToClass 它没有走attachCategories,那么分类的方法时什么时候加载的呢,我们在realizeClassWithoutSwift加个断点,打印一下ro的信息(为了简单,我们这个时候代码中去掉了LGB),由下面的打印知道,这个时候已经有了分类的信息,则这个时候分类的信息是在data()里面获取的。

截屏2021-07-18 下午6.22.44.png

分类中有load方法,类中没有load方法

走如下方法调用 _read_images非懒加载类->realizeClassWithoutSwift->methodizeClass->attachToClass 同理验证分类的方法也是在data()中获取的。

分类中没有load方法,类中没有load方法

懒加载类在第一次消息转发的时候,加载类和分类的方法,也是从data()中获取的。 截屏2021-07-18 下午6.28.31.png

如果有多个分类,分类中有的有load方法有的没有呢

主类load方法没有实现

_read_images非懒加载类->realizeClassWithoutSwift->methodizeClass->attachToClass 分类方法从data()中获取 截屏2021-07-18 下午6.32.37.png

主类有load方法

_read_images非懒加载类->realizeClassWithoutSwift->load_categories_nolock->attachCategories 分类方法从attachCategories中动态添加

综上:尽量避免使用过多的load方法