通过探索Category底层原理回答以下问题
- Category是否可以添加方法、属性、成员变量?Category是否可以遵守Protocol?
- Category的本质是什么,在底层是怎么存储的?
- Category的实现原理是什么,Catagory中的方法是如何调用到的?
- Category中是否有Load方法,load方法是什么时候调用的?
- load、initialize的区别
在Category实现原理(一)中我们通过窥探Category底层结构回答了问题1、2,下面我们继续探究。
Category中的方法调用顺序 - 表象
创建父类MGCPerson,为其添加分类MGCPerson+Sport
和MGCPerson+Eat
,创建子类MGCStudent,并添加分配MGCStudent+Study
,然后分别添加方法- (void)life
和+ (void)life
@implementation MGCPerson
- (void)life {
NSLog(@"MGCPerson : - (void)life");
}
+ (void)life {
NSLog(@"MGCPerson : + (void)life");
}
@end
@implementation MGCPerson (Sport)
- (void)life {
NSLog(@"MGCPerson (Sport) : - (void)life");
}
+ (void)life {
NSLog(@"MGCPerson (Sport) : + (void)life");
}
@end
@implementation MGCPerson (Eat)
- (void)life {
NSLog(@"MGCPerson (Eat) : - (void)life");
}
+ (void)life {
NSLog(@"MGCPerson (Eat) : + (void)life");
}
@end
@implementation MGCStudent
- (void)life {
NSLog(@"MGCStudent : - (void)life");
}
+ (void)life {
NSLog(@"MGCStudent : + (void)life");
}
@end
@implementation MGCStudent (Study)
- (void)life {
NSLog(@"MGCStudent (Study) : - (void)life");
}
+ (void)life {
NSLog(@"MGCStudent (Study) : + (void)life");
}
@end
在main函数中创建MGCStudent类,调用life方法,观察打印
int main(int argc, const char * argv[]) {
@autoreleasepool {
MGCStudent *student = [[MGCStudent alloc] init];
[student life];
[MGCStudent life];
MGCPerson *person = [[MGCPerson alloc] init];
[person life];
[MGCPerson life];
}
return 0;
}
// 打印结果
MGCStudent (Study) : - (void)life
MGCStudent (Study) : + (void)life
MGCPerson (Sport) : - (void)life
MGCPerson (Sport) : + (void)life
观察打印结果我们可以得出一个结论:Category中的方法会”覆盖“原类中的方法;仔细观察我们发现MGCPerson
相关的调用打印全部来自MGCPerson (Sport)
中的方法,但我们明明在MGCPerson的两个分类MGCPerson (Sport)
、MGCPerson (Eat)
中都实现了life
方法,为什么优先调用了MGCPerson (Sport)
中的life
方法?我们猜测这和文件的编译顺序有关,通过在Build Phase -> Compile source中调整文件顺序观察打印结果,我们发现后编译的分类文件优先调用。
总结起来就是:
- 分类中的方法实现会"覆盖"原类中的方法
- 后编译的分类文件优先级更高
继续研究为什么调用顺序是这样
Category方法调用顺序 - 本质
为什么去runtime中找对应源码?
- OC中的方法调用简单的说就是通过实例对象(或类对象)的isa指针和类对象(或元类对象)的superClass指针去类(或元类)对象中查找方法。
- 通上篇文章的探究我们知道,Category中的方法、属性等编译后是存储在
category_t
结构体中的,也就是说编译后分类中的方法并没有合并到类(或元类)中,我们是无法在类对象(或元类对象)中找到Category中的方法的。 - 但是最终调用的时候我们却可以通过isa和superClass指针找到这些方法。所以我们有理由猜测runtime帮我们做了方法合并
为什么从void _objc_init(void)
开始查找?
简单的说下APP安装到运行的过程:
- 从appStore下载app,签名认证通过后装载到手机磁盘中
- 点击APP,系统内核部署好APP(进程)运行的初始环境,找到对应APP的可执行文件(
Mach-O
文件)- Mach-O是苹果系统的一种文件格式,app的可执行文件、动态库、静态库最终都是这种文件格式
- 根据Mach-O文件找到dyld的路径,内核加载dyld,dyld从系统留下的原始调用栈引导和启动自己
- dyld(dynamic link editor) 动态链接器,用于链接动态库、资源、app可执行文件格式的Mach-O文件
- dyld 本身也是一种Mach-O文件
- dyld将APP可执行文件(Mach-O文件)加载到内存中,跟据APP可执行文件中的
LoadCommands
中的信息去系统共享缓存区(dyld shared cache)将引用的动态库(Mach-O文件)递归加载进内存 - 链接包括可执行文件在内APP所需的所有的Mach-O文件
- 初始化除可执行文件外的所有Mach-O文件
- 初始化APP可执行文件,调用app中的
main
函数,到这一步就进入我们熟悉的APP入口main
函数了
_objc_init
就是runtime的初始化函数,也就是在上述第6步时调用的。
在runtime源码中搜索_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();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
可以看到在一系列的初始化后,调用了_dyld_objc_notify_register
并且传递了三个函数,去dyld源码中搜索函数_dyld_objc_notify_register
的定义
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
根据注释在_dyld_objc_notify_register
中会调用传入的mapped
函数,并传入已映射进内存的objc image
(objc对应的模块)
objc image中存储着我们定义的类及分类信息,接下来去runtime源码中查看map_images
函数
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
这个函数实际返回的是map_images_nolock
函数的地址,继续查看map_images_nolock
函数,map_images_nolock
函数中查找了所有包含Objective-C
的image
,然后调用_read_images
处理这些image
,继续查看_read_images
函数
在_read_images
函数中找到处理分类的代码块
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// Discover categories.
for (EACH_HEADER) { // 遍历image
category_t **catlist =
_getObjc2CategoryList(hi, &count); // 获取当前image中的分类列表
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) { // 遍历分类
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) { // 分类对应的原类已经释放了,清空这个分类,打印信息信息,继续下次循环
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties) // 分类有方法、协议或属性
{
addUnattachedCategoryForClass(cat, cls, hi); // 记录这个分类到一个映射表中
if (cls->isRealized()) { // 这个类的所有分类已找到
remethodizeClass(cls); // 重新整合类中信息
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
_read_images
函数读取完image中的Category信息后存放起来,然后调用remethodizeClass
函数。
remethodizeClass
中又调用了附加分类的方法attachCategories
,重点来了,下边我们看下attachCategories
函数。注释也很重要,注意下述代码中的注释
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists)); //malloc一个二维数组存放所有分类的方法列表
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists)); //malloc一个二维数组存放所有分类的属性列表
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists)); //malloc一个二维数组存放所有分类的协议列表
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) { // 注意这里,是i--也就是说是倒序处理分类的
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist; // 按编译倒序(i--)将各分类的方法列表(mlist) 按正序(mcount++) 放进 二维方法列表(mlists)中
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;// 按编译倒序(i--)将各分类的属性列表(proplist) 按正序(propcount++) 放进 二维属性列表(proplists)中
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;// 按编译倒序(i--)将各分类的协议列表(protolist) 按正序(protocount++) 放进 二维协议列表(protolists)中
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount); // 将某个类的所有分类的方法列表附加到类的方法列表中
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);// 将某个类的所有分类的属性列表附加到类的方法列表中
free(proplists);
rw->protocols.attachLists(protolists, protocount);// 将某个类的所有分类的协议列表附加到类的方法列表中
free(protolists);
}
attachCategories
按编译倒序将各分类中的方法、协议、属性列表分别整合成一个二维数组后,又各自调用了attachLists
方法将整合后的分类信息附加到原类(target class)的cls->data->rw中
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])); // 从array()->lists位置开始移动oldCount * sizeof(array()->lists[0])字节到array()->lists + addedCount位置
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0])); // addedLists位置来时,拷贝addedCount * sizeof(array()->lists[0])字节到array()->lists,即将拷贝到array()的前addedCount位置上
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
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;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
分析上述代码,可以看到是先将原类中的方法后移,然后再将分类中的方法加入到原类中的,到此我们可以得出以下结论
- 分类中的方法优先级高于原类中的方法
- 后编译的分类优先级高于先编译的分类
- 我们常说的分类方法覆盖原类方法并不是真正的覆盖,只是objc_msgSend在分类中找到方法实现后不再继续查找
Category中的+load
方法
同样的我们先分别在类及其分类中实现+load
和+ (void)initialize
方法,供后续分析使用
@interface MGCPerson : NSObject
@end
@implementation MGCPerson
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
@implementation MGCPerson (Sport)
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
@implementation MGCPerson (Eat)
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
@interface MGCStudent : MGCPerson
@end
@implementation MGCStudent
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
@implementation MGCStudent (Study)
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
@implementation MGCStudent (Holiday)
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
@interface MGCDog : NSObject
@end
@implementation MGCDog
+ (void)load {
NSLog(@"%s", __func__);
}
+ (void)initialize {
NSLog(@"%s", __func__);
}
@end
添加好代码后,不做任何调用和对象创建,直接运行程序,打印如下
+[MGCDog load]
+[MGCPerson load]
+[MGCStudent load]
+[MGCPerson(Eat) load]
+[MGCPerson(Sport) load]
+[MGCStudent(Holiday) load]
+[MGCStudent(Study) load]
可以发现未做任何调用和对象创建的情况下,也会执行+ (void)load
方法。
尝试在xcode->targets->build phases->compile sources中调整文件编译顺序,发现
- 类中的load方法始终优先于分类调用,且不受编译顺序影响
- 父类中的load方法优先于子类调用,且不受编译顺序影响
- 无继承关系的类,先编译的先调用
- 分类中的load方法先编译的先调用,且与其对应原类的继承关系无关
既然我们没有调用,我们猜测是runtime启动时调用了load方法,继续查看runtime的入口函数_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();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
从前边对void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped);
的分析我们知道第二个参数load_images
函数中调用了+load
方法,从这里也可以看出load_images
中的load不是加载的意思,是load
方法的意思
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
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh); // 准备工作
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods(); // 调用load方法
}
load_images
中先调用了prepare_load_methods
再进一步调用了void call_load_methods(void)
先看下prepare_load_methods
做了什么处理,注释很重要,不要忽略它
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++) { // 正序遍历所有类(编译顺序)
schedule_class_load(remapClass(classlist[i])); // 整理类的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); // 正序添加到表中
}
}
分析上述代码可知:类中的load
方法优先于Category
中的load
方法被规划入表中;且类及分类各自按编译正序规划load
方法到表中。
继续查看schedule_class_load
方法,注释依然很重要,不要忽略它
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方法,则不再处理,所以每个load方法只会调用一次
// Ensure superclass-first ordering
schedule_class_load(cls->superclass); // 递归调用schedule_class_load方法,并传入父类
add_class_to_loadable_list(cls); // 将类中的load方法加入表中
cls->setInfo(RW_LOADED); // 置为已处理
}
分析上述代码可知:父类中的load
方法优先于子类中的load
方法加入表中。
到此准备工作就做完了,接下来看下调用void call_load_methods(void)
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. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads(); // 调用所有类的load方法,地都用完成后loadable_classes_used置为0
}
// 2. Call category +loads ONCE
more_categories = call_category_loads(); // 调用所有分类的load方法
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
分析上述代码可知:调用load
方法时优先调用类中的load方法,然后再调用分类中的load
方法
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);// 调用load方法
}
// Destroy the detached list.
if (classes) free(classes);
}
分析上述代码可知:类中的load方法按表中正序调用,结合前边load方法的入表顺序可知,先编译的类先调用,且父类优先于子类调用。
查看call_category_loads(void)
方法
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;
// Call all +loads for the detached list.
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); // 调用load方法
cats[i].cat = nil;
}
}
分析上述代码可知:分类中的load方法按表中正序调用,结合前边入表顺序可知,先编译的分类先调用。
综合以上分析,已经验证了本小结开始时的现象分析结论,这里不再赘述
Category中的+ (void)initialize
方法
同样先看现象,代码和上一小结(Category中的+load方法)中一样,这里不再列出
上一小结中我们在不添加任何方法调用、对象创建的情况下直接运行程序,发现调用了+load
方法,但是并没有调用+ (void)initialize
尝试添加如下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MGCStudent class];
[[MGCDog alloc] init];
[[MGCDog alloc] init];
[[MGCDog alloc] init];
[[MGCDog alloc] init];
}
return 0;
}
打印结果
+[MGCPerson(Eat) initialize]
+[MGCStudent(Study) initialize]
+[MGCDog initialize]
可以看到在我们没有调用+ (void)initialize
的情况下,+ (void)initialize
方法依然被调用了,且无论调用的是类类方法还是实例方法,+ (void)initialize
都会被调用并且只会调用一次。
因此我们猜测是类第一次接收到消息时调用了+ (void)initialize
方法。即第一次接收objc_msgSend
消息时调用了+ (void)initialize
方法。
尝试去runtime
中查找objc_msgSend
方法,这个方法的源码是汇编的。
最终调用到了void _class_initialize(Class cls)
方法
void _class_initialize(Class cls) {
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls); // 递归调用_class_initialize方法,并传入父类指针
}
// Try to atomically set CLS_INITIALIZING.
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) { // 如果类没有调用Initialize方法,则准备调用
cls->setInitializing();
reallyInitialize = YES;
}
}
if (reallyInitialize) {
callInitialize(cls); // 调用当前类的callInitialize方法
}
}
分析上述代码可知:在调用自己的callInitialize之前会先调用父类的callInitialize
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
分析上述代码可知:Initialize最终是通过objc_msgSend
调用的,既然最终是通过objc_msgSend
调用的,那就遵从objc_msgSend
方法调用的原理。即分类方法会"覆盖"原类的中的方法。
综合以上可知:
- 父类中的
Initialize
优先于子类中的Initialize
方法调用 - 分类中的
Initialize
优先于类中的Initialize
方法调用
注意:如果子类及子类分类中都没有实现Initialize
方法,根据objc_msgSend
方法查找顺序,最终会调用父类的Initialize
方法。也就是说上一小节中定义的MGCStudent
对象及其分类MGCStudent (Study)
和MGCStudent (Holiday)
如果都没实现Initialize
,调用[MGCStudent class]
会调用两次Initialize
方法,一次是递归调用时父类(MGCPerson)调用的,一次是自己调用时objc_msgSend
根据方法查找顺序自己(MGCStudent)调用的