iOS 底层原理:类的加载原理下(分类)

793 阅读11分钟

前言

上一篇 iOS 底层原理:类的加载原理中懒加载类与非懒加载类的探索,涉及了rw、ro的操作、方法列表排序的操作等,今天将紧接上文进行分类加载的探索。

准备工作

一、分类的本质探索

clang 探索分类的本质

先在main.m中添加SSLPerson的分类:

@interface SSLPerson (SSL)

@property (nonatomic, copy) NSString *ssl_name;
@property (nonatomic, assign) int *ssl_age;

- (void)ssl_instanceMethod1;
- (void)ssl_instanceMethod2;
+ (void)ssl_classMethod3;
@end

@implementation SSLPerson (SSL)

- (void)ssl_instanceMethod1
{
    NSLog(@"%s",__func__);
}
- (void)ssl_instanceMethod2
{
    NSLog(@"%s",__func__);
}
+ (void)ssl_classMethod3
{
    NSLog(@"%s",__func__);
}
@end

clang -rewrite-objc main.m -o main.cpp得到main.cpp文件:

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 _category_t _OBJC_$_CATEGORY_SSLPerson_$_SSL __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "SSLPerson",
    0, // &OBJC_CLASS_$_SSLPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_SSLPerson_$_SSL,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_SSLPerson_$_SSL,
    0,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_SSLPerson_$_SSL,
};

static void _I_SSLPerson_SSL_ssl_instanceMethod1(SSLPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_1d79f7_mi_0,__func__);
}

static void _I_SSLPerson_SSL_ssl_instanceMethod2(SSLPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_1d79f7_mi_1,__func__);
}

static void _C_SSLPerson_SSL_ssl_classMethod3(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_1d79f7_mi_2,__func__);
}
  • 分类被译成C++以后是_category_t结构体,通过_OBJC_$_CATEGORY_SSLPerson_$_SSL构造函数进行赋值。
  • 分类没有元类的概念,所以分类中有两个方法列表:instance_methods存储实例方法,class_methods存储类方法。
  • 我们可以找到ssl_instanceMethod1ssl_instanceMethod2ssl_instanceMethod3方法的相关代码,但是没有ssl_namessl_age相关方法的代码,说明属性没有自动生成gettersetter方法。
  • name是分类名,cls是类。

源码 探索分类的本质

打开源码,看一下源码中分类的定义:

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;
    }
};
  • 源码中的结构跟上面编译出来的C++源码基本相似,实例方法类方法协议,和属性都可以找到对应的存储方式。
  • 我们并没有找到成员变量的定义,因此分类中是不能添加成员变量的。

分类的本质 总结

分类的本质是一个category_t的结构体。

  • 有两个method_list_t类型的方法列表:
    • instanceMethods:实例方法列表。
    • classMethods:类方法列表。
  • property_list_t类型的属性列表,表示分类中定义的属性,但是不会自动生成GetterSetter方法。
  • 没有成员变量的定义,因此分类中是不能添加成员变量。
  • 一个protocol_list_t类型的协议列表,表示分类中实现的协议。
  • name(分类的名称)和cls(类对象)。

二、分类的加载引入 反推思路

rwe 反推

再次回到methodizeClass函数:

static void methodizeClass(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro();
    auto rwe = rw->ext();

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
        if (rwe) rwe->methods.attachLists(&list, 1);
    }
    
    property_list_t *proplist = ro->baseProperties;
    if (rwe && proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (rwe && protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }
}
  • 我们发现方法列表属性列表协议列表的加载都用到了rwe的条件判断,rwe通过rw->ext()来赋值。

点击查看rwe

image.png

  • extAllocIfNeeded()rwe的初始化函数,如果没有值就去创建,如果有值时直接获取返回。

attachCategories 反推

搜素extAllocIfNeeded()的调用:

attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    ...
    auto rwe = cls->data()->extAllocIfNeeded();
    ...
}

搜索attachCategories的调用:

image.png image.png

  • 如图,load_categories_nolock函数和attachToClass函数都调用了attachCategories
  • 我们接下来,将以这两个函数反向推导,推导到我们熟悉的代码,来分析分类的加载。

attachToClass 反推

我们先用attachToClass进行反推,全局搜索attachToClass,发现只有在methodizeClass函数中被调用了:

static void methodizeClass(Class cls, Class previously)
{
    ...
    // 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);
}
  • previously是个备用参数,值一直都是nil,通过realizeClassWithoutSwift(cls, nil)传值过来的。
  • 所以,真正调用attachToClass的地方只有objc::unattachedCategories.attachToClass(cls, cls,sMeta ? ATTACH_METACLASS : ATTACH_CLASS)这一个地方。
  • 可以得出attachToClass这条线的调用流程是:_read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories,前半部分是我们熟悉的懒加载流程。

load_categories_nolock 反推

load_categories_nolock这边我们就不去搜索函数的调用了,下面会通过断点调试的方式来推导,现在的流程是:load_categories_nolock -> attachCategories

三、分类和主类加载的4种情况

上一篇 iOS 底层原理:类的加载原理中 我们讲到过懒加载和非懒加载,类加载时调用函数,跟是否调用load是有关系的。

