ios 底层原理之分类加载

287 阅读8分钟

前言

上一篇底层原理之类的加载我们通过read_images探索了类的加载过程,懒加载类在方法第一次调用时realizeClassWithoutSwift这个方法对类、元类以及父类进行了初始化,而非懒加载类即实现load()方法的类在read_images时就通过realizeClassWithoutSwift方法进行了初始化,主要是初始化了ro和rw,rwe是Nil,同时read_images也会处理分类,下面就分析下分类。

1.0 认识分类

我们在main方法里写个分类的demo,clang编译一下看下分类的结构

@interface GYPerson : NSObject
@end
@implementation GYPerson
@end

@interface GYPerson (GY)
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
-(void)say1;
-(void)say2;
@end
@implementation GYPerson (GY)
-(void)say1{};
-(void)say2{};
@end

clang:(clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m)编译后部分代码如下:

struct _category_t {
	const char *name;
	struct _class_t *cls;
	const struct _method_list_t *instance_methods;
	const struct _method_list_t *class_methods;
	const struct _protocol_list_t *protocols;
	const struct _prop_list_t *properties;
};
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_GYPerson_$_GY __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"say1", "v16@0:8", (void *)_I_GYPerson_GY_say1},
	{(struct objc_selector *)"say2", "v16@0:8", (void *)_I_GYPerson_GY_say2}}
};

分析:

  • 分类本质上是一个_category_t结构体,而且里面不仅有instance_methods对象方法还有class_methods类方法,这说明分类没有元类,因为类方法应该在元类中
  • 我们看到有say1、say2方法,但是却没有看到分类中属性的set、get方法,说明分类不会主动生成这两个方法,需要用我们所知道的关联对象的方式实现,这个后面的文章会分析。

2.0 ro、rw、rwe由来

我们知道类中有ro、rw以及rwe,那么为什么会有呢?

  • ro:干净的内存(clean Memory),从磁盘里也就是沙盒中读取,是不会变的,是高效的,比如类的名字、父类
  • rw:脏内存(Dirty Memory),是从ro拷贝过来的,很昂贵,是动态变化的比如方法、属性等,所以需要优化,尽可能的把Dirty Memory转化成Clean Memory。
  • rwe:rw的优化,当我们的方法、属性动态发生变化时就创建了rwe,为什么创建?因为大多数情况下类的方法、属性等都不会发生变化都不会创建rwe,如果一旦变化就放进rw中就太浪费资源了,所以苹果就对rw进行了扩展,用rwe专门存放动态发生变化的内存

3.0 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();
        //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));
        }
    }

分析:我们看到如果调用rweextAllocIfNeeded()方法就会初始化rwe,全局搜索哪里调用了extAllocIfNeeded()方法,在attachCategories()、class_setVersion()、demangledName()、addMethods_finish()、class_addProtocol()、class_addProperty()、_class_addProperty()中会调用extAllocIfNeeded()方法初始化rwe。今天的核心就分类的处理,所以把核心先放在attachCategories()方法上。

  1. 那么再全局搜索下attachCategories()是在哪里调用的,在attachToClass()load_categories_nolock()方法中调用了attachCategories()
  2. 再全局搜索attachToClass()哪里调用了,发现在methodizeClass()中调用,而methodizeClass()又是在realizeClassWithoutSwift中调用的,_read_images中调又用了realizeClassWithoutSwift
  3. 再反推跟下load_categories_nolock(),在loadAllCategories()中调用了load_categories_nolock(),load_images() 中又调用了loadAllCategories()

总结:根据反推法我们在源码中发现了两个流程调用了attachCategories()

  • _read_images->realizeClassWithoutSwift->methodizeClass->attachToClass->attachCategories->rwe
  • load_images->loadAllCategories->load_categories_nolock->attachCategories->rwe

attachCategories()

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{   //...
    //苹果给我们分类中方法的个数设置了一个64的门槛
    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) {
             //如果方法等于64
            if (mcount == ATTACH_BUFSIZ) {
                //方法排序
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            //把mlist指针放进ATTACH_BUFSIZ-1的位置,此时mcount=1
            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;
        }
    }
    //上面++mcount时mcount=1>0
    if (mcount > 0) {
        //mlists + ATTACH_BUFSIZ - mcount是内存平移
        //mlists内存平移ATTACH_BUFSIZ-1的位置
        //排序分类方法列表
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        //rwe 把分类方法添加进主类                   
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
     //...
    }
    //添加属性进主类
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
   //协议进主类
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

