【持续更新中】编写高质量OC 的52个技巧

160 阅读59分钟

OC 七类52技巧

OC核心概念

01 使用@class而非#import

在类的头文件中尽量少引入其他头文件。 场景1: 减少编译依赖 当你在一个类中只需要使用另一个类的指针或引用,而不需访问其成员变量或方法时,可以使用 @class 进行前向声明,从而减少不必要的头文件导入,进而减少编译时间和潜在的编译错误。 例如,在你的 .h 文件中: 而在你的 .m 文件中,你可以安全地 #import "AnotherClass.h" 来获取 AnotherClass 的完整定义。 通过这种方式,可以有效地管理项目的复杂性和编译时间,特别是在大型项目中。

@class AnotherClass;

@interface MyClass : NSObject {
    AnotherClass *anotherClassInstance;
}
@end

场景2: 避免循环引用 假设你有两个类 ClassAClassB,它们各自需要引用对方。如果直接在头文件中导入另一个类的头文件,会导致循环引用的问题。使用 @class 可以解决这个问题: 在这种情况下,每个类只需要知道另一个类的存在即可,而不需要了解其具体的实现细节,直到实际需要使用到这些类的方法或属性时才进行完整的导入。

#import <Foundation/Foundation.h>

@class ClassB;  // 前向声明 ClassB

@interface ClassA : NSObject {
    ClassB *b;
}
@end

场景3: 必须使用#import的情况 A类继承自超类B,则需要 #import "B.h"; A类遵循协议B, 那么该协议必须要有完整定义,不能使用向前声明 @class。 向前声明只能告诉编译器有某个协议,而编译器却要知道该协议中定义的方法。

02 多用字面量语法,少用与之等价的方法

2个好处:更简洁、更安全。

NSString *greeting = @"Hello, World!";
NSString *greeting = [NSString stringWithCString:"Hello, World!" encoding:NSUTF8StringEncoding];
// 或者更常见的
NSString *greeting = [[NSString alloc] initWithCString:"Hello, World!" encoding:NSUTF8StringEncoding];
NSNumber *number = @42; // 整数
NSNumber *floatNumber = @3.14f; // 浮点数
NSNumber *doubleNumber = @3.14; // 双精度浮点数
NSNumber *boolNumber = @YES; // 布尔值
NSNumber *number = [NSNumber numberWithInt:42];
NSNumber *floatNumber = [NSNumber numberWithFloat:3.14f];
NSNumber *doubleNumber = [NSNumber numberWithDouble:3.14];
NSNumber *boolNumber = [NSNumber numberWithBool:YES];

使用字面量语法创建数组[A, B, C]时,若数组对象B为nil,会抛出异常。 但如果用arrayWithObjects,则不会出异常,返回[A] , 遇到nil就停止了。 这样不安全。

NSArray *array = @[@"Apple", @"Banana", @"Cherry"];
NSArray *array = [NSArray arrayWithObjects:@"Apple", @"Banana", @"Cherry", nil];
// 或者
NSArray *array = [[NSArray alloc] initWithObjects:@"Apple", @"Banana", @"Cherry", nil];
NSDictionary *dictionary = @{
                             @"Key1": @"Value1",
                             @"Key2": @42,
                             @"Key3": @YES
                             };
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                            @"Value1", @"Key1",
                            [NSNumber numberWithInt:42], @"Key2",
                            [NSNumber numberWithBool:YES], @"Key3",
                            nil];
// 或者
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
                            @"Value1", @"Key1",
                            [NSNumber numberWithInt:42], @"Key2",
                            [NSNumber numberWithBool:YES], @"Key3",
                            nil];

使用字面量语法创建并转换为可变字符串

NSMutableString *mutableString = [@"" mutableCopy];
// 或者直接使用初始化方法
NSMutableString *mutableString = [NSMutableString stringWithFormat:@"Hello, %@", @"World"];

等价的传统方法

NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"Hello, World"];

使用字面量语法创建并转换为可变数组:

NSMutableArray *mutableArray = [@[@"Apple", @"Banana"] mutableCopy];
// 或者使用类方法
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"Apple", @"Banana", nil];

等价的传统方法

NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithObjects:@"Apple", @"Banana", nil];

使用字面量语法创建并转换为可变字典:

NSMutableDictionary *mutableDictionary = [@{@"Key1": @"Value1", @"Key2": @42} mutableCopy];
// 或者使用类方法
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithObjects:@[@"Value1", @42] forKeys:@[@"Key1", @"Key2"]];

等价的传统方法

NSMutableDictionary *mutableDictionary = [[NSMutableDictionary alloc] initWithObjects:@[@"Value1", @42] forKeys:@[@"Key1", @"Key2"]];

03 定义类型常量使用static const、少用#define预处理指令

使用#define定义出来的常量缺乏类型信息不友好。 常量命名前缀k,表示该常量的作用域为当前类。 常量命名要是公开的话,通常以当前类名为前缀。 若不打算公开某个常量,则把常量定义写在.m文件中。

例如 static const NSTimeInterval kAnimationDuration = 0.3;

#define ANIMATION_DURATION 0.3

若果要定义全局常量可以在.h文件中用extern声明,在.m文件中定义。

// In HeaderFile.h
extern NSString *const SharedString;

// In ImplementationFile.m
NSString *const SharedString = @"This is a shared string.";

extern声明的变量或函数可以在多个文件间共享(具有外部链接性),而static声明的变量或函数只能在其所在的文件内使用(具有内部链接性)。

04 用枚举表示状态、选项、状态码

NS_ENUM用于定义一组互斥的选项,即在同一时间只能选择一个值。它明确了基础数据类型,并提供了更好的类型检查和编译器支持。

typedef NS_ENUM(Type, Name) {
    Enumerator1,
    Enumerator2,
    // ...
};
typedef NS_ENUM(NSInteger, DeviceOrientation) {
    DeviceOrientationUnknown,
    DeviceOrientationPortrait,
    DeviceOrientationPortraitUpsideDown,
    DeviceOrientationLandscapeLeft,
    DeviceOrientationLandscapeRight
};

在这个例子中,DeviceOrientation是一个基于NSInteger的枚举类型,它可以取值为DeviceOrientationUnknown, DeviceOrientationPortrait, 等等。这些值是互斥的,意味着同一时间只能有一个值被选用。

NS_OPTIONS 语法 NS_OPTIONS用于定义可以组合使用的选项,允许同时选择多个值。它特别适用于那些需要通过位运算来组合的标志位或选项。

typedef NS_OPTIONS(Type, Name) {
    Option1 = (1 << 0),
    Option2 = (1 << 1),
    // ...
};
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth       = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin   = 1 << 3,
    UIViewAutoresizingFlexibleHeight      = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

这里,你可以通过按位或运算符|来组合不同的自动调整选项,例如UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight

在处理枚举类型的switch语句中不要实现default分支, 否则加入新的枚举后,编译器会提示开发者,switch语句并未处理所有 枚举。

对象、消息、运行期

05 理解@property

属性 = 实例变量 + get/set方法

当你声明了一个属性后,编译器会自动生成相应的实例变量以及getter和setter方法。例如:

@property (nonatomic, strong) NSString *name;

等价于

@interface MyClass : NSObject {
    NSString *_name; // 自动生成的实例变量,下划线前缀是默认命名方式
}

- (NSString *)name; // getter
- (void)setName:(NSString *)name; // setter

@end

06 属性的4类特质

原子性(atomic/nonatomic):

// atomic:线程安全,但性能略低(默认)
@property (atomic, strong) NSString *safeName;

// nonatomic:不保证线程安全,但性能更高
@property (nonatomic, strong) NSString *unsafeName;

atomic 属性确保了对属性的访问(读取和写入)是原子操作。这意味着当你访问一个 atomic 属性时,编译器会自动生成必要的同步代码来保证在任何时刻只有一个线程能够访问该属性。这种同步机制通常是通过某种形式的锁实现的。

由于每次访问属性都需要进行加锁和解锁的操作,这增加了额外的开销,尤其是在高并发环境下,多个线程竞争同一把锁时会导致上下文切换、等待等额外的时间消耗,从而导致性能下降

atomic不能完全保证线程安全,我们可以创建一个场景,在该场景中多个线程尝试同时修改和读取属性值。

// MyClass.h
#import <Foundation/Foundation.h>

@interface MyClass : NSObject

@property (atomic, strong) NSMutableArray *atomicArray;
@property (nonatomic, strong) NSMutableArray *nonatomicArray;

- (void)addObjectToAtomicArray:(id)object;
- (void)addObjectToNonatomicArray:(id)object;

@end

// MyClass.m
#import "MyClass.h"

@implementation MyClass {
    NSMutableArray *_atomicArray;
    NSMutableArray *_nonatomicArray;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _atomicArray = [NSMutableArray array];
        _nonatomicArray = [NSMutableArray array];
    }
    return self;
}

- (void)addObjectToAtomicArray:(id)object {
    [_atomicArray addObject:object];
}

- (void)addObjectToNonatomicArray:(id)object {
    [_nonatomicArray addObject:object];
}

@end

// 在ViewController或其他地方进行测试
#import "MyClass.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MyClass *myClass = [[MyClass alloc] init];
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            [myClass addObjectToAtomicArray:@(i)];
            [myClass addObjectToNonatomicArray:@(i)];
        });
    }
    
    // 等待所有任务完成后再检查数组大小
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), queue, ^{
        NSLog(@"Atomic Array Count: %lu", (unsigned long)_atomicArray.count); // 可能不是10000
        NSLog(@"Nonatomic Array Count: %lu", (unsigned long)_nonatomicArray.count); // 很可能不是10000
    });
}

@end
  • atomicnonatomic的行为:尽管atomic提供了对单个属性访问的原子性保护,但在上述例子中,当我们试图同时从多个线程向数组中添加对象时,可能会遇到竞态条件问题。这是因为atomic只保证了对属性(如指针)本身的访问是线程安全的,并不保证对属性所指向的对象(这里是NSMutableArray)的操作也是线程安全的。
  • 为什么会出现问题:即使atomic确保了对_atomicArray这个指针的访问是原子性的,但当多个线程同时调用addObject:方法时,由于NSMutableArray本身并不是线程安全的数据结构,这可能导致数据竞争、崩溃或数据丢失等问题

