iOS底层-17:类的加载之category

category

1、category的本质

今天我们先从category讲起,那到底什么是category,我们借助clang看一下它在底层的结构。
代码还是上一章的代码,添加分类:

@protocol LRPersonDelegate <NSObject>

- (void)delegateMethod;

@end


NS_ASSUME_NONNULL_BEGIN

@interface LRPerson (LR)

- (void)instanceMethod;
+ (void)classMethod;

@end


@implementation LRPerson (LR)

- (void)instanceMethod {
    NSLog(@"category----%s",__func__);
}
+ (void)classMethod {
    NSLog(@"category----%s",__func__);
}

@end

clang命令:clang -rewrite-objc main.m -o main.cpp

打开生成的.cpp文件,搜索LRPerson找到以下代码

这是一个_category_t的结构,里面有一个LRPerson的名字,还有两个_method_list_t:一个_CATEGORY_INSTANCE_METHODS_,一个_CATEGORY_CLASS_METHODS_

  • 搜索_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;
};

这是一个结构体,里面有6个变量
name : 类名
cls:属于哪个类
instance_methods:实例方法
class_methods:类方法
protocols:协议
properties:属性

  • 搜索_CATEGORY_INSTANCE_METHODS_

分类中实现的instanceMethod保存在这里,是一个method_t结构

struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
}

  • 搜索_CATEGORY_CLASS_METHODS_

    分类中实现的classMethod保存在这里,是一个method_t结构

综上所述,category在底层其实是一个_category_t结构体,有6个变量。

attachToClass

上一篇文章分析完了prepareMethodLists,这里我们接着分析attachToClass


通过断点可以看到LRPerson类走的是最下面的方法,不是元类 传入的参数为ATTACH_CLASS

  • attachToClass
void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }

我们发现LRPerson类根本就没有走if里面的方法。

那么分类是什么时候加载的呢?

category和类的加载一样,懒加载分类和非懒加载分类有一定的区别:

1.非懒加载类 + 非懒加载分类
load_images -> loadAllCategories() -> load_categories_nolock -> attachCategories -> prepareMethodLists -> attachLists

2.懒加载类 + 非懒加载分类
load_images -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories -> prepareMethodLists -> attachLists

3.非懒加载类 + 懒加载分类
直接写入mach-O

4.懒加载类 + 懒加载分类
直接写入mach-O

1.非懒加载类 + 非懒加载分类


@interface LRPerson : NSObject

@property (nonatomic,strong) NSString *name;
@property (nonatomic,strong) NSString *icon;

- (void)sayHello;
- (void)sayHappy;
- (void)instanceMethod;
+ (void)classMethod;

@end
@implementation LRPerson

//- (void)sayHello {
//    NSLog(@"%@",_cmd);
//}
- (void)sayHappy {
    NSLog(@"%s",__func__);
}
- (void)instanceMethod {
    NSLog(@"%s",__func__);
}
+ (void)classMethod {
    NSLog(@"%s",__func__);
}


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


@protocol LRPersonDelegate <NSObject>

- (void)delegateMethod;

@end


NS_ASSUME_NONNULL_BEGIN

@interface LRPerson (LR)

- (void)cateA;

@end

NS_ASSUME_NONNULL_END

@implementation LRPerson (LR)


- (void)instanceMethod {
    NSLog(@"category----%s",__func__);
}
+ (void)classMethod {
    NSLog(@"category----%s",__func__);
}
- (void)cateA{
    NSLog(@"category----%s",__func__);
}
+ (void)load {
    NSLog(@"category----%s",__func__);
}

@end
  • 进入methodizeClass断点

  • 打印list


    此时category还没有加载进来。

  • 断点进入attachToClass方法


    在这里打一个断点,LLDB调试

    itmap.end()相等,并不会走以下条件。

  • 进入attachCategories方法

在断点处打印堆栈:


从堆栈可以看出是从load_images -> loadAllCategories() -> load_categories_nolock -> attachCategories 进来的


for循环category数组开始处理method_list_t、property_list_t、protocol_list_t

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)" : "");
    }

 
    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();//rwe 初始化


    //开始处理category数据,为写到类中做准备
    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);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            //将mlist插入mlists数组的最后一位  mlists是一个二维数组,里面的元素还是method_list_t
            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) {
        //排序mlists里的元素method_list_t  从插入的最后位置到数组的最后位置
        //排序的是mlists数组里的元素数组
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        //attachLists把方法添加到列表里面
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

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

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

这里穿插一个小细节,对比下面两张图可以发现。编译时,category是以类名LRPerson命名的,运行时category名字变成了LR,并且cls已经跟我们的LRPerson关联起来了。

  • attachLists
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            //当下面第三步 setArray()之后,再有新的list要插入就要走这个条件
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));//将oldArray里面的元素移动到后面
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));//将newArray里面的元素添加到数组前面
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            // 一维数组
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            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;//将oldList放到array()的最后一个位置
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));//从array()->lists的0号位置,放入addedLists,放入的大小是 addedCount * sizeof(array()->lists[0],也就是把addedLists平移放入array()数组,从0号位置开始
        }
    }

