类的加载主要分如下几个阶段阶段:1、类从镜像文件映射到内存中,并存储到类表;2、类结构初始化,包括rw,ro等的setup;3、加载类的properties、methedList、protocols、categorys。
那类是在程序运行的哪个时机加载的?是如何加载的?类的相关方法、属性、协议、还有类别等都是什么时候加载的呢?
类的映射
类的映射时发生在程序启动过程中的。dyld启动工程中会调用libobjc库的objc_init
初始化这个库,此时函数objc_init除了做一些初始工作之外还会向dyld注册监听函数,以便在镜像文件映射、取消映射和初始化objc时调用。这其中就有map_images函数,进行镜像文件映射时会调用map_images函数进行OC类的映射。通过源码分析类的映射函数调用流程大概是这样的:
objc_init
->map_images
-> map_images_nolock
-> _read_images
-> addNamedClass(cls, mangledName, replacing)
-> addClassTableEntry(cls)
。
这其中我们逐个进行讲解。
objc_init源码解析
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();// static_init() Run C++ static constructor functions. libc calls _objc_init() before dyld would call our static constructors, so we have to do it ourselves.
runtime_init(); // 类表allocatedClasses.init()和unattachedCategories.init(32)看似跟category相关的表
exception_init();//异常处理回调函数初始化
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
源码解读:
environ_init()
读取影响运行时的环境变量。如果需要,还可以打印环境变量帮助。
tls_init()
关于线程key的绑定 - 比如每条线程数据的析构函数。
static_init()
有注释可知运行C++静态析构函数。在dyld调用我们的静态构造函数之前,libc会调用_objc_init(), 因此我们必须自己调用。
runtime_init()
类表allocatedClasses.init()和unattachedCategories.init(32)看似跟category相关的表,这个在后面类的
imp_implementationWithBlock_init(void)
启动回调机制。通常不会这么做。因为所有的初始话都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib。
_dyld_objc_notify_register
向dyld注册监听函数。以便在镜像文件映射、取消映射和初始化objc时调用。Dyld将使用包含objc-image-info的镜像文件的数组回调给map_images函数。
runtime_init()
运行时环境初始化,里面主要是类表allocatedClasses 和unattachedCategories
exception_init()
初始化ibobjc的异常处理系统
map_images源码解析
函数map_images是在dyld进行镜像文件映射时调用的回调函数,dyld将使用包含objc-image-info的镜像文件的数组回调给map_images函数,然后调用map_images_nolock完成接下来的事情:
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
map_images_nolock源码解析
主要完成如下几件事情:
- 读取镜像文件
通过函数addHeader读取镜像文件(mach-o)获取文件头部信息列表,获取含有Objective-C元数据(metadata)的镜像文件列表hList和个数hCount,同时统计类的个数totalClasses。
const headerType *mhdr = (const headerType *)mhdrs[i];
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
hList[hCount++] = hi;
- 判断是否第一次调用,做一些初始化工作
// Perform one-time runtime initialization that must be deferred until
// the executable itself is found. This needs to be done before
// further initialization.
// (The executable may not be present in this infoList if the
// executable does not contain Objective-C code but Objective-C
// is dynamically loaded later.
if (firstTime) {
sel_init(selrefCount);
arr_init();
.......
}
- 将第一步获取的hCount、hList[]和totalClasses传递给_read_images完成类的映射。
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
read_images源码解析
read_images是加载类信息的主要函数,看其函数声明:
/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
由于实现代码比较多,这里根据其注释整理了它的主要做的功能:
1、条件控制进行一次的加载,如果是第一次加载会创建一个类名映射表
gdb_objc_realized_classes
;
2、Fix up @selector references.修复预编译阶段的@selector
的混乱问题,比如地址偏移不一致等;
3、Discover classes. Fix up unresolved future classes. Mark bundle classes. 处理错误混乱的类;
4、读取类表,调用readClass
添加类名映射;
5、Fix up remapped classes.修复重映射一些没有被镜像文件加载进来的类;
6、 Fix up old objc_msgSend_fixup call sites.修复一些消息;
7、Discover protocols. Fix up protocol refs.当我们类里面有的时候:readProtocol;
8、Fix up @protocol references.修复没有被加载的协议;
9、Discover categories. Only do this after the initial category attachment has been done. 分类处理;
10、Realize non-lazy classes (for +load methods and static instances).懒加载类的加载处理;
11、Realize newly-resolved future classes, in case CF manipulates them.没有被处理的类,优化那些被非法操作的类。
这其中这里要讲的是1、3、6、8和9。其中6、8和9会在类的初始化过过程中分析。
- gdb_objc_realized_classes的创建
在程序第一次进来的时候会根据传入的类的总数totalClasses创建一个表gdb_objc_realized_classes
,gdb_objc_realized_classes
将存储所有类和类名的映射,不管这个类是否实现。
// namedClasses
// Preoptimized classes don't go in this table.
// 4/3 is NXMapTable's load factor
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
- 读取类表_getObjc2ClassList
第4步中中,根据传入的header_info **hList 通过_getObjc2ClassList
遍历读取出OC类列表,然后把一个个Class交给readClass
函数来处理:
for (EACH_HEADER) {
if (! mustReadClasses(hi, hasDyldRoots)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
classref_t const *classlist = _getObjc2ClassList(hi, &count);
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
if (newCls != cls && newCls) {
// Class was moved but not deleted. Currently this occurs
// only when the new class resolved a future class.
// Non-lazily realize the class below.
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
}
- readClass源码解析
这一步主要读取类名,并通过函数addNamedClass将类名和类进行映射,写入,然后调用addClassTableEntry把类添加到类表里头。
addNamedClass(cls, mangledName, replacing);
} else {
Class meta = cls->ISA();
const class_ro_t *metaRO = meta->bits.safe_ro();
ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass.");
ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class.");
}
addClassTableEntry(cls);
- addNamedClass源码解析
函数addNamedClass中实际上就是把类和类名通过NXMapInsert方法写入到我们之前提到的第一次进来时创建的类名映射表gdb_objc_realized_classes
里面:
/***********************************************************************
* addNamedClass
* Adds name => cls to the named non-meta class map.
* Warns about duplicate class names and keeps the old mapping.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
runtimeLock.assertLocked();
Class old;
if ((old = getClassExceptSomeSwift(name)) && old != replacing) {
inform_duplicate(name, old, cls);
// getMaybeUnrealizedNonMetaClass uses name lookups.
// Classes not found by name lookup must be in the
// secondary meta->nonmeta table.
addNonMetaClass(cls);
} else {
NXMapInsert(gdb_objc_realized_classes, name, cls);
}
ASSERT(!(cls->data()->flags & RO_META));
// wrong: constructed classes are already realized when they get here
// ASSERT(!cls->isRealized());
}
- addClassTableEntry源码解析
函数addClassTableEntry实际上是将类注册到所有类的类表allocatedClasses
中。这个表不止有类,还包含元类。这个表的初始化是在前面提到的_objc_init
中调用runtime_init
完成的。
/***********************************************************************
* addClassTableEntry
* Add a class to the table of all classes. If addMeta is true,
* automatically adds the metaclass of the class as well.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void
addClassTableEntry(Class cls, bool addMeta = true)
{
runtimeLock.assertLocked();
// This class is allowed to be a known class via the shared cache or via
// data segments, but it is not allowed to be in the dynamic table already.
auto &set = objc::allocatedClasses.get();
ASSERT(set.find(cls) == set.end());
if (!isKnownClass(cls))
set.insert(cls);
if (addMeta)
addClassTableEntry(cls->ISA(), false);
}
至此加载结类就都缓存到了类表里面了。类存储到类表之后就开始进行类的初始化。
类的初始化
类的初始化是在函数realizeClassWithoutSwift中实现的。在_readImages完成类的映射之后,接着就调用了realizeClassWithoutSwift进行初始化:
// +load handled by prepare_load_methods()
// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
classref_t const *classlist = hi->nlclslist(&count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
addClassTableEntry(cls);
if (cls->isSwiftStable()) {
if (cls->swiftMetadataInitializer()) {
_objc_fatal("Swift class %s with a metadata initializer "
"is not allowed to be non-lazy",
cls->nameForLogging());
}
// fixme also disallow relocatable classes
// We can't disallow all Swift classes because of
// classes like Swift.__EmptyArrayStorage
}
realizeClassWithoutSwift(cls, nil);
}
}
在这段代码中我们发现它是通过读取nlclslist(&count)表来获取classlist的
classref_t const *classlist = hi->nlclslist(&count);
这与我们之前读取_getObjc2ClassList表不一样。这又是为什么呢?通过上面的注释“Realize non-lazy classes (for +load methods and static instances)”我们发现原来这里面实现的是non-lazy classes,翻译过来就是懒加载类。那什么是懒加载类?注释里也说了,就是实现了+load方法或者静态实例变量。这里我们就明白了,这里的nlclslist其实读取的是懒加载类的表。有了懒加载类就有非懒加载类,那根据注释的意思,以及我们对realizeClassWithoutSwift的调用流程的分析,我们对懒加载类和非懒加载类做了如下总结:
1、为什么要分为懒加载类和非懒加载类?
前面已经完成了类的映射和注册,接下来是类的初始化了。类的初始化实际上就是生成类对象,并设置类结构的各个属性。那这里就有一个问题,初始化类对象势必开辟一定的内存空间,在一个比较大的项目中,如果所有类都在启动时初始化,那一定会占用不少内存空间,而且在程序运行的某一个时间内特别是启动阶段,我们并不一定需要访问所有的类,因此如果在启动阶段就初始话所有的类那必将造成性能损耗,同时也浪费大量内存。因此,类的初始话时机非常重要。那类都在什么时候初始话呢?当然是在需要的时候初始化比较好。因此,根据类的初始化时机,我们把类分成懒加载类和非懒加载类。非懒加载类是在程序启动的时候初始化,也就是在map_images时期初始化;懒加载类是在程序运行之后根据需要初始化的,一般来说是在第一次访问类的时候会判断类有没有实现,没有的话再进行初始化。
2、懒加载类和非懒加载类分别的初始化流程是什么样的呢?
非懒加载类:前面我们知道,非懒加载类是在app启动时完成初始化的。而且前面页说到_dyld_objc_notify_register
像dyld注册了三个函数,其中就有,map_images在镜像文件映射时调用,_load_images则是初始化完成之后并调用类和category的+load方法,要调用类或者category的+load方法,那类或者category所属的类就得在这之前被初始化,因此非懒加载类会在map_images中,在_load_images函数之前被初始化。其函数流程如下:
map_images
->map_images_nolock
->_read_images
->readClass
->realizeClassWithoutSwift
懒加载类:未实现+load方法,在第一次向类发送消息时被动调用realizeClassWithoutSwift以实现类的初始化。其调用流程如下:
lookUpImpOrForward
->realizeAndInitializeIfNeeded_locked
->realizeClassMaybeSwiftAndLeaveLocked
->realizeClassMaybeSwiftMaybeRelock
->realizeClassWithoutSwift
消息发送流程中,只有相应的类初始化才可能有值,才可能存在方法列表。所以消息发送时要先检查类是否已经被初始化了。
realizeClassWithoutSwift源码解析
虽然类的初始化时机不一样,但是不管在哪个时机,初始化过程都是通过调用函数realizeClassWithoutSwift进行实现的:
/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)
关于它初始话的内容,我们必须得先了解类的底层结构。根据这个函数的注释,它是第一次对类执行初始化,不包括swift相关的初始化。接下来通过源码分析realizeClassWithoutSwift的主要功能:
1、创建并设置class_rw_t;
2、初始化缓存;
3、类和元类的实现;
4、设置nonpointer标记;
5、设置父类和元类;
6、设置C++构造和析构函数.cxx_construct/destruct
7、如果有父类则添加到父类的子类表里;如果没有父类,则设置为根类rootClass;
8、调用methodizeClass完成方法、属性、协议等的加载。
接下来主要介绍1、3和8。其他源码比较简单,也有相应的注释。
copy class_ro_t 到 class_rw_t
class_rw_t *rw;
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}
因为大部分的类,包括我们自定义的类都不是 future class,所以这里只分析else部分的代码。当我们把类加载到内存中时,会创建一个Class,同时会将编译时确定的类相关的数据比如说成员变量(ivars)、属性(properties)、方法(methodList)、协议(protocols)等数据都存在class_ro_t结构当中,并在把class_ro_t的指针objc_class存储到类的bits属性中。因此我们可以看到代码中通过cls->data()把原始的class_ro_t(ro)读取出来,然后创建一个class_rw_t (rw),然后把ro复制到rw中,最后把rw关联到class里面,同时设置初始化状态flag。这其中class_ro_t、class_rw_t和Class的关系都可以在类的底层结构里面看到。
父类、元类的初始化
函数realizeClassWithoutSwift中不止当前类初始化,还对父类、元类递归初始化,确立好类的继承链和isa指向:
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
// This assumes that none of those classes have Swift contents,
// or that Swift's initializers have already been called.
// fixme that assumption will be wrong if we add support
// for ObjC subclasses of Swift classes.
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
methodizeClass源码解析
最后realizeClassWithoutSwift会调用methodizeClass函数完成methods、properties、protocols和category等的加载。接下来源码分析:
/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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);
}
// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
}
// 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);
#if DEBUG
// Debug: sanity-check all SELs; log method list contents
for (const auto& meth : rw->methods()) {
if (PrintConnecting) {
_objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-',
cls->nameForLogging(), sel_getName(meth.name()));
}
ASSERT(sel_registerName(sel_getName(meth.name())) == meth.name());
}
#endif
}
加载方法列表(methods)
在类的初始化过程中加载方法列表相对重要,也相对复杂。首先它会从ro中读取编译时确定的方法列表baseMethodList:
// 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);
}
读取baseMethods之后会调用prepareMethodLists,然后baseMethods作为一个元素加入到数组addedLists中。这里也可以看出方法列表底层其实是个二维数组。
// Add method lists to array.
// Reallocate un-fixed method lists.
// The new methods are PREPENDED to the method list array.
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[i];
ASSERT(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
}
然后通过fixupMethodList对方法列表进行排序:
static void
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
runtimeLock.assertLocked();
ASSERT(!mlist->isFixedUp());
// fixme lock less in attachMethodLists ?
// dyld3 may have already uniqued, but not sorted, the list
if (!mlist->isUniqued()) {
mutex_locker_t lock(selLock);
// Unique selectors in list.
for (auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
meth.setName(sel_registerNameNoLock(name, bundleCopy));
}
}
// Sort by selector address.
// Don't try to sort small lists, as they're immutable.
// Don't try to sort big lists of nonstandard size, as stable_sort
// won't copy the entries properly.
if (sort && !mlist->isSmallList() && mlist->entsize() == method_t::bigSize) {
method_t::SortBySELAddress sorter;
std::stable_sort(&mlist->begin()->big(), &mlist->end()->big(), sorter);
}
// Mark method list as uniqued and sorted.
// Can't mark small lists, since they're immutable.
if (!mlist->isSmallList()) {
mlist->setFixedUp();
}
}
可以看出方法排序是按SEL地址大小进行排序的,这也是为什么我们在方法查找的时候遍历方法列表时采用二分法查找的原因。
加载属性列表(baseProperties)和协议列表(baseProtocols)
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的实际结构是class_rw_ext_t,这个rwe是在需要运行时修改类结构时才会被创建的。在_map_images基本不会创建(点击了解class_rw_ext_t可多的信息)。而rwe是有可能在load_images阶段创建,下面会有介绍。
加载类别(categories)
说到类Category加载,相对来说比较复杂。首先Category是类的扩展,所以它加载必然跟类有关,接下来我们先看Category的底层结构category_t:
struct category_t {
const char *name;
classref_t cls;
WrappedPtr instanceMethods;
WrappedPtr 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不仅包含Class指针,还有很多信息,包括方法列表、属性列表等。这些方法在加载的时候都是要合并到类的方法列表里面去的。那Category是什么时候加载的呢?其实Category的也跟Class一样,有懒加载和非懒加载。Category的懒加载和非懒加载根类的懒加载和非懒加载有一定的关系,经过源码运行反复调试,总结了他们之间如下关系:
1、当类和它的Category都实现+load方法,类和category(如果有多个category,只要有一个category实现+load,其他category是非懒加载),是非懒加载的。
2、当类实现+load,Category没有实现+load, 是非懒加载的。
3、当类没有实现+load,Category实现+load,也是非懒加载的。
4、当类和它的Category都没有实现+load方法,是懒加载的。
那么这四种情况的category什么时候被加载的呢?我们在前面提到过,_objc_init会像dlyd注册几个函数,其中就有一个load_images,这个函数是在镜像文件初始化完成之后调用的。第1种情况是类在类初始化阶段(_map_images)被加载,它的Category会在load_images阶段被加载。而另外2、3种情况的Category会在_map_images阶段类加载被读取到class_ro_t中。而第4种情况则会在类第一次被访问的时候跟类一起被加载。接下来我们通过源码调试验证一下。
1、当类和它的Category都实现+load方法
- 调试demo
首先我们创建一个demo在源码调试,在demo中创建一个MyObject和它的一个category(addition),demo如下:
然后运行源码工程。在_map_images流程的methodizeClass函数中有如下一段代码:
// 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到attachCategories,在attachCategories里面实现把Category加载到类结构中。所以我们在attachCategories函数中专门为MyObject类打了个断点。通过断点调试发现这个流程根本不是从_map_images走到attachCategories,而真正调用attachCategories时在load_images这个阶段,一下是源码调试阶段:
这样Category在启动流程的加载时机我们就找到了。其流程就是
load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
。
这一流程只是针对类和分类实现了+load方法的情况,当然在这个流程之前Category的类作为非懒加载类已经在_map_images阶段实现了加载。类加载完成之后在加载分类。加载分类并关联到类是在函数attachCategories中实现的。
- attachCategories源码解析
接下来在attachCategories源码查看Category加载的流程:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
const char *mangledName = cls->mangledName();
if (strcmp("MyObject", mangledName) == 0) {
if(!cls->isMetaClass()){//避免元类的干扰
printf("我来了 MyObject");//MyObject是一个实现了+load方法的类,他还有一个category名字叫addtion也实现了+load方法,
}
}
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)" : "");
}
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
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();
}
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) {
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);
}
首先会先创建一个class_rw_ext_t结构变量rwe:
auto rwe = cls->data()->extAllocIfNeeded();
rwe前面有提到过。然后遍历Category列表,因为可能是多个,然后把每个category的方法列表、属性列表、协议类表等都分别添加到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, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
......
}
首先通过函数methodsForMeta根据是否是元类获取实例方法或者类方法列表。然后通过prepareMethodLists先对方法进行一波排序。接着调用attachLists函数,把方法列表添加到rwe的methods中。接下来看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 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();
}
}
这里我们可以看到methods是个二维数组。而且新增的category方法列表总是被放在最前面。当有新列表进来时,如果没有旧的方法列表则直接赋值,如果有旧的方法列表,会重新计算数组大小重新创建一个新数组,然后把旧的数据移到后面,新的加到前面。这样新的方法总是能够在最前面。这也是为什么方法查找时和类的方法同名时总是先调用category方法的原因。
2、当类和它的Category其中之一实现+load方法
这时候类是非懒加载类,会在启动期间被加载。然后category在类初始化阶段被加载到class_ro_t中。接下来源码调试。首先创建一个类MyObject,并且创建两个Category为CatA和CatB,然后分别实现两个方法,然后我们针对当前类的初始化的流程上打断点,观察方法列表,证明方法列表加载到class_ro_t中。demo如下:
接下来运行程序,定位到MyObject读取方法列表的代码:
根据调试结果证明这两种情况的类确实是非懒加载类,启动时会加载,同时也会加载其对应的category。
3、当类和它的Category都没实现+load方法
在刚才的demo上去掉+load方法,这样类就不会在启动阶段初始化了。接着我们在启动之后去调用类的方法:
总结
类加载过程比较复杂。类加载又分为懒加载和非懒加载。跟+load方法有关。实现不管是分类和本类实现+load方法,他都是非懒加载类。会在启动阶段被初始化,而非懒加载类则会在第一次被访问的时候调用。因此在这里面+load方法在使用上的特别注意,不能用的太多,会影响启动速度(点击了解app启动优化)。类的加载逻辑跟类的底层结构密切相关,想了解加载过程首先得了解类的底层结构(点击了解类的底层结构)和APP的启动流程(点击了解APP的启动流程);
备注:本文调试的源码是objc4-818.2版本的;