016-类的加载原理(下)

016-类的加载原理(下)

通过这篇文章可以获得什么

探索概览

类的加载(下).jpeg

分类的结构

探索方式

  • 将文件编译成.cpp,来查看
  • 直接通过objc4的源码来查看

案例代码(main.m)

@interface FFPerson (AA)

@property (nonatomic, copy) NSString * cate_name;
@property (nonatomic, assign) int cate_age;

- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
+ (void)cate_classMethod3;
@end

@implementation FFPerson (AA)

- (void)cate_instanceMethod1 {
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2 {
    NSLog(@"%s",__func__);
}
+ (void)cate_classMethod3 {
    NSLog(@"%s",__func__);
}

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
}
复制代码

将mian.m编译成mian.cpp

terminal指令

clang rewrite-objc main.m -o main.cpp
复制代码

main.cpp

截取部分跟category相关的.cpp代码

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_FFPerson_$_AA,
};
static struct _category_t _OBJC_$_CATEGORY_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"FFPerson",
	0, // &OBJC_CLASS_$_FFPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_FFPerson_$_AA,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_FFPerson_$_AA,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_FFPerson_$_AA,
};
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_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"cate_instanceMethod1", "v16@0:8", (void *)_I_FFPerson_AA_cate_instanceMethod1},
	{(struct objc_selector *)"cate_instanceMethod2", "v16@0:8", (void *)_I_FFPerson_AA_cate_instanceMethod2}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"cate_classMethod3", "v16@0:8", (void *)_C_FFPerson_AA_cate_classMethod3}}
};

static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_FFPerson_$_AA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"cate_name","T@\"NSString\",C,N"},
	{"cate_age","Ti,N"}}
};
复制代码

category_t

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;
};
复制代码

objc4关于category_t源码

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};
复制代码

通过编译的中间层代码.cpp文件于objc4底层源码比较的到结论:

  1. name代表的是分类名字。
  2. 分类不区分类方法于实例方法,本质类加载过程分类属于插入数据,实例方法插入到类中,类方法插入到元类中,所以自己并不会有分元类。
  3. 分类的属性并不会自动生成getter、setter方法,这在.cpp文件中可以得知。

分类是如何被加载的

在类的加载的时候最终都会走到methodizeClass方法,此函数内有一段获取rwe的操作,但是,rwe是在什么时候创建的就不得而知了。

static void methodizeClass(Class cls, Class previously)
{
   auto rwe = rw->ext();
}  
复制代码
class_rw_ext_t *ext() const {
   return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}
复制代码

rwe在什么时候赋值的

关键函数

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));
        }
}
复制代码

通过搜索哪些函数调用了此函数

  • static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count, int flags)
  • static void addMethods_finish(Class cls, method_list_t *newlist)
  • BOOL class_addProtocol(Class cls, Protocol *protocol_gen)
  • static bool _class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attrs, unsigned int count, bool replace)
  • Class objc_duplicateClass(Class original, const char *name, size_t extraBytes)

通过上述结果得知在附加分类给类添加方法给类添加协议重复类等方法的时候会触发extAllocIfNeeded,也就是说在上述这些现象的时候ext会开辟空间了,也侧面验证了WWDC2020关于runtime的优化内Ben讲到的当类需要动态加载的时候才会触发rwe。

为什么会有rwe

通过objc4源码我验证了类的加载其实是rw、ro装载的过程,在内存中将数据拿出来,通过类型的强制转换,再使用rw->set_ro(ro),将ro装载到rw内,然后使用cls->setData(rw)将rw装载进class内,这样类的加载有完美的完成了。那么为什么会有rwe,因为rw在使用的过程中属于dirty memory,是动态的,这导致了使用rw的代价非常的高,这里也就是解释了为什么Ben为什么讲rw的使用是非常昂贵的,这里的昂贵指的内存的价值,是money。这是必要拆分就变得尤为的重要,讲rw中动态的部分拆分出来。

拆分前:

3327007aff24473eb0c75e2646c3cdc5_tplv-k3u1fbpfcp-watermark.jpg

拆分后:

535c570ecbc44347a9426818cce48863_tplv-k3u1fbpfcp-watermark.jpg

那么分类的加载主要的探索方向就是这个rwe

4b23f5a2d2bb4f40a58dadbc190405ca_tplv-k3u1fbpfcp-watermark.jpg

分类是何时被加载的

通过rwe在什么时候赋值的我得到了一定会触发attachCategories函数

  • 那么探索思路就变成了attachCategories何时被加载?
  • attachCategories被谁调用?
  • 调用顺序是什么样的?

全局搜索attachCategories

线索一: attachToClass -> attachCategories

image.png

线索二:load_categories_nolock -> attachCategories

image.png