category加入到rwe的流程是:
1.初始化rwe,给rwe赋值ro里的原始类数据
2.调用attachLists,创建一个list保存baseMethods
3.当有一个分类时,再次调用attachLists,走else方法。创建一个数组,将原来的baseMethods放到末尾,分类里的数据放到前面。
4.再有分类进来时,走if方法,创建新数组,将老数据放到末尾,新数据放前面。
5.还有分类的话,重复第四步。

extAllocIfNeeded

attachCategories有一个取rwe的方法extAllocIfNeeded。这里简单看一下源码

class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
    runtimeLock.assertLocked();

    auto rwe = objc::zalloc<class_rw_ext_t>();

    rwe->version = (ro->flags & RO_META) ? 7 : 0;

    method_list_t *list = ro->baseMethods();
    if (list) {
        if (deepCopy) list = list->duplicate();
        rwe->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }

    set_ro_or_rwe(rwe, ro);
    return rwe;
}

主要流程:
1.开辟一个空间给rwe
2.设置rwe的version
3.从ro中获取原始类的baseMethods、baseProperties、baseProtocols
4.设置set_ro_or_rwe

2.懒加载类 + 非懒加载分类
attachCategories打上断点,等程序进入断点后,bt打印堆栈信息。

  • bt


    从图上可以看出,attachCategories是在load_images之后调用的。

  • 搜索load_images,打上断点,重新运行程序

  • 发现走入prepare_load_methods

  • prepare_load_methods

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();
    //获取非懒加载的类列表
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    //为所有的非懒加载类和他的父类递归调用 +load方法
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
    //获取非懒加载的分类列表
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);//关联上类和category
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
  • prepare_load_methods中加入以下代码,打上断点
 //—— mark 2021-5-5
          const char *mangledName = cls->mangledName();
          const char *className = "LRPerson";
          if (strcmp(mangledName, className) == 0){
          printf("来了 \n");
          }
          //
  • 跟踪断点,进入realizeClassWithoutSwift
    realizeClassWithoutSwift之后,category后面的加载流程与第一种情况一致

  • 进入methodizeClass 准备好要插入的数据

  • 进入attachToClass

  • 进入attachCategories 获取method_list_t、property_list_t、protocol_list_t数据,rwe在这之前初始化

  • 进入prepareMethodLists 排序method_list_t

  • 进入attachLists 写入到rwe

3.非懒加载类 + 懒加载分类
attachCategories打上断点,等程序进入断点,再bt

然而结果出人意料,程序根本没有进入断点。然而我们在控制台,发现了一些信息,虽然没有进入attachCategories,但是他调用了我研究的其他三个方法。(这里有一些小失误,没有打印方法名)
我们在+load方法里打断点,再bt


通过堆栈我们发现,他并没有走与Category相关的方法。
category的加载是否是在read_images时候呢?

首先在read_imagesdoneOnce的时候,加上以下代码读取一下Mach-O文件里的数据,查看此时的baseMethods


运行程序,进入断点时,打印list

我们意外的发现,baseMethods里的方法已经有了category中重写的方法,而且他的总数count = 9,是我们LRPerson类和category (LR)的总和。因此我们猜测category的加载,可能是在编译期直接写入了mach-O文件。

4.懒加载类 + 懒加载分类
同样我们先查看mach-O文件的数据。


我们发现baseMethods里的方法已经有了category中重写的方法。

补充