竞态条件(Race Condition) 是并发编程中的一种常见问题,它发生在程序的执行结果依赖于不受控制的事件发生顺序时。具体来说,在多线程环境中,当两个或更多的线程尝试同时访问和修改共享资源(如内存中的变量、文件等),且至少有一个线程在进行写操作时,如果没有适当的同步机制来保护这些共享资源,就可能发生竞态条件。

@interface Counter : NSObject {
    int count;
}

- (void)increment;
- (int)getCount;

@end

@implementation Counter

- (instancetype)init {
    self = [super init];
    if (self) {
        count = 0;
    }
    return self;
}

- (void)increment {
    count = count + 1; // 这里包含了读取当前值、增加1、写回新值的操作
}

- (int)getCount {
    return count;
}

@end

如果我们从多个线程同时调用increment方法,可能会遇到以下情况:

  1. 线程A读取了count的当前值为5。
  2. 线程B也读取了count的当前值为5(在A将其更新之前)。
  3. 线程A将count增加到6并写回。
  4. 线程B也将count增加到6(基于它之前读取的值)并写回。

最终的结果是count应该是7,但由于竞态条件的存在,它的值变成了6,丢失了一次递增操

为什么atomic不能完全防止竞态条件?

虽然Objective-C中的atomic属性可以确保对单个属性的访问(读取和设置)是原子性的,这意味着你不会读取到部分更新的数据,但它并不能保证涉及多个步骤的操作是线程安全的。例如,在上面的计数器例子中,即使count被声明为atomic,如果increment方法不是原子性的(即没有额外的同步措施),仍然会发生竞态条件。

这是因为atomic只确保了获取或设置属性值的操作是不可中断的,但对于像count = count + 1这样的复合操作,它实际上包含三个步骤:读取当前值、计算新值、写入新值。在这三个步骤之间,其他线程可能已经改变了属性的状态,导致数据不一致或其他问题。

** 如何避免竞态条件?**

为了避免竞态条件,需要使用同步技术来确保在任何时刻只有一个线程能够访问和修改共享资源。常见的解决方案包括:

使用锁:如@synchronized块或NSLock对象

- (void)increment {
    @synchronized(self) {
        count = count + 1;
    }
}

使用GCD队列:通过串行队列来序列化对共享资源的访问。

dispatch_queue_t queue = dispatch_queue_create("com.example.counterQueue", NULL);

- (void)increment {
    dispatch_sync(queue, ^{
        count = count + 1;
    });
}

读写权限(readwrite/readonly): 深色版本

// readwrite:生成 getter 和 setter 方法(默认)
@property (readwrite, strong) NSArray *items;

// readonly:只生成 getter 方法
@property (readonly, copy) NSString *uuid;

内存管理语义(assign/strong/weak/unsafe_unretained/copy):

// assign:简单赋值,不处理引用计数(适用于基本类型)
@property (assign) NSInteger age;

// strong:强引用,增加对象的引用计数(常用于对象属性)
@property (strong, nonatomic) NSString *name;

// weak:弱引用,不增加引用计数,指向的对象释放后自动置为 nil(防循环引用)
@property (weak, nonatomic) id delegate;

// unsafe_unretained:类似 weak,但对象释放后不会自动置为 nil(容易野指针)
@property (unsafe_unretained) id oldDelegate;

// copy:复制传入的对象,并对其副本保持强引用(防止外部修改)
@property (copy, nonatomic) NSArray *dataList;

指定存取方法的方法名(getter=/setter=):、

// 自定义 getter 方法名为 isFinished
@property (nonatomic, getter=isFinished) BOOL finished;

// 自定义 getter 方法名为 isEnabled
@property (nonatomic, getter=isEnabled) BOOL enabled;

// 自定义 setter 方法名(注意 setter 名必须以冒号结尾)
@property (nonatomic, setter=setMyEnabled:) BOOL myEnabled;

07 在对象内尽量使用_xx访问实例变量

使用_xx和self.xx的区别。 1._xx直接访问实例变量,速度较快 2._xx不会掉用get/set方法 3.sef.xx会调用get/set方法、触发KVO

折中的方案: 写入实例变量时,使用self.xx。 读取实例变量时,使用_xx。

但要注意,如果自己实现了get方法,里面要用_xx,否则会导致死循环。

08 == 和 isEqualToString的区别,判断对象相等

== 比较的是对象的指针
isEuqalToString, 入参必须是NSSTring

09 类族模式

创建一个自定义类并使用类族模式,可以让你隐藏具体的实现细节,并提供一个统一的接口给用户。 步骤 1.定义抽象基类:这个类不会被直接实例化,而是作为其他具体子类的接口。 2.实现具体子类:为不同类型的数值(如整数、浮点数)实现具体的子类。 3.提供工厂方法:用于创建合适的子类实例而不暴露具体的子类类型。

#import <Foundation/Foundation.h>

// 抽象基类
@interface NumberHandler : NSObject

- (instancetype)initWithNumber:(NSNumber *)number;
- (NSString *)description;
+ (instancetype)handlerWithNumber:(NSNumber *)number;
@end

// 实现抽象基类
@interface NumberHandler ()

@property (nonatomic, strong) NSNumber *number;

@end

@implementation NumberHandler

- (instancetype)initWithNumber:(NSNumber *)number {
    self = [super init];
    if (self) {
        _number = number;
    }
    return self;
}

+ (instancetype)handlerWithNumber:(NSNumber *)number {
    // 简单的判断逻辑来决定使用哪个具体子类
    if ([number isKindOfClass:[NSDecimalNumber class]]) {
        return [[FloatNumberHandler alloc] initWithNumber:number];
    } else {
        return [[IntegerNumberHandler alloc] initWithNumber:number];
    }
}

- (NSString *)description {
    return [NSString stringWithFormat:@"Base handler for %@", self.number];
}

@end

// 整数处理的具体子类
@interface IntegerNumberHandler : NumberHandler

@end

@implementation IntegerNumberHandler

- (NSString *)description {
    return [NSString stringWithFormat:@"Integer handler for %@", self.number];
}

@end

// 浮点数处理的具体子类
@interface FloatNumberHandler : NumberHandler

@end

@implementation FloatNumberHandler

- (NSString *)description {
    return [NSString stringWithFormat:@"Float handler for %@", self.number];
}

@end

// 使用示例
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSNumber *integerNumber = @42;
        NSNumber *floatNumber = @3.14;
        
        NumberHandler *integerHandler = [NumberHandler handlerWithNumber:integerNumber];
        NumberHandler *floatHandler = [NumberHandler handlerWithNumber:floatNumber];
        
        NSLog(@"%@", [integerHandler description]);
        NSLog(@"%@", [floatHandler description]);
    }
    return 0;
}

010 在已有的类中使用关联对象存放自定义数据

关联对象,就是给对象A关联一些其它的对象数据。
例如A {String a; void a()}
但是我现在使用A时希望它能额外带一些临时的数据B,可是又不希望数据BA中定义一个属性,这个时候就可以用关联对象,将BA关联。
允许你在不修改原始类的情况下为其添加额外的属性。这种技术特别适用于扩展第三方库中的类或系统类,因为你无法直接修改这些类的源代码。

#pragma mark**- 场景1: 为已有类添加自定义属性(为分类添加属性)、使用关联对象传递数据**

//分类只能实现方法,不能实现属性,如果需要实现属性,需要用关联对象的方式。

@interface UIButton(CustomTag)
@property(nonatomic, strong) id customTag;
@end
    
#import <objc/runtime.h>
static char kCustomTagKey;
@implementation UIButton(CustomTag)

- (void)setCustomTag:(id)customTagVal {
    objc_setAssociatedObject(self, &kCustomTagKey, customTagVal, OBJC_ASSOCIATION_COPY_NONATOMIC);
    
}

- (id)customTag {
    return objc_getAssociatedObject(self, &kCustomTagKey);
}

@end
    
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.customTag = @"buttonTag";
NSLog(@"button.customTag  = %@", button.customTag);

011 objc_msgSend的作用

objc_msgSend 是 Objective-C 运行时系统的核心函数之一,用于实现消息传递机制。Objective-C 的一大特色就是其动态消息发送机制,这使得你可以向对象发送消息(即调用方法),而无需在编译时确定具体的接收者和方法实现。

消息传递机制

[object messageWithParameter:param];
    实际上,在运行时会被转换为类似这样的调用:
objc_msgSend(object, @selector(messageWithParameter:), param);

这里,object 是消息的接收者,@selector(messageWithParameter:) 是选择器(代表了要调用的方法名),param 则是传递给方法的参数。

objc_msgSend 工作原理

  1. 查找方法实现:当一个消息被发送时,objc_msgSend 会首先检查接收者是否能响应这个消息(即是否有相应的方法实现)。这通过查找接收者的类以及其父类中的方法列表来完成。
  2. 执行方法:如果找到了相应的方法实现,objc_msgSend 将控制权转移到该方法,并将任何传递的参数提供给该方法。 3.如果没有找到对应的方法实现,Objective-C 运行时系统会尝试调用一些特殊的方法,比如 -forwardInvocation: 或者触发 doesNotRecognizeSelector: 来处理这种情况,允许开发者有机会动态地处理未知消息。 特点
  • 动态性:由于 objc_msgSend 在运行时才决定调用哪个方法,这赋予了 Objective-C 极大的灵活性。例如,你可以在运行时添加或交换方法实现。
  • 性能优化:尽管 objc_msgSend 看起来像是一个通用的函数调用,但实际上它经过高度优化以减少消息传递的开销。编译器会根据消息的参数类型和数量生成特定版本的 objc_msgSend 函数调用,从而提高效率。
  • 多重调用形式:存在多个 objc_msgSend 的变体,比如 objc_msgSend_stret 用于返回结构体、objc_msgSend_fpret 用于浮点数返回值等,确保不同类型的数据能够正确地被处理。

012 消息转发机制

image.png

消息转发机制共分为3大步骤:

1.Method resolution 方法解析处理阶段

2.fast forwarding 快速转发阶段 

3.Normal forwarding 常规转发阶段

抛出unrecognized selector 的报错,也就是需要从这3步里面.

第一步:我们称之为方法解析:

方法解析的含义就是:当我们的类发现我们调用的方法 unrecognize 的时候就会 调用第一个方法

// 类方法专用

  • (BOOL)resolveClassMethod:(SEL)sel // 对象方法专用
  • (BOOL)resolveInstanceMethod:(SEL)sel

