前言
在之前的文章iOS-类的加载(上),我们探究了类
是如何加载到内存
中以及懒加载类
和非懒加载类
,这篇文章下我们将探寻一下分类
的加载情况。
分类的本质
在main文件中新建一个ZGPerson
分类,
@interface ZGPerson (ZG)
@property (nonatomic, strong) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cateA_instanceMethod1;
- (void)cateA_instanceMethod3;
- (void)cateA_instanceMethod2;
- (void)cateA_classMethod3;
@end
@implementation ZGPerson (ZG)
+ (void)load{
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod2{
NSLog(@"%s",__func__);
}
- (void)cateA_classMethod3{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
ZGPerson *person = [ZGPerson alloc];
NSLog(@"%p",person);
}
return 0;
}
我们可以通过下面几种方式探寻分类本质
- 通过
clang
- 通过Xcode
文档
搜索Category
- 通过objc源码搜索
category_t
通过clang
通过终端命令clang -rewrite-objc main.m -o main.cpp
查看底层编译,即 生成main.cpp
,打开。
_category_t
可见,分类的本质是_category_t
通过Xcode文档搜索Category
可以通过Xcode文档搜索Category
,文档
通过objc源码搜索 category_t
在源码中搜索_category_t
发现以下定义
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *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;
}
};
其中
name
:分类的名字cls
:对应的原类instanceMethods
:实例方法列表classMethods
:类方法列表protocols
:协议列表instanceProperties
:实例属性列表
分类加载流程
首先,创建两个分类ZGPerson + ZGA
和ZGPerson + ZGB
@interface ZGPerson (ZGA)
@property (nonatomic, strong) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cateA_instanceMethod1;
- (void)cateA_instanceMethod3;
- (void)cateA_instanceMethod2;
- (void)cateA_classMethod3;
@end
@implementation ZGPerson (ZGA)
+ (void)load{
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod2{
NSLog(@"%s",__func__);
}
- (void)cateA_classMethod3{
NSLog(@"%s",__func__);
}
@end
ZGPerson (ZGB)
@interface ZGPerson (ZGB)
@property (nonatomic, strong) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cateA_instanceMethod1;
- (void)cateA_instanceMethod3;
- (void)cateA_instanceMethod2;
- (void)cateA_classMethod3;
@end
@implementation ZGPerson (ZGB)
+ (void)load{
}
- (void)cateA_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)cateA_instanceMethod2{
NSLog(@"%s",__func__);
}
- (void)cateA_classMethod3{
NSLog(@"%s",__func__);
}
@end
在上一篇iOS-类的加载(上)文章中分析了类的加载流程:realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories
中提及了rwe
的加载,其中分析了分类的data
数据 时如何 加载到类
中的,分类的加载顺序是:是编译时加载进内存
的顺序决定,并且越晚加进来,越在前面
在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);
#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
}
那么为什么是这样处理的呢?原来是因为在编译阶段
,类就已经被确认好了内存位置,是clean memory
(即实例方法
存储在类
中,类方法
存储在元类
中),而分类
是后面才加进来的
分类通过attatchToClass
添加到本类,下面我们查看attatchToClass
源码
attatchToClass
源码
void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
(flags & ATTACH_METACLASS) ||
(flags & ATTACH_CLASS_AND_METACLASS));
const char *mangledName = cls->mangledName();
const char *ZGPersonName = "ZGPerson";
if (strcmp(mangledName, ZGPersonName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
printf("%s: 这个是我要研究的 %s \n",__func__, ZGPersonName);
}
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);
}
}
但是当我们运行的时候却发现并未走进attachCategories
方法(分类链接方法),这是为什么呢?
attachCategories源码
// 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)
{
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();// rwe 初始化 --> 要对copy出的干净内存进行方法插入操作
// 自己加的调试代码
const char *mangledName = cls->mangledName();
const char *ZGPersonName = "ZGPerson";
if (strcmp(mangledName, ZGPersonName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
if (!kc_isMeta) {
printf("%s: 这个是我要研究的 %s \n",__func__,ZGPersonName);
}
}
// 遍历 分类数据的准备(method property protocol)
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;
}
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);
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);
}
可以看出,这确实是链接分类
的方法,但是为什么没调用呢?这个我们稍后再说,这里我们先分析一下这个方法做了什么?其中我们主要分析一下排序方法prepareMethodLists
和插入方法attachLists
。
prepareMethodLists
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
runtimeLock.assertLocked();
const char *mangledName = cls->mangledName();
const char *ZGPersonName = "ZGPerson";
if (strcmp(mangledName, ZGPersonName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
printf("%s: 这个是我要研究的 %s \n",__func__,ZGPersonName);
}
if (addedCount == 0) return;
// There exist RR/AWZ/Core special cases for some class's base methods.
// But this code should never need to scan base methods for RR/AWZ/Core:
// default RR/AWZ/Core cannot be set before setInitialized().
// Therefore we need not handle any special cases here.
if (baseMethods) {
ASSERT(cls->hasCustomAWZ() && cls->hasCustomRR() && cls->hasCustomCore());
}
// 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*/);
}
}
// If the class is initialized, then scan for method implementations
// tracked by the class's flags. If it's not initialized yet,
// then objc_class::setInitialized() will take care of it.
if (cls->isInitialized()) {
objc::AWZScanner::scanAddedMethodLists(cls, addedLists, addedCount);
objc::RRScanner::scanAddedMethodLists(cls, addedLists, addedCount);
objc::CoreScanner::scanAddedMethodLists(cls, addedLists, addedCount);
}
}
从其中的排序方法fixupMethodList
中可以看出,排序是通过方法的内存地址排序的即SEL name
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.name = sel_registerNameNoLock(name, bundleCopy);
}
}
// sel - imp
// Sort by selector address.//通过内存地址排序
if (sort) {
method_t::SortBySELAddress sorter;
std::stable_sort(mlist->begin(), mlist->end(), sorter);
}
// Mark method list as uniqued and sorted
mlist->setFixedUp();
}
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
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;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));// 扩容
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));// 移动旧的去后面
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));// cpy 新的在前面
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
// 0到1 - list 没有数据第一次 - 第0个元素给list,此时 list 是一维的
list = addedLists[0];
}
else {
// 1 list -> many lists
// 1+many - 举例 many lists 是3个
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;// 容量计算,旧+新的和 1+3=4
setArray((array_t *)malloc(array_t::byteSize(newCount)));// 开辟总大小的空间 newCount
array()->count = newCount;// array 的数量:是新的添加 manylists 后的数量
if (oldList) array()->lists[addedCount] = oldList;// 旧的list 放最后面 第3个位置
// memcpy(位置, 放谁, 大小)
// 把新的 lists 从起始位置0开始放
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
这里加了一些注释,可以看出新
加入的array(方法列表,属性列表,协议列表)
总是会加在旧
的前面,这也就解释了为什么分类的方法会在本类之前调用
。
attachCategories分析
刚才我们一直没调用到attachCategories
方法,那我们只能想一下其他的办法,在源码中搜索attachCategories
,看看都会在哪里调用,发现只有两处调用
attachToClass()
load_categories_nolock()
经过探索,如果想要调用attachCategories
方法,我们只需要在分类里面加一个+ load
方法,变会调用attachCategories
方法
其中的函数调用栈如下
load_categories_nolock
而在 attachToClass
方法中,这里经过调试发现,基本不会进到if流程调用attachCategories
,除非加载两次,一般的类一般只会加载一次
所以接下来我们只需要研究load_categories_nolock()
的调用,我们全局搜索load_categories_nolock
的调用的地方,发现只有两次调用
static void loadAllCategories() {
mutex_locker_t lock(runtimeLock);
for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
load_categories_nolock(hi);
}
}
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
经过我们调试发现,只有loadAllCategories
方法才会调用load_categories_nolock
,另外一个基本不会调用。
继续全局搜索loadAllCategories
,发现只在load_images
中调用
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
所以综上所述,该情况下的分类的数据加载时机的反推路径为:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images
而我们的分类加载正常的流程的路径为:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories
分类和类的各种加载时机
我们可以大致将类 和 分类 是否实现+load
的情况分为4种
类+分类 | |||
---|---|---|---|
分类实现+load | 分类未实现+load | ||
类实现+load | 非懒加载类+非懒加载分类 | 非懒加载类+懒加载分类 | |
类未实现+load | 懒加载类+非懒加载分类 | 懒加载类+懒加载分类 |
-
【情况1】
非懒加载类 + 非懒加载分类
-
【情况2】
非懒加载类 + 懒加载分类
-
【情况3】
懒加载类 + 懒加载分类
-
【情况4】
懒加载类 + 非懒加载分类
非懒加载类 + 非懒加载分类
-
类的数据加载是通过
_getObjc2NonlazyClassList
加载,即对ro
、rw
的操作和对rwe
赋值初始化,在extAlloc
方法中。 -
分类的数据加载是通过
load_images
加载到类中的。
调用路径为
-
map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
,此时的mlists
是一维数组,然后走到load_images
部分。 -
load_images -> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists
,此时的mlists
是二维数组。
非懒加载类 + 懒加载分类
非懒加载类 + 懒加载分类- 类和分类的加载在
read_images
就加载好数据了 - 其中
data
数据在编译期就已经完成了
懒加载类 + 非懒加载分类
懒加载类 + 非懒加载分类- 此情况下会迫使主类提前加载,即主类强行转换为 非懒加载类样式。
懒加载类 + 懒加载分类
懒加载类 + 懒加载分类- 此情况下懒加载类与懒加载分类的数据加载是在 消息第一次调用时加载
总结
- 非懒加载类 + 非懒加载分类,其数据的加载在
load_images
方法中,首先对类进行加载,然后把分类的信息贴到类中。- 非懒加载类 + 懒加载分类,其数据加载在
read_image
就加载数据,数据来自data
,data
在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起。- 懒加载类 + 懒加载分类,其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成。
- 懒加载类 + 非懒加载分类,只要分类实现了
load
,会迫使主类提前加载,即在_read_images
中不会对类做实现操作,需要在load_images
方法中触发类的数据加载,即rwe初始化,同时加载分类数据。
网友评论