前面只是举例了单个category,多个category有点不一样。
我们再新建一个LRPerson+LRB,重写instanceMethod`

- (void)instanceMethod {
    NSLog(@"category----%s",__func__);
}

attachCategories打断点然后bt

1.非懒加载类 + 非懒加载分类
read_images的时候LRB已经写入了mach-O


区别在于加载第二个categoryattachCategories里的打印信息为:

load_images -> loadAllCategories() -> load_categories_nolock -> attachCategories

2.懒加载类 + 非懒加载分类
read_images的时候LRB已经写入了mach-O


attachCategories里的打印信息为:

load_images -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories

3.非懒加载类 + 懒加载分类
read_images的时候,两个分类LRLRB已经写入了mach-O

(lldb) p *list
warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.
(method_list_t) $0 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 10
    first = {
      name = "instanceMethod"
      types = 0x0000000100000e55 "v16@0:8"
      imp = 0x0000000100000cc0 (KCObjc`-[LRPerson(LR) instanceMethod])
    }
  }
}
(lldb) p $0.get(0)
(method_t) $1 = {
  name = "instanceMethod"
  types = 0x0000000100000e55 "v16@0:8"
  imp = 0x0000000100000cc0 (KCObjc`-[LRPerson(LR) instanceMethod])
}
(lldb) p $0.get(1)
(method_t) $2 = {
  name = "cateA"
  types = 0x0000000100000e55 "v16@0:8"
  imp = 0x0000000100000cf0 (KCObjc`-[LRPerson(LR) cateA])
}
(lldb) p $0.get(2)
(method_t) $3 = {
  name = "instanceMethod"
  types = 0x0000000100000e55 "v16@0:8"
  imp = 0x0000000100000c60 (KCObjc`-[LRPerson(LRB) instanceMethod])
}
(lldb) p $0.get(3)
(method_t) $4 = {
  name = "sayHappy"
  types = 0x0000000100000e55 "v16@0:8"
  imp = 0x0000000100000b20 (KCObjc`-[LRPerson sayHappy] at LRPerson.m:15)
}
(lldb) p $0.get(4)
(method_t) $5 = {
  name = "instanceMethod"
  types = 0x0000000100000e55 "v16@0:8"
  imp = 0x0000000100000b50 (KCObjc`-[LRPerson instanceMethod] at LRPerson.m:18)
}
(lldb) p $0.get(5)
(method_t) $6 = {
  name = ".cxx_destruct"
  types = 0x0000000100000e55 "v16@0:8"
  imp = 0x0000000100000b80 (KCObjc`-[LRPerson .cxx_destruct] at LRPerson.m:10)
}
(lldb) p $0.get(6)
(method_t) $7 = {
  name = "name"
  types = 0x0000000100000e69 "@16@0:8"
  imp = 0x0000000100000bc0 (KCObjc`-[LRPerson name] at LRPerson.h:15)
}
(lldb) p $0.get(7)
(method_t) $8 = {
  name = "setName:"
  types = 0x0000000100000e71 "v24@0:8@16"
  imp = 0x0000000100000be0 (KCObjc`-[LRPerson setName:] at LRPerson.h:15)
}
(lldb) p $0.get(8)
(method_t) $9 = {
  name = "icon"
  types = 0x0000000100000e69 "@16@0:8"
  imp = 0x0000000100000c10 (KCObjc`-[LRPerson icon] at LRPerson.h:16)
}
(lldb) p $0.get(9)
(method_t) $10 = {
  name = "setIcon:"
  types = 0x0000000100000e71 "v24@0:8@16"
  imp = 0x0000000100000c30 (KCObjc`-[LRPerson setIcon:] at LRPerson.h:16)
}
(lldb) p $0.get(10)
Assertion failed: (i < count), function get, file /Users/liuyang/Desktop/可编译objc源码/runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb) 

4.懒加载类 + 懒加载分类
read_images的时候,两个分类LRLRB已经写入了mach-O

(lldb) p *list
warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.
(method_list_t) $0 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 10
    first = {
      name = "instanceMethod"
      types = 0x0000000100000e60 "v16@0:8"
      imp = 0x0000000100000cd0 (KCObjc`-[LRPerson(LR) instanceMethod])
    }
  }
}
(lldb) p $0.get(0)
(method_t) $1 = {
  name = "instanceMethod"
  types = 0x0000000100000e60 "v16@0:8"
  imp = 0x0000000100000cd0 (KCObjc`-[LRPerson(LR) instanceMethod])
}
(lldb) p $0.get(1)
(method_t) $2 = {
  name = "cateA"
  types = 0x0000000100000e60 "v16@0:8"
  imp = 0x0000000100000d00 (KCObjc`-[LRPerson(LR) cateA])
}
(lldb) p $0.get(2)
(method_t) $3 = {
  name = "instanceMethod"
  types = 0x0000000100000e60 "v16@0:8"
  imp = 0x0000000100000c70 (KCObjc`-[LRPerson(LRB) instanceMethod])
}
(lldb) p $0.get(3)
(method_t) $4 = {
  name = "sayHappy"
  types = 0x0000000100000e60 "v16@0:8"
  imp = 0x0000000100000b30 (KCObjc`-[LRPerson sayHappy] at LRPerson.m:15)
}
(lldb) p $0.get(4)
(method_t) $5 = {
  name = "instanceMethod"
  types = 0x0000000100000e60 "v16@0:8"
  imp = 0x0000000100000b60 (KCObjc`-[LRPerson instanceMethod] at LRPerson.m:18)
}
(lldb) p $0.get(5)
(method_t) $6 = {
  name = ".cxx_destruct"
  types = 0x0000000100000e60 "v16@0:8"
  imp = 0x0000000100000b90 (KCObjc`-[LRPerson .cxx_destruct] at LRPerson.m:10)
}
(lldb) p $0.get(6)
(method_t) $7 = {
  name = "name"
  types = 0x0000000100000e74 "@16@0:8"
  imp = 0x0000000100000bd0 (KCObjc`-[LRPerson name] at LRPerson.h:15)
}
(lldb) p $0.get(7)
(method_t) $8 = {
  name = "setName:"
  types = 0x0000000100000e7c "v24@0:8@16"
  imp = 0x0000000100000bf0 (KCObjc`-[LRPerson setName:] at LRPerson.h:15)
}
(lldb) p $0.get(8)
(method_t) $9 = {
  name = "icon"
  types = 0x0000000100000e74 "@16@0:8"
  imp = 0x0000000100000c20 (KCObjc`-[LRPerson icon] at LRPerson.h:16)
}
(lldb) p $0.get(9)
(method_t) $10 = {
  name = "setIcon:"
  types = 0x0000000100000e7c "v24@0:8@16"
  imp = 0x0000000100000c40 (KCObjc`-[LRPerson setIcon:] at LRPerson.h:16)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容