这个方法来询问当前对象是否能够处理sel,如果能那么return YES,在这个时候我们就可以通过 class_addMethod 来动态添加方法.让我们自己的类响应这个方法.这里为第一步的消息转发.如果我们的类的有这个方法也不会走到这里,很遗憾我们进入到第二步的转发

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *method = NSStringFromSelector(sel);
    if ([@"playPiano" isEqualToString:method]) {
        /**
         添加方法
         
         @param self 调用该方法的对象
         @param sel 选择子
         @param IMP 新添加的方法,是c语言实现的
         @param 新添加的方法的类型,包含函数的返回值以及参数内容类型,eg:void xxx(NSString *name, int size),类型为:v@i
         */
        class_addMethod(self, sel, (IMP)playPiano, "v");
        return YES;
    }
    return NO;
}

第二步:称之为 "备援接受者" 意思就是:当前的类不能够实现这个sel,但是检查是否有备胎可以实现 这个方法就是消息转发的第二步:来找可以实现该方法的对象.

- (id)forwardingTargetForSelector:(SEL)aSelector

我们的teacher 类不能实现 playPanio的方法 ,消息转发给我的学生来实现.直接将该消息转给student类. 但最后我们的程序崩溃了,说明比较悲哀,连备胎都没有. 那我们就进入最后一步啦,

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *seletorString = NSStringFromSelector(aSelector);
    if ([@"playPiano" isEqualToString:seletorString]) {
        Student *s = [[Student alloc] init];
        return s;
    }
    // 继续转发
    return [super forwardingTargetForSelector:aSelector];
}

第三步: 首先获取当前的方法签名: Signature

还是将消息转发给其他的类.这里不仅可以转给其他的类,而且可以调用其他的方法. 比如:我调用:[Teacher playPaino] 方法,进过最后的消息转发,我调用的 student类的 travel 方法. 但是很少有人会在这一步做处理,我们知道方法越靠后,需要的东西就越多,资源开销就比较大,所以在这种情况下,一般用来增加参数,或者改变选择子.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *method = NSStringFromSelector(aSelector);
    if ([@"playPiano" isEqualToString:method]) {
        
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return signature;
    }
    return nil;
}

然后通过:

- (void)forwardInvocation:(NSInvocation *)anInvocation 进行消息转发: 如果当前类实现了 travel 方法 那你调用 playPaino的方法就不会崩溃, 如果没实现,但 student 实现了 也不会崩溃.

    - (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL sel = @selector(travel:);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setTarget:self];
    [anInvocation setSelector:@selector(travel:)];
    NSString *city = @"北京";
    // 消息的第一个参数是self,第二个参数是选择子,所以"北京"是第三个参数
    [anInvocation setArgument:&city atIndex:2];
    
    if ([self respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self];
        return;
    } else {
        Student *s = [[Student alloc] init];
        if ([s respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:s];
            return;
        }
    }
    
    // 从继承树中查找
    [super forwardInvocation:anInvocation];
}

崩溃的过程大致就是这三步,最后一步还是不能找到实现方法,那就抛出异常,dose not recognize Selector

013 用"方法调配技术"调试"黑盒方法"(Method Swizzling)

什么是方法调配 Method Swizzling? Objective-C对象收到消息之后,究竟会调用何种方法在运行期才能解析出来。 与给定的selector名称相对应的方法是不是也可以在运行期改变呢? 没错,既不需要源代码也不需要通过继承子类来覆写方法就能改变这个类本身的功能。 新功能将在本类的所有实例中生效,而不仅限于覆写了相关方法的子类实例。 此方案称为“方法调配” method swizzling。

类的方法列表会把selector的名称映射到相关的方法实现上,使得“动态消息派发系统”能够找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP。 IMP原型如下: id(*IMP)(id, SEL, ...) 举个例子: NSString类可以响应lowercaseString\uppercaseString\capitalizedString等selector。selector和相关方法的IMP映射如下:

image.png

Objective-C运行期系统提供的几个方法都能用来操作这张表。
开发者可以向其中新增selector,也可以改变某选择子所对应的方法实现。
还可以交换两个选择子所映射的指针。
经过几次操作之后,类的方法表就会变成图2-4这个样子。

image.png 可以看到,在新的映射表中,多了一个名为newSelector的selector,lowercaseString和uppercaseString的实现互换了。 上述修改均无需编写子类,只要修改了方法表的布局。 就会反应到程序中所有的NSString实例之上。

        
    重点方法: void method_exchangeImplementations(Method m1, Method m2);
    此函数的两个参数表示待交换的两个方法实现,而方法实现可通过下列函数获得:
    Method class_getInstanceMethod(Class aClass, SEL aSelector);
    
    接下来交换lowercaseSting 与 uppercaseString的方法实现。
    

//  Demo08ViewController.m

#import "Demo08ViewController.h"
#import <objc/runtime.h>
#define CASE 3

@interface Demo08ViewController ()
@end

@implementation Demo08ViewController
- (void)viewDidLoad {

    [super viewDidLoad];
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn setTitle:@"测试method swizzling" forState:UIControlStateNormal];
    [btn addTarget:self action: @selector(test) forControlEvents:UIControlEventTouchUpInside];
    btn.backgroundColor = [UIColor redColor];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];

    [self.view addSubview:btn];
}

- (void)test {

#if CASE == 1
    NSString *title = @"I don't know how can I make up for that";
    NSLog(@"lowercaseString %@", [title lowercaseString]); //lowercaseString i don't know how can i make up for that
    NSLog(@"uppercaseString %@", [title uppercaseString]); //输出:uppercaseString I DON'T KNOW HOW CAN I MAKE UP FOR THAT

#elif CASE == 2
    Method method1 = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method method2 = class_getInstanceMethod([NSString class], @selector(uppercaseString));
    method_exchangeImplementations(method1, method2);

    NSString *title = @"I don't know how can I make up for that";
    NSLog(@"lowercaseString %@", [title lowercaseString]); //输出:lowercaseString I DON'T KNOW HOW CAN I MAKE UP FOR THAT
    NSLog(@"uppercaseString %@", [title uppercaseString]); //uppercaseString i don't know how can i make up for that

#elif CASE == 3
    //显示中像case 2 这样交换没有什么意义,
    //但是可以通过这种交换方法的手段为既有的方法增添新功能
    //现在展示新编写一个方法,在此方法中实现新功能,并调用原有的实现。
    //接下来的NSString的lowercaseString方法进行扩展

    Method method1 = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method method2 = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
    method_exchangeImplementations(method1, method2);

    NSString *title = @"I don't know how can I make up for that";
    [title lowercaseString];
    //eoc_myLowercaseString => i don't know how can i make up for that

#endif
}
@end

NSString + Addition.h

@interface NSString(Addition)
- (NSString*) eoc_myLowercaseString;
@end
@implementation NSString(Addition)
- (NSString*) eoc_myLowercaseString {
    //这里看起来似乎会陷入递归调用的死循环,但是记住,此方法是准备和NSString 原来的lowercaseString方法
    //交换的,所以在运行期, eoc_myLowercaseString 选择器 实际上对应于 原有的 lowercaseString方法
    //实现。
    NSString *lowercase = [**self** eoc_myLowercaseString];
    NSLog(@"%@ => %@", @"eoc_myLowercaseString", lowercase);
    return lowercase;
}
@end

014 理解“类对象”(使用isKindOfClass/isMemberOfClass判断类型,而不是== 或 isEqual)

上面的method_swizzling,可以看出。 编译器无法确定某类型对象到底有多少种selector,因为运行期可以class_addMethod动态新增。即便使用类动态新增技术,编译器也认为某个头文件中能找到方法原型的定义,据此可以了解完整的方法签名,生成派发消息所需的正确代码。 - 类(Class) :定义了一种类型的数据结构及其行为(方法)。它是创建对象的模板。 实例(Instance) :是根据类创建的具体对象,每个实例都拥有类定义的属性和方法

“类对象”指的是当某个类被加载到运行时环境中时,系统会为该类创建一个唯一的类对象。这个类对象包含了类的所有信息,比如它的方法列表、属性列表等。

如何获取类对象

Class cls = [NSString class]; // 获取 NSString 类的对象
id obj = [[cls alloc] init]; // 使用类对象创建实例
**类对象的数据结构**

Objective-C 的类对象本质上是一个指向 `Class` 类型的指针,而 `Class` 实际上是指向一个包含类信息的结构体的指针。

`objc_class` 结构体定义了类的基本信息。以下是它的简化版定义
struct objc_class {
    Class isa; // 指向元类 (Meta-Class) 或者另一个类对象
    Class superclass; // 指向父类的指针
    const char *name; // 类名
    long version; // 类版本号
    long info; // 类信息标志位
    long instance_size; // 实例大小
    struct objc_ivar_list *ivars; // 成员变量列表
    struct objc_method_list **methodLists; // 方法列表数组
    struct objc_cache *cache; // 方法缓存
    struct objc_protocol_list *protocols; // 协议列表
};
    -   **isa**: 每个 Objective-C 对象(包括类对象)都有一个 `isa` 指针,它指向该对象所属的类对象。对于类对象来说,`isa` 指针指向的是它的元类(Meta-Class)。
    -   **superclass**: 指向该类的直接父类对象。如果这个类没有父类(例如 `NSObject`),则 `superclass` 为 `nil`    -   **ivars 成员变量列表**: 指向一个 `objc_ivar_list` 结构体,该结构体包含了类的所有实例变量(成员变量)的信息。每个 `objc_ivar` 结构体描述了一个实例变量的名称、类型和偏移量等信息。
    -   **methodLists 方法列表**: 指向一个 `objc_method_list` 数组,每个 `objc_method_list` 包含一组方法描述符 (`objc_method`),每个 `objc_method` 描述了一个方法的选择器(selector)、实现函数(IMP)和类型编码。
    -   **cache 方法缓存**: 用于提高方法调用效率的缓存机制。当某个方法被调用时,Objective-C 运行时会首先查找方法缓存,以避免每次都遍历整个方法列表。
    -  **protocols 协议列表**: 指向一个 `objc_protocol_list` 结构体,该结构体列出了该类遵守的所有协议
    每个类对象还有一个对应的元类(Meta-Class)。元类存储了类方法的信息。类方法实际上是通过元类来实现的。元类的 `isa` 指针指向根元类(Root Meta-Class),根元类的 `isa` 指针指向自己。