那么接下来,针对分类和主类有没有实现load方法进行函数调用的测试,看一下不同情况是怎么调用到attachCategories的。

准备

工程中添加下面代码:

image.png

_read_imagesrealizeClassWithoutSwiftmethodizeClassattachToClassload_categories_nolockattachCategories中分别添加打印代码:

image.png

  • 大家可以对这些位置自行加断点,进行相关调试。

main.m中添加断点:

image.png

准备工作完毕,下面开始调试。

分类load + 主类load

分类和主类同时实现load,运行项目:

image.png

  • 根据断点调试、打印结果和堆栈信息,这种情况下函数的调用顺序为:

    • _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> _read_images函数走完。
    • load_images -> loadAllCategories -> load_categories_nolock -> attachCategories
  • 用图来表示这种情况的函数调用:

    image.png

分类load + 主类没有

image.png

  • 这种情况下,attachCategories函数没有被调用,只是_read_images相关函数的调用。

  • 用图来表示这种情况的函数调用:

    image.png

分类没有 + 主类load

image.png

  • 这种情况和上面的结果一样,attachCategories函数也没有被调用。

  • 用图来表示这种情况的函数调用:

    image.png

分类没有 + 主类没有

image.png

  • 这种情况下,前面的这些函数都没有调用。

四、分类加载流程跟踪

分类和主类同时实现load时,上面分析出了attachCategories的函数是如何调用到的,接下来进行断点调试。

load_categories_nolock 跟踪

先断点进入load_categories_nolock

image.png

  • lclocstamped_category_t类型的,hi是符号表的信息,cat是分类。 image.png

打印一下cat

image.png

  • 可以看到,分类名是SSL1instanceMethodclassMethods也是有值的。

单步向下走,走到了attachCategories的函数调用处:

image.png

  • ATTACH_EXISTING的值是8,接下来进入attachCategories函数。

attachCategories 跟踪

先看下attachCategories函数:

static void
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;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    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();
        }
        ...
    }
    
    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);
}

断点进入attachCategories,然后走到下面的代码:

image.png

  • mlist是分类的方法结构体,count = 2代表方法的数量,分别是say1say2方法。

继续向下走:

image.png

  • ATTACH_BUFSIZ的值是64mcount初始值为0mlists共有64个位置。
  • 这里将mlist赋值到mlists64 - 1的位置,并对mcount进行+1操作。

继续向下走:

image.png

  • 通过prepareMethodListsmlists中的方法进行排序,这里mcount1,取的是mlists的第64 - 1的位置的值。
  • 接下来进入attachLists函数,传入mlists + ATTACH_BUFSIZ - mcountmethod_list_t **类型,传入mcount1

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 = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

断点进入attachLists

image.png

  • 这个listcount3,存的是我们主类的3个方法。

断点向下走:

image.png

  • oldCount1newCount2addedLists[i] method_list_t *类型,所以oldList也是method_list_t *类型。
  • oldList也就是主类的method_list_t *,存储在array()->lists的第1个位置。
  • addedLists[i]也就是分类的method_list_t *,存储在array()->lists的第0个位置,可以看到分类被添加到了主类的前面。

五、多个分类加载

上面分析了加载一个分类的情况,接下来探索多个分类的加载。

准备

再添加两个分类:

image.png

load_categories_nolock 跟踪

断点进入load_categories_nolock:

image.png

i = 1时停住断点,进行相关打印:

image.png

  • 此时的count = 3i = 1,分类是SSL3,接下来断点进入attachCategories

attachCategories 跟踪

image.png

  • 传入的mlists + ATTACH_BUFSIZ - mcount依然是method_list_t **类型,也就是method_list_t指针地址类型。

attachLists 跟踪

断点进入attachLists

image.png

  • oldCount = 2,根据之前的分析我们可以知道,它们是主类的method_list_t *和第一个分类的method_list_t *
  • newCount = 3,用这个大小开辟了一个新的newArray
  • 第一个for循环,取array()->lists的值也就是old的值,从后向前存储到newArray的第2和第1个位置。
  • 第二个for循环,将addedLists的值从前向后,存储到newArray中。这里只有一个值,存储到第0位置。

六、五种情况 Categories 数据的加载

我们现在只知道分类主类同时实现load时,Categories的加载情况,下面分析其他情况下Categories是如何加载的。

分类load + 主类没有

为方便测试只保留SSLPersonSSLPerson+SSL1,运行程序,断点进入realizeClassWithoutSwift

image.png

lldb打印:

image.png

  • 可以看到,此时的方法数量count已经是5了,是SSLPerson中的3个,和SSLPerson+SSL1中的2个。方法已经存在了,说明是通过data()加载的,已经被编译器处理了。

分类没有  +  主类load

经过测试,这种情况和上面的一样,在realizeClassWithoutSwift中时,方法数量count也已经是5了,是SSLPerson中的3个,和SSLPerson+SSL1中的2个。方法已经存在了,说明是通过data()加载的,已经被编译器处理了。

分类没有  +  主类没有

分类和主类都没有实现load时,运行程序:

