Files
knowledge-kit/Chapter1 - iOS/1.48.md
2026-01-02 10:28:57 +08:00

3228 lines
118 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Category 底层原理
> 很多人都知道 Category、Extension 的用法,但是对于一些细节就不是很清楚了:
>
> - Category 中添加到属性、对象方法、类方法、协议在内存中是怎么存储的?
> - Category 中添加的到属性、对象方法、类方法、协议,是什么时候、如何与 Class 本身的属性、对象方法、类方法、协议合并的?
> - 假设一个 Person 类存在2个 Category分别是 Person+Work、Person+Study都有对象方法 play当实例对象调用 play 的时候,会调用最后编译的 Category Person+Study里的 play 实现。但为什么 load 不是调用最后一个 Category 的 +load而是3个都调用
>
> 本文主要探索这3个知识点
## 类别Category
### 文件特征
- 类别文件有2个分别为 .h 和 .m
- 命名为: “类名+类别名.h”和“类名+类别名.m”
### 文件内容格式
.h 文件格式
```
#import "类名.h"
@interface 类名 (类别名)
// 在此处声明方法
@end
```
.m 文件格式
```
#import "类名+类别名.h"
@implementation 类名 (类别名)
// 在此处实现声明的方法
@end
```
### 类别的作用
可以把类的实现分开在几个不同的源文件里,所以好处是:
- 减少耽搁文件的代码行数
- 可以把不痛的功能组织到不同的 category 里
- 可以由多个开发者共同完成一个大的类,方便协作
- 拓展当前类,为类添加方法
- 声明私有方法
### 类别的局限性
- 无法向现有的类添加实例变量编译器报“instance variables may not be placed in categories”。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性
- 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。
- 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法)
### Category 的使用和注意
1. Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法比如UIViewCtroller、UITableView这些的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如用Category 重写了 UIViewCtroller 的方法 A那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A这样子类调用方法 A 时是调用的自己重写的方法 A消除了父类 Category 中重写方法对自己的影响
2. Category拓展方法按照有没有重写当前类中的方法分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法
3. Category 中如果重写了 A 类从父类继承来的某方法,不会影响与 A 同层级的 B 类
4. 子类会不会继承父类的 Category Category 中重写的方法会对子类造成影响,但是子类不会继承非重写的方法(现有类中没有的方法)。但是在子类中引入父类 Category 的声明文件后,子类就会继承 Category 的非重写方法。继承的表现是:当子类的方法和父类 Category 中的方法名完全相同,那么子类里的方法会覆盖掉父类 Category相当于子类重写了继承自父类的方法
5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类当给 A 类添加一个 Category sayHello 方法仅有A 类的子类才可以使用 sayHello 方法
## Category 底层原理
### Category 的真面目是 category_t 结构体
来一个简单的 Person 类,为其添加一个 Category增加一些属性和类方法、对象方法、遵循协议
```objectivec
// Person
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHi;
- (void)sleep;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
@implementation Person
- (void)sayHi {
NSLog(@"Hello world");
}
- (void)sleep{
NSLog(@"Time to slepp");
}
@end
// Person+Study.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Study)<NSCopying>
@property (nonatomic, assign) NSInteger score;
- (void)study;
+ (void)sleep;
@end
NS_ASSUME_NONNULL_END
// Person+Study.m
#import "Person+Study.h"
@implementation Person (Study)
- (void)study {
}
+ (void)sleep {
NSLog(@"Time to sleep");
}
- (void)setScore:(NSInteger)score {
}
- (NSInteger)score {
return 100;
}
- (id)copyWithZone:(NSZone *)zone{
return self;
}
@end
```
clang 转为 c++ 代码,具体指令为 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc Person+Study.m`
查看 `Person+Study.cpp` 文件,可以看到 Category 本质是一个结构体
```c++
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[4];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
{{(struct objc_selector *)"study", "v16@0:8", (void *)_I_Person_Study_study},
{(struct objc_selector *)"setScore:", "v24@0:8q16", (void *)_I_Person_Study_setScore_},
{(struct objc_selector *)"score", "q16@0:8", (void *)_I_Person_Study_score},
{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Study_copyWithZone_}}
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"sleep", "v16@0:8", (void *)_C_Person_Study_sleep}}
};
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"@24@0:8^{_NSZone=}16"
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"score","Tq,N"}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;
static struct _category_t _OBJC_$_CATEGORY_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Study,
};
static void OBJC_CATEGORY_SETUP_$_Person_$_Study(void ) {
_OBJC_$_CATEGORY_Person_$_Study.cls = &OBJC_CLASS_$_Person;
}
```
可以看到 `Person+Study` 的 Category 底层赋值代码如下,就是结构体对象的初始化(参考上面的结构体各个成员变量)
```c++
static struct _category_t _OBJC_$_CATEGORY_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Study,
};
```
其中
```c++
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
```
- `name` 是类的名字,不是 category 小括号里写的名字
- `cls` 是要拓展的类对象,编译期间这个值不会有,在 runtime 加载时,才会根据 `name` 对应到类对象
- `instance_methods `这个 category 所有的 `-` 方法(对象方法)
- `class_methods` 这个 category 所有的 `+` 方法(类方法)
- `protocols `这个 category 实现的 protocol
- `properties `这个 category 所有的 property这也是 category 里面可以定义属性的原因,不过不会 `@synthesize` 实例变量,一般有需求添加实例变量属性时会采用 `objc_setAssociatedObject` 和 `objc_getAssociatedObject` 方法绑定方法绑定。
`_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study` 结构体存放的是对象方法信息,如下
```c++
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[4];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
{{(struct objc_selector *)"study", "v16@0:8", (void *)_I_Person_Study_study},
{(struct objc_selector *)"setScore:", "v24@0:8q16", (void *)_I_Person_Study_setScore_},
{(struct objc_selector *)"score", "q16@0:8", (void *)_I_Person_Study_score},
{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Study_copyWithZone_}}
};
```
`_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study` 结构体存放的是类方法信息,如下
```c++
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"sleep", "v16@0:8", (void *)_C_Person_Study_sleep}}
};
```
`_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study` 结构体存放的是遵循的协议信息,如下
```c++
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
```
`_OBJC_$_PROP_LIST_Person_$_Study` 存放的是 Category 中的属性信息,如下
```c++
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
1,
{{"score","Tq,N"}}
};
```
最后,这个类的 category 们生成了一个数组,存在了 `__DATA` 段下的 `__objc_catlist` section 里
```c++
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Person_$_Study,
};
```
看完上述信息,可以得出一个结论:
在编译阶段Class 和 Category 里面的数据是分开的。“将来”分类中的对象方法会塞到类对象中去,分类中的类方法会被塞到元类对象中去。这个“将来”是什么时候?后面继续探究
查看 [Objc 4 源代码](http://opensource.apple.com/tarballs/objc4/)Category 定义如下
```c++
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
WrappedPtr<method_list_t, method_list_t::Ptrauth> 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) const {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi) const;
protocol_list_t *protocolsForMeta(bool isMeta) const {
if (isMeta) return nullptr;
else return protocols;
}
};
```
结论:
- 为某个类添加的分类,分类中可能有属性、对象方法、类方法、遵循的协议、协议方法,本质都是存储到 `category_t` 结构体里面。
- 当有多个分类的时候,是通过 category_t 数组来承载的
### category 中定义的方法,存储在哪?
抛个问题:当对象调用方法的时候,不管这个方法是类自身的方法,还是通过分类添加的方法,本质都是通过 isa 指针去寻找方法实现,(如果是对象方法,则通过 instance 的 isa 去找到类对象,最后找到对象方法的实现去调用;如果是类对象方法,则通过 class 的 isa 找到元类对象,最后找到类方法的实现进行调用),那给 Category 添加的方法,是「**如何“塞到”类对象或者元类对象的方法列表中去的**」?
带着问题查看 [objc4 的源代码](http://opensource.apple.com/tarballs/objc4/) `objc-os.mm` 文件中的 `_objc_init` 方法。
在 library 加载前由 libSystem dyld 调用,进行初始化工作。
```c++
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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);
}
```
`_objc_init` 内部会调用 `map_images` 方法,将文件中的 image镜像map 到内存,其内部如下
```c++
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
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` 内部会调用 `map_images_nolock`
```c++
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[]) {
static bool firstTime = YES;
header_info *hList[mhCount];
uint32_t hCount;
size_t selrefCount = 0;
// Perform first-time initialization if necessary.
// This function is called before ordinary library initializers.
// fixme defer initialization until an objc-using image is found?
if (firstTime) {
preopt_init();
}
if (PrintImages) {
_objc_inform("IMAGES: processing %u newly-mapped images...\n", mhCount);
}
// Find all images with Objective-C metadata.
hCount = 0;
// Count classes. Size various table based on the total.
int totalClasses = 0;
int unoptimizedTotalClasses = 0;
{
uint32_t i = mhCount;
while (i--) {
const headerType *mhdr = (const headerType *)mhdrs[i];
auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
if (!hi) {
// no objc data in this entry
continue;
}
if (mhdr->filetype == MH_EXECUTE) {
// Size some data structures based on main executable's size
#if __OBJC2__
size_t count;
_getObjc2SelectorRefs(hi, &count);
selrefCount += count;
_getObjc2MessageRefs(hi, &count);
selrefCount += count;
#else
_getObjcSelectorRefs(hi, &selrefCount);
#endif
#if SUPPORT_GC_COMPAT
// Halt if this is a GC app.
if (shouldRejectGCApp(hi)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"Objective-C garbage collection "
"is no longer supported.");
}
#endif
}
hList[hCount++] = hi;
if (PrintImages) {
_objc_inform("IMAGES: loading image for %s%s%s%s%s\n",
hi->fname(),
mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
hi->info()->isReplacement() ? " (replacement)" : "",
hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
hi->info()->optimizedByDyld()?" (preoptimized)":"");
}
}
}
// 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();
#if SUPPORT_GC_COMPAT
// Reject any GC images linked to the main executable.
// We already rejected the app itself above.
// Images loaded after launch will be rejected by dyld.
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) {
_objc_fatal_with_reason
(OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
OS_REASON_FLAG_CONSISTENT_FAILURE,
"%s requires Objective-C garbage collection "
"which is no longer supported.", hi->fname());
}
}
#endif
#if TARGET_OS_OSX
// Disable +initialize fork safety if the app is too old (< 10.13).
// Disable +initialize fork safety if the app has a
// __DATA,__objc_fork_ok section.
if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_13) {
DisableInitializeForkSafety = true;
if (PrintInitializing) {
_objc_inform("INITIALIZE: disabling +initialize fork "
"safety enforcement because the app is "
"too old (SDK version " SDK_FORMAT ")",
FORMAT_SDK(dyld_get_program_sdk_version()));
}
}
for (uint32_t i = 0; i < hCount; i++) {
auto hi = hList[i];
auto mh = hi->mhdr();
if (mh->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
DisableInitializeForkSafety = true;
if (PrintInitializing) {
_objc_inform("INITIALIZE: disabling +initialize fork "
"safety enforcement because the app has "
"a __DATA,__objc_fork_ok section");
}
}
break; // assume only one MH_EXECUTE image
}
#endif
}
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
```
`map_images_nolock` 会调用 `_read_images` 方法,用于初始化 map 后的 `image`,这里面干了很多的事情,像 load所有的类、协议和 category著名的 `+ load` 方法就是这一步调用的。如下:
```c++
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertWriting();
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
if (!doneOnce) {
doneOnce = YES;
#if SUPPORT_NONPOINTER_ISA
// Disable non-pointer isa under some conditions.
# if SUPPORT_INDEXED_ISA
// Disable nonpointer isa if any image contains old Swift code
for (EACH_HEADER) {
if (hi->info()->containsSwift() &&
hi->info()->swiftVersion() < objc_image_info::SwiftVersion3)
{
DisableNonpointerIsa = true;
if (PrintRawIsa) {
_objc_inform("RAW ISA: disabling non-pointer isa because "
"the app or a framework contains Swift code "
"older than Swift 3.0");
}
break;
}
}
# endif
# if TARGET_OS_OSX
// Disable non-pointer isa if the app is too old
// (linked before OS X 10.11)
if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_11) {
DisableNonpointerIsa = true;
if (PrintRawIsa) {
_objc_inform("RAW ISA: disabling non-pointer isa because "
"the app is too old (SDK version " SDK_FORMAT ")",
FORMAT_SDK(dyld_get_program_sdk_version()));
}
}
// Disable non-pointer isa if the app has a __DATA,__objc_rawisa section
// New apps that load old extensions may need this.
for (EACH_HEADER) {
if (hi->mhdr()->filetype != MH_EXECUTE) continue;
unsigned long size;
if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
DisableNonpointerIsa = true;
if (PrintRawIsa) {
_objc_inform("RAW ISA: disabling non-pointer isa because "
"the app has a __DATA,__objc_rawisa section");
}
}
break; // assume only one MH_EXECUTE image
}
# endif
#endif
if (DisableTaggedPointers) {
disableTaggedPointers();
}
if (PrintConnecting) {
_objc_inform("CLASS: found %d classes during launch", totalClasses);
}
// 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);
ts.log("IMAGE TIMES: first time tasks");
}
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
for (EACH_HEADER) {
if (! mustReadClasses(hi)) {
// Image is sufficiently optimized that we need not call readClass()
continue;
}
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->isPreoptimized();
classref_t *classlist = _getObjc2ClassList(hi, &count);
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;
}
}
}
ts.log("IMAGE TIMES: discover classes");
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.
if (!noClassesRemapped()) {
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// fixme why doesn't test future1 catch the absence of this?
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
}
ts.log("IMAGE TIMES: remap classes");
// Fix up @selector references
static size_t UnfixedSelectors;
sel_lock();
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
sel_unlock();
ts.log("IMAGE TIMES: fix up selector references");
#if SUPPORT_FIXUP
// Fix up old objc_msgSend_fixup call sites
for (EACH_HEADER) {
message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
if (count == 0) continue;
if (PrintVtables) {
_objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
"call sites in %s", count, hi->fname());
}
for (i = 0; i < count; i++) {
fixupMessageRef(refs+i);
}
}
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
ts.log("IMAGE TIMES: discover protocols");
// Fix up @protocol references
// Preoptimized images may have the right
// answer already but we don't know for sure.
for (EACH_HEADER) {
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}
ts.log("IMAGE TIMES: fix up @protocol references");
// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
// hack for class __ARCLite__, which didn't get this above
#if TARGET_OS_SIMULATOR
if (cls->cache._buckets == (void*)&_objc_empty_cache &&
(cls->cache._mask || cls->cache._occupied))
{
cls->cache._mask = 0;
cls->cache._occupied = 0;
}
if (cls->ISA()->cache._buckets == (void*)&_objc_empty_cache &&
(cls->ISA()->cache._mask || cls->ISA()->cache._occupied))
{
cls->ISA()->cache._mask = 0;
cls->ISA()->cache._occupied = 0;
}
#endif
realizeClass(cls);
}
}
ts.log("IMAGE TIMES: realize non-lazy classes");
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
realizeClass(resolvedFutureClasses[i]);
resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
ts.log("IMAGE TIMES: realize future classes");
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
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);
}
}
}
}
ts.log("IMAGE TIMES: discover categories");
// Category discovery MUST BE LAST to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.
// +load handled by prepare_load_methods()
if (DebugNonFragileIvars) {
realizeAllClasses();
}
// Print preoptimization statistics
if (PrintPreopt) {
static unsigned int PreoptTotalMethodLists;
static unsigned int PreoptOptimizedMethodLists;
static unsigned int PreoptTotalClasses;
static unsigned int PreoptOptimizedClasses;
for (EACH_HEADER) {
if (hi->isPreoptimized()) {
_objc_inform("PREOPTIMIZATION: honoring preoptimized selectors "
"in %s", hi->fname());
}
else if (hi->info()->optimizedByDyld()) {
_objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors "
"in %s", hi->fname());
}
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
PreoptTotalClasses++;
if (hi->isPreoptimized()) {
PreoptOptimizedClasses++;
}
const method_list_t *mlist;
if ((mlist = ((class_ro_t *)cls->data())->baseMethods())) {
PreoptTotalMethodLists++;
if (mlist->isFixedUp()) {
PreoptOptimizedMethodLists++;
}
}
if ((mlist=((class_ro_t *)cls->ISA()->data())->baseMethods())) {
PreoptTotalMethodLists++;
if (mlist->isFixedUp()) {
PreoptOptimizedMethodLists++;
}
}
}
}
_objc_inform("PREOPTIMIZATION: %zu selector references not "
"pre-optimized", UnfixedSelectors);
_objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted",
PreoptOptimizedMethodLists, PreoptTotalMethodLists,
PreoptTotalMethodLists
? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists
: 0.0);
_objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered",
PreoptOptimizedClasses, PreoptTotalClasses,
PreoptTotalClasses
? 100.0*PreoptOptimizedClasses/PreoptTotalClasses
: 0.0);
_objc_inform("PREOPTIMIZATION: %zu protocol references not "
"pre-optimized", UnfixedProtocolReferences);
}
#undef EACH_HEADER
}
```
仔细看 category 的初始化,循环调用了 `_getObjc2CategoryList` 方法,
```c++
// Look for a __DATA or __DATA_CONST or __DATA_DIRTY section
// with the given name that stores an array of T.
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;
}
#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(_getObjc2CategoryList, category_t *, "__objc_catlist");
```
眼熟的 `__objc_catlist`,就是上面 category 存放的数据段了,可以串连起来了。
可以看到内部有 `Discover categories` 相关逻辑,里面和 category 方法相关的有 `remethodizeClass`,其实现如下
```c
static void remethodizeClass(Class cls){
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
```
可以看到内部调用 `attachCategories` 方法。 `attachCategories` 方法传入 3个参数第一个参数是类对象比如 `[Person class]`,第二个参数是 Category 数组,比如 `[category_t(Person+Study), category_t(Person+Work)]`。内部实现如下
```c++
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
// 方法数组,是"二维数组"。比如:[[personWork 对象方法1, personWork 对象方法2, personWork 对象方法3], [personStudy 对象方法1, personStudy 对象方法2, personStudy 对象方法3]]
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
// 属性数组,"二维数组"。比如:[[personWork 属性1, personWork 属性2, personWork 属性3], [personStudy 属性1, personStudy 属性2, personStudy 属性3]]
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
// 协议数组,"二维数组"。比如:[[personWork 协议1, personWork 协议2, personWork 协议3], [personStudy 协议1, personStudy 协议2, personStudy 协议3]]
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// 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--) {
// 取出某个分类。比如 Person+Work
auto& entry = cats->list[i];
// isMeta 为 NO取出分类中的对象方法
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// 将分类中的对象方法数组,放到 mlists 里面去。所以 mlists 是一个"二维数组"。由于 i 是 Category 数组总长度递减的,所以编译越后面的 Category 的对象方法越靠前
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 取出当前分类的属性数组
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
// 将当前分类的属性数组,放到 proplists 中去,所以 proplists 是一个"二维数组"
proplists[propcount++] = proplist;
}
// 取出当前分类的协议数组
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
// 将当前分类的协议数组,放到 protolists 中去,所以 protolists 是一个"二维数组"
protolists[protocount++] = protolist;
}
}
// 取出类对象的 class_rw_t
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);
}
```
观察到采用 `i--` 的方式,当 while 循环结束的时候,方法数组 `mlists` 保存了全部分类中的方法,属性数组 `proplists` 保存了全部分类中的属性,协议数组 `protolists` 保存了所有分类所遵循的协议。
可以看到通过传入的类对象 `cls` 调用其 `cls->data()` 方法,找到对应的类 `class_rw_t` 信息,里面存放:方法列表、属性列表、协议列表信息。
```c
// 类对象结构体
struct objc_class : objc_object {
// Class ISA;
Class superclass;
}cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
}
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
```
可以看到最后调用 `attachLists`,内部实现如下
```c
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]));
}
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]));
}
}
```
其中关键函数 `memmove` 代表将 __src 中的前 __len 个字节长度移动到 __dst 中去。
```c++
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
```
等价于
```c++
memmove(类对象原来的方法列表 + addedCount,
类对象原来的方法列表,
oldCount * sizeof(array()->lists[0]))
memcopy(类对象原来的方法列表,
所有分类的方法列表,
addedCount * sizeof(array()->lists[0]))
```
其中,`array()->lists` 代表类对象原来的方法列表、`oldCount * sizeof(array()->lists[0])` 代表类对象原来方法列表长度,`addedCount` 代表 category 方法列表长度。
c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
`memmove` 的效果是,将类原来的方法列表移动到第 n个n为 category 方法列表长度位置前面空出n个坑位预留坑位给所有分类的方法
`memcopy` 效果将 Category 方法列表拷贝到类原方法列表的前面去。位置刚好是 `memmove` 留出的坑位。
过程如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png)
结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。
效果为 `[PersonWork Category 方法列表, PersonStudy Category 方法列表, Person类自身对象方法列表]`
假设 Person 有 m1、m2 对象方法Person+Study 分类有 m3、m4方法Person+Sleep 分类有 m5、m6方法(Person+Sleep 先编译)合并后的方法列表是 [m5, m6, m3, m4, m1, m2]
总结:
Category 编译之后 底层结构为 struct category_t里面存储着分类的对象方法、类方法、属性、协议信息
程序运行的时候runtime 会将 Category 中的数据,合并到类自身信息中(类对象、元类对象)
QA:
1. Runtime 在将 category 方法和类方法合并的时候,以前版本是
```objective-c
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
```
而新版本为
```objective-c
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]
```
为什么要改?给出专业和权威的回答,尽量参考官方文档
- 线程安全与原子性
旧方案的潜在问题旧版本直接在原内存区域操作memmove 后接 memcpy若此时其他线程访问方法列表可能读到不一致的中间状态如部分旧数据被覆盖、部分新数据未写入导致崩溃或逻辑错误
新方案的改进:新版本通过 分配新内存、完整构建新数组,最后一次性更新指针(如 array()->count 和 array()->lists使得操作具备原子性。其他线程要么看到完整的旧数组要么看到完整的新数组避免了中间状态的不一致性
- 内存拓展的可靠性
`relloc` 的结果不可预测,可能失败或返回新地址,导致原指针失效。若运行时其他模块持有旧指针的引用,会引发野指针问题
新版本显式分配新内存malloc将旧数据迁移到新内存后替换指针。这种方式更可控避免了 `realloc` 的不确定性,同时允许旧内存的安全释放
- 内存重叠与顺序控制
虽 `memmove` 允许源和目标内存重叠,但在原数组基础上直接扩展时,若内存无法原地扩展(如后续内存被占用),`memmove` 可能覆盖有效数据或越界,导致不符合预期的行为
- 手动复制的灵活性
面向未来,方便插入一些其他逻辑。
2. 将 Category 信息和类自身信息合并放到运行时有什么好处?为什么不放到编译时搞定?
- 动态性。
- 延迟绑定Objective-C 的方法调用基于消息转发Messaging方法实现可以在运行时动态绑定。Category 的方法在启动时被合并到类的方法列表中,使得开发者可以在不修改原始类代码的情况下,动态扩展或替换方法。
- 支持热更新与插件化:通过 dlopen 或动态库加载,可以在程序运行时加载新的 Category如插件功能实现功能扩展而无需重新编译主程序。
- 解决多 Category 方法冲突的灵活性
当多个 Category 定义了同名方法时,最后被加载的 Category 方法会覆盖之前的方法。这种策略虽然简单,但需要运行时动态合并:
编译时无法确定 Category 的加载顺序(例如依赖动态库的加载顺序)。
运行时可以通过调整加载顺序实现方法覆盖的灵活控制。
如果放到编译时处理的潜在优势与取舍
如果选择编译时合并 Category可能带来
- 性能提升:方法查找可能更快(无需运行时遍历 Category 列表)。
- 更强的类型安全:编译时能检查所有方法冲突。
但代价是:
- 丧失动态能力如热修复、Method Swizzling、插件化等场景将无法实现。
- 代码僵化:所有扩展必须在编译时确定,无法适应动态环境(如大型应用的模块化延迟加载)
Objective-C 将 Category 合并推迟到运行时是为了最大化语言的动态性和灵活性支持热更新、AOP、插件化等高级场景。这种设计符合其“动态消息语言”的定位但牺牲了部分编译时优化机会。选择编译时或运行时处理本质是灵活性与性能/安全性的权衡,而 Objective-C 明确选择了前者。
### QA
#### 为什么二维数组打了引号?
`method_array_t` 中存储了 `method_list_t``method_list_t` 的元素为 `method_list_t` 为什么不直接称它为二维数组?
严格来说,不是二维数组,只不过是 array 里添加的对象也是 array且各个数组不等长也不会补空。
为什么需要设计为这样的结构?
调用方法,比如调用 load 是 runtime 加载的时候找到方法地址直接调用的。普通方法走的是消息机制根据对象方法还是类方法会根据isa 找类对象(对象方法)和元类对象(对象方法)信息中先从 cache中找方法是否有没有再通过方法二维数组查找二维是因为类存在分类分类可能也有方法没找到则通过 superclass 继续找父类对象或者父元类对象继续找,找到则执行并给当前类的 cache 方法散列表缓存下来。找到NSObject 还是没找到则走消息转发机制,起死回生几个阶段
另外 load 调用会根据编译顺序决定如果遇到某个类存在父类则先调用父类的load、再执行子类的 load。category 会按照编译顺序runtime 会给方法进行重新组合顺序,源码显示最后 category 的方法会排到最前面。
本类的 class method List 和 category methodList 都是 arraycategory List 会插在本类 method List 前面,匹配到 category method List 同名方法,本类就不调用了,看着有点像被"覆盖"了。
#### 分类中可以写属性吗?
不可以。从源码角度来讲,查看分类的 category_t 结构体可以看到没有 `const ivar_list_t * ivars;` ,所以 category 声明属性底层只会生成 setter、getter 方法声明,没有实现。需要程序员利用 runtime 关联属性自己实现
同理,分类中也不可以添加成员变量,下面代码会报错。
```objective-c
@interface Person (Study)<NSCopying>
{
int _age;
}
@end
```
从代码设计角度来讲,假设一个 Person 类只有1个 `_age` 成员变量,其内存布局在编译阶段就可以确定。内存布局大概为:
```objective-c
struct Person_IMPL {
Class isa;
int _age;
}
```
但 Category 是苹果利用 Runtime 实现的,是运行期动态修改 `class_rw_t` 决定的。
#### 为什么分类中的方法需要放在类自身方法列表的开头?
查看源码为什么分类中的方法需要放在类自身方法列表的开头?因为需要优先保证 Category 中的方法优先被调用。
#### 分类中存在同名方法存在什么问题
对象调用方法的时候会根据对象的 isa 指针,找到类对象方法列表,然后查找方法,由于分类方法在方法列表的前面,类自身方法在方法列表的后面,所以当优先找到分类方法实现的时候就停止查找了,给人的感受就是,方法”被覆盖了 “
Demo: 为 Person 类创建2个 Category分别存在同名方法 study具有不同实现。在控制器的手势事件中打印方法列表
```objective-c
- (void)displayMethodName:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
free(methodList);
NSLog(@"className: %@, methodNames: %@", NSStringFromClass(cls) , methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.p = [[Person alloc] init];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.p study];
[self displayMethodName:[Person class]];
}
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryMethodOrderExplore.png" style="zoom:25%">
可以看到 sayHi 方法存在多个,但是由于 Category 同名的方法在方法列表的前面,所以类自身的方法实现”被覆盖了“(根据 isa 查找方法实现的时候,优先查找到 Category 的方法实现,则停止查找了)
#### 2个分类存在同名方法谁先调用
- 分类方法优先级高于类自身方法
- 同样是分类方法由编译顺序决定哪个方法会被调用XcodeBuild Phases -> Compile Sources编译顺序越后面的方法优先被调用
Demo: 为 Person 类创建2个 Category分别存在同名方法 study具有不同实现。探索编译顺序决定方法实现
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo1.png" style="zoom:25%">
2个对比实验
让 `Person+Study` 参与后编译
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo2.png" style="zoom:25%">
让 `Person+Learn` 参与后编译
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo3.png" style="zoom:25%">
## 拓展Extension
### 文件特征
- 只存在一个文件
- 命名方式“类名_拓展名.h”
```
#import "类名.h"
@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end
```
### 拓展的作用
1. 为类增加额外的属性、成员变量、方法声明
2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建
3. 一般私有的属性和方法写到类拓展中
4. 和 Category 类似,但是小括号里面没有拓展的名字
5. 拓展里面的属性和方法,会在编译阶段将相关数据和类本身合并(分类 Category 在编译期无法确定,只有在运行时才会合并到类对象、元类对象的属性、方法列表中去)
### 拓展的局限性
1. Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。
2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错
3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。
```objectivec
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END
//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
self.skills = @"iOS && Web && Node && Hybrid";
self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
NSLog(@"%@",self.skillStacks);
}
@end
```
4. 不能为系统类添加拓展
### QA
Category 的实现原理是什么?
Category 编译之后的底层实现是 struct category_t里面存储着分类的对象方法、类方法、属性、协议信息。
程序运行过程中runtime 会将 Category 的数据,合并到类信息中(类对象、元类对象)
Category 和 Class Extension 的区别是什么?
Class Extension 在编译的时候,它的数据就已经包含在类信息中。
Category 是在运行时,才将数据合并到类信息中(类对象、元类对象)
## 总结
1. Category 能拓充实例方法、类方法、协议、属性(属性虽然不可以直接拓展,利用 Runtime 关联属性可以实现)、不能拓展成员变量(包含成员变量会报错)
2. 如果 Category 中声明了1个属性那么 Category 只会生成 setter 和 getter 的声明,不会有实现
3. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
4. Category 具有运行时决议的特点。为某个类添加的 Category 被 runtime 加载后,原本来的方法,可能会被优先找到分类中的方法实现,而“覆盖”(假的覆盖,只是优先查找到 category 的方法实现的时候,原始类的方法查找中止而已)
5. Category 可以为系统类添加分类,而拓展不行。
关于第3点我们可以查看源代码印证下。去 opensource 下载 objc4
OC 入口函数`_objc_init`
```objectivec
void _objc_init(void)
{
    // ...
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
```
之后注册各种镜像,那么 map_images 哪里来的?
```objectivec
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
// ...
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
```
_read_images 方法内部会调用 remethodizeClass
```objectivec
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// ...
    if (cls->isRealized()) {
        remethodizeClass(cls);
// ...
}
```
remethodizeClass 内部会调用 attachCategories
```objectivec
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
```
attachCategories 会调用 attachLists
```objectivec
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));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// 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--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
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);
}
```
attachLists 内部会调用 realloc、memmove、memmcpy
```objectivec
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]));
}
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]));
}
}
```
最后会把类对象、元类对象、分类二元数组整体处理,结果为最后编译的分类在整合后数组的最前面,也就是为什么说分类和原类存在同名方法时,会被覆盖,且最后编译的分类的方法实现是会被调用的原因。
- 通过 Runtime 加载某个类所有的 Category
- 所有的 Category 方法、属性、协议数据,合并到一个大数组中,后面参与编译的 Category 数据,会放在数组前面
- 合并后的 Category 数据(属性、方法、协议)插入到类原来数据的前面(比如class_rw_t->methods)
## 底层剖析 load 方法
假设一个 Person 类存在2个 Category分别是 Person+Work、Person+Study都有对象方法 play当实例对象调用 play 的时候,会调用最后编译的 Category Person+Study里的 play 实现。但为什么 load 不是调用最后一个 Category 的 +load而是3个都调用
Demo 验证
```objectivec
@interface Person : NSObject
@end
@interface Student : Person
@end
@interface Student (Good)
@end
@interface Student (Bad)
@end
// 其中每个类都存在3个方法
+ (void)load{
NSLog(@"%s", __func__);
}
+ (void)initialize{
NSLog(@"%s", __func__);
}
- (void)test{
NSLog(@"%s", __func__);
}
// Test
Student *st = [[Student alloc] init];
2022-04-16 01:35:22.237692+0800 Main[8752:2908124] +[Person load]
2022-04-16 01:35:22.238305+0800 Main[8752:2908124] +[Student load]
2022-04-16 01:35:22.238450+0800 Main[8752:2908124] +[Student(Good) load]
2022-04-16 01:35:22.238562+0800 Main[8752:2908124] +[Student(Bad) load]
2022-04-16 01:35:22.238664+0800 Main[8752:2908124] +[Person initialize]
2022-04-16 01:35:22.238733+0800 Main[8752:2908124] +[Student(Bad) initialize]
2022-04-16 01:35:22.238794+0800 Main[8752:2908124] -[Student(Bad) test]
```
### 为什么 load 方法不是按照 Category 编译顺序倒序调用 load 方法?
看源代码 Objc4我的版本是 objc4-838.1
```c
// objc-os.mm
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
// Objc 启动会调用该方法,内部会调用 _dyld_objc_notify_register。其中 load_images 方法用于加载模块
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();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
// objc-runtime-new.mm
// load_images 方法中最后会调用 call_load_methods 方法,用于调用全部的 +(void)load 方法
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();
}
// objc-loadmethod.mm
/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first.
* Category +load methods are not called until after the parent class's +load.
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
* (a) there are more classes to load, OR
* (b) there are some potential category +loads that have
* still never been attempted.
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
*
* Locking: loadMethodLock must be held by the caller
* All other locks must not be held.
**********************************************************************/
// 在调用 call_load_methods 方法内部,会先调用类的 +load 方法,再调用分类的 +load
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
// 先调用全部类的 +load
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
// 再调用分类的 +load
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
// 该方法用于调用全部类的 +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;
// 从可加载的类数组 classes 中取出当前类的 method.这个类是 loadable_class 结构体只有2个成员cls 和 method所以该 method 就是 +load 方法
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 方法,而不是采用发消息的形式
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
typedef void(*load_method_t)(id, SEL);
struct loadable_class {
Class cls; // may be nil
IMP method;
};
struct loadable_category {
Category cat; // may be nil
IMP method;
};
/***********************************************************************
* call_category_loads
* Call some pending category +load methods.
* The parent class of the +load-implementing categories has all of
* its categories attached, in case some are lazily waiting for +initalize.
* Don't call +load unless the parent class is connected.
* If new categories become loadable, +load is NOT called, and they
* are added to the end of the loadable list, and we return TRUE.
* Return FALSE if no new categories became loadable.
*
* Called only by call_load_methods().
**********************************************************************/
// 该方法用于调用分类的 +load 方法
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
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;
// 从可加载的分类数组 cats 中取出当前分类,这个类是 loadable_category 结构体只有2个成员cat 和 method所以该 method 就是 +load 方法
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));
}
// 直接调用 Category 的 +load不是采用发消息的方式
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
// Compact detached list (order-preserving)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[i];
} else {
shift++;
}
}
used -= shift;
// Copy any new +load candidates from the new list to the detached list.
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i];
}
// Destroy the new list.
if (loadable_categories) free(loadable_categories);
// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}
return new_categories_added;
}
```
阅读源码可以得出2个结论
1. `+load` 方法是系统启动后,系统主动调用的;
2. 在调用 `+load` 方法的时候,系统会先调用(可加载)类的 `+load` 方法,再调用分类的 `+load` 方法(先调用 `call_class_loads` 再调用 `call_category_loads`)
3. `call_class_loads`、`call_category_loads` 方法内部实现,是通过 `loadable_class` `loadable_category` 结构体的 method 成员值 ,通过 `load_method_t load_method = (load_method_t)classes[i].method` 找到 `+ load` 方法地址。最后直接调用 `(*load_method)(cls, SEL_load)` 方法本身,没有采用消息机制。
### 为什么总是父类的 +load 比子类先执行?
无论在 Xcode 中怎么调节编译顺序,发现总是父类的 +load 比子类先执行,为什么?还是从源码角度白盒探究下。
梳理下思路:关于 +load 的执行顺序,类的 +load 比分类先执行,这一点认知是拉齐的吧。那我们聚焦下,看看类里面父类、子类这样的情况是如何调用 +load 的 。
```c++
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, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
```
我们在类的 +load 执行方法 `call_class_loads` 里看到,方法内部说顺序遍历并执行 +load 的。看到 `loadable_classes` 很可疑。它是不是决定了父类、子类的顺序?
```c++
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();
}
```
发现在调用 `call_load_methods` 方法之前,有个 `prepare_load_methods` 方法。看名字,感觉像是在做调用 +load 方法前的准备工作。
```c++
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 做一些前置定制动作
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *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
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
// 递归调用,传入当前类的父类
schedule_class_load(cls->getSuperclass());
// 将 cls 添加到 loadable_classes 数组的最后面
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
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());
}
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));
}
// 将
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
```
通过源码,我们可以看出:
- 系统会通过 `schedule_class_load` 方法,保证优先调用当前类的全部父类的加入到 `loadable_classes`,然后将当前类加入到 `loadable_classes`
- 最后执行 +load 的时候会按照 `loadable_classes` 里的顺序,依次调用 +load 方法
顺便看看分类的调用顺序是怎么控制的?
```c++
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *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
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
```
可以看到通过 `schedule_class_load` 处理完类之后,分类是直接通过 `_getObjc2NonlazyCategoryList(mhdr, &count)` 获取的,之后也是直接添加到 `loadable_categories` 中去。
举个例子:
存在下面 4个类
```objective-c
Class Person : NSObject
Class Person+Good : NSObject
Class Person+Bad : NSObject
Class Student : Person
Class Student+Good : Person
Class Student+Bad : Person
Class Cat: NSObject
Class Dog: NSObject
```
如果在 Xcode 的 Build Phases 中,顺序为
```objective-c
Cat.m
Dog.m
Student+Good.m
Student+Bad.m
Student.m
Person+Good.m
Person+Bad.m
Person.m
运行后打印为:
- Cat load
- Dog load
- Person load
- Student load
- Student+Good + load
- Student+Bad + load
- Person+Good load
- Person+Bad load
```
如果在 Xcode 的 Build Phases 中,顺序为
```objective-c
Cat.m
Student+Good.m
Student+Bad.m
Student.m
Person+Good.m
Person+Bad.m
Person.m
Dog.m
运行后打印为:
- Cat load
- Person load
- Student load
- Student+Good + load
- Student+Bad + load
- Person+Good load
- Person+Bad load
- Dog load
```
### +load 总结
- `+load` 方法是系统通过 Runtime 在加载类、分类的时候调用的
- 每个类、分类的 `+load` 在程序运行过程中只调用1次
- 调用顺序方面:
- 先调用类的 `+load` 方法
- 存在继承关系的话,调用子类的 `+load` 之前会调用父类的 `+load` 方法Runtime 会保证好,先调用父类的 `+load` ,再调用子类的 `+load`
- 不存在继承关系的话,会按照编译顺序调用 `+load`(先编译的先调用,编译顺序参考 Xcode 的 Build Phases -> Compile Sources 中的顺序)
- 再调用分类的 `+load` 方法
- 按照编译顺序调用 `+load`(先编译的先调用)
- 全局数组 `loadable_classes`,确保后续运行时能按正确顺序(父类 → 子类 → 分类)调用这些方法
### QA
1.Category 中有 `+load` 方法吗?`+load` 调用时机是什么时候?`+load` 可以继承吗?
Category 存在 `+load` 方法。`+load` 是系统在启动阶段通过 runtime 来加载、准备(通过递归的手段保证,如果当前类存在父类,则会加入到 `loadable_classes` 中去),然后先调用类的 `+load`,再调用分类的 `+load`。
`+load` 可以继承。但是这个问法背后的想法有点神奇,因为继承的本质是面向对象,类继承了,方法会重写逻辑。但是 iOS 官方文档说 `+load` 适合做和本类相关的逻辑。所以这个继承就显得不那么合理。但是可以继承的,比如下面的代码:
```objective-c
@interface Person : NSObject
@end
@implementation Person
+ (load) {
NSLog(@"Person +load");
}
@end
@interface Student : Person
@end
@implementation Person
@end
[Student load];
// console
Person +load
```
可以看到,我们主动调用 `[Student load]` 由于 Student 自身没有实现 `+load`,由于存在继承,所以调用了 Person 的 `+load` 。手动调用 `+load` 本质上就是给 runtime 消息机制,等价于 `objc_msgSend([Student class], @selector(load))` ,通过 Student 类对象的 isa找到 Student 元类对象,判断有没有类方法 `+load`,发现没有,则根据 Student 元类对象的 `superclass` 找到父类的元类对象,也就是 Person 的元类对象,发现有 `+load` 实现,即调用了 `+load` ,打印 `Person +load`
2.为什么 load 方法打印顺序是这样的?
因为调用 student alloc相当于发送了消息。则肯定先执行 load 方法。类在 Runtime 启动阶段会调用 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面。load 方法在本质上是执行 `call_load_methods`,方法地址是确定的(查看下面的源代码可以发现 `load` 方法是在编译期就可以确定的)。不走 `objc_msgSend` 这套流程。所以先打印父类 load、再打印子类 load、最后打印分类 load。如果存在多个分类则按照编译顺序打印 load。
## 底层剖析 Initialize 方法
上 Demo
```objectivec
@interface Person : NSObject
@end
@interface Student : Person
@end
@interface Student (Good)
@end
```
`Person *p1 = [[Person alloc] init];` 这句代码输出什么?
这个比较简单initialize 方法在类第一次收到消息的时候调用。所以输出 `+[Person initialize]`
`Student *st = [[Student alloc] init];` 输出什么?
```objectivec
+[Person initialize]
+[Student(Go od) initialize]
```
查看分类在 Runtime 加载类信息时候的调用原理可以知道分类中的类方法、对象方法都会被加载原始类的前面去initialize 是类方法)如下图:
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png)
### 为什么给子类发消息,父类和子类的 +initialize 都会被调用?且父类的先调用
梳理下目前掌握的信息:当类第一次接收消息,也就是第一次调用对象方法的时候,该类的 `+initialize` 方法会被调用。所以需要从 objc_msgSend 或者获取对象方法的角度去查看源码。聚焦下,以 `class_getInstanceMethod` 方法为入口,分析源码
```c++
/***********************************************************************
* class_getInstanceMethod. Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
```
内部调用的是 `lookUpImpOrForward`
```c++
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
checkIsKnownClass(cls);
// 关注这里
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
```
` cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);` 看上去是实现 initialize 相关逻辑的。
```c++
/***********************************************************************
* realizeAndInitializeIfNeeded_locked
* Realize the given class if not already realized, and initialize it if
* not already initialized.
* inst is an instance of cls or a subclass, or nil if none is known.
* cls is the class to initialize and realize.
* initializer is true to initialize the class, false to skip initialization.
**********************************************************************/
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
runtimeLock.assertLocked();
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 需要初始化并且没有被初始化过,则执行 if 里面的逻辑
if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
return cls;
}
```
`realizeAndInitializeIfNeeded_locked` 内部判断,当 class 需要被初始化且没有初始化过的时候则执行 `initializeAndLeaveLocked`
```c++
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
return initializeAndMaybeRelock(cls, obj, lock, true);
}
/***********************************************************************
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
* inst is an instance of cls, or nil. Non-nil is better for performance.
* Returns the class pointer. If the class was unrealized then
* it may be reallocated.
* Locking:
* runtimeLock must be held by the caller
* This function may drop the lock.
* On exit the lock is re-acquired or dropped as requested by leaveLocked.
**********************************************************************/
static Class initializeAndMaybeRelock(Class cls, id inst,
mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
ASSERT(cls->isRealized());
if (cls->isInitialized()) {
if (!leaveLocked) lock.unlock();
return cls;
}
// Find the non-meta class for cls, if it is not already one.
// The +initialize message is sent to the non-meta class object.
Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// Realize the non-meta class if necessary.
if (nonmeta->isRealized()) {
// nonmeta is cls, which was already realized
// OR nonmeta is distinct, but is already realized
// - nothing else to do
lock.unlock();
} else {
nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
// runtimeLock is now unlocked
// fixme Swift can't relocate the class today,
// but someday it will:
cls = object_getClass(nonmeta);
}
// runtimeLock is now unlocked, for +initialize dispatch
ASSERT(nonmeta->isRealized());
initializeNonMetaClass(nonmeta);
if (leaveLocked) runtimeLock.lock();
return cls;
}
```
重点在 `initializeNonMetaClass` 方法中
```c++
/***********************************************************************
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
ASSERT(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
supercls = cls->getSuperclass();
// 存在父类,并且父类没有被初始化,则递归调用 initializeNonMetaClass
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
// Try to atomically set CLS_INITIALIZING.
SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
// Grab a copy of the will-initialize funcs with the lock held.
localWillInitializeFuncs.initFrom(willInitializeFuncs);
}
}
if (reallyInitialize) {
// We successfully set the CLS_INITIALIZING bit. Initialize the class.
// Record that we're initializing this class so we can message it.
_setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {
// LOL JK we don't really call +initialize methods after fork().
performForkChildInitialize(cls, supercls);
return;
}
for (auto callback : localWillInitializeFuncs)
callback.f(callback.context, cls);
// Send the +initialize message.
// Note that +initialize is sent to the superclass (again) if
// this class doesn't implement +initialize. 2157218
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
objc_thread_self(), cls->nameForLogging());
}
// Exceptions: A +initialize call that throws an exception
// is deemed to be a complete and successful +initialize.
//
// Only __OBJC2__ adds these handlers. !__OBJC2__ has a
// bootstrapping problem of this versus CF's call to
// objc_exception_set_functions().
#if __OBJC2__
@try
#endif
{
callInitialize(cls);
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
objc_thread_self(), cls->nameForLogging());
}
}
#if __OBJC2__
@catch (...) {
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: +[%s initialize] "
"threw an exception",
objc_thread_self(), cls->nameForLogging());
}
@throw;
}
@finally
#endif
{
// Done initializing.
lockAndFinishInitializing(cls, supercls);
}
return;
}
else if (cls->isInitializing()) {
// We couldn't set INITIALIZING because INITIALIZING was already set.
// If this thread set it earlier, continue normally.
// If some other thread set it, block until initialize is done.
// It's ok if INITIALIZING changes to INITIALIZED while we're here,
// because we safely check for INITIALIZED inside the lock
// before blocking.
if (_thisThreadIsInitializingClass(cls)) {
return;
} else if (!MultithreadedForkChild) {
waitForInitializeToComplete(cls);
return;
} else {
// We're on the child side of fork(), facing a class that
// was initializing by some other thread when fork() was called.
_setThisThreadIsInitializingClass(cls);
performForkChildInitialize(cls, supercls);
}
}
else if (cls->isInitialized()) {
// Set CLS_INITIALIZING failed because someone else already
// initialized the class. Continue normally.
// NOTE this check must come AFTER the ISINITIALIZING case.
// Otherwise: Another thread is initializing this class. ISINITIALIZED
// is false. Skip this clause. Then the other thread finishes
// initialization and sets INITIALIZING=no and INITIALIZED=yes.
// Skip the ISINITIALIZING clause. Die horribly.
return;
}
else {
// We shouldn't be here.
_objc_fatal("thread-safe class init in objc runtime is buggy!");
}
}
```
可以看到,存在父类,并且父类没有被初始化,则递归调用 `initializeNonMetaClass`。如果不存在父类或者父类已经初始化了,则继续走下面的逻辑。会调用 `callInitialize` 方法
```c++
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
```
可以看到,调用 initialize 方法的本质是通过 runtime 消息机制的能力。
至此,可以解释 Demo 中的现象了。
再举个例子
```objective-c
class Student: Person
class Person: NSObject
class Person+Good: NSObject
class Person+Bad: NSObject
class Teacher: Person
```
其中 Student、Teacher 都没有实现 initialize 方法。只有 Person 实现了、Person+Good 和 Person+Bad(编译顺序较后) 也实现了 initialize 方法.
调用
```objective-c
[Student alloc]
[Teacher alloc]
```
打印输出
```objective-c
- Person+Bad initialize
- Person+Bad initialize
- Person+Bad initialize
```
为什么输出这样的信息?
`[Student alloc]` 等价于 `objc_msgSend([Student Class], @sel(initialize))` 然后 `objc_msgSend` 汇编实现里会调用 `lookUpImpOrForward``lookUpImpOrForward` 内部会调用 `initializeNonMetaClass`,其内部实现会判断是否存在父类,存在父类且父类没有调用过 `initialize`,则会递归先调用当前类的父类的 `initialize` 方法。递归结束则调用 `callInitialize` 方法去完成当前类的 `initialize`
所以上述代码底层类似于
```Objective-c
if (Student 类没有初始化过) {
    if (Student 父类Person 父类存在,但存在分类,也就是 Person+Bad存在 && 父类没有初始化) {
    objc_msgSend(Person + Bad@selector(initializ)) // Person+Bad initialize
    }
objc_msgSend([Student class]@selector(initializ))  // Student 没有 initialize 方法,则根据 isa 查找父类方法。打印 Person+Bad initialize  
}
if (Teacher 类没有初始化过) {
// Person 已经 initialize 过if 里的逻辑进不去
    if (Teacher 父类Person 父类存在,但存在分类,也就是 Person+Bad存在 && 父类没有初始化) {
    objc_msgSend(Person + Bad@selector(initializ))
    }
objc_msgSend([Teacher class]@selector(initializ))  // Teacher 没有 initialize 方法,则根据 isa 查找父类方法。打印 Person+Bad initialize  
}
```
### 为什么 `initialize`方法存在“覆盖”的情况?
有个现象:打印了 Student Category 的 `initialize`,却没有打印 Student 自己的 `initialize`,为什么?
查看 objc 源码发现调用 `initialize` 方法本质上就是通过 runtime 的 `objc_msgSend ` 发消息来实现的。也就是通过 isa 指针查看类对象或元类对象的方法列表中(方法列表中包含当前类各个 Category 的各个方法)查找方法实现。所以当找到方法列表中排列较前的 `initialize` 时,就不再继续查找方法实现了,也就出现了 `initialize` 被覆盖的情况了。
```c++
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
```
### `+initialize` 总结
`+initialize` 和 `+load` 最大区别是 `+initialize` 是通过 `objc_msgSend` 进行调用的
- 调用方式:`load` 根据函数地址直接调用,`initialize` 是根据 `objc_msgSend` 调用的
- 调用时刻load 是 runtime 加载类、分类的时候调用的。initialize 是在类第一次接收消息的时候调用的。每个类只会 initialize 一次,但是父类的 initialize 可能会调用多次(比如子类第一次接收消息,但是子类内部没有实现 `+initialize`,由于消息机制,父类如果实现了 `+initialize` 则会被调用。该父类至少调用2次
- 调用顺序:
- load先调用类的 load先编译的类优先调用 load、调用子类的 load会先调用父类的 load、再调用分类的 load先编译的分类优先调用 load
- 如果子类没有实现 `+initialize` 则会调用父类的 `+initialize`(所以父类的 `+initialize` 可能会被调用多次)
- 如果分类实现了 `+initialize`,就会覆盖类本身的 `+initialize` 调用
查看源码,伪代码如下:
```objective-c
if (自己没有初始化) {
    if (父类存在 && 父类没有初始化) {
    objc_msgSend(父类,@selector(initializ))
    }
objc_msgSend(子类,@selector(initializ))    
}
```
## 为 Category 实现属性的 Setter 和 Getter
为一个类写一个 `@property nonatomic, strong) NSString *name` 编译器会做3件事情
- 生成成员变量 `_name`
- 在 `.h` 中生成 getter、setter 声明,即 `-(void)setName:(NSString *)name`、`-(NSString *)name`
- 在 `.m` 中生成 setter、getter 的实现
```objective-c
-(void)setName:(NSString *)name {
_name = name;
}
-(NSString *)name {
return _name;
}
```
但是为一个分类写 `@property nonatomic, strong) NSString *name` 编译器会做1件事情
- 在 `.h` 中生成 getter、setter 声明,即 `-(void)setName:(NSString *)name`、`-(NSString *)name`
所以在 Category 中增加分类,需要我们自己实现,利用关联对象的技术。
| **objc_AssociationPolicy** | **对应的修饰符** |
| --------------------------------- | ----------------- |
| OBJC_ASSOCIATION_ASSIGN | assign |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
| OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
| OBJC_ASSOCIATION_RETAIN | strong, atomic |
| OBJC_ASSOCIATION_COPY | copy, atomic |
```objectivec
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Student)
@property (nonatomic, strong) NSString *studyNumber;
@end
NS_ASSUME_NONNULL_END
#import "Person+Student.h"
#import <objc/message.h>
@implementation Person (Student)
- (void)sayHi {
NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
}
/*
* 传统的做法是在 setter 里面这样写
_studyNumber = studyNumber;
ARC 自动管理内存
MRC
[_studyNumber release];
[studyNumber retain];
_studyNumber = studyNumber;
但是在 Category里面不会生成对应的实例变量因此我们可以利用 Runtime 为我们的 category 关联属性的值
setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
getter:objc_getAssociatedObject(self, @selector(firstView));
}
*/
- (void)setStudyNumber:(NSString *)studyNumber {
objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
, OBJC_ASSOCIATION_RETAIN);
}
//@selector(studyNumber)
- (NSString *)studyNumber {
return objc_getAssociatedObject(self, @selector(studyNumber));
}
@end
```
说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式。
给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。
比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候,可以这样命名 LR_Title。请查看下面的代码
```Objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (Title)
@property (nonatomic, copy) NSString *LR_title;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+Title.h"
#import <objc/runtime.h>
@implementation NSURL (Title)
- (void)setLR_title:(NSString *)LR_title
{
objc_setAssociatedObject(self, @selector(LR_title), LR_title
, OBJC_ASSOCIATION_RETAIN);
}
- (NSString *)LR_title
{
return objc_getAssociatedObject(self, @selector(LR_title));
}
@end
```
QA
1. 为什么 `category_t` 结构体里的属性命名为 `instanceProperties`? `classProperties` 何时使用?
普通的 property 就是 instanceProperties而用 `@property (class, nonatomic, copy) NSString *name;` class 修饰的就是 `classProperties`。
类属性是 Objective-C 在 Xcode 8 / LLVM 3.0 后引入的特性,旨在提供一种更简洁的方式声明与类相关的全局状态或行为。
属性描述中有 `class` 表明这是类属性,通过类名直接访问(如 ClassName.defaultName而非实例属性。
类属性存储在类的元类metaclass与实例属性完全隔离。
尽管声明方式类似实例属性,类属性不会自动生成存取方法或存储,需开发者手动实现。
```
// Person.h
@interface Person : NSObject
@property (class, nonatomic, copy) NSString *defaultName;
@end
// Person.m
@implementation Person
static NSString *_defaultName = nil;
+ (NSString *)defaultName {
return _defaultName;
}
+ (void)setDefaultName:(NSString *)defaultName {
_defaultName = [defaultName copy]; // 执行 copy 操作
}
@end
// 使用
// 设置类属性
Person.defaultName = @"Anonymous";
// 获取类属性
NSString *name = Person.defaultName;
```
类属性的使用场景:
- 为类定义全局可访问的默认值,例如应用的主题颜色、默认语言等。`@property (class, nonatomic, strong) UIColor *appThemeColor;`
- 单例模式的替代方案.若只需一个简单的共享实例,类属性可以替代传统单例模式。
2. 为什么要给 Category 添加的属性只有属性的 setter、getter却没有成员变量和对应的 setter、getter 实现,为什么这么设计?存在什么优点
为什么不能直接添加成员变量运行时限制Runtime 在程序加载时就确定了类的内存布局,包括成员变量的存储结构,如果允许在运行时动态添加成员变量,可能会破坏类的内存布局,导致兼容性问题
编译时的静态性:成员变量的存储结构需要在编译时确定,而 Category 是一种运行时机制,无法在编译时修改类的结构
设计哲学OC 的设计哲学强调动态性,而动态性体现在方法上,而不是成员变量上。成员变量的静态性有助于保持类的稳定性和可预测性。
Objective-C 的 Category 设计允许动态添加方法,但不允许直接添加成员变量,这是基于运行时机制的限制和语言设计哲学的考虑。这种设计的优点在于灵活性和扩展性,同时避免了对类内存布局的破坏。通过关联对象等机制,开发者可以在 Category 中实现类似成员变量的功能,从而充分利用运行时的动态性。
## 关联对象的底层实现
技术实现的几个核心类:
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
查看 objc 源码
```c++
// objc-runtime.mm
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
```
简化版
```c++
class AssociationsManager {
static AssociationsHashMap *_map;
}
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
class ObjcAssociation {
uintptr_t _policy;
id _value;
}
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object); // 指针地址,按位取反
// 新值有值
if (new_value) {
// break any existing association.
// 从全局的 AssociationsHashMap 中根据对象地址按位取反后的结果为 key查找对应的 ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
// 如果找到对应的 ObjectAssociationMap
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 从 ObjectAssociationMap 中,根据 key 查找有没有值
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) { // 有值,则说明该 key 之前设置过关联对象,本次就做更新
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else { // 没有值,则给 ObjectAssociationMap 中添加一个 ObjcAssociation 类型的值。ObjcAssociation 由 policy 和 newValue 构成
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// 如果没有找到则说明第一次为该对象设置关联对象,本次需要创建一个新的 ObjectAssociationMap
ObjectAssociationMap *refs = new ObjectAssociationMap;
// AssociationsHashMap 中以对象地址为 keyObjectAssociationMap 为 value新增一条数据
associations[disguised_object] = refs;
// ObjectAssociationMap 中以关联对象传入的 key 为 keyvalue 为 ObjcAssociation 为 value 插入一条数据。ObjcAssociation 由 policy 和 newValue 组成
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
// 没有 newValue 则说明需要将 AssociationHashMap 中对象地址对应 ObjectAssociationMap 中 key 对应的的 value 移除
AssociationsHashMap::iterator i = associations.find(disguised_object);
// 以对象地址为 key从 AssociationsHashMap 中找到了 ObjectAssociationMap
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
// 从 ObjectAssociationMap 中找到了 key 记录
if (j != refs->end()) {
// 从 ObjectAssociationMap 中移除 ObjectAssociation
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
```
梳理后,如下图所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%">
AssociationsManager 管理的 AssociationsHashMap 结构如下:
```objective-c
{
// Person 实例对象的地址
"0x4927298732": {
"@selector(studyNumber)的地址" : {
"value": "2022122201",
"policy": "retain"
},
"@selector(title)的地址": {
"value": "Hello category",
"policy": "retain"
}
},
// UIView 实例对象的地址
"0x3666444222": {
"@selector(backgroundColor)"的地址 : {
"value": "0xff0021",
"policy": "retain"
}
}
}
```
## Category 的使用场景
### 给现有类添加方法
拓展功能,最基础的
### 将类的实现代码分散到多个分类中
类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件中。可以通过 Category 机制,把类代码按照逻辑划分到几个分区中。
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的category里
- 可以由多个开发者共同完成一个类
- 可以按需加载想要的category等等
比如 Person 类
```objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, copy, readonly) NSArray *friends;
- (instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
#pragma mark - FriendShip methods
- (void)addFriend:(Person *)person;
- (void)removeFriends:(Person *)person;
- (BOOL)isFriendsWith:(Person *)person;
#pragma mark - Work methods
- (void)performDaysWork;
- (void)takeVacationFromWork;
#pragma mark - Play methods
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
NS_ASSUME_NONNULL_END
```
拆分后
```objective-c
// Person.h
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, copy, readonly) NSArray *friends;
- (instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
@end
// Person+FriendShip.h
@interface Person(FriendShip)
- (void)addFriend:(Person *)person;
- (void)removeFriends:(Person *)person;
- (BOOL)isFriendsWith:(Person *)person;
@end
// Person+Work.h
@interface Person(Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
// Person+Play.h
@interface Person(Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
```
使用分类后,依然可以把整个类都定义在一个接口文件中,但把实现写到 Category 中。但如果分类越来越多或者觉得不合理,可以将相关 API 也放到 Category 的 `.h` 中。
### 声明私有方法
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" />
### UI 和点击事件处理的代码聚合
UIAlertView 的点击后的判断是通过 delegate 进行处理的。一个看上去看起来还好,但假如一个类里面存在多个 AlertView 呢。是不是就需要在代理方法里面判断 alertView 属于哪个buttonIndex 属于哪个? UIAlertView 的视图创建和点击事件的处理放在2个地方阅读起来不方便。
```objective-c
- (void)showAlertView {
UIAlertView *alertView = [UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
[alertView show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
[self handleCancel];
} else {
[self handleContinue];
}
}
```
接下去利用关联对象优化下代码
```objective-c
static void *AlertViewDialogHandlerKey = "AlertViewDialogHandlerKey";
- (void)showAlertView {
UIAlertView *alertView = [UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
void(^clickHandler)(NSInteger index) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {
[self handleCancel];
} else {
[self handleContinue];
}
};
objc_setAssociatedObject(alertView, AlertViewDialogHandlerKey, clickHandler, OBJC_ASSOCIATION_COPY)
[alertView show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void(^clickHandler)(NSInteger buttonIndex) = objc_getAssociatedObject(self, AlertViewDialogHandlerKey);
clickHandler(buttonIndex);
}
```