类对象的应用: 类型查询方法

    通过这张布局关系图即可执行类型信息查询,可以查出对象是否能响应某个选择子。是否遵从某项协议。
    isMemberOfClass 判断对象是否为某个特定类的实例
    isKindOfClass 判断对象是否为某类或其子类的实例
    
    像这样的类型查询方法使用isa指针获取对象所属的类,然后通过super-class在继承体系中游走。
    由于对象是动态的,所以此特性显得极为重要。
    oc与其他语言不同,在oc中必须查询类型信息才能了解对象的真实类型。
    

image.png

由于oc使用动态类型系统,所以类型查询功能非常有用。 举例如下: 从集合中获取对象时,通常会查询类型信息,这些对象不是强类型的,把他们从集合中取出来时,类型通常是id,如果想知道具体类型,就可以使用类型查询方法。 比如,想根据数组中存储的对象生成以逗号分隔的字符串,并将其存至文本文件。

- (NSString*) commaSeparatedStringFromObjects: (NSArray*)array {
  NSMutableString *string = [NSMutableString new];
  for (id object in array) {
    if ([object isKindOfClass:[NSString class]]) {
      [string appendFormat:@"%@,", object];
    }else if([object isKindOfClass:[NSNumber class]]) {
      [string appendFormat:@"%d,", [object intValue]];
    }else if([object isKindOfClass:[NSData class]])     {
      NSString *base64Encoded = /*encoded data*/;
      [string appendFormat:@"%@,", base64Encoded];
    } else {
       //Type Not Supported
    }
  }
  return string;
}

关于使用isEqual方法的注意事项

[object isKindOfClass:[NSData class]]
也可以用 == 类判断
object class] == [NSData class]
但是不要用isEuqal: 方法。
即便能用 == 也应该使用 isKindOfClass 或 isMemberOfClass这类类型查询方法,而不是直接比较两个对象是否等同。
因为类型查询方法能正确处理了使用消息传递机制的对象。
比方说,某个对象可能会把其收到的所有selector都转发给另外一个对象。
这样的对象叫做代理,此种对象均以NSProxy为根类。
如果在此代理对象上调用class方法,那么返回的上代理对象本身NSProxy,而非接受的代理的对象所属的类。
若使使用isKindOfClass,那么代理对象就会把消息转发给接受代理的对象。

接口与API设计

15.用3个字母的前缀避免命名冲突

oc没有内置的命名空间机制。 所以在命名时要设法避免潜在的命名冲突,否则应用程序链接过程会出错,因为出现了重复符号。

命名冲突的错误场景

image.png 上面提示 something 和 something_else文件中都实现了名为EOCTheClass的类,这导致EOCTheClass所对应的类符号和元类符号各定义了2次。 也许是把2个相互独立的程序库引入到了当前项目中,而他们刚好有重名的类,所以产生了这个问题。

避免方法 为所有名称都加上适当前缀,变相实现命名空间。(前缀与公司、应用程序相关)。 比如说公司名为Effective Widgets。 那么一些不同项目通用的代码就以EWS作前缀。 如果有些代码只用于名为Effective Browser的浏览器项目中,那这个项目中的部分代码就以EWB为前缀。 加了前缀可以极大程度上避免命名冲突。

必须添加前缀的点 类名、分类的方法(因为分类的方法是全局的)、全局变量或者纯c函数。(纯c函数不添加前缀会无法再写相同名称的函数)

为什么是3个字母的前缀而不是2个 Apple公司保留使用所有2个字母前缀的权利,一旦用了2个前缀的命名,万一那天iOS SDK更新和你的冲突了就麻烦了。

16.提供“全能”初始化方法

一般初始化方法可以分为便捷初始化方法和全能初始化方法。 提供全能初始化方法主要是为了确保对象能够被正确和完整地初始化。全能初始化方法是指一个类中负责执行所有必要的初始化工作的初始化方法。其他初始化方法通常会调用这个全能初始化方法来完成初始化过程。 使用全能初始化方法的好处

1.保证一致性:通过全能初始化方法,可以确保每个实例都以相同的方式进行初始化,避免遗漏重要的初始化步骤。

2.简化代码维护:如果所有的初始化逻辑都集中在一个全能初始化方法中,那么当需要修改初始化逻辑时,只需在一个地方进行更改。

3.支持继承:在子类中重写初始化方法时,可以通过调用父类的全能初始化方法来确保父类的部分也被正确初始化

若子类初始化方法与超类不同,应该覆写对应方法。 如init
如果超类的初始化方法不适用于子类,也应该覆写并抛出异常提示。
如initWithModel

示例代码

@interface Vehicle : NSObject
@property (nonatomic, strong) NSString *model;
- (instancetype)initWithModel:(NSString *)model;
@end
    

@implementation Vehicle
- (instancetype)initWithModel:(NSString *)model {
    self = [super init];
    if (self) {
        _model = model;
    }
    return self;
}
@end

@interface Car : Vehicle

@property (nonatomic, assign) BOOL hasSunroof;
@property (nonatomic, assign) BOOL hasLeatherSeats;
@property (nonatomic, assign) BOOL hasNavigationSystem;

// 全能初始化方法
- (instancetype)initWithModel:(NSString *)model year:(NSInteger)year hasSunroof:(BOOL)hasSunroof hasLeatherSeats:(BOOL)hasLeatherSeats hasNavigationSystem:(BOOL)hasNavigationSystem;

// 便捷初始化方法
- (instancetype)init; // 默认初始化方法
- (instancetype)initWithModelAndYear:(NSString *)model year:(NSInteger)year;
- (instancetype)initWithModelYearAndSunroof:(NSString *)model year:(NSInteger)year hasSunroof:(BOOL)hasSunroof;
- (instancetype)initWithModelYearAndLeatherSeats:(NSString *)model year:(NSInteger)year hasLeatherSeats:(BOOL)hasLeatherSeats;

@end


@implementation Car

// 全能初始化方法
- (instancetype)initWithModel:(NSString *)model year:(NSInteger)year hasSunroof:(BOOL)hasSunroof hasLeatherSeats:(BOOL)hasLeatherSeats hasNavigationSystem:(BOOL)hasNavigationSystem {
    self = [super initWithModel:model year:year];
    if (self) {
        _hasSunroof = hasSunroof;
        _hasLeatherSeats = hasLeatherSeats;
        _hasNavigationSystem = hasNavigationSystem;
    }
    return self;
}

// 便捷初始化方法1:默认初始化方法
- (instancetype)init {
    // 提供默认值
    return [self initWithModel:@"Unknown" year:2023 hasSunroof:NO hasLeatherSeats:NO hasNavigationSystem:NO];
}

// 不推荐使用的初始化方法,抛出异常(场景二)
- (instancetype)initWithModel:(NSString *)model {
    @throw [NSException exceptionWithName:NSInvalidArgumentException
                                 reason:@"initWithModel: is not supported. Use designated initializer instead."
                               userInfo:nil];
    return nil;
}

// 便捷初始化方法2:只设置 model 和 year
- (instancetype)initWithModelAndYear:(NSString *)model year:(NSInteger)year {
    return [self initWithModel:model year:year hasSunroof:NO hasLeatherSeats:NO hasNavigationSystem:NO];
}

// 便捷初始化方法3:设置 model, year 和 sunroof
- (instancetype)initWithModelYearAndSunroof:(NSString *)model year:(NSInteger)year hasSunroof:(BOOL)hasSunroof {
    return [self initWithModel:model year:year hasSunroof:hasSunroof hasLeatherSeats:NO hasNavigationSystem:NO];
}

// 便捷初始化方法4:设置 model, year 和 leather seats
- (instancetype)initWithModelYearAndLeatherSeats:(NSString *)model year:(NSInteger)year hasLeatherSeats:(BOOL)hasLeatherSeats {
    return [self initWithModel:model year:year hasSunroof:NO hasLeatherSeats:hasLeatherSeats hasNavigationSystem:NO];
}

@end

017. 实现description方法

调试程序时,经常需要打印并查看对象信息。 如果是想调试控制台中 po 打印出来的信息,那需要实现debugDescription

    NSLog(@"object = %@", object); 
    //这样执行出来的是object的description方法
    //例如
    NSArray *object1 = @[@"A string", @(123)];
    NSLog(@"object1 = %@", object1); 
    //输出
    /*
    object1 = (
      "A string",
      123,
    )
    */
    //但是自定义的类输出的是
    //object = <ClassName: 0x7fd9a1600600>
    //所以如果想要有效信息,就要自己实现description方法
    //建议用字典的形式输出
    
    -(NSString*)description {
      return [NSStringStringWithFormat:@"<%@: %p>, %@", [self class],
      self,
      @{
         @"title": _title,
         @"latitude": @(_latitude),
         @"longitude": @(_longitude)
       }
    ];
    }
    

019.私有方法加_前缀

方便区分公开方法和私有方法。

020.NSCopying和NSMutableCopying协议实现自定义类的复制

如果你想实现自定义类的对象复制功能(即支持 copymutableCopy),就需要让你的类遵守 NSCopyingNSMutableCopying 协议,并分别实现它们的协议方法:

  • copyWithZone: —— 用于实现不可变副本
  • mutableCopyWithZone: —— 用于实现可变副本 Objective-C 的对象默认是不能直接调用 copymutableCopy 的,除非你做了以下事情:
  1. 遵守 NSCopying / NSMutableCopying
  2. 实现对应的 copyWithZone: / mutableCopyWithZone: 方法
  3. 确保属性也进行了深拷贝(如果需要)

否则会抛出异常:

-[YourClass copyWithZone:]: unrecognized selector sent to instance

1. 浅拷贝(Shallow Copy)

只复制对象本身,不复制对象内部引用的对象。两个对象共享内部对象。

2. 深拷贝(Deep Copy) 不仅复制对象本身,还递归地复制它所引用的所有对象,形成一个完全独立的新对象。

示例:实现自定义类的复制功能

// Person.h

@interface Person : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;

@end

