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]),用于在集合(如 NSArrayNSDictionary)中表示“空值”。

场景:

  • 集合不能存储 nilnil 表示数组或字典的结束),所以用 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 协议),比较两个对象的 逻辑相等性。
  • 默认行为与 == 相同(比较指针),但许多类(如 NSStringNSArray)会重写它来比较内容。

场景

  • 需要比较任意对象的内容(如集合类、自定义类)。
  • 支持跨类比较(如 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)

  • class_copyPropertyList / class_copyMethodList

    作用:动态获取类的属性或方法列表(需导入 <objc/runtime.h>)。
    示例:
    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 是常量指针

记忆技巧

  1. const 在 * 左侧 → 值不可修改(如 const int *p)。
  2. const 在 * 右侧 → 指针不可修改(如 int * const p)。
  3. 两侧都有 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_getClassobject_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

作用

  • 用于修饰 不可变对象(如 NSStringNSArrayNSDictionary 等)。
  • 在 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

作用

  • 用于修饰 基本数据类型(如 intfloatBOOLNSInteger 等)。
  • 不进行引用计数管理,直接赋值(类似于简单的指针赋值)。

场景

  • 基本数据类型(非对象类型)。
  • 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(避免野指针)。

场景

  • 解决循环引用问题(如 delegateblock 内部引用 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 方法都会被调用,即使类未被使用。
  • 调用顺序:
    1. 父类的 +load 方法(优先于子类)。
    2. 子类的 +load 方法。
    3. 分类(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 方法

调用时机

  • 在类第一次被使用时调用(如调用类方法、实例化对象)。
  • 只会调用一次(即使多次使用该类)。
  • 调用顺序:
    1. 父类的 +initialize(如果父类未初始化)。
    2. 子类的 +initialize
    3. 分类会覆盖类的 +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_addMethodclass_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_tmethods 数组中,分类的方法排在前面,因此实际调用分类的方法。

  • 动态添加方法
    class_addMethod([MyClass class], @selector(newMethod), imp, "v@:");

    class_ro_t:不受影响。

    class_rw_tmethods 数组中新增 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 中,IMPSEL 和 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(方法描述)

特点:

  • 是一个结构体,包含 SELIMP 和类型编码(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。 

posted on 2025-07-07 01:08  梁飞宇  阅读(39)  评论(0)    收藏  举报