全局搜索attachToClass

调用attachToClass只有methodizeClass,OK,到这里就又找回来了,形成了闭环,这里特别熟悉了,类加载的相关操作

image.png

关键源码:

// Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
复制代码

通过这段源码可以得知进入attachToClass一共有3个入口,其中有2个入口受previously这个变量控制,而这个变量是methodizeCLass传递进来的参数,一路向上找去,最终得出结论只做objc4内部调试使用。此时attachToClass的调用范围降低至1个,也就是objc::unattachedCategories.attachToClass(cls, cls,isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

核心源码分析

attchCategories

将方法列表、属性和协议从类别附加到类。 假设cats中的categories都加载了,并按加载顺序排序,最旧的类别在先。

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

   /**
     * 只有少数班级在发布时有超过 64 个类别。
     * 这使用了一点堆栈,并避免了 malloc。
     * 类别必须以正确的顺序添加,即从后到前。为了通过分块来做到这一点,我们迭代cats_list
     * 从前到后,向后构建本地缓冲区,并在块上调用 attachLists。 attachLists 将列表放在前面,因此最终结果按预期顺序排列。
     */
    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);
    auto rwe = cls->data()->extAllocIfNeeded();
    
    const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        if (!isMeta) {
            printf("%s -FFPerson....\n",__func__);
        }
    }

    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();
        }
        //插入属性:与method一致,没做研究
        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;
        }
        //插入协议与method一致,没做研究
        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);
}
复制代码

attachLists

分为三种情况:

  • 0 lists -> 1 list
  • 1 list -> many lists
  • many lists -> many lists
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 = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }
复制代码

lldb调试1 list -> many lists图解

iShot2021-07-28 16.59.13.png 结论:无论是1 list -> many lists还是many lists -> many lists,数组结构本身都没有变化,都是一维的指针数组,只是里面存放的指针指向的不同,有的直接指向method_t,有的指针指向method_list_t

核心算法图解

类的加载-60.gif

类的加载many-30.gif

总结:

  • 如果当前methodList不存在addedCount为1的时候,list=addedLists[0],则当前methodList只有一个元素
  • 当前存在一个methodList,将此list整体看作一个元素,与category的method合并,category的method在新合成的methodList的0号位置开始存放,oldMethod放在最后,仅占用一个位置,此时该methodList末尾存放的指针为指向method_list_t的二级指针,仅此一个特殊,其他的均指向method_t,此时为1 list -> many lists
  • 当前存在一个methodList,与category的method合并,Category的method在新合成的methodList的0号位置开始存放,遍历oldMethod,在最末位置开始向前存放,最终的结果依然是一个指针数组,本质上只是变成了元素更多的指针数组,也就是说还是manyList

分类加载时机实验

实验代码

main.m

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%s",__func__);
        FFPerson *objc = [FFPerson alloc];
        [objc likeGirls];
    }
}
复制代码

FFPerson.h

@interface FFPerson : NSObject

- (void)likeFood;
- (void)likeLive;
- (void)likeSleep;
- (void)likeGirls;
+ (void)enjoyLife;

@end
复制代码

FFPerson.m

@implementation FFPerson

+ (void)load {
    NSLog(@"%s",__func__);
}

- (void)likeFood {
    NSLog(@"%s",__func__);
}
- (void)likeLive {
    NSLog(@"%s",__func__);
}
- (void)likeSleep {
    NSLog(@"%s",__func__);
}
- (void)likeGirls{
    NSLog(@"%s",__func__);
}

+ (void)enjoyLife {
    NSLog(@"%s",__func__);
}

@end
复制代码

FFPerson+BBLv.h

@interface FFPerson (BBLv)
- (void)likeGirls;
- (void)cate_bblv1;
- (void)cate_bblv2;
- (void)cate_bblv3;
- (void)cate_bblv4;
@end
复制代码

FFPerson+BBLv.m

@implementation FFPerson (BBLv)
+ (void)load {
    NSLog(@"%s",__func__);
}

- (void)cate_bblv1 {
    NSLog(@"%s",__func__);
}
- (void)cate_bblv2 {
    NSLog(@"%s",__func__);
}
- (void)cate_bblv3 {
    NSLog(@"%s",__func__);
}
- (void)cate_bblv4 {
    NSLog(@"%s",__func__);
}

- (void)likeGirls{
    NSLog(@"%s",__func__);
}
@end
复制代码

对FFPerson锁定,对部分关键函数插入打印代码

_read_images

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{

    const char *mangledName = cls->nonlazyMangledName();
    const char *bblvPersonName = "FFPerson";
    if (strcmp(mangledName, bblvPersonName) == 0) {
          printf("Realize non-lazy classes -- %s -- BBLv -- %s\n",__func__,mangledName);
    }
}
复制代码