// Person.m
@implementation Person
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
    self = [super init];
    if (self) {
        _name = [name copy]; // 拷贝一份
        _age = age;
    }
    return self;
}
// NSCopying 协议方法:生成不可变副本
- (id)copyWithZone:(NSZone *)zone {
    Person *copy = [[Person allocWithZone:zone] initWithName:self.name age:self.age];
    return copy;
}
// NSMutableCopying 协议方法:生成可变副本
- (id)mutableCopyWithZone:(NSZone *)zone {
    return [self copyWithZone:zone]; // 可以返回相同的副本,或者创建可变版本
}
@end
Person *p1 = [[Person alloc] initWithName:@"Tom" age:20];
// 不可变复制
Person *p2 = [p1 copy];
// 可变复制
Person *p3 = [p1 mutableCopy];

深拷贝怎么做?(嵌套对象)

@interface Address : NSObject <NSCopying>
@property (nonatomic, copy) NSString *city;
@end

@implementation Address
- (id)copyWithZone:(NSZone *)zone {
    Address *copy = [[Address allocWithZone:zone] init];
    copy.city = [self.city copy];
    return copy;
}
@end


@interface Person : NSObject <NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) Address *address;
@end

@implementation Person

- (id)copyWithZone:(NSZone *)zone {
    Person *copy = [[Person allocWithZone:zone] init];
    copy.name = [self.name copy];
    copy.address = [self.address copy]; // 深拷贝
    return copy;
}

@end

copy 返回的是不可变对象 , 即使原对象是可变的 mutableCopy 返回的是可变对象 , 即使原对象是不可变的 使用 copyWithZone: 是为了兼容内存区(zones)机制,在现代 iOS 开发中一般可以忽略 zone 建议对字符串等属性使用 copy 而不是 strong, 防止外部传入 NSMutableString 后修改影响内部状态

协议与分类

oc的一项特性叫做协议protocol,它与Java接口interface类似。 Objective-C不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里。 协议最常见的用途是委托代理模式。

分类Category无须继承子类即可为当前类添加方法,而在其它编程语言中,则需通过继承子类来实现。

022.委托代理模式

  • EOCDataModel:数据模型类,用于接收并处理网络请求的结果。
  • EOCNetworkFetcher:网络请求类,负责发起网络请求,并通过代理回调通知结果。
// EOCNetworkFetcher.h
// 定义协议
@protocol EOCNetworkFetcherDelegate <NSObject>
@required
// 请求成功时调用,传入返回的数据
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
// 请求失败时调用,传入错误信息
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
@end

@interface EOCNetworkFetcher : NSObject
// 代理属性
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
// 模拟网络请求方法
- (void)startFetchingData;
@end
    
@implementation EOCNetworkFetcher
- (void)startFetchingData {
    // 模拟异步网络请求
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 模拟请求成功
        NSData *responseData = [@"Hello from server!" dataUsingEncoding:NSUTF8StringEncoding];
        // 回到主线程回调代理
        dispatch_async(dispatch_get_main_queue(), ^{
            if ([self.delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]) {
                [self.delegate networkFetcher:self didReceiveData:responseData];
            }
        });
    });
}
@end
// EOCDataModel.h
@interface EOCDataModel : NSObject <EOCNetworkFetcherDelegate>
@property (nonatomic, strong) NSString *receivedDataString;
- (void)fetchData;
@end
 

@implementation EOCDataModel {
    EOCNetworkFetcher *_fetcher;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        _fetcher = [[EOCNetworkFetcher alloc] init];
        _fetcher.delegate = self; // 设置自己为代理
    }
    return self;
}

- (void)fetchData {
    [_fetcher startFetchingData]; // 开始获取数据
}

#pragma mark - EOCNetworkFetcherDelegate Methods
// 实现协议方法:接收到数据
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data {
    self.receivedDataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"数据接收成功: %@", self.receivedDataString);
}

// 实现协议方法:请求失败
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error {
    NSLog(@"请求失败: %@", error.localizedDescription);
}

@end

023.分类的使用

分类(Category) 是一种为现有的类添加方法的机制。它允许你在不访问原始源代码的情况下扩展类的功能。分类可以添加实例方法和类方法,但不能添加新的属性或实例变量。 通过分类,将类代码分成多个易于管理的小块。方便代码管理。

分类的主要用途包括:

  • 扩展系统类:例如为 NSStringNSArray 等系统类添加自定义方法。
  • 模块化代码:将一个大类的不同功能分组到不同的文件中,便于管理和维护。
  • 避免代码重复:当你需要在多个地方使用相同的方法时,可以通过分类来实现复用。

使用分类划分 Person 类的功能 假设我们有一个 Person 类,它具有两种不同类型的“功能区”方法:

  1. 基本信息管理:如设置/获取名字、年龄等。
  2. 行为管理:如跑步、吃饭等。 我们可以创建两个分类来分别处理这两种类型的功能。
// Person.h
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// Person.m
@implementation Person
@end

// Person+BasicInfo.h
@interface Person (BasicInfo)

- (void)setName:(NSString *)name;
- (NSString *)name;

- (void)setAge:(NSInteger)age;
- (NSInteger)age;

@end

// Person+BasicInfo.m
#import "Person+BasicInfo.h"
@implementation Person (BasicInfo)
- (void)setName:(NSString *)name {
    self.name = name; //由于分类无法直接访问实例变量(`_name`, `_age`),实际上不会编译通过。正确的方式是利用 `@property` 自动合成的 setter 和 getter 方法,或者使用 KVC 来访问这些属性。
}
- (NSString *)name {
    return self.name;
}
- (void)setAge:(NSInteger)age {
    self.age = age;
}
- (NSInteger)age {
    return self.age;
}
@end
    

// Person+Behavior.h
#import "Person.h"
@interface Person (Behavior)
- (void)run;
- (void)eatFood:(NSString *)foodName;
@end
// Person+Behavior.m
#import "Person+Behavior.h"
@implementation Person (Behavior)
- (void)run {
    NSLog(@"%@ is running.", self.name);
}
- (void)eatFood:(NSString *)foodName {
    NSLog(@"%@ is eating %@", self.name, foodName);
}

@end

024.分类方法要加上前缀

分类能为既有类中新增功能。 但是要注意,分类的方法是直接添加在类里面的,好比于这个类的固有方法。 如果分类A实现了C方法,分类B也实现了C方法。 那么分类中的C方法会被覆盖,C方法的实际实现以最后一个覆盖结果为准。

而且不会报错,只会有warning。 所以一定记得为分类方法加上前缀(前缀,以本分类名)

025.不要在分类中声明属性

分类中无法向类种新增实例变量,不会合成SET\GET方法。

当然通过关联对象可以实现属性,但是不提倡。

只读属性还是可以在分类中使用的,由于获取方法不需要访问数据,而且属性也不需要实例变量来实现。可以像下面。

@interface NSCalendar (EOC_Aditions)
- (NSArray*) eoc_allMonths;
@end
    
@interface NSCalendar (EOC_Aditions)
@property (nonatomic, strong, readonly) NSArray *eoc_allMonths;
@end
    
@implementation NSCalendar (EOC_Aditions)
- (NSArray*) eoc_allMonths {
   return @[
      @"January",
      @"February",
      @"March",
      @"April",
      @"May",
      @"June",
      @"July",
      @"August",
      @"September",
      @"October",
      @"November",
      @"December",
    ];
}
@end

025.class-continuation分类私有化变量和方法

class-continuation分类就是在本类的.m方法中实现的分类。

在该分类中实现的变量为私有变量,私有化方法,不能被外界访问。如果项使得类遵循的协议不为人知,则可以在class-continuation分类中声明。

属性对外为只读属性,对内扩展为可读写。
@interface A: NSObject
@property(nonatomic, strong , readonly) NSString* str;
@end

//A.m
@interface A()<SomeDelegate> //类遵循的协议
//扩展对外属性
@property(nonatomic, strong , readonly) NSString* str;
//新增私有属性
@property(nonatomic, strong , readonly) NSString* pStr;
//新增私有方法
- (void)private_method();
@end
    
@implentation A

@end

026.使用协议实现匿名对象、匿名对象的应用场景

匿名对象(Anonymous Object) 通常指的是一个没有显式类名的对象,它可能是通过协议(Protocol)和运行时特性动态创建的。

✅ 场景说明

你希望:

  • 创建一个实现了某个协议的对象;
  • 不定义一个具体的类;
  • 动态绑定方法行为;
  • 实现类似于 Swift 中的闭包对象或 Java 的匿名类。

这在某些回调、代理、插件系统中非常有用。

我们使用 NSObject 的分类(Category)配合 @protocolNSProxyobjc/runtime.h 来动态创建一个符合协议的匿名对象。 示例代码:匿名对象的简单实现


@protocol MyProtocol <NSObject>
- (void)doSomething;
@end
    

id<MyProtocol> anonymousObject = [[NSObject alloc] init];
Class class = [anonymousObject class];

// 动态添加方法实现
SEL selector = @selector(doSomething);
Method method = class_getInstanceMethod(class, selector);
if (!method) {
    IMP imp = imp_implementationWithBlock(^(id self) {
        NSLog(@"Doing something anonymously!");
    });
    class_addMethod(class, selector, imp, "v@:");
}

// 现在可以调用匿名对象的方法了
[anonymousObject doSomething];

示例代码:通过协议实现匿名对象

Step 1: 定义协议
// MyProtocol.h
@protocol MyProtocol <NSObject>
- (void)doSomething;
- (NSString *)getName;
@end
    
Step 2: 使用 NSObject 分类 + Runtime 动态实现协议方法
// NSObject+Anonymous.m
#import <objc/runtime.h>
#import "MyProtocol.h"

typedef id (^MethodImplementationBlock)(id self, SEL _cmd);

@interface AnonymousObject : NSObject <MyProtocol>
@end

@implementation AnonymousObject

+ (instancetype)objectWithImplementation:(NSDictionary<SEL, MethodImplementationBlock> *)implementations {
    AnonymousObject *obj = [[self alloc] init];
    
    for (SEL selector in implementations) {
        MethodImplementationBlock block = implementations[selector];
        
        // 动态添加方法
        class_addMethod([self class], selector, (IMP)_block_invoke, "v@:");
        
        // 存储 Block 到关联对象中
        objc_setAssociatedObject(obj, (__bridge void *)selector, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    return obj;
}

+ (IMP)_block_invoke {
    return (IMP)^(id self, SEL _cmd) {
        MethodImplementationBlock block = (MethodImplementationBlock)objc_getAssociatedObject(self, (__bridge void *)_cmd);
        if (block) {
            return block(self, _cmd);
        }
    };
}

@end
    
Step 3: 使用匿名对象
#import "MyProtocol.h"
#import "AnonymousObject.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id<MyProtocol> obj = [AnonymousObject objectWithImplementation:@{
            @selector(doSomething): ^(id self, SEL _cmd) {
                NSLog(@"匿名对象正在执行 doSomething");
                return nil;
            },
            
            @selector(getName): ^(id self, SEL _cmd) {
                return @"匿名对象名称";
            }
        }];