image.png

  • 之前的函数都没有调用,会推迟到第一次消息的发送,接下来消息的第一次发送,断点继续调试。

断点进入到realizeClassWithoutSwift

image.png image.png

  • 可以看到count也变成5了,也是通过data()加载的

多个分类不全有  +  主类有

SSLPerson+SSL2SSLPerson+SSL3重新添加回来,让SSLPerson+SSL3不去实现load,运行程序。

断点进入到了load_categories_nolock函数:

image.png

  • 进入到了load_categories_nolock函数,count = 3,非懒加载的相关函数也被调用了。说明这种情况和分类主类都实现load时是一样的,最后都是通过attachCategories来实现分类的加载的。

  • 函数调用也是一样的:

    image.png

七、methodList -> 数据结构

attachCategories -> methods.attachLists这种加载分类的方式,attachLists函数中是方法列表加载的核心代码。

methodList中存储的是method_list_t的数组指针,接下来分析一下method_list_t的数据结构。

method_list_t 结构分析

断点进入attachCategories,走到methods.attachLists调用前的位置:

image.png

  • 根据打印结果,每个分类中取出的方法列表是在method_list_t中取得,通过get方法获取,取出来的是method_t类型。

看一下method_list_tmethod_t的数据结构:

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
    ...
}

template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    // 计算方法大小
    uint32_t entsize() const {
        return entsizeAndFlags & ~FlagMask;
    }
    Element& getOrEnd(uint32_t i) const { 
        ASSERT(i <= count);
        // 平移操作
        return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
    }
    Element& `get`方法(uint32_t i) const { 
        ASSERT(i < count);
        return getOrEnd(i);
    }
}

struct method_t {
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    ...
}
  • 可以看到method_list_t继承于entsize_list_tt,获取方法的代码是在entsize_list_tt中实现的。
  • get方法的调用,内部是调用了getOrEnd方法简化一下:this + sizeof(*this) + i*entsize()方法。
  • getOrEnd方法简化一下:this + sizeof(*this) + i*entsize()
    1. 先从结构体头平移到结构体尾。
    2. 通过entsize()计算出方法的大小。
    3. 再通过平移i个方法的大小,就获取到相应的方法了。

通过lldb打印验证:

image.png

  • method_list_t的地址是0x0000000100008030method_list_t的大小是8
  • 0method_t的地址是0x0000000100008038,和method_list_t的地址正好相差8,证明了我们上面的说法。

methodList 图例

image.png

八、分类加载 是否需要排序

通过methods.attachLists进行加载的类,在调用时是否进行排序了呢,在 OC 原理探索:方法的慢速查找流程 中我们分析过二分查找,今天通过它来进行探索。

getMethodNoSuper_nolock 源码分析

创建下面这些类:

image.png

  • 主类有namesetNamesay1三个方法,分类都实现了say1say2方法。

main.m中调用say1方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SSLPerson *person = [SSLPerson alloc];
        [person say1];
    }   
    return 0;
}

运行程序,断点进入二分查找函数getMethodNoSuper_nolock

image.png

  • 可以看到count = 2,是分类方法的数量,说明方法列表的结构跟上面methods.attachLists结束时一样的。
  • 所以方法列表并没有进行排序,如果方法列表进行排序所有的方法应该都整合在一个数组里,count应该是3+2+2+2 = 9

但是这里有一个矛盾的点,什么矛盾的点呢,下面继续看。

findMethodInSortedMethodList 源码分析

通过getMethodNoSuper_nolock -> search_method_list_inline 进入 findMethodInSortedMethodList函数。

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    auto first = list->begin();
    auto base = first;
    decltype(first) probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;

    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);

        uintptr_t probeValue = (uintptr_t)getName(probe);

        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    return nil;
}
  • 看源码,当keyValue == probeValue时,会有一个while循环继续向前寻找的操作,这明明又是说明对方法列表进行了组合排序,这是为什么呢。
  • 因为分类的加载除了attachCategories的方式,还有一种data()加载的方式,data()加载的方法列表只有一个列表,并且是排好序的,那么data()中的ro是怎么加载的呢。

九、class_ro_t 数据加载

ro的数据加载,是编译阶段就完成了,所以我们用llvm源码来进行分析。

class_ro_t 在 llvm 中的数据结构

打开llvm源码,全局搜索class_ro_t

image.png

  • 可以看到llvm中的class_ro_t,和objc中的class_ro_t成员变量是一样的,只是llvm中多了m_的前缀。
  • 我们注意到有个重要函数Read,这个函数中应该就是从MachO中读取了数据,接下来继续分析。

Read 函数

全局搜索class_ro_t::Read,得到结果:

image.png

  • 源码还是比较清晰的,先计算class大小得到sizeprocess通过sizeaddr读取内存数据。
  • extractor将所有数据进行包装计算,把计算好的结果赋值给m_flagsm_instanceStart等成员变量。

我们对Read函数的实现基本了解了,那么它又是在哪儿里被调用的呢,全局搜索ro->Read(

image.png

  • OK!!,成功找到了Read函数的调用。

有问题可以评论区多多交流,点个赞支持一下吧!!😄😄😄