iOS 面试题(一)
一、对象
1. id 和 NSObject *的区别?
🌾 id 是struct objc_object结构体指针,可以指向任何OC对象,理解为万能指针。
特点
- 泛型对象指针:可以指向 任何 Objective-C 对象(无论它继承自哪个类)。
- 不进行编译时类型检查:
- 编译器不会警告
id
调用不存在的方法(运行时可能崩溃)。 - 允许调用任意方法,适合动态语言特性(如消息转发)。
- 编译器不会警告
- 默认指向
NSObject
子类:- 虽然
id
可以指向任何对象,但通常用于NSObject
派生类。
- 虽然
- 不需要强制类型转换:
- 可以直接赋值给任意对象类型,无需显式转换。
场景
- 动态方法调用(如
performSelector:
)。 - 代理(Delegate)和匿名对象(如
UITableViewDelegate
)。 - JSON 解析、KVC 等运行时场景。
示例
id anything = @"Hello"; // 可以是 NSString anything = @123; // 也可以是 NSNumber [anything length]; // 编译通过,但运行时可能崩溃(NSNumber 没有 length 方法)
🌾 NSObject *
(静态类型,限定为 NSObject 及其子类)
特点
- 限定类型:只能指向
NSObject
或其子类对象(如NSString *
,NSArray *
)。 - 编译时类型检查:
- 调用
NSObject
不存在的方法会 直接报编译警告。 - 更安全,但灵活性较低。
- 调用
- 需要强制类型转换:
- 如果赋值给更具体的类型(如
NSString *
),可能需要强制转换。
- 如果赋值给更具体的类型(如
场景
- 明确类型时(如
NSString *
,NSArray *
)。 - 需要编译器检查,避免调用不存在的方法。
- 与 C 代码交互(
NSObject *
更接近 C 指针风格)。
示例
NSObject *obj = @"Hello"; // OK,NSString 是 NSObject 子类 // [obj length]; // 编译报错:NSObject 没有 length 方法 NSString *str = (NSString *)obj; // 需要强制转换 NSLog(@"%lu", str.length); // 正确
2. id类型为什么无法用点语法
id类型无法确定所指的类型,无法查找setter、getter。
3. Null、nil、Nil、NSNull
🌾 NULL
(C 语言的空指针)
定义:NULL 是 C 语言中的宏,表示空指针((void *)0)。
场景:
- 用于 C 语言指针(如 int *ptr = NULL;)。
- 在 Objective-C 中,通常用于 Core Foundation 对象(如 CFStringRef str = NULL;)。
示例:
int *intPtr = NULL; CFArrayRef cfArray = NULL;
🌾 nil
(Objective-C 对象的空值)
定义:nil
表示 Objective-C 对象指针为空,等同于 (id)0
。
- 用于 Objective-C 对象(如
NSString *str = nil;
)。 - 调用
nil
对象的方法不会崩溃(返回0
或nil
)。
示例:
NSString *name = nil; [name length]; // 返回 0,不会崩溃
🌾 Nil
(Objective-C 类的空值)
定义:Nil
表示 Objective-C 类指针为空,等同于 (Class)0
。
- 用于 类对象(如
Class myClass = Nil;
)。 - 较少使用,通常用于动态检查类是否存在。
示例:
Class someClass = Nil; if (someClass == Nil) { NSLog(@"Class is nil"); }
🌾 NSNull
(表示“空”的 Objective-C 对象)
定义:NSNull
是一个 单例对象([NSNull null]
),用于在集合(如 NSArray
、NSDictionary
)中表示“空值”。
场景:
- 集合不能存储
nil
(nil
表示数组或字典的结束),所以用NSNull
代替nil
。 - 常见于 JSON 解析 或 数据存储 时表示“空值”。
示例:
NSArray *array = @[@"Hello", [NSNull null], @"World"]; if (array[1] == [NSNull null]) { NSLog(@"This is a null placeholder"); }
🍯 拓展
-
nil
和NULL
的区别?nil
用于 Objective-C 对象,NULL
用于 C 指针。
在 OC 中,推荐用nil
代替NULL
表示对象为空。
-
为什么
NSArray
不能存nil
?NSArray 使用 nil 作为 终止标记(类似 C 字符串的 \0),所以不能直接存储 nil,要用 [NSNull null] 代替。
-
[NSNull null]
是单例吗?是的,
[NSNull null]
返回全局唯一的NSNull
实例,可以用==
比较。
-
如何判断
NSNull
?if (obj == [NSNull null]) { // 这是一个 NSNull 对象 }
4. ==、 isEqualToString、isEqual区别?
🌾 ==
(指针比较)
作用
- 比较两个对象的指针地址(是否指向同一块内存)。
- 不关心对象的内容,仅判断是否是同一个实例。
- 检查两个变量是否引用 同一个对象实例。
- 适用于
单例模式
或需要精确内存地址匹配的情况。
示例
NSString *str1 = @"Hello"; NSString *str2 = @"Hello"; NSString *str3 = str1; NSLog(@"%d", str1 == str2); // 0(可能为 0 或 1,取决于字符串驻留优化) NSLog(@"%d", str1 == str3); // 1(同一对象)
📌 对于NSString
,编译器可能对相同的字面量字符串进行优化(字符串驻留),此时==
可能返回1
,但 不可依赖此行为。
🌾 isEqualToString:
(字符串内容比较)
作用
- 专用于
NSString
,比较两个字符串的 字符内容是否完全相同。 - 比
isEqual:
更高效,因为直接针对字符串优化。
- 需要精确比较两个
NSString
或NSMutableString
的内容时。
示例
NSString *str1 = @"Hello"; NSString *str2 = [NSString stringWithFormat:@"Hello"]; NSLog(@"%d", [str1 isEqualToString:str2]); // 1(内容相同) NSLog(@"%d", [str1 isEqualToString:@"hello"]); // 0(大小写敏感)
📌 必须确保比较对象是NSString
,否则会崩溃(先检查类型或使用isEqual:
)。
🌾 isEqual:
(对象逻辑相等比较)
作用
- 通用比较方法(来自
NSObject
协议),比较两个对象的 逻辑相等性。 - 默认行为与
==
相同(比较指针),但许多类(如NSString
、NSArray
)会重写它来比较内容。
场景
- 需要比较任意对象的内容(如集合类、自定义类)。
- 支持跨类比较(如
NSString
与NSMutableString
)。
示例
NSString *str1 = @"Hello"; NSMutableString *str2 = [NSMutableString stringWithString:@"Hello"]; NSNumber *num = @123; NSLog(@"%d", [str1 isEqual:str2]); // 1(内容相同) NSLog(@"%d", [str1 isEqual:num]); // 0(类型不同)
🍯 自定义类实现
- 若需自定义比较逻辑,重写
isEqual:
和hash
方法:
- (BOOL)isEqual:(id)object { if (self == object) return YES; if (![object isKindOfClass:[MyClass class]]) return NO; return [self.property isEqual:((MyClass *)object).property]; } - (NSUInteger)hash { return [self.property hash]; }
❓常见问题
isEqual:
和 hash
的关系?
- 若两个对象
isEqual:
返回YES
,它们的hash
值必须相同(反之不一定成立)。 - 在集合类(如
NSSet
)中,hash
用于快速查找。
为什么 isEqualToString:
比 isEqual:
快?
isEqualToString:
直接比较字符串底层存储(如CFString
),而isEqual:
需要额外的类型检查。
如何实现大小写不敏感的比较?
NSString *str1 = @"Hello"; BOOL isEqual = [str1 caseInsensitiveCompare:@"hello"] == NSOrderedSame;
5. iOS中内省的几个方法?
🔊 在 iOS 开发中,内省(Introspection) 是指程序在运行时动态检查对象类型、方法、属性等信息的能力。
🌾检查对象类型
-
作用:判断对象是否是指定类或其子类的实例。isKindOfClass:
示例:
id obj = @"Hello"; if ([obj isKindOfClass:[NSString class]]) { NSLog(@"obj 是 NSString 或其子类"); }
-
作用:判断对象是否是指定类的实例(不包括子类)。isMemberOfClass:
示例:
id obj = @"Hello"; if ([obj isMemberOfClass:[NSString class]]) { NSLog(@"obj 是 NSString 的直接实例"); }
-
作用:判断对象是否实现了某个协议。conformsToProtocol:
示例:
if ([obj conformsToProtocol:@protocol(NSCopying)]) { NSLog(@"obj 实现了 NSCopying 协议"); }
🌾 检查方法是否存在
-
respondsToSelector:
作用:判断对象是否能响应某个方法(包括继承的方法)。
示例:if ([obj respondsToSelector:@selector(length)]) { NSLog(@"obj 可以调用 length 方法"); }
-
作用:判断类的实例是否能响应某个方法。instancesRespondToSelector:
(类方法)
示例:
if ([NSString instancesRespondToSelector:@selector(length)]) { NSLog(@"NSString 的实例可以调用 length 方法"); }
🌾 动态获取类信息
-
作用:class:获取对象的类。 superclass:获取对象的父类。class
/superclass
示例:
NSLog(@"对象类名: %@", [obj class]); NSLog(@"父类名: %@", [NSString superclass]); // 输出 NSObject
-
作用:通过字符串获取类对象(更底层的方式)。objc_getClass
(Runtime API)
示例:
Class cls = objc_getClass("NSString"); NSLog(@"类名: %@", cls);
🌾 检查属性与方法列表(Runtime)
-
作用:动态获取类的属性或方法列表(需导入 <objc/runtime.h>)。class_copyPropertyList
/class_copyMethodList
示例:
unsigned int count; objc_property_t *properties = class_copyPropertyList([obj class], &count); for (unsigned int i = 0; i < count; i++) { NSLog(@"属性名: %s", property_getName(properties[i])); } free(properties); // 释放内存
🌾 Swift 中的内省方法
is
(类型检查)
if obj is String { print("obj 是 String 类型") }
-
as?
/as!
(类型转换)if let str = obj as? String { print("转换成功: \(str)") }
-
Mirror
(反射)let mirror = Mirror(reflecting: obj) for child in mirror.children { print("属性名: \(child.label ?? ""), 值: \(child.value)") }
7. 深拷贝和浅拷贝
1、浅拷贝是引用的复制
2、深拷贝是值的复制,会新建一个对象
3、copy出来的对象是不可变类型,mutableCopy出来的对象是可变类型
8. @synthesize, @dynamic, readonly关键字
在 Objective-C 中,@synthesize
、@dynamic
和 readonly
是与属性(@property
)相关的关键字,它们分别用于控制属性的存储、访问方式以及读写权限。以下是它们的详细说明和区别:
🌾 @synthesize
作用
- 自动生成属性的存取方法(getter/setter)和实例变量(ivar)。
- 如果没有显式使用
@synthesize
,编译器默认会自动添加@synthesize propertyName = _propertyName
(即生成_propertyName
实例变量)。
场景
- 需要自定义实例变量名时(如
@synthesize name = _customName
)。 - 早期代码中显式指定实例变量(现代 Objective-C 通常省略)。
示例
// 显式指定实例变量名 @synthesize name = _customName; // 默认行为(现代代码通常省略) // @synthesize name = _name;
📌 如果同时重写了 getter 和 setter,编译器不会自动合成实例变量,需手动 @synthesize
或声明 ivar。
🌾 @dynamic
作用
- 告诉编译器不自动生成存取方法,开发者需在运行时动态提供(如通过动态方法解析或消息转发)。
- 常用于 Core Data 或动态代理模式。
场景
- 属性存取方法由父类、运行时或动态机制(如 KVC)提供时。
- 避免编译器警告(如未实现的存取方法)。
示例
@dynamic name; // 不生成 getter/setter,需在运行时处理 // 动态方法解析(可选的补救措施) - (void)dynamicMethod { class_addMethod([self class], @selector(name), (IMP)dynamicGetter, "@@:"); }
id dynamicGetter(id self, SEL _cmd) { return @"Dynamic Value"; }
注意
-
如果没有实现动态解析,调用
@dynamic
属性的方法会引发unrecognized selector
崩溃。
🌾 readonly
作用
- 将属性声明为只读,编译器只生成 getter 方法,不生成 setter。
- 通常用于保护数据不被外部修改。
场景
- 公开不可变数据(如模型对象的 ID)。
- 结合类扩展(Class Extension)实现私有可写。
示例
// 头文件(公开只读) @interface Person : NSObject @property (nonatomic, copy, readonly) NSString *personID; @end // 实现文件(类扩展中重新声明为可写) @interface Person () @property (nonatomic, copy, readwrite) NSString *personID; @end @implementation Person // 内部可修改 _personID @end
📌 通过类扩展(@interface Person ()
)可将readonly
改为readwrite
,实现对外只读、对内可写。
9. NSString类型为什么要用copy修饰 ?
主要防止NSString被修改。
当NSString的赋值来源是NSString时,strong和copy作用相同。
当NSString的赋值来源是NSMutableString,copy会做深拷贝重新生成一个新的对象,修改赋值来源不会影响NSString的值。
10. int * const p 、int const *p 、const int *p 、 const int * const p
🌾 int * const p
(常量指针)
含义
p
是一个常量指针,指向一个int
类型变量。- 指针本身不可修改(即
p
不能指向其他地址),但 指向的值可以修改。
示例
int a = 10; int b = 20; int * const p = &a; // p 必须初始化,且不能重新赋值 *p = 30; // ✅ 可以修改指向的值 // p = &b; // ❌ 编译错误:p 是常量指针,不能修改
🌾 int const *p
或const int *p
(指向常量的指针)
含义
p
是一个指针,指向一个const int
(常量整数)。- 指针可以修改(即
p
可以指向其他地址),但 指向的值不可修改。
示例
int a = 10; int b = 20; const int *p = &a; // 或 int const *p = &a; p = &b; // ✅ 可以修改指针指向 // *p = 30; // ❌ 编译错误:*p 是常量,不能修改
📌int const *p
和const int *p
完全等价,只是写法不同。
🌾 const int * const p
(指向常量的常量指针)
含义
p
是一个常量指针,且指向一个常量整数。- 指针和指向的值均不可修改。
示例
int a = 10; int b = 20; const int * const p = &a; // p 必须初始化 // *p = 30; // ❌ 编译错误:*p 是常量 // p = &b; // ❌ 编译错误:p 是常量指针
记忆技巧
const
在*
左侧 → 值不可修改(如const int *p
)。const
在*
右侧 → 指针不可修改(如int * const p
)。- 两侧都有
const
→ 全部不可修改(如const int * const p
)。
11. @public、@protected、@private、@package 声明各有什么含义?
🌾 @public
(完全公开)
任何代码 都可以直接访问该变量(不推荐,破坏封装性)。
@interface Person : NSObject { @public NSString *_name; // 公开变量 } @end // 外部直接访问 Person *p = [Person new]; p->_name = @"Alice"; // ✅ 允许
🌾 @protected
(受保护,默认)
当前类及其子类 可直接访问。
@interface Person : NSObject { @protected NSInteger _age; // 受保护变量(默认可省略) } @end @interface Student : Person - (void)printAge { NSLog(@"%ld", _age); // ✅ 子类可访问 } @end // 外部访问会报错 Person *p = [Person new]; // p->_age = 20; // ❌ 编译错误
🌾 @private
(私有)
仅当前类内部 可直接访问,子类和外部均不可访问。
@interface Person : NSObject { @private NSString *_secretID; // 私有变量 } - (void)showSecret { NSLog(@"%@", _secretID); // ✅ 当前类可访问 } @end @interface Student : Person - (void)tryAccessSecret { // NSLog(@"%@", _secretID); // ❌ 子类不可访问 } @end
🌾 @package
(框架内公开)
同一框架(库)内 可访问,框架外不可访问(适用于 SDK 开发)。
// 在框架(MyFramework)中定义 @interface MyClass : NSObject { @package NSString *_internalData; } @end // 同一框架内的代码可访问 MyClass *obj = [MyClass new]; obj->_internalData = @"Framework Internal"; // 框架外的代码不可访问 // obj->_internalData = @"Hello"; // ❌ 编译错误
12. static有什么作用?
🌾 修饰局部变量(函数/方法内)
作用:
- 使局部变量的生命周期延长至 整个程序运行期间(存储在静态存储区)。
- 变量只初始化一次,后续调用保留上一次的值。
示例:
- (void)incrementCounter { static int count = 0; // 只初始化一次 count++; NSLog(@"Count: %d", count); } // 调用 [self incrementCounter]; // 输出 1 [self incrementCounter]; // 输出 2(保留上次值)
用途:
- 统计方法调用次数。
- 实现单次初始化(如配合
dispatch_once
)。
🌾 修饰全局变量(文件内)
作用:
-
将全局变量或函数的作用域限制在 当前文件(内部链接),避免被其他文件访问。
示例:
// File: MyClass.m static NSString * const kInternalSecret = @"Private"; // 仅当前文件可见 @implementation MyClass - (void)logSecret { NSLog(@"%@", kInternalSecret); // ✅ 可访问 } @end // File: OtherClass.m extern NSString *kInternalSecret; // ❌ 无法访问其他文件的 static 变量
用途:
- 隐藏模块私有常量或工具函数。
- 避免全局命名冲突。
🌾 模拟类静态变量(Objective-C 无原生支持)
作用:
-
通过
static
全局变量 + 类方法模拟类静态变量(所有实例共享数据)。
示例:
// MyClass.m static int _sharedCount = 0; // 模拟类静态变量 @implementation MyClass + (void)incrementSharedCount { _sharedCount++; } + (int)sharedCount { return _sharedCount; } @end // 使用 [MyClass incrementSharedCount]; NSLog(@"%d", [MyClass sharedCount]); // 输出 1
📌 需手动管理线程安全(如加锁或使用
OSAtomic
)。
🌾 单例模式(结合
dispatch_once
)
作用:
-
确保静态变量只初始化一次,实现线程安全的单例。
示例:
+ (instancetype)sharedInstance { static MyClass *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[MyClass alloc] init]; }); return instance; }
二、类
1. final关键字有什么用
🌾 final
在 Swift 中的作用
在 Swift 中,final
用于禁止 类被继承 或 方法/属性被重写:
final class
:该类不能被继承。final func
或final var
:方法或属性不能被子类重写。
示例:
final class MyClass {} // 不能继承 MyClass class Parent { final func doSomething() {} // 不能重写该方法 }
🌾 Objective-C 中的替代方案
🔊 由于 Objective-C 是动态语言,没有直接的final
关键字,但可以使用用__attribute__((objc_subclassing_restricted))
(Xcode 12+ 支持)
作用:禁止类被继承(类似 Swift 的 final class
)。
示例:
__attribute__((objc_subclassing_restricted)) @interface MyFinalClass : NSObject @end @interface SubClass : MyFinalClass // ❌ 编译错误:Cannot subclass a class with objc_subclassing_restricted @end
❓ 为什么 Objective-C 不直接支持
final
?动态性:Objective-C 是动态语言,允许运行时方法替换(如 Method Swizzling),强制限制继承会违背其设计哲学。
历史原因:Objective-C 的设计更倾向于灵活性而非严格限制。
2. 实例方法和类方法的区别?
1、实例方法能访问成员变量。
2、类方法中必须创建或者传入对象才能调用对象方法。
3、实例方法存储在类对象的方法列表里,类方法存在于元类的方法列表里。
4、类方法可以和对象方法重名。
3. 如果在类方法中调用self会发生什么?
访问到的是类对象而不是实例对象。可以通过self调用其他的类方法,但是无法调用对象方法。
4. 讲一下对象,类对象,元类结构体的组成以及他们是如何相关联的?
在 Objective-C 中,对象、类对象和元类构成了一个三层结构,理解它们的组成和关联关系是掌握 Objective-C 运行时机制的关键。
🌾 1. 核心三者的定义与关系
对象(Instance)
定义:通过类实例化得到的个体,存储具体的数据(实例变量)。
内存结构:
struct objc_object { Class isa; // 指向对象的类(类对象) };
特点:每个对象都有一个 isa
指针,指向它的类对象。
类对象(Class)
定义:描述对象的行为(方法列表、属性等),是 objc_class
结构体的实例。
内存结构:
struct objc_class { Class isa; // 指向元类(Meta Class) Class super_class; // 指向父类 method_list_t *methods; // 实例方法列表 ivar_list_t *ivars; // 实例变量列表 property_list_t *properties; // 属性列表 protocol_list_t *protocols; // 协议列表 cache_t cache; // 方法缓存 };
特点:
- 类对象的
isa
指向元类。 - 类对象存储 实例方法,供对象调用。
元类(Meta Class)
定义:类对象的类,存储类方法(+
方法)。
内存结构:与类对象相同(objc_class
),但用途不同。
特点:
- 元类的
isa
指向根元类(Root Meta Class)。 - 元类的
super_class
指向父类的元类。 - 元类存储 类方法,供类对象调用。
🌾 2. 三者的关联方式
对象(Instance)
│
↓ isa
类对象(Class)
│
↓ isa
元类(Meta Class)
│
↓ isa
根元类(Root Meta Class) → isa → 自身(形成闭环)
5. object_getClass、objc_getClass、class方法有什么区别?
在 Objective-C 运行时中,objc_getClass
、object_getClass
和 class
方法都与类相关,但它们的用途和行为有所不同:
🌾 object_getClass
作用:获取对象的 isa
指针,即对象所属的类(或元类)。
参数:一个 Objective-C 对象(id
类型)。
返回值:
- 如果传入的是实例对象,返回它的类对象(Class)。
- 如果传入的是类对象,返回它的元类(Meta Class)。
特点:
- 可以获取实例对象的类,也可以获取类对象的元类。
- 比
[obj class]
更底层,能获取元类信息。
示例:
NSObject *obj = [NSObject new]; Class objClass = object_getClass(obj); // 返回 NSObject 的类对象 Class metaClass = object_getClass([NSObject class]); // 返回 NSObject 的元类
🌾 objc_getClass
作用:根据类名获取类对象(Class 对象)。
参数:类名的字符串(char *
类型)。
返回值:对应的类对象(Class 对象),如果类不存在则返回 nil
。
特点:
- 用于运行时动态获取类对象。
- 只能获取类对象,不能获取元类(Meta Class)。
- 类似于
NSClassFromString
,但NSClassFromString
是 Foundation 提供的 API,而objc_getClass
是更底层的运行时函数。
示例:
Class class = objc_getClass("NSString"); // 获取 NSString 的类对象
🌾 class
方法([obj class]
或[NSObject class]
)
作用:
- 如果是实例对象调用(
[obj class]
),返回对象的类。 - 如果是类对象调用(
[NSObject class]
),返回类本身(即self
)。
返回值:
- 实例调用:返回类对象(Class)。
- 类调用:返回类本身(不会返回元类)。
特点:
- 是 Objective-C 的高层方法,不能获取元类。
- 如果对象重写了
class
方法(如某些代理对象或 KVO 动态生成的类),可能返回不同的结果。
示例:
NSObject *obj = [NSObject new]; Class objClass = [obj class]; // 返回 NSObject 的类对象 Class nsObjectClass = [NSObject class]; // 返回 NSObject 类本身(不是元类)
6. 类与类之间的消息传递,有哪几种方式呢?
在 Objective-C 中,类与类之间的消息传递(即方法调用或通信)主要有以下几种方式:
🌾 performSelector:
动态调用
方式:使用 performSelector:
系列方法在运行时动态调用方法。
特点:
- 适用于运行时决定调用的方法(方法名可以是变量)。
- 可以传递参数(
performSelector:withObject:
)。 - 最多支持 2 个参数(更多参数需用
NSInvocation
)。 - 如果方法不存在,可能会 crash(可先用
respondsToSelector:
检查)。
示例:
SEL method = @selector(sayHello); if ([obj respondsToSelector:method]) { [obj performSelector:method]; } // 带参数 [obj performSelector:@selector(setName:) withObject:@"Alice"];
🌾 NSInvocation
(更灵活的动态调用)
方式:通过 NSInvocation
封装方法调用,支持多参数和返回值。
特点:
- 适用于参数个数不确定或较多的场景。
- 可以获取返回值。
- 代码较复杂,但灵活性高。
示例:
SEL selector = @selector(addNumber:toNumber:); NSMethodSignature *signature = [MyClass instanceMethodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = myObj; invocation.selector = selector; NSInteger arg1 = 10, arg2 = 20; [invocation setArgument:&arg1 atIndex:2]; // 参数从 2 开始 [invocation setArgument:&arg2 atIndex:3]; [invocation invoke]; NSInteger result; [invocation getReturnValue:&result]; // 获取返回值 NSLog(@"Result: %ld", (long)result);
🌾 Objective-C 运行时(Runtime API)
方式:直接使用 objc_msgSend
或相关运行时函数。
特点:
- 最底层的方式,完全动态。
- 需要处理类型转换(在 ARC 下需特别小心)。
- 可以绕过编译期检查,实现黑魔法(如动态替换方法)。
示例:
#import <objc/message.h> // 调用无参数方法 ((void (*)(id, SEL))objc_msgSend)(obj, @selector(sayHello)); // 调用带参数方法 NSString *result = ((NSString * (*)(id, SEL, NSString *))objc_msgSend)(obj, @selector(greet:), @"Alice");
📌 在 ARC 下直接调用objc_msgSend
可能需要强制类型转换或关闭编译器检查(-fno-objc-arc
)。
🌾 代理模式(Delegate)
方式:通过定义 protocol
和 delegate
实现类间通信。
特点:
- 适用于一对一的回调场景(如
UITableViewDelegate
)。 - 需要定义协议和实现代理方法。
示例:
@protocol MyDelegate <NSObject> - (void)didCompleteTask:(NSString *)task; @end @interface ClassA : NSObject @property (nonatomic, weak) id<MyDelegate> delegate; @end // ClassB 实现代理 @interface ClassB : NSObject <MyDelegate> @end // 调用代理方法 [self.delegate didCompleteTask:@"Done"];
🌾 通知中心(NotificationCenter)
方式:通过 NSNotificationCenter
广播消息。
特点:
- 适用于一对多的解耦通信。
- 发送方和接收方不需要直接引用。
示例:
// 发送通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"TaskCompleted" object:nil]; // 接收通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TaskCompleted" object:nil];
🌾 Block / 闭包回调
方式:通过传递 Block 实现回调。
特点:
- 适用于异步操作(如网络请求完成回调)。
- 代码可读性高,但需注意循环引用(
weakself
)。
示例:
// 定义 Block typedef void (^CompletionBlock)(NSString *result); // 调用方 [self doTaskWithCompletion:^(NSString *result) { NSLog(@"Result: %@", result); }]; // 实现方 - (void)doTaskWithCompletion:(CompletionBlock)completion { completion(@"Success"); }
🌾 KVO(键值观察)
方式:通过 addObserver:forKeyPath:options:context:
监听属性变化。
特点:
- 适用于监听对象属性的变化。
- 需手动实现
observeValueForKeyPath:ofObject:change:context:
。
示例:
// 监听 [obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; // 回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) { NSLog(@"Name changed to %@", change[NSKeyValueChangeNewKey]); } }
7. copy assign retain weak关键字
🌾 copy
作用
- 用于修饰 不可变对象(如
NSString
、NSArray
、NSDictionary
等)。 - 在
setter
方法中,会 复制传入的对象(调用copy
方法),生成一个新的不可变对象。
场景
- 防止外部可变对象修改影响属性值(如
NSMutableString
赋值给NSString
)。 - 适用于需要保持独立性的数据(如深拷贝)。
示例
@property (nonatomic, copy) NSString *name; // 推荐 NSString 用 copy // 赋值时: NSMutableString *mutableName = [NSMutableString stringWithString:@"Alice"]; self.name = mutableName; // 实际会调用 [mutableName copy],生成不可变 NSString [mutableName appendString:@"Bob"]; // 不影响 self.name(仍然是 "Alice")
注意
- 如果修饰 可变对象(如
NSMutableString
),赋值后会变成 不可变对象(因为copy
返回的是不可变副本)。 - 如需真正的可变拷贝,需用
mutableCopy
(但属性修饰符仍为copy
)。
🌾 assign
作用
- 用于修饰 基本数据类型(如
int
、float
、BOOL
、NSInteger
等)。 - 不进行引用计数管理,直接赋值(类似于简单的指针赋值)。
场景
- 基本数据类型(非对象类型)。
- MRC 时代可用于对象,但在 ARC 下不推荐(有野指针风险)。
示例
@property (nonatomic, assign) NSInteger age; // 基本数据类型用 assign @property (nonatomic, assign) CGFloat height; // CGFloat 也是基本类型
注意:
不要用于 OC 对象,因为对象释放后不会自动置 nil
,可能导致野指针 crash:
@property (nonatomic, assign) NSObject *unsafeObj; // 错误!对象释放后访问会崩溃
🌾 weak
作用
- 用于修饰 OC 对象,表示 弱引用(不增加引用计数)。
- 对象被释放后,自动置为
nil
(避免野指针)。
场景
- 解决循环引用问题(如
delegate
、block
内部引用self
)。 - 父子对象关系(如
child.parent
通常用weak
避免循环)。
示例
@property (nonatomic, weak) id<MyDelegate> delegate; // delegate 用 weak // Block 内避免循环引用: __weak typeof(self) weakSelf = self; [self doSomethingWithCompletion:^{ [weakSelf handleResult]; // 防止强引用 self }];
📌 只能修饰 OC 对象,不能用于基本数据类型。
三、分类
1. category 的实现原理,如何被加载的
🌾 Category 的本质
Category(分类)是 Objective-C 中用于扩展已有类的机制,可以在不修改原始类的情况下添加方法、属性(关联对象)、协议等。它的底层实现依赖于 运行时(Runtime)。
- 编译时:Category 会被编译成
struct category_t
结构体,存储新增的方法、属性、协议等信息。 - 运行时:通过 Runtime 动态合并到原始类中。
🌾 Category 的底层结构(
category_t
)
在 objc-runtime-new.h
中,category_t
的定义如下:
struct category_t { const char *name; // 分类的名称(如 "MyClass+Category") classref_t cls; // 对应的原始类 struct method_list_t *instanceMethods; // 实例方法列表 struct method_list_t *classMethods; // 类方法列表 struct protocol_list_t *protocols; // 协议列表 struct property_list_t *instanceProperties; // 实例属性列表 };
🌾 Category 的加载过程
Category 的加载发生在 Runtime 初始化时(_objc_init
),具体步骤如下:
-
读取 Mach-O 文件中的 Category 数据
在程序启动时,dyld(动态链接器)加载可执行文件后,Runtime 会读取 Mach-O 文件的__objc_catlist
段,获取所有的 Category 信息。 -
调用
attachCategories
合并到类Runtime 调用
attachCategories(cls, cats, flush_caches)
方法,将 Category 的方法、属性、协议合并到原始类中。
合并顺序:- 方法:Category 的方法会 追加到原始类的方法列表前面(后编译的 Category 方法优先级更高)。
- 协议:合并到类的协议列表。
- 属性:仅关联对象(
objc_setAssociatedObject
),不会自动生成成员变量和 setter/getter。
-
方法列表重组
合并后的方法列表会被重新整理,Category 的方法会 覆盖 原始类的同名方法(但不会真正替换,只是由于方法查找顺序优先访问 Category 的方法)。
🌾 Category 的特点
-
方法覆盖:如果 Category 和原始类有同名方法,Category 的方法会 优先调用(因为方法列表顺序靠前)。
-
无成员变量:Category 不能直接添加成员变量(
ivar
),但可以通过 关联对象(Associated Object) 间接实现。 -
加载顺序影响优先级:后编译的 Category 方法优先级更高(因为后加载的方法会插入到方法列表前面)。
🌾 Category 的常见用途
-
扩展方法:给现有类添加新方法(如
NSString+Utils
)。 -
拆分代码:将大型类按功能拆分成多个 Category。
-
实现协议:让类额外遵循某个协议(如
UITableViewDelegate
)。 -
关联对象:通过
objc_setAssociatedObject
添加“伪属性”。
🌾 源码分析(简化版)
// 伪代码:attachCategories 的核心逻辑 static void attachCategories(Class cls, category_list *cats, bool flush_caches) { // 遍历所有 Category for (uint32_t i = 0; i < cats->count; i++) { category_t *cat = cats->list[i]; // 合并方法 if (cat->instanceMethods) { addMethods(cls, cat->instanceMethods); // 插入到方法列表前面 } // 合并协议 if (cat->protocols) { addProtocols(cls, cat->protocols); } // 合并属性(仅关联对象) if (cat->instanceProperties) { addProperties(cls, cat->instanceProperties); } } }
2. load 与 initialize 方法
🌾 +load
方法
时机
- 在程序启动时(Runtime 加载阶段)调用,早于
main()
函数。 - 所有类和分类(Category)的
+load
方法都会被调用,即使类未被使用。 - 调用顺序:
- 父类的
+load
方法(优先于子类)。 - 子类的
+load
方法。 - 分类(Category)的
+load
方法(后编译的分类先调用)。
- 父类的
特点
- 自动调用,无需手动触发。
- 线程安全,由 Runtime 保证调用顺序。
- 适合在
+load
里执行全局初始化(如 Method Swizzling)。 - 不会触发
+initialize
(即使调用了[self class]
)。
示例
// 父类 @implementation ParentClass + (void)load { NSLog(@"ParentClass +load"); } @end // 子类 @implementation ChildClass + (void)load { NSLog(@"ChildClass +load"); } @end // 分类 @implementation ChildClass (Category) + (void)load { NSLog(@"ChildClass (Category) +load"); } @end
输出:
ParentClass +load ChildClass +load ChildClass (Category) +load
🌾 +initialize
方法
调用时机
- 在类第一次被使用时调用(如调用类方法、实例化对象)。
- 只会调用一次(即使多次使用该类)。
- 调用顺序:
- 父类的
+initialize
(如果父类未初始化)。 - 子类的
+initialize
。 - 分类会覆盖类的
+initialize
(如果分类实现了,则原类不会调用)。
- 父类的
特点
- 懒加载,只有在类被使用时才会触发。
- 线程安全,Runtime 使用锁保证只执行一次。
- 适合做类级别的初始化(如静态变量初始化)。
- 如果子类未实现
+initialize
,会调用父类的+initialize
(类似继承)。
示例
// 父类 @implementation ParentClass + (void)initialize { NSLog(@"ParentClass +initialize"); } @end // 子类 @implementation ChildClass // 未实现 +initialize,会调用父类的 @end // 分类 @implementation ChildClass (Category) + (void)initialize { NSLog(@"ChildClass (Category) +initialize"); } @end
调用方式:
[ChildClass class]; // 触发 +initialize
输出:
ChildClass (Category) +initialize
📌 由于分类实现了+initialize
,原类的+initialize
不会执行。
🌾 关键结论
+load
的执行顺序:
- 父类 → 子类 → 分类(后编译的分类先调用)。
- 与是否被使用无关,一定会执行。
+initialize
的执行顺序:
- 父类 → 子类(如果子类未实现,会调用父类的)。
- 分类会覆盖原类的
+initialize
。
+load
比 +initialize
更早执行:
+load
在 Runtime 加载阶段调用,+initialize
在类首次使用时调用。
+load
适合做 Method Swizzling:
- 因为它在所有类加载完成后调用,可以安全替换方法。
+initialize
适合初始化静态变量:
- 由于懒加载特性,避免不必要的初始化。
❓常见问题如果多个分类都实现了
+load
,调用顺序是怎样的?按照编译顺序,后编译的分类的 +load 方法先调用(因为后加载的 Category 会插入到方法列表前面)。
+initialize
会被继承吗?如果子类没有实现 +initialize,会调用父类的 +initialize(类似方法继承)。
+load
里调用 [self class]
会触发 +initialize
吗?不会,+load 的执行早于 +initialize 的触发机制。
四、分类
1. OC是动态运行时语言是什么意思?
动态类型:运行时确定对象的类型,编译时期能通过,但不代表运行过程中没有问题
id obj = @"Hello"; [obj addObject:@"World"]; // 编译通过,但运行时崩溃(NSString 没有 addObject: 方法)
id unknownObj = [SomeClass new]; // 动态检查类型 if ([unknownObj isKindOfClass:[NSString class]]) { NSLog(@"It's a string!"); } else { NSLog(@"Not a string."); } // 动态检查方法 if ([unknownObj respondsToSelector:@selector(someMethod)]) { [unknownObj someMethod]; }
动态绑定:运行时才确定对象调用的方法(消息转发)
// 动态添加方法 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(dynamicMethod)) { class_addMethod(self, sel, (IMP)dynamicIMP, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } // 转发给其他对象 - (id)forwardingTargetForSelector:(SEL)aSelector { if ([alternateObject respondsToSelector:aSelector]) { return alternateObject; } return [super forwardingTargetForSelector:aSelector]; } // 完整转发 - (void)forwardInvocation:(NSInvocation *)invocation { if ([alternateObject respondsToSelector:invocation.selector]) { [invocation invokeWithTarget:alternateObject]; } else { [super forwardInvocation:invocation]; } }
动态加载:动态库的方法实现不拷贝到程序中,只记录引用,直到使用相关方法的时候才到库里面查找方法实现
常用方式
dlopen
/dlsym
:手动加载动态库并获取符号地址。@import
或#import <Framework/Header.h>
:系统自动处理动态库加载。
// 手动加载动态库 void *handle = dlopen("libMyLibrary.dylib", RTLD_LAZY); if (handle) { void (*myFunction)(void) = dlsym(handle, "myFunction"); if (myFunction) { myFunction(); // 调用动态库中的函数 } dlclose(handle); }
2. runtime能做什么?
🌾 获取类的成员变量、方法、协议
#import <objc/runtime.h> // 获取类的成员变量 unsigned int ivarCount; Ivar *ivars = class_copyIvarList([MyClass class], &ivarCount); for (unsigned int i = 0; i < ivarCount; i++) { Ivar ivar = ivars[i]; NSLog(@"成员变量名:%s", ivar_getName(ivar)); } free(ivars); // 获取类的方法 unsigned int methodCount; Method *methods = class_copyMethodList([MyClass class], &methodCount); for (unsigned int i = 0; i < methodCount; i++) { Method method = methods[i]; NSLog(@"方法名:%@", NSStringFromSelector(method_getName(method))); } free(methods); // 获取类的协议 unsigned int protocolCount; Protocol * __unsafe_unretained *protocols = class_copyProtocolList([MyClass class], &protocolCount); for (unsigned int i = 0; i < protocolCount; i++) { Protocol *protocol = protocols[i]; NSLog(@"协议名:%s", protocol_getName(protocol)); } free(protocols);
🌾 为类添加成员变量、方法、协议
-
动态添加方法
// 定义一个方法实现 void dynamicMethodIMP(id self, SEL _cmd) { NSLog(@"动态添加的方法被调用了!"); } // 在运行时添加方法 class_addMethod( [MyClass class], @selector(dynamicMethod), // 方法名 (IMP)dynamicMethodIMP, // 方法实现 "v@:" // 方法类型编码(void, self, _cmd) ); // 调用动态添加的方法 MyClass *obj = [MyClass new]; [obj performSelector:@selector(dynamicMethod)]; // 输出:动态添加的方法被调用了!
用途:
- 动态实现未实现的方法(避免 unrecognized selector 崩溃)。
- 实现懒加载(如某些方法只在特定情况下才实现)。
-
动态交换方法(Method Swizzling)
// 原始方法 - (void)originalMethod { NSLog(@"原始方法"); } // 替换方法 - (void)swizzledMethod { NSLog(@"替换后的方法"); [self swizzledMethod]; // 实际调用原始方法(因为方法名已交换) } // 在 +load 里交换方法 + (void)load { Method originalMethod = class_getInstanceMethod(self, @selector(originalMethod)); Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzledMethod)); method_exchangeImplementations(originalMethod, swizzledMethod); } // 调用 MyClass *obj = [MyClass new]; [obj originalMethod]; // 输出:替换后的方法
用途:
- AOP(面向切面编程),如日志统计、埋点。
- 修复系统 Bug(替换系统方法)。
- 实现 Hook(如 NSURLSession 网络监控)。
-
动态添加属性(关联对象)
由于 Category 不能直接添加成员变量,可以使用objc_setAssociatedObject
实现类似效果:
#import <objc/runtime.h> // 定义关联的 Key static const void *kAssociatedObjectKey = &kAssociatedObjectKey; // 添加属性 objc_setAssociatedObject( obj, // 目标对象 kAssociatedObjectKey, // 关联的 Key @"动态属性值", // 关联的值 OBJC_ASSOCIATION_RETAIN // 内存管理方式 ); // 获取属性 NSString *value = objc_getAssociatedObject(obj, kAssociatedObjectKey); NSLog(@"%@", value); // 输出:动态属性值
用途:
- 为 Category 添加存储属性(如 UIView 添加 tagString)。
- 实现懒加载属性。
-
动态创建类
// 创建一个新类 Class newClass = objc_allocateClassPair([NSObject class], "DynamicClass", 0); // 添加一个方法 class_addMethod(newClass, @selector(dynamicMethod), (IMP)dynamicMethodIMP, "v@:"); // 注册类 objc_registerClassPair(newClass); // 使用新类 id dynamicObj = [newClass new]; [dynamicObj performSelector:@selector(dynamicMethod)]; // 输出:动态添加的方法被调用了!
用途:
- 实现 KVO(系统会动态生成 NSKVONotifying_ClassName 子类)。
- 动态生成代理类(如 NSProxy)。
🌾 动态方法解析与消息转发
如果对象收到未知消息(未实现的方法),Runtime 提供 三次补救机会:
// 动态添加方法实现。 +resolveInstanceMethod: / +resolveClassMethod:
// 将消息转发给其他对象。 -forwardingTargetForSelector:
// 完全自定义消息转发。 -methodSignatureForSelector: + -forwardInvocation:
示例:
// 1. 动态解析 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(missingMethod)) { class_addMethod(self, sel, (IMP)missingMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } // 2. 转发给其他对象 - (id)forwardingTargetForSelector:(SEL)aSelector { if ([alternateObject respondsToSelector:aSelector]) { return alternateObject; } return [super forwardingTargetForSelector:aSelector]; } // 3. 完整转发 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(missingMethod)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([alternateObject respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:alternateObject]; } else { [super forwardInvocation:anInvocation]; } }
用途:
- 避免
unrecognized selector
崩溃。 - 实现多继承(通过消息转发给多个对象)。
🌾 其他 Runtime 应用
-
KVO 实现:动态生成子类并重写
setter
方法。KVO(Key-Value Observing)是 Objective-C 的观察者模式实现,其底层依赖 Runtime 动态生成子类并重写
setter
方法。实现原理
动态生成子类:
当调用addObserver:forKeyPath:
时,Runtime 会动态生成一个NSKVONotifying_ClassName
的子类。重写
setter
方法:
子类会重写被观察属性的setter
,在赋值前后触发通知。修改
isa
指针:
对象的isa
指针会指向新生成的子类,使方法调用走新的逻辑。示例代码
// 观察者 [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; // 回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"age"]) { NSLog(@"年龄变化:%@", change[NSKeyValueChangeNewKey]); } } // 手动触发 KVO(可选) [self.person willChangeValueForKey:@"age"]; _person.age = 25; [self.person didChangeValueForKey:@"age"];
底层模拟(伪代码)
// Runtime 动态生成的子类 @interface NSKVONotifying_Person : Person @end @implementation NSKVONotifying_Person - (void)setAge:(int)age { [self willChangeValueForKey:@"age"]; [super setAge:age]; // 调用父类实现 [self didChangeValueForKey:@"age"]; } @end
-
自动归档/解档:遍历类的属性自动实现
NSCoding
。通过 Runtime 获取类的所有属性,自动实现
encodeWithCoder:
和initWithCoder:
。示例
// 自动归档/解档 @implementation Person - (void)encodeWithCoder:(NSCoder *)coder { unsigned int count; objc_property_t *properties = class_copyPropertyList([self class], &count); for (unsigned int i = 0; i < count; i++) { const char *propertyName = property_getName(properties[i]); NSString *key = [NSString stringWithUTF8String:propertyName]; [coder encodeObject:[self valueForKey:key] forKey:key]; } free(properties); } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super init]) { unsigned int count; objc_property_t *properties = class_copyPropertyList([self class], &count); for (unsigned int i = 0; i < count; i++) { const char *propertyName = property_getName(properties[i]); NSString *key = [NSString stringWithUTF8String:propertyName]; [self setValue:[coder decodeObjectForKey:key] forKey:key]; } free(properties); } return self; } @end
使用
Person *person = [Person new]; person.name = @"Alice"; person.age = 25; // 归档 NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person]; // 解档 Person *decodedPerson = [NSKeyedUnarchiver unarchiveObjectWithData:data]; NSLog(@"%@, %d", decodedPerson.name, decodedPerson.age); // Alice, 25
-
JSON 转 Model:动态获取属性名并映射 JSON 字段。
通过 Runtime 获取类的属性名,与 JSON 的 Key 匹配,实现自动转换。
示例
@implementation NSObject (JSONModel) + (instancetype)modelWithJSON:(NSDictionary *)json { id obj = [[self alloc] init]; unsigned int count; objc_property_t *properties = class_copyPropertyList([self class], &count); for (unsigned int i = 0; i < count; i++) { const char *propertyName = property_getName(properties[i]); NSString *key = [NSString stringWithUTF8String:propertyName]; id value = json[key]; if (value) { [obj setValue:value forKey:key]; } } free(properties); return obj; } @end
使用
NSDictionary *json = @{@"name": @"Bob", @"age": @30}; Person *person = [Person modelWithJSON:json]; NSLog(@"%@, %d", person.name, person.age); // Bob, 30
-
AOP 编程:通过 Method Swizzling 无侵入式修改方法。
示例:统计按钮点击次数
@implementation UIButton (AOP) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method originalMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:)); Method swizzledMethod = class_getInstanceMethod([self class], @selector(swizzled_sendAction:to:forEvent:)); method_exchangeImplementations(originalMethod, swizzledMethod); }); } - (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { NSLog(@"按钮被点击:%@", self); [self swizzled_sendAction:action to:target forEvent:event]; // 调用原始方法 } @end
效果:所有 UIButton 的点击事件都会自动打印日志,无需修改原有代码。
3. class_copyIvarList与class_copyPropertyList的区别?
-
class_copyIvarList可以获取.h和.m中的所有属性以及@interface大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)。
#import <objc/runtime.h> @interface Person : NSObject { NSString *_privateName; // 私有成员变量 } @property (nonatomic, copy) NSString *name; // 属性 @end // 获取所有成员变量 unsigned int ivarCount; Ivar *ivars = class_copyIvarList([Person class], &ivarCount); for (unsigned int i = 0; i < ivarCount; i++) { Ivar ivar = ivars[i]; NSLog(@"成员变量名:%s", ivar_getName(ivar)); } free(ivars);
输出:
成员变量名:_privateName 成员变量名:_name // @property 生成的 _name
-
class_copyPropertyList只能获取由@property声明的属性(包括.m),获取的属性名称不带下划线。
unsigned int propertyCount; objc_property_t *properties = class_copyPropertyList([Person class], &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { objc_property_t property = properties[i]; NSLog(@"属性名:%s", property_getName(property)); } free(properties);
输出:
属性名:name // 只有 @property 声明的属性
4. class_ro_t和class_rw_t的区别?
基本定义
class_ro_t (class read-only):存储类的 编译期确定的原始数据(不可修改)。
class_rw_t (class read-write):存储类的 运行时动态扩展的数据(可修改,如方法列表、协议等)。
核心区别
对比维度 | class_ro_t (只读) | class_rw_t (可读写) |
---|---|---|
数据来源 | 编译期生成,来自 Mach-O 文件的 __objc_classlist 段 |
运行时动态创建,基于 class_ro_t 扩展 |
可变性 | ❌ 不可变(内存固定) | ✅ 可变(Runtime 可修改方法、属性、协议等) |
包含内容 | - 类名、父类、实例大小 - 基础方法列表 - 基础属性列表 - 基础协议列表 - 成员变量布局 |
- 动态添加的方法、属性、协议 - 方法列表的二维数组(分类的方法合并) - 类的状态标志(如是否初始化) |
内存占用 | 较小(仅原始数据) | 较大(包含动态扩展数据) |
生命周期 | 程序启动后一直存在 | 可能在运行时被创建或修改 |
源码结构(简化版)
-
class_ro_t
(只读部分)struct class_ro_t { const char *name; // 类名 const char *superclass; // 父类名 uint32_t instanceSize; // 实例大小 uint32_t flags; // 标志位 const ivar_list_t *ivars; // 成员变量列表 const method_list_t *baseMethodList; // 基础方法列表(编译期) const protocol_list_t *baseProtocols; // 基础协议列表(编译期) const property_list_t *baseProperties; // 基础属性列表(编译期) };
class_rw_t
(可读写部分)
struct class_rw_t { uint32_t flags; // 状态标志(如 RW_REALIZED) uint32_t version; // 版本号 const class_ro_t *ro; // 指向只读部分的指针 method_array_t methods; // 方法列表(含分类的方法) property_array_t properties; // 属性列表(含分类的属性) protocol_array_t protocols; // 协议列表(含分类的协议) Class firstSubclass; // 第一个子类 Class nextSiblingClass; // 下一个兄弟类 };
关键流程
- 编译期:
编译器生成class_ro_t
,存储在 Mach-O 文件的__objc_classlist
段。
- 运行时初始化:
Runtime 读取class_ro_t
,创建class_rw_t
,并将class_ro_t
的指针赋值给class_rw_t->ro
。
- 动态修改:
运行时通过class_addMethod
、class_addProtocol
等函数修改class_rw_t
的内容。
- 方法列表的合并
分类(Category)的方法会被插入到
class_rw_t->methods
中,优先于原始类的方法(后编译的分类方法先调用)。class_ro_t->baseMethodList
仍保留编译期的原始方法,不会被修改。
示例场景
- 分类方法的影响
// 原始类 @interface MyClass : NSObject - (void)originalMethod; @end // 分类 @implementation MyClass (Category) - (void)originalMethod { NSLog(@"分类的方法"); } @end
class_ro_t
:仍保存原始的originalMethod
实现。class_rw_t
:methods
数组中,分类的方法排在前面,因此实际调用分类的方法。
- 动态添加方法
class_addMethod([MyClass class], @selector(newMethod), imp, "v@:");
class_ro_t
:不受影响。class_rw_t
:methods
数组中新增newMethod
。
❓为什么需要两种结构?
性能优化:class_ro_t 是只读的,可存储在干净的内存页(无需写权限),提高访问速度。
内存节省:多数类的数据在运行时不会修改,class_ro_t 共享不变部分。
动态扩展支持:class_rw_t 允许运行时动态修改(如添加方法、协议),而无需重新编译。
五、消息发送
1. 消息机制
Objective-C 的方法调用本质上是 消息发送(Messaging),其核心流程分为三个阶段:
第一阶段:快速查找,方法缓存
作用
- 优先从缓存中查找方法,提升调用效率(缓存命中率约 90%+)。
- 缓存使用 哈希表 存储,Key 是方法选择器(
SEL
),Value 是方法实现(IMP
)。
流程
- 调用
objc_msgSend(receiver, selector, ...)
。 - 检查
receiver
是否为nil
(如果是,直接返回nil
)。 - 从
receiver->isa
(类对象)的cache_t
中查找selector
对应的IMP
。
命中:直接跳转执行。 未命中:进入慢速查找。
源码(伪代码)
IMP objc_msgSend(id self, SEL sel) { if (self == nil) return nil; IMP imp = cache_getImp(self->isa, sel); // 查找缓存 if (imp) return imp; return lookUpImpOrForward(self->isa, sel, self); // 慢速查找 }
第二阶段:慢速查找,方法列表
作用
-
如果缓存未命中,遍历类的方法列表(包括分类)查找方法实现。
流程
- 从 class_rw_t 的 methods 数组中查找 selector。
顺序:分类方法 → 原始类方法 → 父类方法(递归)。 - 如果找到,将 (SEL, IMP) 写入缓存(cache_t),供下次快速查找。
- 如果未找到,触发 消息转发。
源码(伪代码)
IMP lookUpImpOrForward(Class cls, SEL sel) { // 遍历当前类的方法列表 for (Class curClass = cls; curClass; curClass = curClass->superclass) { method_t *method = findMethodInClass(curClass, sel); if (method) { cache_fill(cls, sel, method->imp); // 写入缓存 return method->imp; } } return forward_imp; // 进入消息转发 }
第三阶段:消息转发
如果慢速查找仍未找到方法,Runtime 提供 三次补救机会:
- 1. 动态方法解析(Dynamic Method Resolution)
+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(missingMethod)) { class_addMethod(self, sel, (IMP)dynamicIMP, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; }
用途:动态添加方法实现(如懒加载)。
- 2. 备用接收者(Fast Forwarding)
- (id)forwardingTargetForSelector:(SEL)aSelector { if ([alternateObject respondsToSelector:aSelector]) { return alternateObject; // 转发给其他对象处理 } return [super forwardingTargetForSelector:aSelector]; }
用途:将消息转发给其他对象(类似多继承)。
- 3. 完整消息转发(Normal Forwarding)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(missingMethod)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([alternateObject respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:alternateObject]; } else { [super forwardInvocation:anInvocation]; } }
用途:完全自定义转发逻辑(如实现消息代理)。
2. IMP、SEL、Method的区别和使用场景
在 Objective-C Runtime 中,IMP
、SEL
和 Method
是三个核心概念,用于描述方法的底层实现。以下是它们的详细对比和典型应用场景:
SEL
(方法选择器)
特点:
- 是一个 字符串映射的唯一标识,与方法名相关,但与实现无关。
- 通过
@selector()
获取(如@selector(viewDidLoad)
)。 - 不同类的同名方法,
SEL
相同(因为方法名相同)。
用途:
- 用于动态调用方法(如
performSelector:
)。 - 方法交换(Method Swizzling)。
- 检查对象是否能响应方法(
respondsToSelector:
)。
示例
SEL sel = @selector(sayHello); [obj performSelector:sel]; // 动态调用方法
IMP
(方法实现)
特点:
- 是一个 函数指针,指向方法的具体实现。
- 定义形式:
id (*IMP)(id self, SEL _cmd, ...)
。self
:调用对象。_cmd
:当前方法的SEL
。...
:方法的参数。
- 直接调用方法实现(绕过消息发送,性能更高)。
- 动态修改方法实现(Hook)。
示例
// 获取方法的 IMP IMP imp = class_getMethodImplementation([MyClass class], @selector(sayHello)); // 直接调用 IMP(等效于 [obj sayHello]) ((void (*)(id, SEL))imp)(obj, @selector(sayHello));
Method
(方法描述)
特点:
- 是一个结构体,包含
SEL
、IMP
和类型编码(types
)。 - 通过 Runtime API 获取(如
class_getInstanceMethod
)。
用途:
- 方法交换(Swizzling)。
- 动态添加/替换方法。
- 获取方法的详细信息(如参数类型、返回值类型)。
示例
// 获取 Method Method method = class_getInstanceMethod([MyClass class], @selector(sayHello)); // 获取 SEL、IMP、Type Encoding SEL sel = method_getName(method); IMP imp = method_getImplementation(method); const char *types = method_getTypeEncoding(method);
总结
SEL
:
- 方法的“名字”,与实现无关。
- 用于动态调用、方法名匹配。
IMP
:
- 方法的“实现”,即函数指针。
- 用于直接调用、动态修改实现。
Method
:
- 方法的完整描述(
SEL
+IMP
+ 类型编码)。 - 用于方法交换、获取方法元信息。
SEL
是方法名,IMP
是函数指针,Method
是前两者的包装+类型编码。
动态调用用SEL
,直接调优用IMP
,方法操作(如 Swizzling)用Method
。