// 匿名对象正在执行 doSomething
// 名字是:匿名对象名称
        [obj doSomething];
        NSLog(@"名字是:%@", [obj getName]);
    }
    return 0;
}

匿名对象的使用 场景描述:网络请求回调的模拟

假设你在开发一个 iOS App,需要从服务器获取用户信息。你使用了一个第三方网络库(比如 AFNetworking),它提供了一个接口方法:

- (void)fetchUserInfoWithCompletion:(void (^)(NSDictionary *userInfo))completion;

你希望为这个接口写一个Mock 对象用于单元测试,这样在不联网的情况下也能验证逻辑是否正确。

但你不想为此专门新建一个类 MockNetworkManager,因为这个 Mock 只在测试中使用一次,没必要单独建一个类。

这时候,你就可以使用“匿名对象”来快速构造一个满足协议的 mock 实现。

✅ 示例:使用匿名对象做 Mock 测试

Step 1: 定义协议
// NetworkManagerProtocol.h
@protocol NetworkManagerProtocol <NSObject>
- (void)fetchUserInfoWithCompletion:(void (^)(NSDictionary *userInfo))completion;
@end
    
Step 2: 使用匿名对象模拟实现
id<NetworkManagerProtocol> mockManager = [AnonymousObject objectWithImplementation:@{
    @selector(fetchUserInfoWithCompletion:): ^id(id self, SEL _cmd, void (^completion)(NSDictionary *)) {
        NSDictionary *mockData = @{@"name": @"张三", @"age": @25};
        completion(mockData);
        return nil;
    }
}];
Step 3: 在测试中使用
[mockManager fetchUserInfoWithCompletion:^(NSDictionary *userInfo) {
    NSLog(@"收到用户信息:%@", userInfo);
    // 验证 userInfo 是否符合预期
    XCTAssertNotNil(userInfo);
    XCTAssertEqualObjects(userInfo[@"name"], @"张三");
}];

这个场景的价值在哪?

优点说明
不需要创建新类仅用于测试的 mock,无需定义完整类
快速构建行为直接用 Block 写回调逻辑,简洁直观
协议驱动设计确保接口一致性,便于替换实现
提高可测试性脱离网络依赖,方便自动化测试

类似应用场景举例

插件系统动态加载模块时,临时生成某个协议的实现
事件总线/消息中间件注册监听器时,直接传入 Block 形式的对象
UI 组件代理某些组件需要设置 delegate,但不想新建类时
回调封装多个异步操作统一处理逻辑时,简化代码结构

常见的场景2:封装网络请求回调为统一接口

假设你在做一个电商 App,需要调用多个后端接口:

  • 获取商品信息
  • 获取用户信息
  • 下单支付

这些接口都有类似的结构:

深色版本

- (void)fetchProductWithCompletion:(void (^)(NSDictionary *product, NSError *error))completion;
- (void)fetchUserWithCompletion:(void (^)(NSDictionary *user, NSError *error))completion;
- (void)placeOrderWithCompletion:(void (^)(BOOL success, NSError *error))completion;

如果你直接在每个 VC 中写 completion block,会有很多重复代码,也不利于统一处理错误、加载状态等。

于是你想抽象出一个统一的回调接口:

@protocol ResponseHandler <NSObject>
- (void)onSuccess:(id)data;
- (void)onError:(NSError *)error;
@end

但你又不想为每个请求都创建一个新的类(如 ProductResponseHandlerUserResponseHandler),这时候就可以使用匿名对象 + 协议来封装回调逻辑。

**✅ 示例:用匿名对象封装回调行为 **

Step 1: 定义协议
// ResponseHandler.h
@protocol ResponseHandler <NSObject>
- (void)onSuccess:(id)data;
- (void)onError:(NSError *)error;
@end

Step 2: 创建匿名对象工厂方法(核心)
// AnonymousHandler.m
#import "ResponseHandler.h"
#import <objc/runtime.h>

@interface AnonymousHandler : NSObject <ResponseHandler>
@end

@implementation AnonymousHandler {
    void (^_onSuccess)(id);
    void (^_onError)(NSError *);
}

+ (id<ResponseHandler>)handlerWithSuccess:(void (^)(id))success
                                    failure:(void (^)(NSError *))failure {
    AnonymousHandler *handler = [[self alloc] init];
    _onSuccess = [success copy];
    _onError = [failure copy];
    return handler;
}

- (void)onSuccess:(id)data {
    if (_onSuccess) {
        _onSuccess(data);
    }
}

- (void)onError:(NSError *)error {
    if (_onError) {
        _onError(error);
    }
}

@end
    
Step 3: 在业务代码中使用这个匿名回调对象
比如,在某个 ViewController 中发起网络请求:

- (void)viewDidLoad {
    [super viewDidLoad];

    id<ResponseHandler> handler = [AnonymousHandler handlerWithSuccess:^(id data) {
        NSLog(@"请求成功,数据是:%@", data);
        // 更新 UI
    } failure:^(NSError *error) {
        NSLog(@"请求失败:%@", error.localizedDescription);
        // 显示错误提示
    }];

    [[NetworkManager shared] fetchProductWithCompletion:^(NSDictionary *product, NSError *error) {
        if (error) {
            [handler onError:error];
        } else {
            [handler onSuccess:product];
        }
    }];
}
    

这个场景的意义在哪?

优势描述
统一接口所有网络请求都使用相同的 ResponseHandler 协议回调,便于统一处理
减少冗余代码不必为每个请求写大量重复的 completion block
解耦逻辑请求和回调分离,ViewController 只负责展示,不处理复杂逻辑
可扩展性强后续可以添加 loading 状态、日志记录、重试机制等通用逻辑

更进一步:将回调封装进 ViewModel 或 Service 层

你还可以把这个匿名回调对象封装到更上层的服务中,比如:

- (void)loadProductWithHandler:(id<ResponseHandler>)handler {
    [[NetworkManager shared] fetchProductWithCompletion:^(NSDictionary *product, NSError *error) {
        if (error) {
            [handler onError:error];
        } else {
            [handler onSuccess:product];
        }
    }];
}

[self.viewModel loadProductWithHandler:[AnonymousHandler handlerWithSuccess:^(id data) {
    self.productLabel.text = data[@"name"];
} failure:^(NSError *error) {
    [self showAlert:error.localizedDescription];
}]];

内存管理

27.引用计数

引用计数用来管理内存,每个对象都有个可以递增递减的计数器。如果想使某个对象继续存活,那就增加其引用计数。用完了之后,就减少其计数。计数变为0。表示该对象可以销毁了。

a.引用计数工作原理

在引用计数架构下,每个对象都会有个计数器,表示当前有多少个对象希望此对象继续存在。 这是对象的引用计数。 引用计数的计数器有3个方法:

retain引用计数 + 1
release引用计数 - 1
autorelease待稍后清理自动释放池(autorelease pool) 再引用计数 - 1
retainCount查看引用计数 (已废弃)

对象被创建出来时,引用计数至少为1。若想让其继续存活,调用retain方法。要是某部分代码不想让其继续存活,那就调用release或autorelease方法。当引用计数 = 0 时,对象就会被回收deallocated了。

下面演示对象自创建出来后,历经一次保留以及两次释放的操作过程。

image.png

应用程序在生命周期中会创建很多对象,这些对象相互联系,形成了一张张对象图(object graph)。对象如果持有其它对象的强引用,那么前者就拥有后者。

下图所示的对象图,表明B\C都引用了A。若B\C都不再引用A,则A的引用计数降为0,系统便会销毁A。(而应用程序的里又有其它对象想令B\C继续存活,如果按引用树回溯,最终会发现一个根对象root object。 在iOS中是UIApplication对象)

image.png

下面的代码演示引用计数操作方法的用法

   NSMutableArray *array = [[NSMutableArray alloc] init];
   NSNumber *number = [[NSNumber alloc] initWithInt:1337];
   [array addObject:number]; //array引用number对象,number 引用计数 + 1 = 2
   [number release]; //代码中直接调用release方法在ARC下无法编译  number 引用计数 - 1 = 1  
   [array release];  //此时number引用计数 -1 = 0
    
//在这个例子中我们知道array仍然持有number,所以number仍然存活。但是不应该假设number对象一定存活,因为绝大多数情况,**我们并不清楚是否还有其它对象如array这样引用着number对象**。
//这意味不要这样写代码,在对象release后仍使用对象。
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject:number];
[number release];
NSLog(@"number = %@", number); //不要这样子,因为number可能已经被释放了,
    //大多数情况下,我们是不确定是否还有其它对象仍引用着number的。

这里有个细节,如果[number release];后number引用计数变为0了。那么number对象所占内存也许会回收,这样的话,调用NSLog使用number就可能崩溃了。

这里说可能,没说一定。因为对象所占的内存在解除分配deallocated后,只是放回可用内存池avaiable pool。如果执行NSLog时尚未覆写对象内存。那么该对象仍然有效,这时程序不会崩溃。

由此可见,因过早释放对象而导致的bug很难调试。(因为覆写对象内存再使用,那崩溃。美覆写前使用,不会崩溃。 覆写内存对于开发者是不可见的)。

基于上述原因,编码建议

b.防止悬挂指针的编码建议
//为避免在不经意间使用了无效对象,一般调用完release之后,清空指针,这样就能保证不会
出现可能指向无效对象的指针。这种指针称为悬挂指针。
NSNumber *number = [NSNumber alloc] initWithInt: 1223];
[array addObject:number];
[number release];
 number = nil;
c.属性存取方法中的内存管理
不只是数组,通过 a.b 点语法,a持有b也能增加引用计数。   
设置属性时,会调用set方法。若属性为strong引用,那么设置的属性值会保留。
比如foo属性由名为_foo的实例变量所实现,那么设置方法实际上是这样的。
-(void)setFoo:(id)foo {  
  //执行前此时_foo 和 foo 可能指向同一内存区域(值没有变的情况)
  [foo retain];  //如果先[_foo release],会导致foo 和 _foo都被释放了,后面再用到 foo就会报错,所以先[foo retain];
  [_foo release]; //准备销毁旧的_foo进行重写了
  _foo = foo;  //更新实例变量
  //上面步骤总结:保留新值,释放旧值,更新实例变量指向新值。
  //顺序很重要,如果还未保留新值就把旧值释放了,而且两个值又指向同一个对象。
  //那么先执行的release操作就可能导致系统将此对象回收。
  //而后续的retain操作无法令已经回收的对象复生
  //于是实例变量就变成了悬挂指针
}
d.自动释放池 autorelase

