iOS底层-类的加载(下)

451 阅读4分钟

本文我们研究分类的加载流程。

分类的本质

在研究对象的本质的时候,我们都用clang命令将main.m转换成main.cpp文件查看其本质,分类也不例外,我们使用相同的方法分析。

首先我们定义一个JSPerson的分类:

@interface JSPerson (TestLoad)
​
- (void)sayCategory;
​
@end
@implementation JSPerson (TestLoad)
​
- (void)sayCategory{
    NSLog(@"JSPersonCategpry say : %s",__func__);
}
​
+ (void)load{
    NSLog(@"JSPersonCategpry load");
}
@end

然后在main.m文件中使用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JSPerson *p = [JSPerson alloc];
        [p sayCategory];
        NSLog(@"Hello, World!");
    }
}

我们执行:

clang -rewrite-objc main.m -o main.cpp

我们打开main.cpp文件,找到分类相关的代码。

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
  &_OBJC_$_CATEGORY_JSPerson_$_TestLoad,
};
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;
};

我们从源码里也能搜索到它的定义:

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;
    }
};

可以看到分类的本质是结构体category_t

我们继续回到_read_images函数探索。

rwe的赋值

auto rwe = cls->data()->extAllocIfNeeded();

extAllocIfNeeded方法的实现:

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));
      }
}
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);
    }
​
    // See comments in objc_duplicateClass
    // property lists and protocol lists historically
    // have not been deep-copied
    //
    // This is probably wrong and ought to be fixed some day
    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;
}

auto rwe = cls->data()->extAllocIfNeeded();是进行rwe的创建,那么为什么要在这里进行rwe的初始化??因为我们现在要做一件事:往本类添加属性、方法、协议等,即对原来的 clean memory要进行处理了

  • 进入extAllocIfNeeded方法的源码实现,判断rwe是否存在,如果存在则直接获取,如果不存在则开辟
  • 进入extAlloc源码实现,即对rwe 0-1的过程,在此过程中,就将本类的data数据加载进去了
  • 其中关键代码是rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);即存入mlists的末尾,mlists的数据来源前面的for循环
  • 在调试运行时,发现category_t中的name编译时是JSPerson(参考clang编译时的那么),运行时是TestLoad即分类的名字
  • 代码mlists[ATTACH_BUFSIZ - ++mcount] = mlist;,经过调试发现此时的mcount等于1,即可以理解为 倒序插入,64的原因是允许容纳64个(最多64个分类)

小结

本类 中 需要添加属性、方法等,所以需要初始化rwe,rwe的初始化主要涉及:分类、addMethod、addProperty、addprotocol , 即对原始类进行修改或者处理时(运行时),才会进行rwe的初始化

attachCategories

通过attachCategories反推分类的加载,我们libobjc源码全局搜索attachCategories的调用,发现有两个地方调用:attachToClassload_categories_nolock

attachToClass

调用attachToClass的方法只有一个methodizeClass

