Objc源码之对象创建alloc和init
Objc源码之initialize实现
Objc源码之Load方法实现
Objc源码之NSObject和isa
Objc源码之引用计数实现
objc源码之Method消息发送
前言
Load方法作为OC中一个特殊的方法,在Main函数之前执行,由于这个特性的存在,一方面可以用来在应用执行前,进行一些准备工作,比如用来hook方法,对系统方法进行替换,另一方面,由于Load方法在Mian函数之前执行,如果使用不当,会引起app启动超时、甚至出现crash,使app不能启动,因此要详细了解Load方法,并正确的使用它。
接下来,我会从源码的角度,来说明一下几个问题:
- Load方法调用时机
- 系统如何加载所有类的Load方法
- 类和分类的Load方法调用顺序。
注:本文分析基于objc4-750源码进行的。
一、Load方法调用时机
1.Load方法调用栈
首先,我们创建一个对象TestObject,添加Load方法,并在load方法中打上断点,这时我们可以看到下图的调用栈,我们可以看到Load函数是通过动态链接器dyld调用,并最终调用load_images
函数,来调用方法中的Load函数,调用栈如下图所示:
2.load_images源码分析
load_images中大概分为三部分:
1)判断镜像中是否有Load方法,没有直接返回,通过hasLoadMethods
函数完成
2)查找所有的Load方法,通过prepare_load_methods
函数完成
3)调用所有Load方法,通过call_load_methods
函数完成
void
load_images(const char *path __unused, const struct mach_header *mh)
{
//1.判断镜像中是否有Load方法,没有直接返回
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// 2.查找所有的Load方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// 3.调用所有Load方法
call_load_methods();
}
二、系统如何加载所有类的Load方法
1.判断镜像中是否有有Load方法(镜像是一种macho文件)
从load_images
方法,我们可以知道,通过hasLoadMethods
方法,来判断是否有Load方法,下面我们看下Load方法的实现:
bool hasLoadMethods(const headerType *mhdr)
{
size_t count;
if (_getObjc2NonlazyClassList(mhdr, &count) && count > 0) return true;
if (_getObjc2NonlazyCategoryList(mhdr, &count) && count > 0) return true;
return false;
}
hasLoadMethods
中分为两步:
1)通过 _getObjc2NonlazyClassList获取所有类中的Load方法数量,也就是下面的GETSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");**
2)通过 _getObjc2NonlazyCategoryList获取所有Category中的Load方法数量,也就是下面的GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist") ;
而这两个方法,又是如何读取到类和category中是否有Load方法呢,通过下面的方法:
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
}
// function name content type section name
GETSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist");
_getObjc2NonlazyClassList
和_getObjc2NonlazyCategoryList
方法都是最终调用getDataSection方法,这个方法是从镜像文件中读取响应的代码段,判断具体的是否有load方法, __objc_nlclslist
标识的是类+load 函数列表,__objc_nlcatlist
标识的是分类中+load 函数列表。从下面代码可以看出,最终是通过读取镜像中的__DATA
来找到对应的方法。
template <typename T>
T* getDataSection(const headerType *mhdr, const char *sectname,
size_t *outBytes, size_t *outCount)
{
unsigned long byteCount = 0;
T* data = (T*)getsectiondata(mhdr, "__DATA", sectname, &byteCount);
if (!data) {
data = (T*)getsectiondata(mhdr, "__DATA_CONST", sectname, &byteCount);
}
if (!data) {
data = (T*)getsectiondata(mhdr, "__DATA_DIRTY", sectname, &byteCount);
}
if (outBytes) *outBytes = byteCount;
if (outCount) *outCount = byteCount / sizeof(T);
return data;
}
总结一下整个过程就是:
1.调用hasLoadMethods,找类和Category中的Load方法。具体执行者是_getObjc2NonlazyClassList和_getObjc2NonlazyCategoryList。
2. _getObjc2NonlazyClassList和_getObjc2NonlazyCategoryList又通过调用getDataSection读取__DATA段,来获取是否有Load方法。
2.准备所有加载的load方法
在通过hasLoadMethods
判断有Load方法以后,我们需要知道具体有哪些Load方法,读取所有的load方法,这一步是通过prepare_load_methods
来完成的,会将所有load方法读取到一个列表中。下面是prepare_load_methods
的具体实现:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
//获取所有类的Load方法,添加到数组中
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
//获取Category类的Load方法,添加到数组中
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
hasLoadMethods
主要分为两部分:
1)获取所有类的Load方法,添加到列表(loadable_classes)中。
这一步是通过_getObjc2NonlazyClassList
获取类列表,然后通过schedule_class_load
递归添加数组中,我们知道父类的Load方法是早于子类的方法调用,就是在这一步处理的下面我们看下schedule_class_load
方法的具体实现:
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
//1.判断类的load方法是否添加过
if (cls->data()->flags & RW_LOADED) return;
// 2.保证父类优先于子类调用
schedule_class_load(cls->superclass);
//3.将类添加到数组中,并标注类方法已添加
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
schedule_class_load
中主要做了一下事情:
1.判断类的load方法是否添加过
2.递归调用父类,保证父类先于子类执行。
3.将类通过add_class_to_loadable_list
添加到loadable_classes中。
add_class_to_loadable_list
源码如下:
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
//1.从class_ro_t中获取load方法
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
//2.如果内存已满的话,申请现有内存2倍的内存空间
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
//3.保存多有类和方法到loadable_classes中
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
add_class_to_loadable_list
内容主要分为以下几步:
1.从class_ro_t中获取load方法
2.如果内存已满,申请现有内存2倍的内存空间
3.保存多有类和方法到loadable_classes中
2)获取Category类的Load方法,添加到列表(loadable_categories)中。
category类中的加载过程和类的耳机在过程基本一致,不同的是,load方法最终会加载到loadable_categories列表中。
总结:
- 类中的load方法都加载到loadable_classes列表中
- Category的load方法都加载到loadable_categories列表中
三、类和分类的Load方法调用顺序。
类和category中load方法,都是通过call_load_methods
方法加载的,下面是源码。
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. 加载类中的所有方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. 加载category中的所有方法
more_categories = call_category_loads();
// 3. 如果有类或者category中load方法没有加载,继续加载
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
对于 load 方法的调用顺序有两条规则:
- 类先于分类调用
- 父类先于子类调用
1)类先于分类调用
do {
// 1. 加载类中的所有方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. 加载category中的所有方法
more_categories = call_category_loads();
// 3. 如果有类或者category中load方法没有加载,继续加载
} while (loadable_classes_used > 0 || more_categories);
从上面代码可以看出,类的load方法先于Category中的load方法,也就是类先于分类。
如果category的镜像先于类的镜像加载,那么怎么保证类先于分类加载呢???
我们看下call_category_loads
的源码
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// 1.获取分类列表
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// 2.调用分类方法
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
...
return new_categories_added;
}
从上面代码可以看出,在加载Category的load方法时,会判断类的load方法是否已经调用,从而保证类优先于分类加载。
if (cls && cls->isLoadable()) {
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
2)父类先于子类调用
类的Load方法是在call_class_loads
中调用的,我们可以看下源码。
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
我们可以看出,call_class_loads
通过获取loadable_classes
列表中的数据,进行顺序调用的,所以类和父类的调用顺序与loadable_classes
创建时的添加顺序有关,所以我们看下loadable_classes创建的顺序:
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// 有父类,先添加父类Load方法到loadable_classes中
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
从上面代码可以看出,当有父类时,也就是cls->superclass不为空,先添加父类Load方法到loadable_classes
中,所以最终父类先于子类调用。
四、Load方法的应用
因为Load方法只会调用一次,并且在Main函数之前执行,所以Load方法可以用于执行一些靠前的操作,比如:Load方法用于hook一些系统方法,像NSArray的objectAtIndex:方法,防止数组越界,避免crash。
#import <objc/runtime.h>
@implementation NSArray (Safe)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(objectAtIndex:);
SEL swizzledSelector = @selector(xxx_objectAtIndex:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_objectAtIndex:(NSInteger)index {
[self xxx_objectAtIndex:index];
NSLog(@"objectAtIndex: %@", self);
}
@end
总结一下开头说的三点:
- Load方法调用时机(Main函数之前,通过dyld进行加载)
- 系统如何加载所有类的Load方法(通过读取镜像,获取类列表和Category列表,加载执行)
- 类和分类的Load方法调用顺序。(类先于分类调用,父类先于子类调用)
参考:
objc4-750源码
你真的了解 load 方法么?
NSObject +load and +initialize - What do they do?
method-swizzling
网友评论