调用release会立刻将对象的引用计数-1(这可能令系统回收此对象),然而有时候可以不调用它。改为调用autorelease,此方法会在稍后递减引用计数。通常是在下一次事件循环时递减。

-(NSString*)stringVal {
  NSString* str = [[NSString alloc] initWithFormat:@"I am this: %@", self];    //alloc使得引用计数+1
  return str;  //1+retainCount, 调用者要处理掉这多出来的引用计数
  //但是不能在方法内释放str,否则就相当于没返回了。
  //这是就可以用autorelease,稍后释放对象(当前线程下一次事件循环时释放对象)
}
    
//改写
 -(NSString*)stringVal {
  NSString* str = [[NSString alloc] initWithFormat:@"I am this: %@", self];   
  return [str autorelease];  
} 
//这样的话,stringVal返回str给调用者时,此对象必然存活。
NSString *str = [self stringVal];
NSLog(@"The string is:%@", str); 
e.循环引用

循环引用会导致内存泄漏。 A和C间接互相引用。 使用weak弱引用来解决循环引用问题。 image.png

28.ARC自动管理引用计数

Clang编译器项目自带静态分析器(static analyzer),用于指明引用计数出问题的地方。

ARC自动管理引用计数,retain \ release操作由ARC自动添加。 由于ARC会自动执行retain\release\autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。

具体来说,不能调用下列方法: retain\release\autorealease\dealloc, 否则会产生编译错误。因为手工会干扰ARC工作。

    //在MRC下该内存泄漏,因为没有释放msg对象
    if([self shouldLogMessage]) {
      NSString *msg = [[NSString alloc] initWithFormat:@"I am obm %p", self];
      NSLog(@"msg = %@", msg);
      
      [message release]; //ARC环境下会自动加上这一行
    }

使用ARC时方法命名特性:

将内存管理语义在方法名中表示出来是oc的惯例,而ARC则将他确立为硬性规定。规则体现在方法名上,如果方法名以alloc\new\copy\mutableCopy开头,则其返回的对象归调用者所有,即对象调用者要负责释放方法返回的对象。如果不以上面4个词语开头,则表示方法返回的对象不归调用者所有,返回的对象会自动释放。这种情况下要使得对象多存活一段时间,必须让调用者保留它才行。 上面的内存管理都是ARC自动处理,下面演示ARC如何自动管理的

+(EOCPerson*)newPerson { //new开头,alloc引用计数+1, 但是return person时我们无需释放,因为new开头给代码调用者就去释放。
  EOCPerson *person = [[EOCPerson alloc] init];
  return person;
}
+(EOCPerson*)somePerson { //非特定开头,person不归调用者释放,由方法本身释放。
  EOCPerson *person = [[EOCPerson alloc] init];
  return person;  //arc会自动把它变成 [person autorelease]
}

-(void) doSomething { EOCPerson *personOne = [EOCPerson newPerson]; //由本代码块释放personOne对象 EOCPerson *personTwo = [EOCPerson somePerson]; //由somePerson函数自己释放对象

[personOne release]; //ARC自动添加这一行,内存管理 }

29.dealloc方法中释放引用,销毁监听器

dealloc中释放掉OC对象(ARC自动处理) 其它非OC对象,比如CoreFoundation,需要手动释放(由纯C的API所生成的)

销毁监听器(如果用NSNotificationCenter给对象订阅过通知,那么一般应该在这里注销,这样,通知系统就不会再把通知发给回收后的对象了,若还是向其发送通知,应用程序会崩溃)。

另外dealloc不要再调用方法了。

 
- (void)dealloc {
  CFRelease(coreFoundationObject);
  [[NSNotificationCenter defaultCenter] removeObserver:self];
   //如果是手动管理,最后还需要执行[super dealloc] ,但是ARC会自动添加。
}

30.编写"异常安全代码"时留意内存管理问题

发生异常时该如何管理内存?

在try块中,如果先保留了对某个对象,然后在释放它之前抛出了异常。 除非catch能处理此问题,否则对象所占内存就会泄漏。

以MRC环境销毁为例

@try {
  EOCSomeClass *object = [[EOCSomeClass alloc] init];
  [object doSomethingThatMayError]; //如果异常,执行过程会终止并跳转至catch块,后面的release操作不会运行,对象泄漏。
  [object release];
}@catch(...) {
  
} 
//解决方法,在finally块释放对象
 EOCSomeClass *object = [[EOCSomeClass alloc] init];
@try {
  [object doSomethingThatMayError]; 
}@catch(...) {
  
}@finally {
  [object release]; 
   //这里由于finally要访问object,所以移动到了@try块之外。
   //如果所有对象都这样释放会非常麻烦。
}

30.弱引用避免循环引用

对象互相引用,形成循环引用,导致对象无法释放,内存泄漏。 image.png

一个简单的循环引用

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA: NSObject;
  @property(nonatomic, strong) EOCClassB *other;
@end
   
@interface EOCClassB: NSObject;
  @property(nonatomic, strong) EOCClassA *other;
@end

image.png

避免循环引用的方式就是弱引用,表示非拥有关系。

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA: NSObject;
  @property(nonatomic, strong) EOCClassB *other;
@end
   
@interface EOCClassB: NSObject;
  @property(nonatomic, unsafe_unretained) EOCClassA *other;
@end
 //这样EOCClassB实例不再通过other属性来拥有EOCClassA实例来。
 //unsafe_unretained表明属性值可能不安全,不归此实例所有。
 //unsafe_unretained和assign语义等价,但是assign用于基本类型 int\float\结构体等, unsafe_unretained用于对象类型。
 还有一种是weak关键词,weak 与unsafe_unretained作用完全相同。但是weak声明的属性,如果属性回收了,属性值会变为nil。(所以使用weak,现在基本没人用unsafe_unretained了)

image.png

31.@autoreleasepool自动释放池块,降低内存峰值

OC对象的生命期取决于其引用计数。释放对象的2种方式:1.调用release方法,引用计数理解递减。2.autorelease方法,加入自动释放池中。自动释放池存放稍后释放的对象,清空释放池,就会向其中的对象发送release消息。

比如main函数里
@autoreleasepool {
   return UIApplicationMain(...);
}
@autoreleasepool {
   //像这种,for创建的临时对象会使得内存爆增。要等for执行完毕才会释放。
   for(int i=0; i<1000000; i++) {
      [self doSomething:i];                        
   }
}  
//所以把autoreleasepool放里面,避免循环尤其是,循环创建图片等大容量的对象时,导致内存爆增进程被杀死。
   for(int i=0; i<1000000; i++) {
      @autoreleasepool {
        [self doSomething:i]; 
      }                       
   }

32.用'僵尸对象'调试内存管理问题

a.僵尸对象解决的调试内存困境

僵尸对象时调试内存管理问题的最佳方式。

调试内存管理问题的困境。向已回收的对象发送消息是不安全的,这么做有时可以有时不行。完全取决于对象所占内存也没有被其它内容覆写,而这块内存有没有移作他用,无法确定。

因此,应用程序只是偶尔崩溃。 在没有崩溃的情况下,那块内存可能只是复用了其中一部分,所以对象的某些二进制数据依然有效。

还有一种可能, 就是那块内存刚好被另外一个有效且存活的对象占据。在这种情况下,运行期系统会把消息发送到新对象里,而此对象也许能应答,也许不能。如果能,那程序就不会崩溃。有时候你遇到相关问题会奇怪,为什么收到消息的对象不是预想中的那个呢? 就是这个原因。若新对象无法响应选择子,那也会崩溃

基于上述场景,Xcode提供了僵尸对象(Zombie Object)的调试功能。运行期间,系统会把所有已经回收的实例转换成特殊的‘僵尸对象’,而不会真正回收它们。僵尸对象所在的核心内存无法重用,因此不会被覆写。

b.Xcode开启僵尸对象调试功能

僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的对象。

给僵尸对象发送消息后,控制台会打印消息,而应用程序会终止。打印出来的消息就像这样。

-[CFString respondsToSelector:]: message sent to deallocated instance 0x7ff9e9c080e0

Xcode打开僵尸对象,Xcode->Product->Scheme->Run->Diagnostics->Memory Management -> Zombie Objects

image.png

c.僵尸对象的工作原理

系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,将执行一个附加步骤,把对象转换为僵尸对象,而不彻底回收。

下面MRC模式下的代码有助于理解这一附加步骤

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
    
@interface EOCClass: NSObject
@end
    
@implementation EOCClass
@end
//打印类信息  
void PrintClassInfo(id obj) {
  Class cls = object_getClass(obj); //这里没有直接用class方法获取class,是因为如果obj已经是僵尸对象了,那么给其发送oc消息后,控制台会打印错误的消息。而且应用程序会崩溃。
  Class superClas = class_getSuperClass(cls);
  NSLog(@"=== %s: %s ===", class_getName(cls),        class_getName(superClass));
}
    
int main(int argc, char *argv[]) {
  EOCClass *ojb = [[EOCClass alloc] init];   
  NSLog(@"Before release:");
  PrintClassInfo(obj);
  [obj release];
  NSLog(@"After release:");
  PrintClassInfo(obj);
} 

代码输出如下

Before release:
=== EOCClass: NSObject ===
After release:
=== _NSZombie_EOCClass: nil ===

可以看到对象释放后再调用,类型变成了_NSZombie_EOCClass。这是运行期生成的,在首次碰到EOCClass类的对象编程僵尸对象时,就会创建这么一个类。

僵尸类是从名为_NSZombie_的模板类复制出来的。下面是模板类的复制代码。

//获取将要被释放的类
Class cls = object_getClass(self);
//获取类名
const char *clsName = class_getName(cls);
//加上 _NSZombie_ 前缀
const chat *zombieClsName = "_NSZombie_" + clsName;
//判断之前是否创建过该类的僵尸类
Class zombieCls = objc_lookUpClass(zombieClsName);
//不存在就创建
if (!zombieCls) {
   Class baseZombieCls = objc_lookUpClass("_NSZombie_");
   zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
objc_desturctInstance(self); //销毁
objc_setClass(self, zombieCls);

代码的关键之处在于,对象所占内存没有释放。因此这块内存不可服用。虽然说内存泄漏了,但这只是个调试手段,正式版本的应用不会把这个功能打开。 系统会修改对象的isa指针,令其只想对应的僵尸类。从而使得该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

块与大中枢派发(block与GCD)

苹果多线程编程的核心是块(block)与大中枢派发(GCD Grand Central Dispatch)。

块是一种在C\C++\OC中使用的词法闭包,让代码像对象一样传递。

GCD是一种与块有关的技术,提供了对线程的抽象,基于派发队列。开发者将块排入队列中,GCD负责调度。GCD会根据系统资源情况,创建、复用、摧毁后台线程,以便处理每个队列。

a.什么是块?

块与函数类似,只不过是直接定义在另一个函数里,和定义它的函数共享同一个范围的东西。块用 ^ 符号表示。

// 一个简单的块
^ {
  //块的实现
}
// 块其实就是个值,可以把块赋值给变量。
void(^someBlock)() = ^{
  //返回值/(^块名)(入参)
}
    
//定义好后像函数一样使用
int (^addBlock)(int a, int b) = ^(int a, int b) {
  return a+b;    
}
//定义好之后,就可以像函数一样使用
int add = addBlock(2,5); //add = 7
b.使用__block 修改块外部的变量

块的生命范围内,所有变量都可以为其使用。

int additional = 6;
int (^addBlock)(int a, int b) = ^(int a, int b) {
  return a+b+additional; //使用了外部的additional    
}
int add = addBlock(2 ,5); //add = 13
//但是block里不能修改外部的变量,如果要修改的话
//用 __block声明对应变量
__ block NSInteger count = 0;
NSArray *array = @[@0, @1, @2];
array enumerateObjectsUsingBlock: ^(NSNumber *number, NSUInteger idx, BOOL *stop) {
  if([number compare:@2] == NSOrderedAscending) {
    count++; //__block声明的变量可以在块内部修改
  }
}

c.block中容易产生的循环引用

如果block定义在OC的实例方法中,除了可以访问类的所有实例变量之外,还可以使用self变量。

块总能修改实例变量,所以在声明时无需加__block。如果在block里使用了self,会把self变量捕获/持有。

这个时候要注意,如果block本身被self持有,block里又调用self。导致循环引用。

@interface MyViewController ()
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myBlock = ^{
        // Block 内部强引用了 self
        [self doSomething];
    };
}
- (void)doSomething {
    NSLog(@"Doing something...");
}
@end

解决方案,用weak打破循环引用。

为了打破循环,我们可以在 Block 外部创建一个对 self 的弱引用,然后在 Block 内部使用这个弱引用:

  1. __weak typeof(self) weakSelf = self;

    • 创建了一个对 self 的弱引用,这样 Block 不会增加 self 的引用计数。
  2. __strong typeof(weakSelf) strongSelf = weakSelf;

    • 在 Block 内部先将 weakSelf 提升为强引用,防止在执行过程中 weakSelf 被释放。
    • 如果不这么做,在多线程环境下,有可能在 Block 执行期间 weakSelf 变成 nil。
  3. if (strongSelf)

    • 判断是否已经被释放,避免访问空指针。
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            [strongSelf doSomething];
        }
    };