分析:根据注释attachCategories的作用是创建rwe,然后获取类的所有分类,遍历所有分类,获取分类排完序的方法添加进主类,以及获取属性、协议添加进主类别,我们看下添加进主类的方法attachLists

attachLists

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        //只要setArray 此时为真
        if (hasArray()) {
            //many lists -> many lists
            //获取主类中的方法个数oldCount
            uint32_t oldCount = array()->count;
            //新的个数等于oldCount+addedCount
            uint32_t newCount = oldCount + addedCount;
            //重新开辟大小
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;
            //遍历oldcout,把主类中的方法放在newArray的后面
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
             //把添加的方法放在newArray的前面
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            //是否旧的    
            free(array());
            //赋值新的
            setArray(newArray);
            validate();
        }
        //主类没有list
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            //主类有list
            // 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 = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }

    }

分析:attachLists的作用就是遍历分类中的方法或者属性或者协议,把分类中添加的放在主类的列表前面,把主类中原来的放在列表的后面,以方法为例,也就是说分类中的方法在主类的前面,所以调用一个同名方法会先调用分类中的,这可能也是一个面试题哦~

我们分析了attachCategories方法的作用,但是什么时候调用的呢?它的调用堆栈是什么样的?上面我们通过反推法分析了源码调用流程如下

  • _read_images->realizeClassWithoutSwift->methodizeClass->attachToClass->attachCategories->rwe,这是非懒加载的流程,也就是说必须要实现load()方法,那么主类实现load()还是分类实现呢?
  • load_images->loadAllCategories->load_categories_nolock->attachCategories->rwe,要走这个流程,那么必须要有load()方法,因为load_images就是加载类的load()方法。下面就通过几种情况分析一下到底是load()方法对attachCategories的影响。

主类非懒加载+分类非懒加载

我们新建一个LGPerson的分类,LGPerson主类实现load(),LGPerson的分类也实现load(),在attachCategories处下个断点看一下调用堆栈

image.png 我们看到调用堆栈 load_image->loadAllCategories()->load_categoried_nolock()->attachCategories,确实是调用了attachCategories方法把分类中方法添加进了主类中

再来看下macho中分类表和非懒加载表

image.png

image.png 非懒加载分类表以及分类表值都对应上,没毛病。

主类非懒加载+分类懒加载

LGPerson主类实现load(),LGPerson的分类不实现load(),同样在attachCategories出下断点

image.png 根据调用堆栈发现没有调用attachCategorie,也就是说分类没有实现load()就不会调用attachCategorie方法,那么分类中的方法去哪了呢?

再看下macho中分类表

image.png 分类表怎么是空的?难道分类没有实现load()方法编译时就会被优化掉吗,是优化进类中了吗?我们在初始化类方法realizeClassWithoutSwift中断点看一下ro

image.png 果然分类中的方法已经添加进了主类中。也就是说,如果分类是懒加载即没有实现load(),在编译时就会把分类中的方法、属性等放进主类中,并且分类被优化掉了,因为分类表中没有该分类。

主类懒加载+分类非懒加载

LGPerson主类不实现load(),LGPerson的分类实现load(),同样在attachCategories出下断点 image.png 根据调用堆栈发现也没有调用attachCategorie,也就是说主类没有实现load()就不会调用attachCategorie方法即使分类实现也没用,貌似被编译器优化了,我们再来看下Macho

macho中类表,非懒加载类表、分类表以及非懒加载分类表 image.png image.png image.png image.png 我们的分类明明是非懒加载,但是非懒加载分类表中却没有并且分类中也没有,而主类LGPerson明明没有实现load(),但是非懒加载类表中却存在。说明编译时会把主类强制优化成非懒加载类同时把分类优化掉,把分类中方法放进主类中。这个跟上面主类非懒加载+分类懒加载是一样的,只是会强制把主类变成非懒加载类

主类非懒加载+分类非懒加载

LGPerson主类不实现load(),LGPerson的分类不实现load(),同样在attachCategories处下断点,发现断点并没有走,而且也没有走realizeClassWithoutSwift方法,想想也对,没有实现load()那么也就是懒加载,懒加载类是在方法第一次调用是才加载。我们调用一下LGPerson alloc方法看一下。

image.png 走的是lookUpImpOrForward慢速方法查找流程,最后调用了realizeClassWithoutSwift初始化类。

总结

  • 分类是非懒加载即没有实现load(),在编译时就会把分类中的方法、属性等放进主类中,并且分类被优化掉了,不会调用attachCategories()
  • 主类懒加载没有实现load(),而分类是非懒加载编译时会把主类强制优化成非懒加载类同时把分类优化掉,把分类中方法放进主类中,不会调用attachCategories()。