static void methodizeClass(Class cls, Class previously)
{
    ///省略代码
    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执行条件,我们依次网上查找调用链,发现previously==nil,previously参数只是方便动态化调试,所以实际调用的只有下面这一处代码:

objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

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 (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category ???(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }
​
            // Process this category.
            if (cls->isStubClass()) {
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }
​
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };
​
    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

全局搜素load_categories_nolock,发现调用load_categories_nolock的地方有两处

  • loadAllCategories

  • _read_images

    但是经过调试发现,是不会走_read_images方法中的if流程的,而是走的loadAllCategories方法中的。

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

这个函数一共三部分我们分别看:

else if (!list  &&  addedCount == 1) {
      // 0 lists -> 1 list
      list = addedLists[0];
       validate();
 } 

addedLists[0]赋值给list,list是一维数组。

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

这个情况是list不为空,新建一个扩容的数组,将之前的数据放在lists[addedCount]位置,新元素放到之前list的前面。

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

这段其实和上次类似,依然将新数组插入到前面,新数组中的新元素在数组首部。

分类加载的四种情况

根据分类是否实现+load()方法分为四种情况。

我们先定义JSPerson及它的分类

@interface JSPerson : NSObject
​
- (void)sayHello;
​
@end
#import "JSPerson.h"@implementation JSPerson
​
- (void)sayHello{
​
    NSLog(@"JSPerson say : Hello!!!");
}
​
+ (void)load{}
​
@end
@interface JSPerson (Test)
​
- (void)saySomething;
​
@end
#import "JSPerson+Test.h"@implementation JSPerson (Test)
​
- (void)saySomething{
    NSLog(@"%s",__func__);
}
​
+ (void)load{}
​
@end

非懒加载分类和非懒加载类

我们在JSPerson类和分类中都实现load,我们在realizeClassWithoutSwift添加断点

类和分类都是非懒加载分类加载.jpg

我们用lldb打印当前ro的方法

readClass JSPerson....
_read_images JSPerson....
(lldb) p ro
(const class_ro_t *) $0 = 0x0000000100004280
(lldb) p *$0
(const class_ro_t) $1 = {
  flags = 0
  instanceStart = 8
  instanceSize = 8
  reserved = 0
   = {
    ivarLayout = 0x0000000000000000
    nonMetaclass = nil
  }
  name = {
    std::__1::atomic<const char *> = "JSPerson" {
      Value = 0x0000000100003bbc "JSPerson"
    }
  }
  baseMethodList = 0x00000001000042c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x00000001000042c8
(lldb) p *$2
(method_list_t) $3 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
  name = "sayHello"
  types = 0x0000000100003cfd "v16@0:8"
  imp = 0x00000001000036b0 (KCObjcBuild`-[JSPerson sayHello])
}

发现此时只有的方法,并没有分类的方法,说明分类目前还没有加载。

我们在attachCategories添加断点,继续执行代码:

非懒加载分类.jpg

继续用lldb调试程序:

(lldb) p mlist
(method_list_t *) $5 = 0x0000000100004420
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $6.get(0).big()
(method_t::big) $7 = {
  name = "saySomething"
  types = 0x0000000100003cfd "v16@0:8"
  imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) 

说明现在分类被加载了,被加载到了rwe中:

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

注意:ATTACH_BUFSIZ=64也就是说分类的方法个数不能大于64。

非懒加载分类和懒加载类

我们删除JSPerson类的load方法,重新运行程序。

类和分类都是非懒加载分类加载.jpg

继续在我们第一个断点位置使用lldb调试:

_read_images JSPerson....
(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000042a8
(lldb) p *$0
(const class_ro_t) $1 = {
  flags = 0
  instanceStart = 8
  instanceSize = 8
  reserved = 0
   = {
    ivarLayout = 0x0000000000000000
    nonMetaclass = nil
  }
  name = {
    std::__1::atomic<const char *> = "JSPerson" {
      Value = 0x0000000100003bbc "JSPerson"
    }
  }
  baseMethodList = 0x0000000100004178
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x0000000100004178
(lldb) p *$2
(method_list_t) $3 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 2)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
  name = "saySomething"
  types = 0x0000000100003cfd "v16@0:8"
  imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) p $3.get(1).big()
(method_t::big) $5 = {
  name = "sayHello"
  types = 0x0000000100003cfd "v16@0:8"
  imp = 0x00000001000036b0 (KCObjcBuild`-[JSPerson sayHello])
}
(lldb) 

发现分类已经加载了,也加载了,说明非懒加载类会使懒加载的类在启动时提前加载(如果没有分类是第一次调用时加载),说明加载的时机是编译期

懒加载分类和非懒加载类

我们删除JSPerson分类的load方法,重新运行程序。

类和分类都是非懒加载分类加载.jpg

使用lldb调试:

(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000042a8
(lldb) p *$0
(const class_ro_t) $1 = {
  flags = 0
  instanceStart = 8
  instanceSize = 8
  reserved = 0
   = {
    ivarLayout = 0x0000000000000000
    nonMetaclass = nil
  }
  name = {
    std::__1::atomic<const char *> = "JSPerson" {
      Value = 0x0000000100003bbc "JSPerson"
    }
  }
  baseMethodList = 0x0000000100004158
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x0000000100004158
(lldb) p *$2
(method_list_t) $3 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 2)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
  name = "saySomething"
  types = 0x0000000100003cfd "v16@0:8"
  imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) p $3.get(1).big()
(method_t::big) $5 = {
  name = "sayHello"
  types = 0x0000000100003cfd "v16@0:8"
  imp = 0x00000001000036c0 (KCObjcBuild`-[JSPerson sayHello])
}
(lldb) 

发现分类都已经加载了,说明加载的时机也是编译期

懒加载分类和懒加载类

我们把分类load方法都删除,重新运行程序

懒加载分类第一次调用

依然走到断点,注意观察左边的调用栈,发现是从lookUpImpOrForward,说明是在第一次调用方法的时候加载的。

One More Condition

前面四种情况基本能包括了分类,但是还有种情况就是:有多个分类,部分分类实现了load方法主类也实现了load。我们就探索一下这个情况,新建一个JSPerson的分类

@interface JSPerson (Test2)
​
- (void)saySomething2;
​
@end
@implementation JSPerson (Test2)
​
- (void)saySomething2{
    NSLog(@"%s",__func__);
}
​
@end

根据前面我们其实应清楚,就是实现load的分类肯定会在运行时加载,我们关注的点就在于没有实现load方法的分类是什么时候加载的呢也就是attachCategories是否会加载未实现load方法的分类,运行程序

多个分类加载.jpg

使用lldb调试:

lldb) p cat
(category_t *) $1 = 0x0000000100004448
(lldb) p *$1
(category_t) $2 = {
  name = 0x0000000100003b8f "Test2"
  cls = 0x00000001000049a0
  instanceMethods = {
    ptr = 0x0000000100004428
  }
  classMethods = {
    ptr = 0x0000000000000000
  }
  protocols = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  _classProperties = 0x0000000000000000
}
operator() -JSPerson....
attachCategories -JSPerson....
(lldb) p cat
(category_t *) $3 = 0x00000001000044c8
(lldb) p *$3
(category_t) $4 = {
  name = 0x0000000100003b95 "Test"
  cls = 0x00000001000049a0
  instanceMethods = {
    ptr = 0x0000000100004488
  }
  classMethods = {
    ptr = 0x00000001000044a8
  }
  protocols = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  _classProperties = 0x0000000000000000
}

调试我们发现,两个分类都加载了,也就是只要有一个分类实现了load,其他分类都会在启动时加载。

总结

本篇主要是探索了分类的加载,主要分为5种情况

  • 非懒加载类和非懒加载分类:此时分类是在运行时,也就是程序启动的时候加载的。
  • 懒加载类和非懒加载分类:此时分类是在编译时加载
  • 非懒加载类和懒加载分类:此时分类也是在编译时加载
  • 懒加载类和懒加载分类:此时分类在第一次调用时加载。
  • 非懒加载类,多个分类,部分是非懒加载分类:此时所有分类都是在程序启动时加载。