d.全局块、栈块、堆块 - 使用block的常见错误注意

定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。

比如下面这段代码,就有危险。

void (^block)();
    
if (condition) {
  block = ^{
    NSLog(@"Block A"); 
  } 
} else {
  block = ^{
    NSLog(@"Block B"); 
  }     
}
    
block();
    
ifelse的两个block分配在栈内存中。然而等离开了if\else块后,编译器可能会把分配给block的内存覆写掉。于是这两个block,只能保证在对应的if \ else语句范围内有效。
    
**这样写出来的代码可以编译,但是运行起来,时而正确时而错误。**
若编译器未覆写待执行的块,则程序照常运行。弱覆写了,则程序崩溃。
    
//解决方案
为了解决该问题,可以给块对象发送copy消息,将块从栈内存复制到堆内存。拷贝后的块,可以在定义它的范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。需要释放回收,而分配在栈上的块,无需明确释放,系统自动回收。
    
void (^block)();
    
if(condition) {
  block = [^{
    NSLog(@"Block A");
  } copy];    
} else {
  block = [^{
    NSLog(@"Block B");
  } copy];       
}
    
block();

全局块

还有一种块叫做全局块,栈块如果没有捕获到变量就变成了全局块。(这不是说它没有捕获变量的能力,而是没有变量供它捕获)

块所使用的内存区域在编译期就确定了,因此全局块可以声明在全局内存里,不用每次用到的时候在栈中创建。(相当于一个单例了)。

Block 的三种类型

类型名称特点
_NSConcreteGlobalBlock全局块存在于程序的整个生命周期,不依赖于任何局部变量
_NSStackBlock栈块位于栈上,离开作用域后会被销毁
_NSMallocBlock堆块从栈拷贝到堆上,由 ARC 管理内存,具有持久性
int globalVar = 10;

void testGlobalBlock() {
    // 全局块:不捕获任何局部变量的 block
    void (^globalBlock)(void) = ^{
        NSLog(@"Global block");
    };
    
    NSLog(@"%@", [globalBlock class]); // 输出:__NSGlobalBlock__
}

void testStackBlock() {
    int localVar = 20;
    
    // 栈块:捕获了局部变量
    void (^stackBlock)(void) = ^{
        NSLog(@"Stack block: localVar = %d", localVar);
    };
    
    NSLog(@"%@", [stackBlock class]); // 输出:__NSStackBlock__
}

void testMallocBlock() {
    int localVar = 30;
    
    // 把栈块 copy 到堆上,变成堆块
    void (^mallocBlock)(void) = [^{
        NSLog(@"Malloc block: localVar = %d", localVar);
    } copy];
    
    NSLog(@"%@", [mallocBlock class]); // 输出:__NSMallocBlock__
    
    // 不要忘记 release(在非 ARC 下才需要)
    [mallocBlock release];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testGlobalBlock();
        testStackBlock();
        testMallocBlock();
    }
    return 0;
}

1. 全局块(Global Block)

  • 没有捕获任何局部变量
  • 编译器会把它放到 .data 段中,类似全局变量
  • 生命周期和程序一致

2. 栈块(Stack Block)

  • 捕获了局部变量
  • 默认分配在栈上,不能脱离当前作用域使用
  • 如果你在方法中返回一个栈块,调用者访问时可能出错(野指针)

3. 堆块(Malloc Block)

  • 是栈块被 copy 后的结果
  • 被拷贝到堆上,可以安全地跨作用域使用
  • 在 ARC 环境下,编译器会自动帮你做 copy(例如赋值给 copy 属性时)

33.为常用的块类型创建typedef

假设有一个这样的块
int (^blockName)(BOOL flag, int val) = ^(BOOL flag, int value) {
  if(flag) {
    return val*5;
  }else {
    return val*10;
  }    
}
    
typedef int(^EOCSomeBlock)(BOOL flag, int val);
    
EOCSomeBlock block = ^(BOOL flag, int value) {
  if(flag) {
    return val*5;
  }else {
    return val*10;
  }    
}

34.用handler块降低代码分散程度

封装网络请求API时,回调请求结果。一般有2种方式,使用handler块回调,或者代理delegate回调。

1.使用delegate实现网络请求的回调

//1.定义代理协议
// NetworkRequestDelegate.h
#import <Foundation/Foundation.h>
@protocol NetworkRequestDelegate <NSObject>
- (void)didReceiveData:(NSString *)result;
@end
    
//2.定义网络请求类
// NetworkRequestWithDelegate.h
#import <Foundation/Foundation.h>
#import "NetworkRequestDelegate.h"

@interface NetworkRequestWithDelegate : NSObject
@property (nonatomic, weak) id<NetworkRequestDelegate> delegate;
- (void)startRequest;
@end

// NetworkRequestWithDelegate.m
#import "NetworkRequestWithDelegate.h"

@implementation NetworkRequestWithDelegate

- (void)startRequest {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2.0];
        NSString *result = @"Hello from network (Delegate)";

        dispatch_async(dispatch_get_main_queue(), ^{
            if ([self.delegate respondsToSelector:@selector(didReceiveData:)]) {
                [self.delegate didReceiveData:result];
            }
        });
    });
}

@end
    
// 3.在 ViewController 中实现代理
// ViewController.h
#import <UIKit/UIKit.h>
#import "NetworkRequestDelegate.h"

@interface ViewController : UIViewController <NetworkRequestDelegate>
@property (weak, nonatomic) IBOutlet UILabel *resultLabel;
@end

// ViewController.m
- (IBAction)requestButtonTapped:(id)sender {
    NetworkRequestWithDelegate *request = [[NetworkRequestWithDelegate alloc] init];
    request.delegate = self;
    [request startRequest];
}

#pragma mark - NetworkRequestDelegate
- (void)didReceiveData:(NSString *)result {
    self.resultLabel.text = result;
}

2.使用block实现网络请求的回调

//1. 定义网络工具类(NetworkManager)
// NetworkManager.h
#import <Foundation/Foundation.h>

typedef void(^NetworkCompletionHandler)(NSString * _Nullable result);

@interface NetworkManager : NSObject
+ (void)requestDataWithCompletion:(nonnull NetworkCompletionHandler)completion;
@end

// NetworkManager.m
#import "NetworkManager.h"
@implementation NetworkManager
+ (void)requestDataWithCompletion:(nonnull NetworkCompletionHandler)completion {
    // 模拟子线程执行网络请求
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 模拟耗时操作
        [NSThread sleepForTimeInterval:2.0];
        NSString *result = @"Hello from network (Block)";

        // 回调主线程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(result);
        });
    });
}
@end
    
//2. 在 ViewController 中调用
// ViewController.m
#import "ViewController.h"
#import "NetworkManager.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *resultLabel;
@end

@implementation ViewController
- (IBAction)requestButtonTapped:(id)sender {
    [NetworkManager requestDataWithCompletion:^(NSString * _Nullable result) {
        self.resultLabel.text = result;
    }];
}
@end

系统框架