在iOS开发中经常会使用load方法去做方法置换。因为load方法会在整个文件被加载到运行时,在main函数之前调用。
另外注意,load方法中不宜过多,里面代码不应该执行过于复杂的操作。这样会影响我们app的启动时间。
提出问题:
1.load方法的加载顺序是什么?
2.load方法会被category中的load方法覆盖吗?
3.如果多个分类执行load方法交换了相同的方法会是什么结果?
load方法加载过程
创建一个工程在load方法打断点看看加载的方法。
load方法加载dyld 是 the dynamic link editor 的缩写,它是苹果的动态链接器
在系统内核做好程序准备工作之后,交由 dyld 负责余下的工作
从图中可以看到load方法之前有call_load_method和load_images方法,再往前看到第三个执行方法是Runtime接收到dyld的函数回调
我们可以查看runtime源码来分析加载过程
_objc_init
函数位于objc-os.mm
文件中
// runtime加载入口
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();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
可以看到Runtime接收到dyld的函数回调,开始执行map_images、load_images等操作,并回调+load方法。接下来我们主要看load_images方法
准备load方法
load_images
方法位于源码objc-runtime-new.mm
文件中
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// 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
{
//定义可递归锁对象
// 由于 load_images 方法由 dyld 进行回调,所以数据需上锁才能保证线程安全
// 为了防止多次加锁造成的死锁情况,使用可递归锁解决
rwlock_writer_t lock2(runtimeLock);
// 准备Class list和Category list
prepare_load_methods((const headerType *)mh);
}
// 调用已经准备好的Class list和Category list
call_load_methods();
}
接下来先查看prepare_load_methods方法
// 准备Class list 和 Category list
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
// 获取到非懒加载的类的列表
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 设置Class的调用列表
schedule_class_load(remapClass(classlist[i]));
}
// 获取到非懒加载的Category列表
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());
// 设置Category的调用列表
add_category_to_loadable_list(cat);
}
}
prepare_load_methods 作用是为 load 方法做准备,从代码中可以看出 Class 的 load 方法是优先于 Category。其中在收集 Class 的 load 方法中,因为需要对 Class 关系树的根节点逐层遍历运行,在 schedule_class_load 方法中使用深层递归的方式递归到根节点,优先进行收集。
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
// 已经添加Class的load方法到调用列表中
if (cls->data()->flags & RW_LOADED) return;
// 确保super已经被添加到load列表中,默认是整个继承者链的顺序
schedule_class_load(cls->superclass);
// 将IMP和Class添加到调用列表
//方法位于objc-loadmethod.mm文件中
add_class_to_loadable_list(cls);
// 设置Class的flags,表示已经添加Class到调用列表中
cls->setInfo(RW_LOADED);
}
load方法调用
call_load_methods
位于objc-loadmethod.mm
文件中
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.
// 由于 loading 是全局静态布尔量,如果已经录入方法则直接退出
if (loading) return;
loading = YES;
// 声明一个 autoreleasePool 对象
// 使用 push 操作其目的是为了创建一个新的 autoreleasePool 对象
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
// 重复调用 load 方法,直到没有
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
// 将创建的 autoreleasePool 对象释放
objc_autoreleasePoolPop(pool);
loading = NO;
}
call_load_methods
函数执行这些load方法。在这个方法中,call_class_loads
函数是负责调用类方法列表的,call_category_loads
负责调用Category的方法列表。
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;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
小重点:最后看到是通过(*load_method)(cls, SEL_load)
方法调用load方法的。这是一个C语言的函数,通过指针运行对应的地址部分。而不是通过objc_msgSend
消息发送执行。
load方法的加载流程:
1.load_images 通过 dyld 载入 image 文件,引入 Class
- prepare_load_methods准备load方法。过滤无效类、无效方法,将 load 方法指针和所属 Class 指针收集至全局 Class 存储线性表 loadable_classes 中,其中会涉及到自动扩展空间和父类优先的递归调用问题。category类中同样操作
- call_load_methods调用load方法,先调用类的再调用分类的
所有源码都看完了,回答开篇问题。
load方法的加载顺序是什么?
父类 >子类> 分类。
类和分类中的load方法都会执行
父类永远大于子类,子类永远大于分类
如果有多个分类会按照编译顺序执行,先编译先执行
load方法会被category中的load方法覆盖吗?
不会被覆盖。load方法是通过指针地址直接访问。不和category中方法一样,category中方法是通过objc_msgSend消息发送,会导致先收到消息的方法直接返回。
如果多个分类执行load方法交换了相同的方法会是什么结果?
我们创建student类,同时创建student+A ,student+B两个分类
目录
编译顺序
在student.m中代码实现
@implementation Student
+ (void)load
{
NSLog(@"studet load");
}
- (void)sayHello
{
NSLog(@"studect sayHello");
}
@end
student+A.中代码实现
+ (void)load
{
NSLog(@"studectA load");
[self fn_SwizzleSEL:@selector(s_sayHello) withSEL:@selector(sayHello)];
}
- (void)s_sayHello {
[self s_sayHello];
NSLog(@"StudentA + swizzle say hello");
}
+ (void)fn_SwizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
BOOL didAddMethod =
class_addMethod(class,
originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
student+B.m中代码实现,和A中相似
+ (void)load
{
NSLog(@"studectB load");
[self fn_SwizzleSEL:@selector(s_sayHelloB) withSEL:@selector(sayHello)];
}
- (void)s_sayHelloB {
[self s_sayHelloB];
NSLog(@"StudentB + swizzle say hello");
}
执行[[Student new] sayHello];
方法查看打印结果
2019-04-01 15:18:28.807291+0800 LoadCategoryDemo[25843:18719577] studet load
2019-04-01 15:18:28.808022+0800 LoadCategoryDemo[25843:18719577] studectB load
2019-04-01 15:18:28.808331+0800 LoadCategoryDemo[25843:18719577] studectA load
2019-04-01 15:18:43.246020+0800 LoadCategoryDemo[25843:18719577] studect sayHello
2019-04-01 15:18:43.246231+0800 LoadCategoryDemo[25843:18719577] StudentB + swizzle say hello
2019-04-01 15:18:43.246322+0800 LoadCategoryDemo[25843:18719577] StudentA + swizzle say hello
可以看到load方法的打印顺序和上面的结论一致,类大于分类,分类是按照编译顺序执行
从图中可以看到方法指向已经交换。交换方法的本质其实就是交换IMP方法实现。
Student+A load执行后
从图中可以看到load方法执行完以后方法执行所对应的方法实现
category中后编译的类的方法会先执行会先执行student+A中的方法
[[Student new] sayHello];
->s_sayHello IMP
方法内部调用[self s_sayHello];
[self s_sayHello];
指向s_sayHelloB IMP
s_sayHelloB方法内部调用 [self s_sayHelloB]
[self s_sayHelloB]
指向sayHello IMP
。
打印顺序是sayHello IMP
中@"studect sayHello"
->@"StudentB + swizzle say hello"
->@"StudentA + swizzle say hello"
目前只是分析了student两个分类load交换方法时候调用方法的顺序。
如果父类中有同名的方法,子类中没有实现相应的方法,父类中也有一个分类这个时候情况就非常复杂了,会有很多种可能。
参考链接:http://yulingtianxia.com/blog/2017/04/17/Objective-C-Method-Swizzling/
网友评论