realizeClassWithoutSwift

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        if (!isMeta) {
            printf("%s -FFPerson....\n",__func__);
        }
    }
}
复制代码

methodizeClass

static void methodizeClass(Class cls, Class previously)
{
bool isMeta = cls->isMetaClass();
const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        if (!isMeta) {
            printf("%s -FFPerson....\n",__func__);
        }
    }
}
复制代码

attachToClass

void attachToClass(Class cls, Class previously, int flags)
{
    bool isMeta = cls->isMetaClass();
    const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        if (!isMeta) {
            printf("%s -FFPerson....\n",__func__);
        }
    }
}
复制代码

load_categories_nolock(operator())

static void load_categories_nolock(header_info *hi) 
{
    const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        printf("%s -FFPerson....\n",__func__);
    }
}
复制代码

attachCategories

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
bool isMeta = (flags & ATTACH_METACLASS);
const char *mangledName = cls->nonlazyMangledName();
    if (strcmp(mangledName, "FFPerson") == 0)
    {
        if (!isMeta) {
            printf("%s -FFPerson....\n",__func__);
        }
    }
}
复制代码

上述对类与分类加载过程中必要的函数进行监听,包括但不限于_read_imagesrealizeClassWithoutSwift、``methodizeClass、attachToClassload_categories_nolockattachCategories。至此所有的准备工作都已经完毕。

验证阶段

load方法为类是否为懒加载的关键因素,所以采用是否实现load方法的方式来进行测试

验证一:FFPerson与FFPerson+BBLv均实现load方法

readClass -- BBLv -- FFPerson
Realize non-lazy classes -- _read_images -- BBLv -- FFPerson
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
operator() -FFPerson....
attachCategories -FFPerson....
2021-07-28 16:03:39.456705+0800 KCObjcBuild[6195:211286] +[FFPerson load]
2021-07-28 16:03:39.457140+0800 KCObjcBuild[6195:211286] +[FFPerson(BBLv) load]
2021-07-28 16:03:39.457257+0800 KCObjcBuild[6195:211286] main
复制代码

结论一:如果本类与分类都实现了load方法,会执行attachCategories关键函数,那么分类的加载方式为非懒加载方式,即类的加载在main函数之前

验证二:FFPerson实现load方法,FFPerson+BBLv不实现load方法

readClass -- BBLv -- FFPerson
Realize non-lazy classes -- _read_images -- BBLv -- FFPerson
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
2021-07-28 17:29:27.798125+0800 KCObjcBuild[6737:254965] +[FFPerson load]
2021-07-28 17:29:27.798714+0800 KCObjcBuild[6737:254965] main
2021-07-28 17:29:30.243474+0800 KCObjcBuild[6737:254965] -[FFPerson(BBLv) likeGirls]
复制代码

lldb调试图:

iShot2021-07-28 17.58.23.png 结论二:如果本类FFPerson实现load方法,分类FFPerson+BBLv不实现load方法,在编译期间已经决定了类是非懒加载,在编译期间已经完成对方法列表的添加,分类与主类的方法均在macho中直接读取,不会执行attachCategories函数加载分类了。如果分类和主类又相同的方法,分类的方法在前

验证三:FPerson未实现load方法,FFPerson+BBLv实现load方法

readClass -- BBLv -- FFPerson
Realize non-lazy classes -- _read_images -- BBLv -- FFPerson
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
2021-07-28 18:10:19.204251+0800 KCObjcBuild[6923:272796] +[FFPerson(BBLv) load]
2021-07-28 18:10:19.204738+0800 KCObjcBuild[6923:272796] main
复制代码

lldb调试图

iShot2021-07-28 18.13.34.png

结论三:如果本类FFPerson未实现load方法分类FFPerson+BBLv实现了load方法,在编译期间已经决定了类是非懒加载,这个时候就属于类被迫营业,分类都实现了load方法,你主类必须跟。在编译期间已经完成对方法列表的添加,这个时候methodList的读取也是在macho文件直接读取

验证四:FPerson、FFPerson+BBLv均未实现load方法

readClass -- BBLv -- FFPerson
2021-07-28 18:24:31.842952+0800 KCObjcBuild[7005:280092] main
realizeClassWithoutSwift -FFPerson....
methodizeClass -FFPerson....
attachToClass -FFPerson....
2021-07-28 18:24:42.963068+0800 KCObjcBuild[7005:280092] -[FFPerson(BBLv) likeGirls]
复制代码

结论四:如果FPerson、FFPerson+BBLv均未实现load方法,那么类的加载的方式将被定义为懒加载,所有加载将会放在main函数之后,也就是说,在类第一次发送消息的时候开始类的加载

分类:
iOS
标签: