内存管理: 引用计数:强引用(Strong Reference)和弱引用(Weak Reference) App优化 crash

171 阅读1小时+

0.预备知识-@property、@synthesize 和 @dynamic

juejin.cn/post/698632…

@property-可以自动生成属性的存取方法

属性用于封装对象中数据,属性的本质是 ivar + setter + getter。可以用 @property 语法来声明属性,会帮我们自动生成属性的 setter 和 getter 方法的声明。

常见的property属性关键字及其作用:

Objective-C中,property是一种语法特性,用于声明类的属性。使用property可以自动生成属性的存取方法(即getter和setter方法) ,简化代码编写。

property的声明还可以通过属性关键字来指定属性的一些特性,如内存管理策略、原子性、访问权限等。常见的property属性关键字及其作用:

分类属性关键字
原子性atomic、nonatomic
读写权限readwrite、readonly
方法名setter、getter
内存管理assign、weak、unsafe_unretained、retain、strong、copy
可空性(nullable、_Nullable 、__nullable)、 (nonnull、_Nonnull、__nonnull)、 (null_unspecified、_Null_unspecified 、__null_unspecified)、 null_resettable
类属性class

1. 内存管理关键字:

  • strong:用于对象类型,表示强引用,是默认的管理策略。

  • weak:用于对象类型,表示弱引用,当对象被释放时,属性自动置为nil

  • assign:用于基本数据类型(如intfloat等)和C数据类型,以及不需要增加引用计数的对象,如 delegate。使用 assign 的对象属性在对象释放后不会自动置为 nil,可能会产生野指针。

点击展开内容

assign 属性是 Objective-C 中属性声明的一个关键字,主要用于基本数据类型(如 NSInteger、CGFloat 等)和不需要增加引用计数的对象(如 delegate 通常被声明为 assign 来避免循环引用)。assign 的作用和特点如下:

作用

  1. 基本数据类型:对于基本数据类型的属性,assign 用于直接赋值,不涉及引用计数的管理。这是因为基本数据类型不是对象,所以不需要进行内存管理。

  2. 不拥有对象:对于对象类型的属性,使用 assign 表示属性的设置不会增加对象的引用计数(retain count)。这意味着属性不会“拥有”这个对象,如果对象在其他地方被释放了,这里的引用将变成野指针。

使用场景

  1. 基本数据类型:如 NSIntegerCGFloat 等,因为它们不是对象,所以使用 assign

  2. 委托(Delegate)属性:为了避免循环引用(retain cycles),委托属性通常使用 assign(在 ARC 下通常使用 weak,因为 weak 会在对象释放时自动置为 nil,避免野指针问题)。

  3. C 结构体:对于非对象类型,如 C 语言中的结构体(struct),也使用 assign

示例


@interface MyClass : NSObject

@property (nonatomic, assign) NSInteger count;

@property (nonatomic, assign) id<MyDelegate> delegate;

@end

assignweak 的区别

  • assign:不会增加对象的引用计数,用于基本数据类型和对象类型。对于对象类型,如果对象被释放,assign 属性不会自动置为 nil,存在野指针的风险。

  • weak:不会增加对象的引用计数,专用于对象类型。当对象被释放时,weak 属性会自动置为 nil,避免了野指针问题。weak 是 ARC 环境下防止循环引用的首选方式。

注意事项

使用 assign 属性时,特别是在对象类型上,需要注意对象的生命周期,确保在对象被释放后不再访问该属性,以避免野指针导致的崩溃问题。在现代 Objective-C 开发中,对于对象类型,推荐使用 strongweakcopy,根据具体场景选择最合适的属性关键字。

-retain:用于对象类型,表示属性在赋值时会对新对象发送 retain 消息,对旧对象发送 release 消息,从而接管新对象的所有权并释放旧对象。这与 ARC 下的 strong 类似。

  • copy:用于对象类型,设置属性时,会对对象进行拷贝操作。---TODO:

Copy的使用场景与条件

copy 在 Objective-C 中主要用于以下场景:

  1. 保持对象的不变性:当你需要保证对象的状态不被外部修改时,使用 copy。例如,对于 NSString 属性,使用 copy 而不是 strong,可以防止外部传入一个 NSMutableString 并在后续修改它,从而影响到你的类的状态。

  2. 避免意外修改:在集合对象(如 NSArrayNSDictionary)中,使用 copy 可以避免因原集合对象被修改而导致的数据不一致问题。

  3. 实现自定义的拷贝逻辑:当对象内部持有的数据需要特殊处理才能正确拷贝时(例如深拷贝某些属性),通过重写 -copyWithZone: 方法实现。

如果没有重写copyWithZone:会怎么样

如果一个对象实现了 NSCopying 协议但没有重写 -copyWithZone: 方法,尝试对该对象执行 copy 操作时,运行时会抛出异常,因为默认的方法实现并不知道如何拷贝对象。这意味着,如果你的类声明遵循了 NSCopying 协议,你必须提供 -copyWithZone: 方法的自定义实现。

-copyWithZone:方法的默认行为

如果一个类没有实现 NSCopying 协议,直接调用其实例的 copy 方法会导致运行时错误,因为对象不知道如何复制自己。如果类遵循了 NSCopying 协议但没有重写 -copyWithZone:,编译器会警告该类没有实现协议要求的方法。

示例

假设有一个简单的 Person 类,它有两个属性:nameage。如果 Person 类声明遵循了 NSCopying 协议但没有提供 -copyWithZone: 的实现,尝试复制一个 Person 实例将导致运行时错误。

正确的做法是这样的:


@interface Person : NSObject <NSCopying>

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@end

@implementation Person

- (id)copyWithZone:(NSZone *)zone {

Person *copy = [[[self class] allocWithZone:zone] init];

if (copy) {

copy.name = [self.name copy];

copy.age = self.age;

}

return copy;

}

@end

在这个例子中,Person 类正确实现了 NSCopying 协议的要求,提供了一个 -copyWithZone: 方法,确保了 Person 实例可以被正确复制。这里对 name 属性使用了 copy,这是因为 NSString 可能是可变的,使用 copy 可以确保 name 属性的内容不会被外部修改。

总结

在 Objective-C 中,正确使用 copy 是管理对象所有权和内存的重要部分。当你的对象需要被复制时,确保遵循并正确实现 NSCopying 协议是非常重要的。这不仅保证了对象的拷贝行为符合预期,也避免了潜在的运行时错误。

无主引用:

在 Objective-C 中,管理内存通常依赖于引用计数(Reference Counting),ARC(自动引用计数)简化了内存管理,但在处理循环引用时仍需要开发者手动干预。循环引用发生时,两个对象互相持有对方的强引用(strong reference),导致它们的引用计数永远不会降到0,从而无法被ARC回收,造成内存泄漏。为了解决这个问题,Objective-C 提供了弱引用(weak reference)和无主引用(__unsafe_unretained)。

OC/Swift

弱引用(weak)

弱引用不会增加对象的引用计数,当对象被销毁时,所有指向它的弱引用自动置为nil,这样可以避免循环引用。使用weak关键字声明弱引用:

@property (weak) MyClass *delegate;

无主引用(__unsafe_unretained)

无主引用与弱引用类似,不会增加对象的引用计数,但与弱引用不同的是,当所引用的对象被销毁时,无主引用不会自动置为nil,它会继续指向被销毁对象的内存地址,这可能会导致野指针错误。 因此,使用无主引用时需要确保引用的对象在引用期间不会被提前释放。使用__unsafe_unretained关键字声明无主引用:

@property (nonatomic, __unsafe_unretained) MyClass *delegate;

使用场景

  • 当你确定引用的对象在引用期间不会被释放时,可以使用无主引用。

  • 当存在循环引用的风险时,应该使用弱引用。

注意事项

  • 使用无主引用时要特别小心,因为它可能会导致野指针错误,访问已经被释放的内存地址。

  • 在大多数情况下,推荐使用弱引用来避免循环引用,因为弱引用更安全,被引用的对象释放后,弱引用会自动置为nil。

总的来说,Objective-C 中的无主引用(__unsafe_unretained)和弱引用(weak)都是解决循环引用问题的手段,但在使用时需要根据具体情况选择合适的引用类型,以确保程序的稳定性和安全性。

在 Swift 中,无主引用(Unowned References)是一种避免循环引用的机制,特别适用于两个实例之间存在生命周期不等的情况。无主引用假设它引用的实例总是有值的,因此无主引用总是被定义为非可选类型。在一个实例的生命周期中,如果另一个实例有更短的生命周期,那么可以使用无主引用。

无主引用的使用场景主要包括两种情况:

  1. 两个属性都必须有值,并且初始化完成后永远不会为nil。在这种情况下,一个类使用强引用来持有另一个类的实例,而另一个类使用无主引用来引用这个类的实例,以避免循环引用。

  2. 一个属性可以为nil,而另一个属性不允许为nil,并且可能会在其生命周期内变为nil。这种情况下,应该将允许为nil的属性定义为弱引用(weak reference)。

使用无主引用时,需要确保引用总是指向一个未被销毁的实例,因为访问被销毁的实例的无主引用会触发运行时错误。因此,在使用无主引用时要特别小心。

下面是一个使用无主引用的例子:


class Customer {

let name: String

var card: CreditCard?

init(name: String) {

self.name = name

}

deinit {

print("\(name) 被销毁")

}

}

class CreditCard {

let number: UInt64

unowned let customer: Customer

init(number: UInt64, customer: Customer) {

self.number = number

self.customer = customer

}

deinit {

print("卡号 \(number) 被销毁")

}

}

var john: Customer? = Customer(name: "John Appleseed")

john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

john = nil

// 输出:

// John Appleseed 被销毁

// 卡号 1234567890123456 被销毁

在这个例子中,CustomerCreditCard之间存在相互引用。Customer实例有一个可选类型的CreditCard属性,而CreditCard实例有一个无主引用属性customer。当你将john设置为nil时,Customer实例被销毁,随后CreditCard实例也被销毁。这里使用无主引用是因为一个信用卡总是关联到一个客户,即customer属性总是有值的。

内存管理

属性关键字用法
assign1. 既可以修饰基本数据类型,也可以修饰对象类型; 2. setter 方法的实现是直接赋值,一般用于基本数据类型 ; 3. 修饰基本数据类型,如 NSInteger、BOOL、int、float 等; 4. 修饰对象类型时,不增加其引用计数; 5. 会产生悬垂指针(悬垂指针:assign 修饰的对象在被释放之后,指针仍然指向原对象地址,该指针变为悬垂指针。这时候如果继续通过该指针访问原对象的话,就可能导致程序崩溃)。
weak1. 只能修饰对象类型; 2. ARC 下才能使用; 3. 修饰弱引用,不增加对象引用计数,主要可以用于避免循环引用; 4. weak 修饰的对象在被释放之后,会自动将指针置为 nil,不会产生悬垂指针; 5. 对于视图,通常还是用在 xib 和 storyboard 上;代码中对于有必要进行 remove 的视图也可以使用 weak,这样 remove 之后会自动置为 nil。
unsafe_unretained1. 既可以修饰基本数据类型,也可以修饰对象类型; 2. MRC 下经常使用,ARC 下基本不用; 3. 同 weak,区别就在于 unsafe_unretained 会产生悬垂指针; 4. weak 对性能会有一定的消耗,当一个对象 dealloc 时,需要遍历对象的 weak 表,把表里的所有 weak 指针变量值置为 nil,指向对象的 weak 指针越多,性能消耗就越多。所以 unsafe_unretained 比 weak 快。当明确知道对象的生命周期时,选择 unsafe_unretained 会有一些性能提升。比如 A 持有 B 对象,当 A 销毁时 B 也销毁。这样当 B 存在,A 就一定会存在。而 B 又要调用 A 的接口时,B 就可以存储 A 的 unsafe_unretained 指针。虽然这种性能上的提升是很微小的。但当你很清楚这种情况下,unsafe_unretained 也是安全的,自然可以快一点就是一点。而当情况不确定的时候,应该优先选用 weak。「比如笔者封装了一个 DisplayLink 类,在内部使用 NSProxy 中间变量来更好地避免循环引用,将 displayLink 的 target 设置为 proxy,而 proxy 当中需要调用 displayLink 的 API,为避免循环引用 proxy 就需要弱引用 displayLink。因为这里当 displayLink 存在 proxy 就存在,displayLink 销毁 proxy 就销毁,所以 proxy 就可以存储 displayLink 的 unsafe_unretained 指针。」
retain1. MRC 下使用,ARC 下基本使用 strong; 2. 修饰强引用,将指针原来指向的旧对象释放掉,然后指向新对象,同时将新对象的引用计数加 1; 3. setter 方法的实现是 release 旧值,retain 新值,用于 OC 对象类型。
strong1. ARC 下才能使用; 2. 原理同 retain; 3. 但是在修饰 block 时,strong 相当于 copy,而 retain 相当于 assign。
copysetter 方法的实现是 release 旧值,copy 新值,一般用于 block、NSString、NSArray、NSDictionary 等类型。使用 copy 或 strong 修饰 block 其实都一样,用 copy 是为了和 MRC 下保持一致的写法;用于 NSString、NSArray、NSDictionary 是为了保证赋值后是一个不可变对象,以免遭外部修改而导致不可预期的结果。

原子性

  • atomic:默认行为,保证属性的线程安全,但性能较低。

  • nonatomic:不保证线程安全,但性能较高。

属性关键字用法
atomic原子性(默认),编译器会自动生成互斥锁(以前是自旋锁,后面改为了互斥锁),对 setter 和 getter 方法进行加锁,可以保证属性的赋值和取值的原子性操作是线程安全的,但不包括操作和访问。 比如说 atomic 修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以保证线程安全的。 但是如果我们对数组进行操作,比如说给数组添加对象或者移除对象,是不在 atomic 的负责范围之内的,所以给被 atomic 修饰的数组添加对象或者移除对象是没办法保证线程安全的。
nonatomic非原子性,一般属性都用 nonatomic 进行修饰,因为 atomic 耗时。

读写权限

  • readwrite:默认行为,属性可读可写。

  • readonly:属性只读,只会生成getter方法。

属性关键字用法
readwrite可读可写(默认),同时生成 setter 方法和 getter 方法的声明和实现。
readonly只读,只生成 getter 方法的声明和实现。为了达到封装的目的,我们应该只在确有必要时才将属性对外暴露,并且尽量把对外暴露的属性设为 readonly。如果这时候想在对象内部通过 setter 修改属性,可以在类扩展中将属性重新声明为 readwrite;如果仅在对象内部通过 _ivar 修改,则不需要重新声明为 readwrite。

方法名

属性关键字用法
setter可以指定生成的 setter 方法名,如 setter = setName。这个关键字笔者在给分类添加属性的时候会用得比较多,为了避免分类方法“覆盖”同名的宿主类(或者其它分类)方法的问题,一般我们都会加前缀,比如 bb_ivar,但是这样生成的 setter 方法名就不美观了(为 setBb_ivar),于是就使用到了 setter 关键字 @property (nonatomic, strong, setter = bb_setIvar:) NSObject *bb_ivar;
getter可以指定生成的 getter 方法名,如 getter = getName。使用示例:@property (nonatomic, assign, getter = isEnabled) BOOL enabled;

类属性 class-实例属性与 instance 关联,类属性与 class 关联

Objective-C 类属性是在 Xcode 8 支持的,为了与 Swift 类型属性互操作,它不会进行 property autosynthesis。可以说自从 Swift 发布了后,Objective-C 的更新几乎都是为了更好地与 Swift 混编。

属性可以分为实例属性和类属性

  • 实例属性:每个实例都有一套属于自己的属性值,它们之前是相互独立的;
  • 类属性:可以为类本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份,因为类是单例。

用处:类属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量/变量(就像 C 语言中的静态常量/静态变量)。

通过给属性添加 class 关键字来定义类属性。@property (class, nonatimoc, strong) NSObject *object;

类属性是不会进行 property autosynthesis 的,那怎么关联值呢?

  • 如果是存储属性

    1. 在 .m 中定义一个 static 全局变量,然后在 setter 和 getter 方法中对此变量进行操作
    2. 在 setter 和 getter 方法中使用关联对象来存储值。笔者之前遇到的一个使用场景就是,类是通过 Runtime 动态创建的,这样就没办法使用 static 全局变量存储值。于是笔者在父类中定义了一个类属性并使用关联对象来存储值,这样动态创建的子类就可以给它的类属性关联值了。
  • 如果是计算属性,就直接实现 setter 和 getter 方法就好。

可空性:iOS 混编|为 Objective-C API 指定可空性

应用场景:

  1. 内存管理
  • 使用strong关键字来持有对象,常用于大多数需要持有的对象属性。

  • 使用weak关键字来避免循环引用,常用于delegate属性或者UI控件的IBOutlet属性。

  • 使用assign关键字来声明基本数据类型的属性

  • 使用copy关键字来保持对象的不可变性,常用于NSStringNSArray等集合类的属性。

  1. 多线程环境
  • 在多线程环境下,如果需要保证属性的线程安全,可以使用atomic关键字。但在实际开发中,由于atomic的性能开销,更多的情况下会选择使用nonatomic并通过其他方式来保证线程安全。
  1. 控制属性访问权限
  • 使用readonly关键字来声明只读属性,这在你希望外部只能访问而不能修改属性值的场景下非常有用,比如单例模式的共享实例。

总结:

property在Objective-C中是一个非常重要的特性,通过合理使用property的属性关键字,可以简化代码的编写,同时对属性的内存管理、线程安全性和访问权限进行有效的控制。在实际开发中,根据不同的应用场景选择合适的属性关键字,是提高代码质量和性能的关键。

@synthesize:指定实例变量名字以及自动生成 setter 和 getter 方法的实现以及 _ivar。

你还可以通过 @synthesize 来指定实例变量名字,如果你不喜欢默认的以下划线开头来命名实例变量的话。但最好还是用默认的,否则影响可读性。

eg:@synthesize ivar = _ivar;

如果不想令编译器合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法 setter or getter,那么另一个还是会由编译器来合成。

但是需要注意的是,如果你实现了属性所需的全部方法(如果属性是 readwrite 则需实现 setter and getter,如果是 readonly 则只需实现 getter 方法),那么编译器就不会自动进行 @synthesize,这时候就不会生成该属性的实例变量,需要根据实际情况自己手动 @synthesize 一下

@dynamic

告诉编译器不用自动进行 @synthesize,你会在运行时再提供这些方法的实现,无需产生警告,但是它不会影响 @property 生成的 setter 和 getter 方法的声明。@dynamic 是 OC 为动态运行时语言的体现。动态运行时语言与编译时语言的区别:动态运行时语言将函数决议推迟到运行时,编译时语言在编译器进行函数决议。

eg:@dynamic ivar;

差异

以前我们需要手动对每个 @property 添加 @synthesize,而在 iOS 6 之后 LLVM 编译器引入了 property autosynthesis,即属性自动合成。换句话说,就是编译器会自动为每个 @property 添加 @synthesize。那@synthesize 现在有什么用呢?

  1. 如果我们同时重写了 setter 和 getter 方法,则编译器就不会自动为这个 @property 添加 @synthesize,这时候就不存在 _ivar,所以我们需要手动添加 @synthesize;
  2. 如果该属性是 readonly,那么只要你重写了 getter 方法,property autosynthesis 就不会执行,同样的需要手动添加 @synthesize 如果你需要的话,看你这个属性是要定义为存储属性还是计算属性吧;
  3. 实现协议中要求的属性。

此外需要注意的是,分类当中添加的属性,也不会 property autosynthesis 哦

因为类的内存布局在编译的时候会确定,但是分类是在运行时才加载并将数据合并到宿主类中的,所以分类当中不能添加成员变量,只能通过关联对象间接实现分类有成员变量的效果。如果你给分类添加了一个属性,但没有手动给它实现 getter、setter(如果属性是 readonly 则不需要实现)的话,编译器就会给你警告啦 Property 'ivar' requires method 'ivar'、'setIvar:' to be defined - use @dynamic or provide a method implementation in this category,编译器已经告诉我们了有两种解决方式来消除警告:

  1. 在这个分类当中提供该属性 getter、setter 方法的实现
  2. 使用 @dynamic 告诉编译器 getter、setter 方法的实现在运行时自然会有,您就不用操心了。当然在这里 @dynamic 只是消除了警告而已,如果你没有在运行时动态添加方法实现的话,那么调用该属性的存取方法还是会 Crash

1、内存管理

内存布局:内存管理初识(内存分区)

从C语言看OC的内存布局

内存管理系列—OC的内存管理方案

内存管理系列—OC的内存管理模式

cloud.tencent.com/developer/a…

  • 内存地址由高到低分为栈区(自动处理内存)、堆区(开发者管理,ARC下会自动释放)、静态区(全局变量,静态变量)、数据段(常量)、代码段(编写的二进制代码区)
  • static静态变量的作用域与对象、类、分类没关系,只与文件有关系。

image.png

1.1 内存管理基础-强引用(Strong Reference)和弱引用(Weak Reference)

在Objective-C和Swift中,内存管理是通过引用计数(Reference Counting)来实现的。对象的生命周期由引用它的其他对象的引用类型决定。这里主要涉及到两种引用类型:强引用(Strong Reference)和弱引用(Weak Reference)。理解它们之间的区别对于避免内存泄漏和循环引用非常重要。

强引用(Strong Reference)

  • 默认行为:在没有明确指定的情况下,对象之间的引用是强引用。

  • 引用计数:当一个对象被另一个对象强引用时,它的引用计数会增加。只有当引用计数降到0时,对象才会被销毁。

  • 内存管理强引用保证了对象在其被引用期间不会被销毁,这有助于确保使用对象时的安全性

  • 循环引用问题:如果两个或多个对象互相强引用,会形成一个循环引用,导致它们的引用计数永远不会降到0,从而造成内存泄漏。

弱引用(Weak Reference)

  • 显式声明 :需要明确地将引用声明为弱引用(在Objective-C中使用__weak关键字,在Swift中使用weak关键字)。

  • 引用计数弱引用不会增加对象的引用计数。因此,即使对象被弱引用,它也可以被销毁。

  • 自动置nil当被弱引用的对象被销毁时,所有的弱引用会自动置为nil(在支持自动置nil的语言中,如Swift和带ARC的Objective-C),这有助于避免野指针错误。

  • 避免循环引用:弱引用常用于打破循环引用,特别是在父子关系或委托(delegate)关系中。

使用场景--是否希望对象被销毁

  • 强引用:当你希望保持对象存活,确保它不会在使用期间被销毁时,应使用强引用。例如,一个对象的属性或者集合中保存的元素通常是强引用

  • 弱引用当你希望引用一个对象,但不阻止它被销毁时,应使用弱引用。这在避免循环引用、处理可选的委托关系时非常有用。

总结

强引用和弱引用是管理Objective-C和Swift中对象生命周期的关键。正确地使用它们可以帮助你避免内存泄漏和循环引用的问题,从而编写出更安全、更高效的代码。

在OC中,对象的引用计数会在以下情况减少:

  1. 释放(Release) :当对一个对象发送release消息时,该对象的引用计数会减1。在自动引用计数(ARC)环境下,当一个强引用的变量超出其作用域或被设置为nil时,编译器会自动插入适当的release消息。
  2. 自动释放池(Autorelease Pool) :当一个对象被添加到自动释放池中时,它的引用计数暂时不变。但是,当自动释放池被“排水”(drained)或释放时,池中所有对象都会收到一条release消息,导致它们的引用计数减1。在ARC环境下,自动释放池仍然存在,但是你不需要显式地调用autorelease方法,编译器会为你管理。
  3. 强引用变量被重新赋值:在ARC下,如果一个强引用变量被重新赋值给另一个对象,原来引用的对象会收到一条release消息,因为这个强引用不再指向它了
  4. 强引用变量超出作用域:在ARC下,当一个强引用变量超出其作用域时(例如,一个局部变量在方法返回时),这个变量所引用的对象会收到一条release消息。
  5. 使用weak修饰符的变量:当一个使用weak修饰符的变量所引用的对象被销毁时,这个weak变量会自动被设置为nil。虽然这不直接导致引用计数减少,但它是对象引用计数减少到0并被销毁的结果。
NSObject *obj = [[NSObject alloc] init]; // 引用计数为1
[obj release]; // 手动释放,引用计数减1,现在为0,对象被销毁

NSObject *anotherObj = obj; // anotherObj也指向obj,引用计数不变
[anotherObj release]; // 释放anotherObj,引用计数减1,如果这是唯一的引用,对象被销毁

// 在ARC环境下
{
    NSObject *autoObj = [[NSObject alloc] init]; // 引用计数为1
} // autoObj超出作用域,引用计数减1,现在为0,对象被销毁
// 对象的引用计数会在对象不再被需要时减少,无论是通过显式释放还是由ARC自动管理。

Q:弱引用对象什么时候会被销毁,比如声明一个对象为weak,初始化后且没有其他地方强引用,是否就直接变成nil了。

在Objective-C或Swift中使用弱引用(weak)时,弱引用的对象会在其没有任何强引用时被销毁。 具体来说,当一个对象的强引用计数降到0时,运行时(Runtime)系统会立即销毁这个对象,并释放其占用的内存。在这个过程中,所有指向该对象的弱引用会被自动设置为nil,以避免野指针访问。

弱引用对象被销毁的时机

假设你声明了一个对象为weak,并对其进行了初始化,但没有其他地方对它进行强引用,那么在下一个运行循环开始之前,这个对象就会被销毁,并且该弱引用会变成nil。这是因为在当前的运行循环中,对象至少有一个临时的强引用(即初始化时的强引用),但一旦这个运行循环结束,且没有其他强引用保持该对象,它就会被销毁。

__weak NSObject *weakObject;
@autoreleasepool {
    NSObject *strongObject = [[NSObject alloc] init];
    weakObject = strongObject;
    // 此时 strongObject 是 weakObject 的唯一强引用
}
// 出了 autoreleasepool 的作用域,strongObject 的强引用消失,weakObject 被设置为 nil
NSLog(@"%@", weakObject); // 输出 "nil"

weak var weakObject: NSObject?
do {
    let strongObject = NSObject()
    weakObject = strongObject
    // 此时 strongObject 是 weakObject 的唯一强引用
}
// 出了 do 代码块的作用域,strongObject 的强引用消失,weakObject 被设置为 nil
print(weakObject) // 输出 "nil"

注意事项:弱引用主要用于防止循环引用,特别是在父子关系和委托(delegate)模式中

  • 在使用弱引用时,总是要检查其值是否为nil,因为对象可能在任何时候被销毁

  • 弱引用的自动置nil特性只在支持自动引用计数(ARC)的环境中有效,如现代的Objective-C和Swift。

总之,当一个对象只被弱引用时,它会在当前运行循环结束后被销毁(如果没有其他强引用的话),所有指向它的弱引用会自动变成nil。这个机制有助于防止内存泄漏和野指针错误。

1.2 内存管理机制-Autoreleasepool/ARC引用计数法

内存管理机制主要基于引用计数(Reference Counting) 。在自动引用计数(Automatic Reference Counting, ARC)引入之前,开发者需要手动管理内存,包括手动增加(retain)或减少(release)对象的引用计数,以及在适当的时候释放(dealloc)对象。ARC的引入极大简化了内存管理,编译器会在编译时自动插入适当的内存管理代码。以下是iOS Objective-C内存管理的几个关键点:--iOS在Objective-C开发中提供了一套相对高效的内存管理方案。理解和正确应用这些内存管理原则对于开发稳定、高效的iOS应用至关重要。

  1. 引用计数(Reference Counting) :每个对象都有一个与之关联的引用计数。当对象被创建时,计数为1。当对象被另一个对象引用时,计数增加;当引用被释放时,计数减少。当计数达到0时,对象被销毁。

  2. 自动引用计数(ARC) :ARC自动管理引用计数,开发者不需要手动调用retain、release。ARC工作在编译阶段,编译器会根据代码上下文自动插入内存管理的代码。

  3. 强引用和弱引用

    • 强引用(Strong Reference) :默认情况下,对象间的引用是强引用。强引用会增加对象的引用计数,只要有强引用存在,对象就不会被销毁。
    • 弱引用(Weak Reference) :不会增加对象的引用计数,因此不会阻止对象被销毁。当对象被销毁时,所有指向它的弱引用自动置为nil。这对于避免循环引用(Retain Cycles)非常有用。
  4. 循环引用(Retain Cycles) :当两个对象互相持有对方的强引用时,会形成循环引用,导致它们都无法被释放。解决方法是将其中一个引用改为弱引用

  5. 手动内存管理(MRC) :在ARC之前,开发者需要手动管理内存,通过retain、release和autorelease来控制对象的生命周期。虽然现在已经很少需要使用MRC,但理解其原理对于深入理解iOS内存管理仍然有帮助。

  6. 自动释放池(Autorelease Pool) :在MRC中,autorelease方法可以用于延迟释放对象,直到当前的自动释放池被销毁。在ARC下,自动释放池仍然存在,但其使用被大大简化。

在ARC下,LLVM编译器会自动帮我们生产retain、release和autorelease等代码,减少了在MRC下的工作量。 调用autorelease会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release,所以autorelease相当于延迟了对象的释放。但是在ARC下,autorelease方法已被禁用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到自动释放池中。

1.3 自动释放池的作用

  1. 内存管理:自动释放池提供了一种机制,用于在特定时间点批量释放对象。这对于在循环或大量临时对象创建的场景中尤其有用,可以有效地管理内存使用,避免内存峰值过高。

  2. 临时对象释放:在没有使用自动释放池的情况下,临时对象(通过autorelease方法返回的对象)会在当前的自动释放池中注册,等待释放。如果没有合适的自动释放池,这些对象可能会在未来某个不确定的时间点被释放,可能导致内存占用过高。通过使用@autoreleasepool,可以确保这些对象在适当的时候被释放。

  3. 性能优化通过合理使用自动释放池,可以减少应用的内存占用,避免内存泄漏,从而提高应用的性能

for (NSInteger i = 0; i < 10000; i++) {
    @autoreleasepool {
        // 在这个块内创建的对象会在块的末尾被释放
        NSString *string = [NSString stringWithFormat:@"Item %ld", (long)i];
        // 使用string做一些操作
    }
}
// 每次循环创建的`NSString`对象都会在`@autoreleasepool`块结束时被释放,这样可以避免在循环过程中积累过多的临时对象,从而减少内存占用。

总结:@autoreleasepool块的作用是在其作用域内管理对象的生命周期确保在块结束时能够及时释放那些标记为自动释放的对象。这有助于优化内存使用,特别是在处理大量数据或在循环中创建大量临时对象的场景中。它并不是用来延迟释放对象,而是确保对象在适当的时候被释放,以避免不必要的内存占用。

静态变量的作用域与对象、类、分类没关系,只与文件有关系。

自动释放池与线程、runloop的关系

AppKit 和 UIKit 框架在事件循环(RunLoop)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它。在销毁时释放自动释放池中的所有autorelease对象。通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。

自动释放池与线程一一对应;

  • @autoreleasepool底层是创建了一个 __AtAutoreleasePool结构体对象;先进后出的行为
  • 在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj(POOL_BOUNDARY存放的内存地址;
  • 在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。
  • AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成;遵循先进后出规则,整个自动释放池由一系列的AutoreleasePoolPage组成的,而AutoreleasePoolPage是以双向链表的形式连接起来。
  • 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。(505)。要注意的是第一页只有504个对象,因为在创建page的时候会在next的位置插入1个POOL_SENTINEL。
  • POOL_BOUNDARY为哨兵对象,入栈时插入,出栈时释放对象到此传入的哨兵对象。POOL_SENTINEL。
  • hotPage()方法就是用来获得新创建的未满的Page。

image.png

Autoreleasepool用于创建一个自动释放池(Autorelease Pool)块,它的主要作用是在其作用域内创建的对象,可以在块结束时接收到release消息,从而被释放或者其引用计数减一。这并不是延迟释放对象,而是管理在自动释放池块内部分配的对象的生命周期,确保它们在不再需要时能够及时释放,从而帮助减少内存占用。

1.4 ARC引用计数法

相比较于 GC(Garbage Collection,垃圾回收)的标记清除算法,ARC引用计数法可以及时地回收引用计数为 0 的对象,减少查找次数。但是,引用计数会带来循环引用的问题,比如当外部的变量强引用 Block 时,Block 也会强引用外部的变量,就会出现循环引用。我们需要通过弱引用,来解除循环引用的问题。

在 ARC(自动引用计数)之前,一直都是通过 MRC(手动引用计数) 这种手写大量内存管理代码的方式来管理内存,因此苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工作。但是,ARC 依然需要注意循环引用的问题。当 ARC 的内存管理代码交由编译器自动添加后,有些情况下会比手动管理内存效率低。所以对于一些内存要求较高的场景,我们还是要通过 MRC 的方式来管理、优化内存的使用。---juejin.cn/post/684490…

引用计数规则:juejin.cn/post/684490…

引用计数是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。在iOS中,对象执行reatin等操作后,该对象的引用计数就会+1,调用release等操作后,改对象的引用计数就会-1

关于引用计数的规则,可以总结如下:

    • 自己生成的对象,自己持有。(alloc,new,copy,mutableCopy等)

      alloc只申请内存空间,不增加引用计数的。此时isa中extra_rc为0。只是调用rootRetainCount其本身前面是会加一个常量1的,用来标记自己生成的对象的引用计数。

      有关alloc的流程,可以阅读☞iOS底层学习 - OC对象前世今生但是有一个细节需要注意,alloc本身是只申请内存空间,不增加引用计数的。此时isa中extra_rc为0。但是为什么我们打印retainCount时,显示的是1呢,我们通过查看源码可以发现uintptr_t rc = 1 + bits.extra_rc;其本身前面是会加一个常量1的,用来标记自己生成的对象的引用计数

      inline uintptr_t 
      objc_object::rootRetainCount()
      {
          if (isTaggedPointer()) return (uintptr_t)this;
      
          sidetable_lock();
          isa_t bits = LoadExclusive(&isa.bits);
          ClearExclusive(&isa.bits);
          if (bits.nonpointer) {
              uintptr_t rc = 1 + bits.extra_rc;
              if (bits.has_sidetable_rc) {
                  rc += sidetable_getExtraRC_nolock();
              }
              sidetable_unlock();
              return rc;
          }
      
          sidetable_unlock();
          return sidetable_retainCount();
      }
      

      总结:juejin.cn/post/684490…

    • 非自己生成的对象,自己也能持有。(retain 等)

      reatin原理:

      • 判断当前对象是否一个TaggedPointer,如果是则返回
      • 判断isa是否经过NONPOINTER_ISA优化,如果未经过优化,则将引用计数存储在SideTable中。64位的设备不会进入到这个分支。
      • 判断当前的设备是否正在析构。
      • 将isa的bits中的extra_rc进行加1操作。
      • 如果在extra_rc中已经存储满了,则调用sidetable_addExtraRC_nolock方法将一半的引用计数移存到SideTable中。
    • 不再需要自己持有的对象时释放。(release,dealloc 等)
    • 非自己持有的对象无法释放。

1.5 手动创建自动释放池

在Objective-C中,自动引用计数(ARC)管理内存,但有时候在执行大量操作,如在一个for循环中创建大量对象时,可能会暂时增加内存使用量,因为这些对象会在当前自动释放池块结束时才被释放。为了及时释放这些对象,可以手动创建一个自动释放池(@autoreleasepool)块,在这个块中执行操作。这样,块中的对象在块结束时就会被释放,从而帮助减少内存占用。

假设你在一个`for`循环中创建了大量的临时对象:
for (NSInteger i = 0; i < 10000; i++) {
    @autoreleasepool {
        // 创建临时对象
        NSObject *tempObject = [[NSObject alloc] init];
        // 执行一些操作...
    }
    // 在这里,每次循环结束时,@autoreleasepool 块内创建的对象都会被释放
}
```
在上面的代码中,`@autoreleasepool`块确保每次循环结束时,其中创建的`tempObject`对象都会被及时释放,而不是等到外部的自动释放池(通常是当前的run loop迭代)结束时才释放。
这对于在循环中处理大量数据或执行密集的计算任务时,减少内存峰值非常有用。

### 注意事项
- 使用`@autoreleasepool`块会带来一定的性能开销,因为它需要创建和销毁自动释放池。因此,仅在确实需要及时释放内存时使用它,比如处理大量的临时对象。
- 在ARC环境下,大多数情况下不需要手动管理内存,但理解自动释放池的工作原理仍然很重要,特别是在处理大量数据或性能敏感的代码时。
通过合理使用`@autoreleasepool`,可以有效地控制内存使用,避免内存峰值过高,从而提高应用的性能和稳定性。

1.6 RunLoop和@autoreleasepool的关系--主线程的RunLoop中注册了两个Observer

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();
  • 第2个Observer
    ① 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
    ② 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。

image.png

所以,在iOS工程中系统干预释放的autorelease对象的释放时机是由RunLoop控制的,会在当前RunLoop每次循环结束时释放。以上person对象在viewWillAppear方法结束后释放,说明viewDidLoad和viewWillAppear方法在同一次循环里

  • kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。
  • kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()。
  • kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()。

1.7 问题

Q1:ARC 环境下,autorelease 对象在什么时候释放?

在ARC环境下,autorelease对象在什么时候释放?我们就分系统干预释放和手动干预释放两种情况回答。

Q2:ARC 环境下,需不需要手动添加 @autoreleasepool?

AppKit 和 UIKit 框架会在RunLoop每次事件循环迭代中创建并处理@autoreleasepool,因此,你通常不必自己创建@autoreleasepool,甚至不需要知道创建@autoreleasepool的代码怎么写。但是,有些情况需要自己创建@autoreleasepool。

例如,如果我们需要在循环中创建了很多临时的autorelease对象,则手动添加@autoreleasepool来管理这些对象可以很大程度地减少内存峰值。比如在for循环中alloc图片数据等内存消耗较大的场景,需要手动添加@autoreleasepool。

苹果给出了三种需要手动添加@autoreleasepool的情况:

  • ① 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  • ② 如果你编写的循环中创建了大量的临时对象;
    你可以在循环内使用@autoreleasepool在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool有助于减少应用程序的最大内存占用。
  • ③ 如果你创建了辅助线程。
    一旦线程开始执行,就必须创建自己的@autoreleasepool;否则,你的应用程序将存在内存泄漏。

Q:如果对 NSAutoreleasePool 对象调用 autorelease 方法会发生什么情况?

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

[pool autorelease];

答:抛出异常NSInvalidArgumentException并导致程序Crash,异常原因:不能对NSAutoreleasePool对象调用autorelease。

2、内存优化方案

2.1 Tagged Pointer/NONPOINTER_ISA/SideTable

  • Tagged Pointer

    • Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate等。

    • Tagged Pointer指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。

      所以,它的内存并不存储在堆中,也不需要malloc和free。

    • 当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。

    • 在内存读取上有着3倍的效率,创建时⽐以前快106倍。

      使用Tagged Pointer之前,如果声明一个NSNumber *number = @10;变量,需要一个占8字节的指针变量number,和一个占16字节的NSNumber对象。
      指针变量number指向NSNumber对象的地址。这样需要耗费24个字节内存空间。
      而使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。
      直接将数据10保存在指针变量number中,这样仅占用8个字节。
      只有当指针不够存储数据时,会使用动态分配内存的方式来存储数据。
      
      eg:第1段会发生崩溃,而第2段不会。
      //第1段代码
      dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
      for (int i = 0; i < 1000; i++) {
          dispatch_async(queue, ^{
              self.name = [NSString stringWithFormat:@"asdasdefafdfa"];
          });
      }
      NSLog(@"end");
       //第2段代码
      dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
      for (int i = 0; i < 1000; i++) {
          dispatch_async(queue, ^{
              self.name = [NSString stringWithFormat:@"abc"];
          });
      }
      NSLog(@"end");
      创建多个线程来对name进行操作时,name的值会不断的retain和release,此时就会出现资源竞争而崩溃,但是第二段却不会崩溃.
      说明在Tagged Pointer下,较小的值,不会调用set和get等方法,由于其值是直接存储在指针变量中的,所以可以直接修改。
      通过源码,我们也可以比较直观的看出,Tagged Pointer类型对象是直接返回的。
      

      image.jpeg

    • image.jpeg

    • image.jpeg

    • 系统调用了 _objc_decodeTaggedPointer和_objc_taggedPointersEnabled这两个方法对于taggedPointer对象的指针进行了编码和解编码,这两个方法都是将指针地址和objc_debug_taggedpointer_obfuscator进行异或操作,我们都知道将a和b异或操作得到c再和a进行异或操作便可以重新得到a的值,通常可以使用这个方式来实现不用中间变量实现两个值的交换。

    • Tagged Pointer正是使用了这种原理。通过这种解码的方法,我们可以得到对象真正的指针地址。

  • NONPOINTER_ISA:用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。

    • isa 指针第一位为 1 即表示使用优化的 isa 指针。
    • 通过nonpointer标志是否开启指针优化
    • 通过extra_rc来存储引用计数,根据系统架构不同,长度不同
    • 通过has_sidetable_rc来判断超出extra_rc后,是否有全局SideTable存储引用计数
  • SideTable: 在NONPOINTER_ISA中有两个成员变量has_sidetable_rc和extra_rc,当extra_rc的19位内存不够存储引用计数时has_sidetable_rc的值就会变为1,那么此时引用计数会存储在SideTable中。

    SideTables可以理解为一个全局的hash数组,里面存储了SideTable类型的数据,其长度为64,就是里面有64个SideTable。

  • SideTables是一个全局的哈希数组,里面存储了SideTable类型数据
  • SideTable由spinlock_t(自旋锁,用于上/解锁)、RefcountMap(存储extra_rc溢出或者未开启优化的引用计数)、weak_table_t(存储弱引用表)
struct SideTable {
    spinlock_t slock; // 自旋锁,用于上锁/解锁 SideTable。
    RefcountMap refcnts; // 用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
    weak_table_t weak_table; // 存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。
}

Q:hash如何解冲突

哈希表(Hash Table)是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。哈希表的一个常见问题是“冲突”(Collision),即不同的键(Key)通过哈希函数得到相同的哈希值(或索引)。解决哈希表冲突的常用方法有以下几种:

1. 链地址法(Separate Chaining)

链地址法是一种常见的解决冲突的方法,它将具有相同哈希值的所有元素存储在同一个索引位置,但是每个索引位置不再直接存储元素,而是存储一个链表的头节点。当发生冲突时,新的元素会被添加到对应索引位置的链表中。搜索时,需要遍历该索引位置的链表来找到目标元素。

2. 开放地址法(Open Addressing)

开放地址法通过寻找空闲的哈希表槽来解决冲突。如果一个元素的目标索引已经被占用,哈希表会根据某种探测序列在表中寻找下一个空闲位置。常见的探测方法有:

  • 线性探测(Linear Probing):顺序查找下一个空闲位置。

  • 二次探测(Quadratic Probing):使用二次方的探测序列来查找空闲位置。

  • 双重哈希(Double Hashing):使用另一个哈希函数来确定探测序列。

3. 再哈希法( Rehashing)

再哈希法是在发生冲突时使用另一个哈希函数来计算新的索引。如果再次发生冲突,可以继续使用其他哈希函数,直到找到空闲位置。这种方法需要设计多个好的哈希函数,以确保分布的均匀性

4. 建立一个更大的哈希表

当哈希表的填充因子(即表中已占用的位置与总位置的比例)达到一定阈值时,性能会下降。这时可以通过建立一个更大的哈希表,并将所有现有元素重新哈希到新表中来解冲突和提高性能。这个过程称为“扩容”(Resizing)或“重哈希”(Rehashing)。

5. 一致性哈希(Consistent Hashing)

一致性哈希主要用于分布式缓存系统中解决节点增减时的数据重新分配问题。它通过将哈希值空间组织成一个逻辑上的环,确保数据分布的均匀性和稳定性,从而减少因节点变化导致的数据迁移量。

每种方法都有其适用场景和优缺点。在实际应用中,选择合适的冲突解决策略是提高哈希表性能的关键。

Q:hash怎么减少碰撞

点击展开内容

哈希碰撞是指不同的输入数据经过哈希函数计算后得到相同的哈希值。减少哈希碰撞的方法主要集中在两个方面:改进哈希函数和使用更有效的冲突解决策略。以下是一些常见的方法:

  1. 改进哈希函数
  • 均匀分布:设计哈希函数时,应尽量使哈希值均匀分布在整个输出空间,这样可以减少碰撞的概率。

  • 雪崩效应:一个好的哈希函数应该对输入的微小变化非常敏感,即输入的微小变化应该导致输出的显著变化。

  • 使用加密哈希函数:加密哈希函数(如SHA-256、SHA-3)通常具有更好的均匀分布和雪崩效应,因此碰撞概率较低。

  1. 冲突解决策略
  • 开放寻址法:当发生碰撞时,通过某种探测序列(如线性探测、二次探测、双重哈希等)寻找下一个可用的槽位。

  • 链地址法:每个哈希表的槽位指向一个链表,所有哈希值相同的元素都存储在这个链表中。

  • 再哈希法:当发生碰撞时,使用第二个哈希函数计算新的位置。

  • 使用更大的哈希表:增加哈希表的大小可以减少碰撞的概率,但这会增加内存消耗。

  1. 动态调整哈希表大小
  • 负载因子:负载因子是哈希表中元素数量与槽位数量的比值。当负载因子超过某个阈值时,可以考虑扩容哈希表,以减少碰撞。

  • 扩容和重哈希:当哈希表扩容时,需要对所有元素重新进行哈希计算,以确保它们在新表中均匀分布。

  1. 使用完美哈希函数
  • 完美哈希函数:在某些特定情况下,可以设计出无碰撞的哈希函数,即完美哈希函数。这通常需要预先知道所有可能的输入集合。
  1. 使用布隆过滤器
  • 布隆过滤器:布隆过滤器是一种空间效率高的概率数据结构,用于检测一个元素是否在一个集合中。它可以减少哈希碰撞的影响,但可能会产生误判。

通过上述方法,可以有效地减少哈希碰撞的发生,提高哈希表的性能和可靠性。

2.2 SideTable 运行时(runtime)用于存储对象额外信息的一种数据结构

SideTable是Objective-C运行时中用于管理对象的附加信息的一个内部数据结构,它主要用于存储和管理对象的引用计数(retain counts)和关联对象(associated objects)、弱引用(weak references) 。SideTable的设计目的是为了提高内存使用效率和访问速度,特别是在多线程环境下。

Q:为什么不直接存在对象里,而是需要一个全局的SideTable来存取。

这些信息如果直接存储在对象本身会增加对象的大小,特别是对于那些不需要这些额外信息的对象来说,这是一种资源的浪费。因此,Objective-C运行时使用sideTable来外部存储这些信息,以减少每个对象的内存占用。

存储位置: sideTable并不存储在对象本身,而是由运行时系统在需要时为对象动态创建并管理。具体来说,sideTable是存储在**全局的、由运行时维护的一个或多个散列表**(hash tables)中。每个对象通过其**内存地址作为键(key)来访问对应的sideTable`条目**。

结构和功能

  • 引用计数管理:在Objective-C中,对象的生命周期通常由引用计数来管理。SideTable存储了对象的强引用(strong reference)和弱引用(weak reference)计数。当对一个对象进行retain或release操作时,其引用计数会相应地增加或减少。当引用计数降到0时,对象会被销毁。

  • 关联对象管理:Objective-C允许在运行时动态地给对象添加额外的属性,这些属性被称为关联对象。SideTable负责存储这些关联对象的信息,包括键(key)和值(value)。

    (存储通过objc_setAssociatedObjectobjc_getAssociatedObject函数设置的关联对象。这些关联对象在当前对象释放时也会被释放。)

  • 弱引用表(Weak Reference Table):存储指向当前对象的所有弱引用。当对象被释放时,所有的弱引用自动置为nil,以避免悬挂指针

实现细节 TODO:Hash Table、锁、分离存储

sideTable的管理是自动进行的,开发者通常不需要直接与之交互。Objective-C运行时会在对象进行相关操作(如引用计数变化、设置关联对象等)时自动创建、更新或查询sideTable 。当对象不再需要这些额外信息时(例如,对象被销毁),运行时也会负责清理对应的sideTable条目,以释放资源。

  • 散列表(Hash Table) :SideTable内部使用散列表来存储对象的引用计数和关联对象。这使得查找和更新操作可以在常数时间内完成,从而提高效率。
  • :为了保证多线程环境下的线程安全,SideTable使用锁(如自旋锁)来同步对散列表的访问。确保了即使在并发访问的情况下,对象的引用计数和关联对象的管理也是安全
  • 分离存储:SideTable的设计采用了分离存储的策略,即对象的主体数据和附加信息(如引用计数和关联对象)分别存储。这种设计有助于减少对象本身的大小,特别是对于那些没有或很少使用附加信息的对象。

优点

  • 性能优化:通过使用散列表和锁,SideTable能够高效地管理对象的引用计数和关联对象,特别是在多线程环境下
  • 内存优化:分离存储策略有助于减少每个对象的内存占用,从而提高整体的内存使用效率。

应用场景

SideTable主要在Objective-C运行时的内部使用,开发者通常不需要直接与之交互。但了解SideTable的存在和工作原理对于理解Objective-C对象的内存管理和性能优化是有帮助。

总结:

sideTable是Objective-C运行时用于存储对象额外信息的一种机制,它存在于由运行时维护的全局散列表中,而不是存储在对象本身。这种设计有效减少了每个对象的内存占用,同时提供了一种灵活的方式来管理对象的引用计数、弱引用和关联对象等信息。

2.3 引用计数存放位置:非指针型isa(指针优化)/侧表(Side Tables) 🌲

在Objective-C中,对象的引用计数(Reference Count)的存储位置取决于对象是否使用了非指针型isa以及对象的具体情况。

具体来说,引用计数可以存储在以下两个地方之一:

  1. 非指针型isa:对于启用了非指针型isa的现代Objective-C对象,一部分引用计数可以直接嵌入到对象的isa指针中。这是因为isa指针的结构被优化以包含除了类指针之外的其他信息,如引用计数的一部分、是否有C++析构函数、是否有关联对象等。这种方式允许快速地访问和修改小的引用计数值,但由于空间有限,它只能存储有限范围内的引用计数。

  2. 侧表(Side Tables):当对象的引用计数值较大,无法直接嵌入到isa指针中,或者对象使用了传统的指针型isa时,引用计数会存储在一个全局的侧表(Side Tables)中。侧表是一种全局的数据结构,用于存储那些不能直接存储在对象本身中的信息,包括引用计数、弱引用列表和关联对象等。每个对象通过其地址来在侧表中查找对应的条目。

在实际操作中,当对一个对象进行retainrelease操作时,Objective-C运行时会首先检查是否可以直接在isa中更新引用计数。如果可以,就直接在那里进行操作;如果不行,就会转而使用侧表来存储和更新引用计数。

这种设计允许Objective-C运行时在保持性能的同时,灵活地管理对象的生命周期。通过将引用计数的管理分散到isa和侧表中,Objective-C能够在不同的场景下选择最合适的方式来处理引用计数,从而优化内存管理的效率。

2.4 弱引用表weak_table_t---Weakify机制

extra_rc就是用来存放引用计数的,它使用了isa上面的19个二进制为作为存储空间extra_rc这个命名含义是额外的引用计数,也就是除了创建时候的那一次retain操作之外,在其他时刻对象进行过retain操作的次数。因此一个对象实际的引用计数 = extra_rc + 1(创建的那一次)。

当然extra_rc能够表达的数量也是有限的 ,当对象的引用超过了extra_rc的表示范围之后,isa内部的has_sidetable_rc,用来指示对象的引用计数无法存储在isa当中,并且将引用计数的值存放到一个叫SideTable的类的属性当中。SideTable的定义可以在objc源码的NSObject.m文件中找到。如下

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    }
如果isa存不下引用计数的话,那么引用计数就会被存放在SideTable的refcnts中,从类型RefcountMap可以看出,它实际上是一个散列表的结构(类似OC中的字典)。


union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };

实质上是模板类型objc::DenseMap。模板的三个类型参数DisguisedPtr<objc_object>、size_t、true 分别表示DenseMap的 key类型、value类型、是否需要在value == 0 的时候自动释放掉响应的hash节点,这里是true。juejin.cn/post/684490…

  • __strong修饰后,对象的引用计数会增加,在作用域外不会销毁
  • __weak修饰后,对象引用计数不会增加,在作用域外会自动置为nil
  • __unsafe_unretained修饰后,引用计数不会增加,在作用域外不会置空,会造成野指针崩溃
NSLog(@"临时作用域开始");
{
    LGPerson *person = [[LGPerson alloc] init];
    NSLog(@"person对象:%@", person);
}
NSLog(@"临时作用域结束");

***************************打印结果******************************
2020-01-19 10:57:13.910542+0800 objc-debug[74175:19740208] 临时作用域开始
2020-01-19 10:57:13.911181+0800 objc-debug[74175:19740208] person对象:<LGPerson: 0x10221c900>
2020-01-19 10:57:13.911277+0800 objc-debug[74175:19740208] LGPerson -[LGPerson dealloc]
2020-01-19 10:57:13.911367+0800 objc-debug[74175:19740208] 临时作用域结束

__strong LGPerson *strongPerson; // 可以发现修饰的对象在作用域结束之后并没有销毁,说明该对象的引用计数增加了
NSLog(@"临时作用域开始");
{
    LGPerson *person = [[LGPerson alloc] init];
    NSLog(@"person对象:%@", person);
    strongPerson = person;
}
NSLog(@"临时作用域结束");
NSLog(@"strongPerson:%@", strongPerson);
***************************打印结果******************************
2020-01-19 11:54:44.079292+0800 objc-debug[74452:19777011] 临时作用域开始
2020-01-19 11:54:44.080060+0800 objc-debug[74452:19777011] person对象:<LGPerson: 0x101945ae0>
2020-01-19 11:54:44.080172+0800 objc-debug[74452:19777011] 临时作用域结束
2020-01-19 11:54:44.080292+0800 objc-debug[74452:19777011] strongPerson:<LGPerson: 0x101945ae0>

__weak LGPerson *weakPerson; // 用__weak修饰后,并没有增加引用计数,并且作用域结束,对象释放后,修饰的对象为nil,没有造成野指针的崩溃,可以说是一种安全的方案
NSLog(@"临时作用域开始");
{
    LGPerson *person = [[LGPerson alloc] init];
    NSLog(@"person对象:%@", person);
    weakPerson = person;
}
NSLog(@"临时作用域结束");
NSLog(@"weakPerson:%@", weakPerson);

***************************打印结果******************************
2020-01-19 11:58:08.842409+0800 objc-debug[74479:19780263] 临时作用域开始
2020-01-19 11:58:08.843151+0800 objc-debug[74479:19780263] person对象:<LGPerson: 0x101712030>
2020-01-19 11:58:08.843382+0800 objc-debug[74479:19780263] LGPerson -[LGPerson dealloc]
2020-01-19 11:58:08.843572+0800 objc-debug[74479:19780263] 临时作用域结束
2020-01-19 11:58:08.843762+0800 objc-debug[74479:19780263] weakPerson:(null)


// 作用域消失后,对象就进行了销毁,并且在出作用域打印修饰对象时,出现了野指针的崩溃EXC_BAD_ACCESS
// 这样就看出了__weak和__unsafe_unretained的区别就是前者会在对象被释放的时候自动置为nil,而后者却不行。
__unsafe_unretained LGPerson *unsafePerson;
NSLog(@"临时作用域开始");
{
    LGPerson *person = [[LGPerson alloc] init];
    NSLog(@"person对象:%@", person);
    unsafePerson = person;
}
NSLog(@"临时作用域结束");
NSLog(@"unsafePerson:%@", unsafePerson);

***************************打印结果******************************
2020-01-19 12:02:34.428120+0800 objc-debug[74513:19785153] 临时作用域开始
2020-01-19 12:02:34.428813+0800 objc-debug[74513:19785153] person对象:<LGPerson: 0x1019159f0>
2020-01-19 12:02:34.428901+0800 objc-debug[74513:19785153] LGPerson -[LGPerson dealloc]
2020-01-19 12:02:34.429015+0800 objc-debug[74513:19785153] 临时作用域结束

整体流程/实现原理

www.jianshu.com/p/6b041b4bb…

  • 当一个对象obj被weak指针指向时,这个weak指针会以obj作为key,被存储到sideTable类的weak_table这个散列表上对应的一个weak指针数组里面。
  • 当一个对象obj的dealloc方法被调用时,Runtime会以obj为key,从sideTable的weak_table散列表中,找出对应的weak指针列表,然后将里面的weak指针逐个置为nil。

image.jpeg

Runtime维护了一个弱引用表,将所有弱引用obj的指针地址都保存在obj对应的weak_entry_t中。

  1. 创建时,先从找到全局散列表SideTables中对应的弱引用表weak_table
  2. 在weak_table中被弱引用对象的referent,并创建或者插入对应的weak_entry_t
  3. 然后append_referrer(entry, referrer)将我的新弱引⽤的对象加进去entry
  4. 最后weak_entry_insert 把entry加⼊到我们的weak_table

销毁流程小结:--weak_entry_for_referent

通过需要释放的对象referent根据一定的算法得出一个索引index,然后再从weak_table里面利用index拿到对象referent所对应的weak指针,这就说明weak_table内部其实就是一个散列表结构,通过对象作为key,value就是指向该对象的weak指针组成的数组。

  1. 首先根据对象地址获取所有weak指针地址的数组

  2. 然后遍历这个数组把对应的数据清空置为nil

  3. 同时,将weak_entry_t移除出弱引用表weak_table。

    image.jpeg

weak的创建 objc_initWeak->storeWeak->weak_register_no_lock

location:表示__weak指针的地址,即例子中的weak指针取地址: &weakObjc 。它是一个指针的地址。
之所以要存储指针的地址,是因为最后我们要讲__weak指针指向的内容置为nil,如果仅存储指针的话,是不能够完成这个功能的。
newObj:所引用的对象,即例子中的person 。
id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

// 查看storeWeak源码,根据注释,可以知道如下几点
HaveOld:weak指针之前是否已经指向了一个弱引用
HaveNew:weak指针是否需要指向一个新引用
CrashIfDeallocating:如果被弱引用的对象正在析构,此时再弱引用该对象,是否应该crash。
// 第一次调用,所以是一个新的对象,也就是haveNew的情况,获取到的是新的散列表SideTable,主要执行了weak_register_no_lock方法来进行插入
// Update a weak variable.
// If HaveOld is true, the variable has an existing value 
//   that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be 
//   assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is 
//   deallocating or newObj's class does not support weak references. 
//   If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    ✅// 如果weak指针之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTable
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        // 没有弱引用过,则oldTable = nil
        oldTable = nil;
    }
    ✅// 如果weak指针要弱引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTable
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    ✅// 加锁操作,防止多线程中竞争冲突
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    ✅// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        ✅// 如果cls还没有初始化,先初始化,再尝试设置弱引用
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.// 完成初始化后进行标记
            previouslyInitializedClass = cls;
            ✅// newObj 初始化后,重新获取一遍newObj
            goto retry;
        }
    }

    // Clean up old value, if any.// 如果weak指针之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak指针地址
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.// 如果weak指针需要弱引用新的对象newObj
    if (haveNew) {
       ✅ // 调用weak_register_no_lock方法,将weak指针的地址记录到newObj对应的weak_entry_t中
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.// 更新newObj的isa指针的weakly_referenced bit标志位
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.// *location 赋值,也就是将weak指针直接指向了newObj,而且没有将newObj的引用计数+1
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

我们发现weak_register_no_lock函数内部主要进行了isTaggedPointer和deallocating的判断等前置条件,这些都是不能进行弱引用的情况。

如果可以被弱引用,则将被弱引用对象所在的weak_table中的weak_entry_t哈希数组中取出对应的weak_entry_t,如果weak_entry_t不存在,则会新建一个。然后将指向被弱引用对象地址的指针referrer通过函数append_referrer插入到对应的weak_entry_t引用数组。至此就完成了弱引用。(append_referrer找到弱引用对象对应的weak_entry哈希数组中,基本就是个遍历插入的过程,原理比较简单)

id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    ✅//首先获取需要弱引用对象
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
    ✅// 如果被弱引用对象referent为nil 或者被弱引用对象采用了TaggedPointer计数方式,则直接返回
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // ensure that the referenced object is viable// 确保被引用的对象可用(没有在析构,同时应该支持weak弱引用)
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }
    ✅// 如果是正在析构的对象,那么不能够被弱引用
    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored// 在 weak_table 中找到被弱引用对象 referent 对应的 weak_entry,并将 referrer 加入到 weak_entry 中
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        ✅// 如果能找到 weak_entry,则讲 referrer 插入到 weak_entry 中
        append_referrer(entry, referrer);
    } 
    else {
        ✅// 如果找不到 weak_entry,就新建一个
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    ✅// 如果weak_entry 使用静态数组 inline_referrers
    if (! entry->out_of_line()) {
        // Try to insert inline.
        ✅// 尝试将 referrer 插入数组
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // Couldn't insert inline. Allocate out of line.
        ✅// 如果inline_referrers的位置已经存满了,则要转型为 referrers,动态数组
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    assert(entry->out_of_line());
    ✅// 如果动态数组中元素个数大于或等于数组总空间的3/4,则扩展数组空间为当前长度的一倍,然后将 referrer 插入数组
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    ✅// 如果不需要扩容,直接插入到weak_entry中
    ✅// & (entry->mask) 保证 begin 的位置只能大于或等于数组的长度
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

如果weak指针在指向obj之前,已经弱引用了其他的对象, 则需要先将weak指针从其他对象的weak_entry_t的hash数组中移除。在storeWeak方法中会调用weak_unregister_no_lock函数来做移除操作,我们来看一下weak_unregister_no_lock函数源码

weak_unregister_no_lock函数首先会在weak_table中找出以前被弱引用的对象referent对应的weak_entry_t,在weak_entry_t中移除被弱引用的对象referrer。移除元素后,判断此时weak_entry_t中是否还有元素。如果此时weak_entry_t已经没有元素了,则需要将weak_entry_t从weak_table中移除。

void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    ✅// 拿到以前弱引用的对象和对象的地址
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;
    ✅// 查找到以前弱引用的对象 referent 所对应的 weak_entry_t
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        ✅// 在以前弱引用的对象 referent 所对应的 weak_entry_t 的 hash 数组中,移除弱引用 referrer
        remove_referrer(entry, referrer);
        ✅// 移除元素之后, 要检查一下 weak_entry_t 的 hash 数组是否已经空了
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }
        ✅// 如果 weak_entry_t 的hash数组已经空了,则需要将 weak_entry_t 从 weak_table 中移除
        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }

    // Do not set *referrer = nil. objc_storeWeak() requires that the 
    // value not change.
}

weak的销毁

出作用域,对象dealloc后,会自动把弱引用对象置空,那么他是怎么实现的,我们可以简单查看下类的dealloc流程.

dealloc->_objc_rootDealloc->object_dispose

- (void)dealloc {
    _objc_rootDealloc(self);
}
**********************************
void _objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
***********************************
inline void objc_object::rootDealloc()
{
    //✅如果是Tagged Pointer,就直接返回
    if (isTaggedPointer()) return;  // fixme necessary?

    /*
    ✅如果同时满足 
    1. 是优化过的isa、
    2. 没有被weak指针引用过、
    3. 没有关联对象、
    4. 没有C++析构函数、
    5. 没有sideTable,
    就可以直接释放内存free()
    */
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {//否则的话就需要通过下面的函数处理
        object_dispose((id)this);
    }
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
***********************************
// 销毁C++析构函数以及移除关联对象的操作
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.// 如果有C++析构函数,则从运行相关函数
        if (cxx) object_cxxDestruct(obj); 
        ✅// 如果有关联对象,则移除所有的关联对象,并将其自身从Association Manager的map中移除
        if (assoc) _object_remove_assocations(obj); 
        ✅// 继续清理其它相关的引用
        obj->clearDeallocating(); 
    }
    return obj;
}

inline void objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.// 如果要释放的对象没有采用了优化过的isa引用计数
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.// 如果要释放的对象采用了优化过的isa引用计数,并且有弱引用或者使用了sideTable的辅助引用计数
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

NEVER_INLINE void objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    ✅// 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        ✅//要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    ✅//使用了sideTable的辅助引用计数,直接在SideTable中擦除该对象的引用计数
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    ✅//获取被弱引用对象的地址
    objc_object *referent = (objc_object *)referent_id;
    ✅// 根据对象地址找到被弱引用对象referent在weak_table中对应的weak_entry_t
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); 
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    ✅// 找出弱引用该对象的所有weak指针地址数组
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    ✅// 遍历取出每个weak指针的地址
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i]; 
        if (referrer) {
            ✅// 如果weak指针确实弱引用了对象 referent,则将weak指针设置为nil
            if (*referrer == referent) { 
                *referrer = nil;
            }
            ✅// 如果所存储的weak指针没有弱引用对象 referent,这可能是由于runtime代码的逻辑错误引起的,报错
            else if (*referrer) { 
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    weak_entry_remove(weak_table, entry);
}

TODO:在深入了解下

3、内存分析

3.1 内存分类

  • RAM:运行内存,不能掉电储存;
  • ROM:储存性内存,可以掉电储存,例如:内存卡,flash;

RAM和ROM的区别

  1. RAM的访问速度要远高于ROM,价格也要高;
  2. CPU只能从RAM直接读取指令;
  3. app程序一般存放于ROM中。启动app时,系统会把开启的app程序从ROM中转移到RAM中

3.2 从内存分配看app加载过程

在了解OC的内存分配之前,先看下app的加载过程:

  • 当APP没有打开时,ipa或者app文件都是存在于ROM中,即我们说一个叫Application的文件夹中
  • 在APP启动的时候iOS系统会先为app从RAM中分配一个独立的内存空间(即沙盒),app所有的内存操作都在这个独立的沙盒中进行。
  • 接着系统会先加载二进制代码到内存中,然后加载常量区中的常量,接着加载全局区和静态区(初始化过的静态区和没有初始化过的静态区是分开的)

之后程序会找main入口函数开始执行代码,在执行代码的过程中,会创建对象和一些局部变量,其中对象存放在堆中,变量存放在栈上。

3.3 内存分析手段--Instruments

在iOS开发中,对内存的分析和查找内存泄漏是提高应用性能和稳定性的重要步骤。Apple提供了一些强大的工具和框架来帮助开发者进行内存分析和检测内存泄漏。以下是一些常用的方法和工具:

1. Instruments

Instruments是Xcode附带的一个性能分析工具,它提供了多种用于监控和分析应用性能的模板,其中就包括用于检测内存泄漏的模板。

  • Leak检测:Leak模板可以帮助你发现应用中的内存泄漏。它会监控应用运行过程中分配的内存,查找没有正确释放的内存。

  • Allocations:Allocations模板可以帮助你了解应用的内存使用情况,包括内存的分配和释放。通过这个模板,你可以查看应用的总内存使用量,以及每个对象类型的内存使用情况。

  • *VM Tracker *:VM Tracker模板可以帮助你分析应用的虚拟内存使用情况。

TODO:第三方插件

使用Instruments检测内存泄漏的基本步骤:

  1. 在Xcode中,选择你的设备和要分析的目标应用。

  2. 选择顶部菜单栏的Product > Profile,或者使用快捷键Command + I。这将会启动Instruments。

  3. 在Instruments启动界面,选择LeaksAllocations模板开始分析。

  4. 运行你的应用,并在Instruments中开始记录。尝试触发应用的各种操作,特别是那些你怀疑可能会导致内存泄漏的操作。

  5. 分析Instruments提供的数据,查找内存泄漏的源头。

2. Xcode Memory Graph Debugger

Xcode的Memory Graph Debugger是一个强大的工具,可以帮助你可视化应用的内存使用情况,查找内存泄漏。当你的应用在调试模式下运行时,你可以通过Xcode的调试栏中的Memory Graph按钮来启动这个工具。

Memory Graph Debugger会显示应用中所有活跃对象的图表,以及这些对象之间的关系。通过这个工具,你可以轻松地找到被意外保留的对象,这些对象可能是内存泄漏的原因。

使用Memory Graph Debugger的基本步骤:

  1. 在Xcode中运行你的应用。

  2. 在Xcode的调试栏中,点击Memory Graph按钮(看起来像一个链接的圆圈)。

  3. Xcode会暂停应用的运行,并显示内存图表。

  4. 通过检查图表和对象的保留周期,查找可能的内存泄漏。

通过这些工具和方法,你可以有效地对iOS应用进行内存分析,查找并解决内存泄漏问题,从而提高应用的性能和稳定性。

3.4 内存泄漏分析

juejin.cn/post/719029…消息转发机--TODO

@property (nonatomic, strong) Person *person;

- (void)setPerson:(Person *)person {
    if (_person != person) {
        [_person release];
        _person = [person retain];
    }
}

- (Person *) person {
    return _person;
}

内存泄漏处理:程序中已动态分配的堆内存(程序员自己管理的空间)由于某些原因未能释放或无法释放,造成系统内存的浪费,导致程序运行速度变慢甚至系统崩溃。

通过遵循最佳实践,可以有效地避免内存泄漏问题。在使用闭包时要小心避免循环引用,及时释放子视图和delegate,避免自引用问题

juejin.cn/post/737433…

循环引用

当对象 A 强引用对象 B,而对象 B 又强引用对象 A,或者多个对象互相强引用形成一个闭环,这就是循环引用

  • Block块中的强引用

    在Swift中,block(闭包)默认会对其捕获的对象(包括self)进行强引用。这意味着,闭包中的self会被闭包持有,导致self无法被释放,从而造成内存泄漏。 使用 [weak self]解决

  • 多层Block块嵌套中的引用问题。--将 [weak self] 放在最外层

  • 子视图的delegate未释放

    当一个视图控制器(ViewController)作为某个子视图的delegate时,如果没有在适当的时候将delegate置为nil,该视图控制器就会一直被持有,导致无法释放内存。

    class MyViewController: UIViewController, UITableViewDelegate {
        var tableView: UITableView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tableView = UITableView()
            tableView.delegate = self
        }
        
        // 错误示例,没有在合适的地方将delegate置为nil
        // 这会导致视图控制器无法释放
      	// 解决
      override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            tableView.delegate = nil
        }
    }
    
  • 子视图的互相引用问题

    class ChildView: UIView {
        var parentView: MyViewController?
    
        func setup(parentView: MyViewController) {
            self.parentView = parentView
        }
        
        deinit {
            print("ChildView deinit")
        }
    }
    
    class MyViewController: UIViewController {
        var childView: ChildView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            childView = ChildView()
            childView.setup(parentView: self)
            view.addSubview(childView)
        }
        
        deinit {
            print("MyViewController deinit")
        }
    }
    
  • 未正确释放子视图导致的整体视图无法释放

    如果一个视图控制器中的子视图未能正确释放,则整个视图控制器也无法被释放。这通常是由于子视图中持有对父视图控制器的强引用。

    解决方案:确保所有子视图都能正确释放

  • 自引用问题

当一个对象(如视图控制器)在其内部持有对自身的强引用时,会导致内存泄漏。例如,定时器或通知中心等异步任务在持有对self的强引用时,会导致self无法被释放。

4、内存优化

4.1 减少静态链接库的数量或大小

减少静态链接库的数量或大小确实可以在一定程度上减少应用的内存占用,但这种影响主要体现在应用的磁盘占用和启动时间上,对运行时内存的影响相对较小。

下面分别解释这两方面的影响:

磁盘占用和启动时间

静态链接库(Static Libraries,以.a或.framework结尾)在应用编译时会被整合到最终的可执行文件中。这意味着,如果你的应用包含了大量的静态链接库,那么最终的应用大小会增加,因为所有使用到的静态库代码都会被包含在内。

此外,应用启动时,操作系统需要加载应用的可执行文件到内存中。如果可执行文件很大,那么加载过程可能会消耗更多的时间,从而影响应用的启动速度。

4.2 运行时内存占用--TODO:减少静态链接库

应用的运行时内存占用主要取决于应用在运行过程中创建和使用的对象、数据结构、图像资源等。静态链接库的大小或数量对运行时内存的直接影响相对较小。减少静态链接库可能不会直接减少应用运行时的内存占用,除非这些库在运行时动态地加载了大量的资源或数据。

4.3 减少应用内存占用的建议

  1. 优化数据结构和算法:检查和优化应用中的数据处理逻辑,使用更高效的数据结构和算法。
  2. 按需加载资源避免预加载大量资源,尽可能地按需加载
  3. 使用弱引用和自动释放池:合理使用弱引用避免循环引用,使用自动释放池减少内存峰值。
  4. 图片和资源优化:对图片资源进行压缩,使用适当的格式和分辨率,避免加载过大的图片。
  5. 检查内存泄漏和僵尸对象:使用Xcode的Instruments工具检查内存泄漏和未被释放的对象。
  6. 动态链接库(Dynamic Libraries) :考虑使用动态链接库(以.dylib或.framework结尾),它们在应用运行时按需加载,可能有助于减少应用的初始内存占用。

总之,减少静态链接库可以减少应用的磁盘占用和可能的启动时间,但对运行时内存占用的影响有限。优化应用的内存使用,更应关注于运行时的数据处理和资源管理

4.4 App启动时间过长问题

应用内置资源过多确实有可能导致应用启动变慢。这是因为在应用启动过程中,尤其是在应用的launch阶段,操作系统需要加载应用的可执行文件和相关资源。如果这些资源过多或过大,它们会占用更多的I/O时间来从存储中读取,以及更多的CPU时间来处理,从而延长应用的启动时间。以下是一些可能导致启动变慢的因素:

  1. 大量的图片资源:如果应用包含大量的图片资源,尤其是高分辨率的图片,它们会占用大量的磁盘空间,并且在加载时需要更多的时间。

  2. 大型的数据文件:内置的大型数据文件(如数据库文件、音频文件、视频文件等)也会增加应用的加载时间。

  3. 复杂的启动逻辑:在应用启动时执行复杂的初始化操作,如大量的数据处理、网络请求等,也会延长启动时间。

  4. 动态库和框架:应用使用的动态库和框架数量过多,每个动态库加载时都会增加启动时间。

优化建议

为了减少应用启动时间,可以采取以下一些优化措施:

  • 优化资源文件:对图片等资源文件进行压缩,减少它们的文件大小,同时考虑使用适当的文件格式。

  • 延迟加载:将一些不是立即需要的初始化操作延迟到启动过程之后异步执行,以减少启动阶段的工作量。

  • 减少动态库的使用:减少不必要的动态库和框架的使用,考虑使用静态库替代,或者合并一些小的动态库。

  • 使用App启动时间分析工具:利用Xcode提供的工具,如Instruments,分析应用的启动时间,找出瓶颈所在。

  • 资源按需加载:尽可能地按需加载资源,避免在应用启动时就加载所有资源。

通过上述优化措施,可以有效减少应用的启动时间,提升用户体验。

4.5 App 启动时长优化

具体了解:juejin.cn/search?quer…

juejin.cn/post/733751…

juejin.cn/post/684490…

启动阶段

App 的启动时间,指的是从用户点击 App 开始,到用户看到第一个界面之间的时间;也就是 didBecomeActive 的时候或者 rootVC 的 viewDidAppear 的时候。

juejin.cn/post/684490…

冷启动耗时,主要分为了 pre-main 阶段和 main() 阶段,接下来分别在这两个阶段查看耗时以及优化。

  • pre-main 阶段-指的是从用户唤起 App 到 main() 函数执行之前的过程。

    • 动态库载入耗时--多使用静态库,少使用动态库

    • 修正符号和绑定符号的耗时;--指针重定向

    • ObjC setup time:Objc 类的注册、category 的定义插入方法列表、保证 selector 唯一的耗时

    • initializer time:Objc 类的 +load()、C++ 的构造函数属性函数的构造函数、C++ 的静态全局变量创建的耗时---懒加载

      Mach-O扩展

      Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。App 编译生成的二进制可执行文件就是 Mach-O 格式的,iOS 工程所有的类编译后会生成对应的目标文件 .o 文件,而这个可执行文件就是这些 .o 文件的集合。

      Mach-O 文件主要由三部分组成:

      • Mach header:描述 Mach-O 的 CPU 架构、文件类型以及加载命令等;

      • Load commands:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令;

      • Data:Data 中的每个段(segment)的数据都保存在这里,每个段都有一个或多个 Section,它们存放了具体的数据与代码,主要包含这三种类型:

        • __TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
        • __DATA 包含全局变量,静态变量等。可读写(rw-)。
        • __LINKEDIT 包含了加载程序的元数据,比如函数的名称和地址。只读(r–-)。

      dylib

      dylib 也是一种 Mach-O 格式的文件,后缀名为 .dylib 的文件就是动态库(也叫动态链接库)。动态库是运行时加载的,可以被多个 App 的进程共用。

      如果想知道 TestDemo 中依赖的所有动态库,可以通过下面的指令实现:

      复制代码

      otool -L /TestDemo.app/TestDemo

      动态链接库分为系统 dylib内嵌 dylib(embed dylib,即开发者手动引入的动态库)。系统 dylib 有:

      • iOS 中用到的所有系统 framework,比如 UIKit、Foundation;
      • 系统级别的 libSystem(如 libdispatch(GCD) 和 libsystem_blocks(Block));
      • 加载 OC runtime 方法的 libobjc;
      • ……

      dyld

      dyld(Dynamic Link Editor):动态链接器,其本质也是 Mach-O 文件,一个专门用来加载 dylib 文件的库。 dyld 位于 /usr/lib/dyld,可以在 mac 和越狱机中找到。dyld 会将 App 依赖的动态库和 App 文件加载到内存后执行。

      dyld shared cache

      dyld shared cache 就是动态库共享缓存。当需要加载的动态库非常多时,相互依赖的符号也更多了,为了节省解析处理符号的时间,OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于 /private/var/db/dyld/,iOS 的则在 /System/Library/Caches/com.apple.dyld/。

      当加载一个 Mach-O 文件时,dyld 首先会检查是否存在于共享缓存,存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。

      images

      images 在这里不是指图片,而是镜像。每个 App 都是以 images 为单位进行加载的。images 类型包括:

      • executable:应用的二进制可执行文件;
      • dylib:动态链接库;
      • bundle:资源文件,属于不能被链接的 dylib,只能在运行时通过 dlopen() 加载。

      framework

      framework 可以是动态库,也是静态库,是一个包含 dylib、bundle 和头文件的文件夹。

      启动过程分析与优化

      启动一个应用时,系统会通过 fork() 方法来新创建一个进程,然后执行镜像通过 exec() 来替换为另一个可执行程序,然后执行如下操作:

      1. 把可执行文件加载到内存空间,从可执行文件中能够分析出 dyld 的路径;
      2. 把 dyld 加载到内存;
      3. dyld 从可执行文件的依赖开始,递归加载所有的依赖动态链接库 dylib 并进行相应的初始化操作。

      结合上面 pre-main 打印的结果,我们可以大致了解整个启动过程如下图所示:

      image.jpeg

      Load Dylibs

      这一步,指的是动态库加载。在此阶段,dyld 会:

      1. 分析 App 依赖的所有 dylib;
      2. 找到 dylib 对应的 Mach-O 文件;
      3. 打开、读取这些 Mach-O 文件,并验证其有效性;
      4. 在系统内核中注册代码签名;
      5. 对 dylib 的每一个 segment 调用 mmap()。

      一般情况下,iOS App 需要加载 100-400 个 dylibs。这些动态库包括系统的,也包括开发者手动引入的。其中大部分 dylib 都是系统库,系统已经做了优化,因此开发者更应关心自己手动集成的内嵌 dylib,加载它们时性能开销较大。

      App 中依赖的 dylib 越少越好,Apple 官方建议尽量将内嵌 dylib 的个数维持在6个以内。

      优化方案

      • 尽量不使用内嵌 dylib;
      • 合并已有内嵌 dylib;
      • 检查 framework 的 optional 和 required 设置,如果 framework 在当前的 App 支持的 iOS 系统版本中都存在,就设为 required,因为设为 optional 会有额外的检查;
      • 使用静态库作为代替;(不过静态库会在编译期被打进可执行文件,造成可执行文件体积增大,两者各有利弊,开发者自行权衡。)
      • 懒加载 dylib。(但使用 dlopen() 对性能会产生影响,因为 App 启动时是原本是单线程运行,系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这样不仅会使性能降低,可能还会造成死锁及未知的后果,不是很推荐这种做法。)

      Rebase/Binding

      这一步,做的是指针重定位

      在 dylib 的加载过程中,系统为了安全考虑,引入了 ASLR(Address Space Layout Randomization)技术和代码签名。由于 ASLR 的存在,镜像会在新的随机地址(actual_address)上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide,slide=actual_address-preferred_address),因此 dyld 需要修正这个偏差,指向正确的地址。具体通过这两步实现:

      第一步:Rebase,在 image 内部调整指针的指向。将 image 读入内存,并以 page 为单位进行加密验证,保证不会被篡改,性能消耗主要在 IO。

      第二步:Binding,符号绑定。将指针指向 image 外部的内容。查询符号表,设置指向镜像外部的指针,性能消耗主要在 CPU 计算。

      通过以下命令可以查看 rebase 和 bind 等信息:

      复制代码

      xcrun dyldinfo -rebase -bind -lazy_bind TestDemo.app/TestDemo

      通过 LC_DYLD_INFO_ONLY 可以查看各种信息的偏移量和大小。如果想要更方便直观地查看,推荐使用 MachOView 工具。

      指针数量越少,指针修复的耗时也就越少。所以,优化该阶段的关键就是减少 __DATA 段中的指针数量。

      优化方案

      • 减少 ObjC 类(class)、方法(selector)、分类(category)的数量,比如合并一些功能,删除无效的类、方法和分类等(可以借助 AppCode 的 Inspect Code 功能进行代码瘦身);
      • 减少 C++ 虚函数;(虚函数会创建 vtable,这也会在 __DATA 段中创建结构。)
      • 多用 Swift Structs。(因为 Swift Structs 是静态分发的,它的结构内部做了优化,符号数量更少。)

      ObjC Setup

      完成 Rebase 和 Bind 之后,通知 runtime 去做一些代码运行时需要做的事情:

      • dyld 会注册所有声明过的 ObjC 类;
      • 将分类插入到类的方法列表中;
      • 检查每个 selector 的唯一性。

      优化方案

      Rebase/Binding 阶段优化好了,这一步的耗时也会相应减少。

      Initializers

      Rebase 和 Binding 属于静态调整(fix-up),修改的是 __DATA 段中的内容,而这里则开始动态调整,往堆和栈中写入内容。具体工作有:

      • 调用每个 Objc 类和分类中的 +load 方法;
      • 调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数);
      • 创建非基本类型的 C++ 静态全局变量。

      优化方案

      • 尽量避免在类的 +load 方法中初始化,可以推迟到 +initiailize 中进行;(因为在一个 +load 方法中进行运行时方法替换操作会带来 4ms 的消耗)
      • 避免使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时再执行。比如用 dispatch_once()、pthread_once() 或 std::once(),相当于在第一次使用时才初始化,推迟了一部分工作耗时。:
      • 减少非基本类型的 C++ 静态全局变量的个数。(因为这类全局变量通常是类或者结构体,如果在构造函数中有繁重的工作,就会拖慢启动速度)

      总结一下 pre-main 阶段可行的优化方案:

      • 重新梳理架构,减少不必要的内置动态库数量

      • 进行代码瘦身,合并或删除无效的ObjC类、Category、方法、C++ 静态全局变量等

      • 将不必须在 +load 方法中执行的任务延迟到 +initialize 中

      • 减少 C++ 虚函数

      作者:FiTeen
      链接:juejin.cn/post/684490…
      来源:稀土掘金
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • main() 阶段耗时

    主要是 application:didFinishLaunchingWithOptions: 中的 SDK 初始化、业务注册、各个模块业务处理等耗时。

    处理:不重要的业务后移(启动项后置)、懒加载的操作、首屏渲染

  • 首屏渲染

  • 其他优化:GCD 多线程优化--能放子线程的放子线程。

    所有在主线程执行的任务都可以考虑是否能放在一个单独的子线程去完成,这样就可以把主线程腾出来高效的去做其他任务。

    不改一行业务代码,飞书 iOS 低端机启动优化实践

    开发者创建的 GCD 队列,默认的 QoS 实际为 User-Initiated,User-Interactive 和 User-Initiated 对主线程有明显抢占。尽量将启动无关的线程设置为 Utility 或者 Background,减少对主线程的抢占。

  • TODO:监测工具:juejin.cn/post/711200…

  • juejin.cn/post/727859…

  • juejin.cn/post/715941… 懒加载 && 延时加载

4.6 耗电优化-CPU,GPU,网络请求,定位

juejin.cn/post/684490…

juejin.cn/post/706297…

juejin.cn/post/710202…

juejin.cn/post/710234…

juejin.cn/post/684490…

定位精度,更高的精度,往往意味着更高的能耗,因此要平衡好精度和功耗,避免我们的APP过多的电量消耗。

  • Identify: 保证一致性,可复用
  • Optimize : 优化,更高效
  • Coalesce :合并
  • Reduce : 减少不必要处理

优化

1.1 缩减网络请求

1)减少、压缩网络数据。 可以降低上传或下载的多媒体内容质量和尺寸等。

2)使用缓存,不要重复下载相同的数据

3)使⽤断点续传,否则网络不稳定时可能多次传输相同的内容。

4)网络不可用时不要尝试执行网络请求,尽量只在Wi-Fi情况下联网。

5)让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间。

6)网络请求失败后用SCNetworkReachability的通知监测网络状态,网络可用后再重试。

1.2 延迟联网

1)分批传输。 比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。 如果提供广告,一次性多下载一些,然后再慢慢展示。 如果要从服务器下载电子邮件,一次下载多条,不要一条一条地下载。

2)网络操作能推迟就推迟。 如果通过HTTP上传、下载数据,建议使用NSURLSession中的后台会话,这样系统可以针对整个设备所有的网络操作优化功耗。将可以推迟的操作尽量推迟到设备充电状态并且连接Wi-Fi时进行,比如同步和备份工作。

针对不同场景:

Feed:对于信息流的处理,可以做到在需要时去刷新数据(提升体验的前提下可以预加载),对于已经加载的数据做本地缓存,这样可以节省流量以及不必要的网络开销

Post a Photo:发送一张图片,通常情况我们会选择立即上传,当上传失败后的重新上传。但其实我们可以做的更好,我们可以批处理上传图片,设置超时时间,以及减少重试。 达到重试限制后使用后台会话去上传图片。(发送图片资源、媒体资源时,压缩资源也是很必要的)

Analytics: 上传用户分析数据,苹果推荐使用NSURLSession Background Session,好处有自动重试、全程监控,以及全新的属性Properties( 包括了Start time、Workload size),帮助app知道处理的最佳时机。(适用于一些埋点需求)

  • Identify确保事务不重复
  • Optimize使用background session
  • Coalesce批处理事务
  • Reduce减少重试次数

2.定位

1)如果你的app只是需要快速确定一下用户的位置,最好⽤CLLocationManager的requestLocation (iOS9引⼊入)方法。 定位完成之后会 自动让硬件断电。

2)除非是在导航的时候,app大部分时间不需要实时更新,降低位置的更新频率。

3)尽量降低定位精度。iOS设备默认采用最⾼精度定位,如果你的app不是确实需要米级的位置信息,不要用最高精度(kCLLocationAccuracyBest)或10米左右的精度(kCLLocationAccuracyNearestTenMeters)。一般来说Core Location提供的精度比你设置的要好,比如你设置为3公里左右的精度,可能会收到100米左右的精度信息。

4)如果定位精度一直达不到设置的精度时,停止更新位置,稍后再试。

5)需要后台更新位置时,尽量把pausesLocationUpdatesAutomatically设为YES,如果用户不太可能移动的时候系统会自动暂停位置更 新。

6)后台定位时延时更新位置。如果要做一个健身类的软件追踪用户徒步的距离,可以等用户移动一段距离或者过一段时间之后再更新位置,这样可以让系统优化能耗。

  • Identify : 清楚 app 需要的定位精确度(适合你的需求就好)
  • Optimize : 使用其它来替代 Continuous location(因为这个真的很耗电)
  • Coalesce :不需要使用定位时,就停止定位
  • Reduce : 延后定位更新

3.CPU

CPU方面需要明确 app 要完成的任务,并且高效的完成任务,避免使用定时器timer,如果必须使用,设置一个较长的间隔时间。

3.1 尽量减少计时器使用

使用计时器时,设置一个合适的超时,不再需要时及时关闭重复性定时器。用事件通知代替定时器。有些app用定时器监控文件内容、 网络或者其他状态的变化,这会导致CPU无法进入闲置状态而增加功耗。

3.2 减少后台工作

实现UIApplicationDelegate中的方法,应用进入后台前做好暂停任务,保存数据等工作。如果确实需要完成用户执行的一些任务,应该调 用UIApplicationDelegate中的beginBackgroundTaskWithExpirationHandler: 方法,这样后台任务可以继续执行几分钟。 任务执行完毕后 一定要调用endBackgroundTask:方法,不要等着系统强行挂起进程。

3.3 用QoS分级有序工作

多个app和众多操作需要共享CPU、缓存、网络等资源,为了保持高效,系统需要根据不同任务的优先级智能地管理这些工作。 比如更新UI这种重要的事需要多分配资源,而一些后台任务可以延迟一些执行。 服务质量(quality of service, 以下简称QoS, iOS8引⼊入)级别可以通过NSOperation, NSOperationQueue, NSThread objects, dispatch queues, 和pthreads (POSIX threads)指定工作的优先级。

3.4 优化I/O访问

app每次执行I/O任务,比如写⽂件,会导致系统退出闲置模式。 而且写入缓存格外耗电。 通过下列方法可以提高能效、改善app性能。

1)减小写入数据。 数据有变化再写文件,尽量把多个更改攒到一起一次性写入。 如果只有几个字节的数据改变,不要把整个文件重新写入一次。 如果你的app经常要修改大⽂件⾥很少的内容,可以考虑用数据库存储这些数据。

2)避免访问存储频度太高。如果app要存储状态信息,要等到状态信息有变化时再写入。尽量分批修改,不要频繁地写入这些小变动。

3)尽量顺序读写数据。在文件中跳转位置会消耗一些时间。

4)尽量从⽂件读写大数据块,一次读取太多数据可能会引发一些问题。比如,读取一个32M⽂件的全部内容可能会在读取完成前触发内容分页。

5)读写大量重要数据时,考虑用dispatchio,其提供了基于异步操作文件I/O的API。用dispatchio系统会优化磁盘访问。

6)如果你的数据由随机访问的结构化内容组成,建议将其存储在数据库中,可以使用SQLite或CoreData访问。特别是需要操作的内容可能增长到超过几兆的时候。

7)了解系统如何缓存文件、如何优化缓存的使用。如果你不打算多次引用某些数据,不要⾃己缓存数据。

  • Identify在后台完成工作
  • Optimize使用后台应用刷新
  • Coalesce使用background session
  • Reduce限制事务处理

4.GPU

1)减少app使用的视图数量。

2)少用运算获得圆角,不论view.maskToBounds还是layer.clipToBounds都会有很大的资源开销。

3)尽量少用透明或半透明,会产⽣额外的运算。

4)执行动画时不要修改帧率。比如,你的app帧率是60fps,整个动画就保持这个帧率不要变。

5)视频播放时,app尽量不要在全屏视频上添加额外的图层(即使是隐藏的图层)。

关于图像处理有两条建议:

  1. 保证在 UI 真的需要有变化时,进行屏幕更新
  2. 避免使用高斯模式blur。另外MacOS 尽量少使用独立显卡。
  • Identify高斯模糊的使用
  • OptimizeMacOS 尽量少使用独立显卡,只在动画性能吃紧、或者其独有功能时才去使用它
  • Reduce在UI真的有必要变化时,更新屏幕

5 .优化通知

1)尽量用本地通知(local notification),如果你的app不依赖外部数据,而是需要基于时间的通知,应该用本地通知,可以让设备的网络硬件休息一下。

2)远程推送有两个级别,一个是立即推送,另一个是针对功耗优化过的延时推送。如果不是真的需要即时推送,尽量使⽤延时推送。

4.7 卡顿常见的问题:文件加载过大、阻塞主线程、渲染 等

优化

  • 绝大多数的耗时操作,尽量放到子线程去做
  • 最终在主线程更新UI
  • 在操作多线程的时候一定要注意线程安全

可以多用 Cache 比较大的数据的是做缓存处理

  • 渲染的优化:

  • 离屏渲染:在对layer做复杂操作的时候,会触发离屏渲染

    • shouldRasterize(光栅化)
    • masks(遮罩)
    • shadows(阴影)
    • edge antialiasing(抗锯齿)
    • group opacity(不透明)
  • 能操作view的尽量不去操作layer

table View 优化

  1. 避免使用比较复杂的Cell,可以使用hidden来隐藏,尽量减少在复用的时候去添加、减少view
  2. 尽量少用自动计算高度,label的自适应等(对于复杂的cell,尽量提前计算好)
  3. 减少xib、sb的cell布局(这个影响其实很小)
  4. 在滚动过程中,减少对cell的操作,布局等
  5. 根据滚动状态,cell的显示,来优化数据显示,cell的刷新,减少没显示的cell过多消耗性能
  6. 一定一定要检查UI更新正常, 不被网络,等其他异步操作阻塞线程

5、内存交换机制

内存对齐为两个原则: 原则 1. 前面的地址必须是后面的地址正数倍,不是就补齐。 原则 2. 整个Struct的地址必须是最大字节的整数倍。

juejin.cn/post/735603…

内存交换机制是操作系统中重要的内存管理技术之一,它通过将进程或进程的一部分在物理内存和磁盘之间移动,来动态管理有限的物理内存资源。尽管交换机制有其缺点,但通过合理的设计和优化,可以最大限度地减少其对系统性能的影响

5.1 物理内存和虚拟内存

物理内存和虚拟内存是计算机系统中两种重要的内存管理技术,它们共同工作以提高系统的性能和内存使用效率。

物理内存

物理内存,也称为实际内存或RAM(Random Access Memory),是计算机硬件的一部分。它由实体的内存条组成,为计算机提供存储空间以存放正在运行的程序和数据。物理内存的大小是有限的,由计算机中安装的RAM芯片的容量决定。

物理内存的特点包括:

  • 有限性物理内存的容量有限,通常为几GB到几十GB

  • 易失性:当计算机断电时,存储在物理内存中的数据会丢失。

  • 快速访问:CPU可以直接访问物理内存,访问速度远快于硬盘等其他存储设备。

虚拟内存

虚拟内存是一种内存管理技术,它允许计算机的操作系统使用硬盘空间作为临时的内存使用。通过虚拟内存,操作系统创建了一个虚拟的地址空间,这个地址空间对每个程序来说都是独立的,使得每个程序都认为自己拥有连续的、完整的内存空间。

虚拟内存的特点包括:

  • 扩展性:虚拟内存可以使用硬盘空间作为额外的内存,从而扩展了可用的内存容量。

  • 隔离性:每个程序都在自己的虚拟地址空间中运行,不会直接影响到其他程序,增强了系统的稳定性和安全性。

  • 交换机制:操作系统会根据需要将部分数据从物理内存交换到硬盘上(称为“换出”),或者从硬盘交换回物理内存(称为“换入”),以优化内存使用。

二者的关系

虚拟内存和物理内存共同工作,通过虚拟内存技术,操作系统可以更有效地利用有限的物理内存资源,允许运行更多的程序,同时提高系统的响应速度和稳定性。虚拟内存通过分页(paging)或分段(segmentation)机制,将虚拟地址映射到物理地址,使得程序在使用内存时更加灵活。

总的来说,物理内存提供了实际的存储空间,而虚拟内存则通过软件技术扩展了内存的使用可能性,二者共同支撑着现代计算机系统的内存管理。

如果程序占用内存过大,系统为什么可能会强制关闭程序,造成程序崩溃?

当程序占用的内存过大时,系统可能会强制关闭程序,造成程序崩溃,这主要是由于以下几个原因:

  1. 资源保护:操作系统负责管理计算机的硬件资源,包括CPU、内存、存储等。当一个程序占用过多内存时,可能会影响到系统的稳定性和其他程序的运行。为了保护系统资源,确保系统稳定运行,操作系统可能会选择终止占用过多资源的程序。

  2. 避免内存耗尽:如果系统允许一个程序无限制地占用内存,可能会导致系统可用内存耗尽,这种情况被称为内存泄漏。内存耗尽不仅会影响到当前程序的运行,还可能导致系统其他程序无法正常工作,甚至导致整个系统崩溃。因此,操作系统会监控程序的内存使用情况,一旦发现某个程序占用过多内存,可能会采取措施终止该程序。

  3. 保证公平性:在多任务操作系统中,系统需要确保所有运行的程序都能公平地访问资源。如果一个程序占用了过多的内存资源,会影响到其他程序的正常运行。为了保证资源的公平分配,操作系统可能会限制单个程序的资源使用,或在必要时终止占用过多资源的程序

  4. 响应内存压力:现代操作系统通常具有内存管理机制,如虚拟内存系统,允许程序使用比物理内存更多的地址空间。当系统检测到内存压力(即物理内存使用接近极限)时,会尝试通过各种机制 (如页面置换、压缩内存等) 释放内存。如果这些措施仍然无法缓解内存压力,系统可能会选择终止一些内存使用量大的程序,以保护系统的稳定性和响应性。

为了避免程序因占用过多内存而被系统强制关闭,开发者应该注意合理管理程序的内存使用,避免内存泄漏,定期检查和优化内存使用情况。此外,可以利用操作系统提供的工具和API监控程序的内存使用情况,及时响应可能的内存问题。

5.2 分页与缺页中断

内存分页是现代操作系统中用于内存管理的一种技术。它将物理内存和虚拟内存都划分为固定大小的块,这些块被称为“页”(Page)。通过分页,操作系统可以更有效地管理内存,提高内存的使用效率,同时也为实现虚拟内存提供了基础。

分页的主要特点:

  1. 虚拟内存地址空间:操作系统为每个进程提供了一个连续的虚拟地址空间,进程只与虚拟地址空间交互,而不直接访问物理内存。

  2. 物理内存的非连续分配:分页允许物理内存被非连续地分配给进程,从而有效地利用内存,减少内存碎片。

  3. 页表:操作系统维护一个页表来记录虚拟页和物理页之间的映射关系。当进程访问其虚拟内存时,操作系统通过页表将虚拟地址转换为相应的物理地址。

  4. 缺页中断:当进程访问的虚拟页尚未被加载到物理内存时,会触发缺页中断(Page Fault),操作系统负责将缺失的页从磁盘加载到物理内存中。

分页的优点:

  1. 简化内存管理:分页简化了内存的分配和管理过程,因为所有的页都是固定大小。

  2. 支持虚拟内存:分页是实现虚拟内存的基础,使得进程可以使用比实际物理内存更大的地址空间。

  3. 提高内存利用率:分页减少了内存碎片,提高了内存的利用率。

  4. 内存保护:分页机制可以实现进程间的内存隔离,提高系统的安全性。

分页的缺点:

  1. 页表开销:维护页表需要额外的内存开销,尤其是在地址空间很大的系统中。

  2. 页表查找时间:虚拟地址到物理地址的转换需要通过页表查找,这可能会增加内存访问的延迟。

  3. 内存碎片:虽然分页减少了外部碎片,但仍可能存在内部碎片,即页的部分空间未被有效利用。

分页是现代操作系统中广泛采用的内存管理技术,它通过将内存划分为固定大小的页来实现高效、灵活的内存管理。

当操作系统遇到页面错误(缺页中断)时,它需要从磁盘中读取缺失的页面到内存中。这个过程涉及的字节数取决于系统的页面大小。在现代操作系统中,常见的页面大小有4KB(4096字节)、2MB(大页)等。因此,每次缺页中断时系统从磁盘读取的字节数至少等于系统页面的大小。

磁盘抖动(Thrashing)是指当系统花费大部分时间处理缺页中断和页面的读写,而不是执行用户进程的代码时发生的现象。这通常发生在系统的物理内存不足以支持所有运行中的进程需要的内存时。此时,操作系统不得不频繁地在内存和磁盘之间交换数据(页面置换),导致系统性能急剧下降。

如何减少磁盘抖动:

  1. 增加物理内存:增加系统的物理内存可以直接减少页面置换的频率,从而减少磁盘抖动。

  2. 优化内存使用:优化应用程序的内存使用,减少不必要的内存占用,可以间接减少磁盘抖动。

  3. 使用交换空间:虽然交换空间(Swap Space)也是基于磁盘的,但合理配置和使用交换空间可以作为物理内存的补充,缓解内存压力。

  4. 调整页面置换算法:操作系统使用页面置换算法来决定哪些页面被置换出内存。优化这些算法,如使用最近最少使用(LRU)算法等,可以减少不必要的页面置换。

  5. 限制进程数量:限制同时运行的进程数量可以减少对物理内存的需求,从而减少磁盘抖动。

磁盘抖动严重影响系统性能,通过上述方法可以在一定程度上减轻或避免磁盘抖动的发生。

内存交换机制(Swapping)是操作系统中用于管理内存资源的一种技术。当系统的物理内存不足以满足所有运行中的进程的需求时,操作系统会将一些进程或进程的一部分从物理内存移动到磁盘上的一个特定区域,这个区域被称为交换空间(Swap Space)或交换文件(Swap File)。通过这种方式,操作系统可以释放物理内存,使得其他进程可以继续运行。当被交换出去的进程需要再次运行时,操作系统会将其从交换空间调回物理内存,这个过程可能会导致另一个进程或进程的一部分被交换到磁盘。

内存交换机制的主要特点:

  1. 释放物理内存:当系统的物理内存不足时,通过将进程或进程的一部分移动到磁盘上的交换空间,可以释放物理内存,使得其他进程可以使用这部分内存。

  2. 虚拟内存实现:内存交换机制是实现虚拟内存系统的一种方式。虚拟内存允许操作系统为进程提供比实际物理内存更大的地址空间。

  3. 提高系统的整体性能:通过使用内存交换机制,操作系统可以在物理内存有限的情况下运行更多的进程,从而提高系统的整体性能和资源利用率。

内存交换机制的缺点:

  1. 交换开销:从物理内存交换到磁盘,再从磁盘交换回物理内存的过程涉及大量的磁盘I/O操作,这会导致显著的性能开销。

  2. 磁盘抖动:当系统频繁进行交换操作时,可能会出现磁盘抖动(Thrashing)现象,即系统花费大部分时间进行交换操作而不是执行用户进程,导致系统性能急剧下降。

  3. 交换空间的管理:操作系统需要管理交换空间的使用,这增加了系统的复杂性。

优化内存交换机制:

  1. 足够的物理内存:增加物理内存可以减少对交换空间的依赖,从而减少交换操作的频率。

  2. 智能的页面置换算法:采用高效的页面置换算法(如最近最少使用LRU算法)可以减少不必要的交换操作。

  3. 优化交换策略:操作系统可以根据系统的运行情况和进程的特性,动态调整交换策略,以减少交换操作的影响。扩展

6、APP级别

6.1 沙盒机制

沙盒机制(Sandboxing)是操作系统中用于提高安全性的一种技术,它通过限制应用程序的访问权限来防止恶意软件造成的损害。在沙盒环境中,应用程序只能访问授权的资源和数据,对系统其他部分的访问则被严格限制。这种机制在现代操作系统中非常普遍,特别是在移动操作系统如iOS和Android中。

iOS沙盒机制

iOS为每个应用程序提供了一个独立的运行环境——沙盒。应用程序只能在自己的沙盒目录中读写文件,不能访问其他应用程序的数据,也不能访问系统核心文件。这样做的目的是为了保护用户的数据安全,防止应用程序之间相互干扰或恶意访问数据。

iOS沙盒目录结构主要包括:

  • Documents:用于存储用户数据或应用程序不能重新创建的文件。

  • Library:包含应用程序的默认设置和其他状态信息。

    • Caches:用于存储应用程序使用时生成的支持文件,不会被iTunes备份。
    • Preferences:包含应用程序的偏好设置文件。
  • tmp:用于存储临时文件,应用程序关闭时可能会被清除。

Android沙盒机制

Android同样为每个应用程序提供了独立的运行环境。每个应用程序运行在自己的进程中,并且在一个独立的Dalvik/ART虚拟机实例中执行。应用程序只能访问为其分配的文件目录和特定的系统资源。对于需要访问其他应用程序数据或系统资源的操作,必须通过用户授权的方式进行。

Android沙盒的特点包括:

  • 应用程序隔离:每个应用程序都在自己的Linux用户ID下运行,这意味着它们不能访问彼此的文件。
  • 权限系统:应用程序需要声明它们需要的权限,如访问相机、联系人等。用户在安装或运行应用程序时可以授予或拒绝这些权限。

沙盒机制的限制

虽然沙盒机制提高了系统的安全性,但也带来了一些限制,例如:

  • 应用程序不能自由地访问系统资源,这可能会限制某些功能的实现。
  • 应用程序之间的交互受到限制,需要通过特定的接口或服务进行。

总结

沙盒机制是现代操作系统中一项重要的安全特性,它通过限制应用程序的访问权限来保护用户数据和系统安全。不同的操作系统实现了不同的沙盒机制,但目的都是为了提供一个更安全的运行环境。

6.2 常见报错问题:ARC forbids explicit message send of 'dealloc'

在自动引用计数(ARC)管理的Objective-C代码中,你不能直接调用对象的dealloc方法。这个错误信息 "ARC forbids explicit message send of 'dealloc'" 意味着你尝试直接调用了一个对象的dealloc方法,这是不允许的。

背景

在ARC下,内存管理(包括对象的释放和引用计数的管理)是自动进行的。当一个对象的引用计数变为0时,ARC会自动调用该对象的dealloc方法来释放它占用的资源。因此,开发者不需要(也不应该)手动调用dealloc

正确的做法

尽管你不能直接调用dealloc,但如果你需要在一个类的实例被释放前执行一些清理工作,你可以通过重写dealloc方法来实现:

- (void)dealloc {
    // 执行清理工作,比如释放C语言资源等
    // 注意:在ARC下,不要调用[super dealloc],ARC会自动处理
}
在重写的`dealloc`方法中,你可以放置自定义的清理代码,比如释放使用Core Foundation创建的资源等。
但重要的是,**不要**在`dealloc`方法中调用`[super dealloc]`,因为在ARC下,这会导致编译错误。ARC会自动为你调用超类的`dealloc`方法。

### 示例

假设你有一个类,它持有一个Core Foundation类型的对象,你需要在这个类的实例被释放时释放那个Core Foundation对象:
- (void)dealloc {
    if (_myCoreFoundationObject != NULL) {
        CFRelease(_myCoreFoundationObject);
        _myCoreFoundationObject = NULL;
    }
    // 不要调用[super dealloc]
}

总结

在ARC管理的Objective-C代码中,不要尝试直接调用对象的dealloc方法。如果需要在对象被释放时执行清理工作,应该通过重写dealloc方法来实现,但记住不要在其中调用[super dealloc]。ARC会自动处理内存管理的细节,包括在适当的时候调用dealloc方法。

6.3 crash问题

self performSelector在self被释放的时候执行产生crash

`self`被释放后执行`performSelector:`方法会导致崩溃。这是因为`performSelector:`方法是在运行时调用`self`上的一个方法。
如果`self`已经被释放,那么尝试访问已经释放的对象会导致访问违规(access violation),进而引发崩溃。

1、原因
当你对一个对象执行`performSelector:`方法时,Objective-C运行时系统会在指定的对象上查找并执行给定的选择器(selector)。
如果这个对象已经被释放并且内存被回收,那么运行时系统尝试访问这块内存时就会发现这是一个野指针(dangling pointer),因为它指向的内存已经不再属于该对象。
这种情况下,程序会因为尝试访问已经释放的内存而崩溃。

2、解决方案:为了避免这种崩溃,你可以采取以下措施之一:
**使用弱引用(Weak Reference)**:如果你在延迟执行`performSelector:`(例如使用`performSelector:withObject:afterDelay:`),确保在对象被释放前取消这个延迟执行的操作。
你可以在对象的`dealloc`方法中使用`[NSObject cancelPreviousPerformRequestsWithTarget:self]`来取消所有未执行的`performSelector:`请求。
- (void)dealloc {
      [NSObject cancelPreviousPerformRequestsWithTarget:self];
  }
2. **检查对象是否仍然存在**:在某些情况下,你可能无法取消`performSelector:`请求,或者取消操作不适用于你的场景。
在这种情况下,确保在调用方法之前对象仍然存在是很重要的。这通常涉及到使用弱引用来持有对象,并在执行操作前检查对象是否仍然存在。

3. **避免在即将释放的对象上使用`performSelector:`**:尽量避免在对象的生命周期即将结束时安排`performSelector:`操作,特别是当你知道对象即将被释放时。
通过采取适当的预防措施,你可以避免因对象被释放后执行`performSelector:`而导致的崩溃。

[obj test];和【obj performSeletcor】实现原理的区别

[obj test];[obj performSelector:@selector(test)];在实现原理上有所区别,尽管它们都是向obj对象发送消息。

[obj test];的实现原理

当你使用[obj test];这样的直接消息发送方式时,编译器在编译时期就会将这个消息发送转换为对objc_msgSend函数的调用。这个过程是静态的,意味着编译器需要在编译时知道obj的类型以及test方法是否存在。objc_msgSend函数的作用是在运行时查找obj对象的类和父类中是否实现了test方法,并执行该方法的实现。如果找不到,就会进入消息转发流程。

[obj performSelector:@selector(test)];的实现原理

当你使用[obj performSelector:@selector(test)];这样的方式时,这个过程更加动态。performSelector:方法是在运行时通过传入的选择器(@selector(test))来查找并执行test方法。这种方式允许你在运行时决定调用哪个方法,而不需要在编译时静态地确定。这给予了程序更大的灵活性,但也意味着失去了编译时的类型检查。

区别总结

  • 编译时与运行时[obj test];在编译时就确定了obj必须能响应test方法,而[obj performSelector:@selector(test)];允许在运行时动态决定调用哪个方法。

  • 类型检查:直接消息发送有编译时的类型检查,而performSelector:没有,这可能导致运行时错误如果被调用的方法不存在。

  • 性能:直接消息发送通常性能更好,因为它避免了performSelector:的一些运行时查找和检查过程。

  • 灵活性performSelector:提供了更大的灵活性,允许动态地决定调用哪个方法。

尽管有这些区别,但在实际使用中,直接消息发送因其简洁性和性能优势而更为常见。performSelector:则在需要动态选择方法时非常有用,例如在处理未知类型的对象或者根据条件动态调用不同方法的场景中。

字典使用crash

点击展开内容

在Objective-C中,使用NSDictionaryNSMutableDictionary 时,如果不当操作可能会导致程序崩溃(crash)。以下是一些常见的导致崩溃的情况及其解决方法:

1.尝试插入nil值/使用nil

NSDictionaryNSMutableDictionary不允许使用nil作为值或键。尝试插入nil值会导致崩溃,使用nil作为键插入字典也会导致崩溃。

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:nil forKey:@"key"]; // 这会导致崩溃

解决方法**:使用`NSNull`对象代替`nil`。
[dict setObject:[NSNull null] forKey:@"key"];

解决方法**:确保键不为`nil`。
NSString *key = nil;
[dict setObject:@"value" forKey:key]; // 这会导致崩溃

2. 修改不可变字典

尝试修改NSDictionary实例会导致崩溃,因为NSDictionary是不可变的。

NSDictionary *immutableDict = @{@"key": @"value"};
[immutableDict setObject:@"newValue" forKey:@"key"]; // 这会导致崩溃

解决方法:使用NSMutableDictionary

3. 多线程同时修改NSMutableDictionary

在多线程环境下,如果有多个线程同时修改NSMutableDictionary,可能会导致数据竞争和崩溃。

解决方法:使用线程同步机制,如@synchronized块、锁(NSLock)、串行队列(dispatch_queue)等,来保证同时只有一个线程修改字典。

@synchronized(dict) {
    [dict setObject:@"value" forKey:@"key"];
}

4. 键或值不支持归档

当使用[NSDictionary writeToFile:atomically:]方法将字典写入文件时,如果字典中的键或值不支持NSCoding协议,会导致写入失败

解决方法:确保所有键和值都遵循NSCoding协议。

总结

在使用Objective-C中的字典时,避免上述常见的错误可以减少程序崩溃的风险。确保键和值不为nil,使用可变字典进行修改,注意线程安全,以及确保键和值支持必要的协议,都是保证字典使用安全的重要措施。

6.4 dSYM文件

dSYM 文件是在 Xcode 构建 iOS 或 macOS 应用时生成的一种特殊文件,全称是 "Debug Symbol" 文件。它包含了应用程序的调试符号信息,但不包含实际的可执行代码。这些调试符号信息包括函数、方法、变量等的名称和它们在源代码中的位置。dSYM 文件使得开发者能够在不暴露源代码的情况下进行调试,特别是在分析崩溃报告时非常有用。

为什么需要 dSYM 文件?

当应用崩溃时,操作系统会生成一个崩溃报告,其中包含了崩溃时的堆栈跟踪信息。但是,为了优化应用的性能和大小,编译器在构建应用的 Release 版本时会去除调试信息,这使得崩溃报告中的堆栈跟踪只包含内存地址,而不是人类可读的函数名和行号。dSYM 文件包含了这些缺失的调试信息,通过使用 dSYM 文件,开发者可以将崩溃报告中的内存地址符号化(Symbolicate),从而得到详细的、人类可读的堆栈跟踪信息。

如何使用 dSYM 文件?

在分析崩溃报告时,你需要确保拥有与崩溃时应用版本相匹配的 dSYM 文件。Xcode 和各种第三方崩溃分析服务(如 Firebase Crashlytics、Sentry、Bugsnag 等)都提供了工具和服务来自动符号化崩溃报告。

如果你需要手动符号化崩溃报告,可以使用 Xcode 自带的 symbolicatecrash 工具,它通常位于:


/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

使用 symbolicatecrash 工具时,你需要提供崩溃报告文件和相应的 dSYM 文件。

管理 dSYM 文件

由于 dSYM 文件对于崩溃分析非常重要,因此建议在每次发布应用时都保存对应版本的 dSYM 文件。一些云服务平台允许你上传 dSYM 文件,以便在收集到崩溃报告时自动进行符号化处理。

总结

dSYM 文件是调试符号文件,对于分析和解决应用崩溃问题至关重要。正确管理和使用 dSYM 文件可以帮助开发者更有效地定位和修复崩溃问题,提高应用的稳定性和用户体验。

6.5 动态库与静态库

use_frameworks!选项,后面可以接:linkage => :static等

image.jpeg

juejin.cn/post/733006…

6.6 iOS scheme跳转机制

www.jianshu.com/p/138b44833…

juejin.cn/post/735730…

6.7 编译原理

编译原理是计算机科学中的一个重要分支,它研究的是如何将高级语言编写的源代码转换成机器语言,使得计算机可以执行的过程。编译器(Compiler)是实现这一过程的关键工具。编译过程一般分为几个阶段进行,每个阶段负责处理编译过程中的特定任务。以下是编译过程中的主要阶段:

1. 词法分析(Lexical Analysis)

  • 目的:将源代码的字符序列转换成一系列的记号(Token),如关键字、标识符、常量等。

  • 工具:词法分析器(Lexer 或 Scanner)。

2. 语法分析(Syntax Analysis)

  • 目的:根据语言的语法规则,将记号序列组织成语法树(Syntax Tree),这个过程会检查源代码是否符合语法规则。

  • 工具:语法分析器(Parser)。

3. 语义分析(Semantic Analysis)

  • 目的:检查源程序是否有语义错误,如类型不匹配、变量未声明等,并收集类型信息用于后续的代码生成阶段。

  • 工具:语义分析器。

4. 中间代码生成(Intermediate Code Generation)

  • 目的:将语法树或抽象语法树(AST)转换成一种中间表示(Intermediate Representation, IR),这种表示既要接近高级语言,又要接近目标机器语言。

  • 工具:中间代码生成器。

5. 代码优化(Code Optimization)

  • 目的:对中间代码进行优化,提高代码的执行效率,减少资源消耗。优化可以在不改变程序语义的前提下进行。

  • 工具:代码优化器。

6. 目标代码生成(Code Generation)

  • 目的:将优化后的中间代码转换成特定机器的机器代码或汇编代码。

  • 工具:代码生成器。

7. 符号表管理(Symbol Table Management)

  • 目的:在整个编译过程中,编译器需要跟踪每个变量的类型、作用域等信息,这些信息存储在符号表中。

  • 工具:符号表管理器。

8. 错误处理(Error Handling)

  • 目的:在编译过程中发现错误时,需要报告错误的位置和性质,帮助程序员定位和修正错误。

  • 工具:错误处理器。

编译原理不仅涉及到计算机科学的多个领域,如自动机理论、语言理论、算法等,而且对于提高程序设计的质量、理解计算机的工作原理等方面都有重要意义。

进程

在操作系统中,僵尸进程(Zombie Process)是一种特殊的进程状态。了解僵尸进程及其相关概念对于系统管理和性能优化是很重要的。以下是一些与僵尸进程相关的关键概念:

僵尸进程(Zombie Process)

  • 定义:当一个子进程在Unix、Linux或类Unix系统中结束时,它的进程描述符(Process Descriptor)和最终的退出状态仍然保存在系统中,直到父进程通过调用wait()系统调用来检索子进程的状态信息。在这段时间里,子进程被称为僵尸进程。

  • 特点:僵尸进程不占用任何系统资源(如内存),除了进程表中的一个条目。但是,如果大量僵尸进程积累,它们可以占用系统的进程号资源,因为每个系统都有对进程号的限制。

  • 解决方法:通常,僵尸进程的存在是因为父进程没有正确地调用wait()来清理已终止的子进程。解决僵尸进程的方法是修改父进程的代码,确保它调用wait()来等待子进程结束,并获取其退出状态。

孤儿进程(Orphan Process)

  • 定义:当一个进程的父进程结束或崩溃,而子进程仍在运行时,这些子进程就成为孤儿进程。

  • 特点:在Unix和类Unix系统中,孤儿进程会被init进程(进程号为1的进程)收养。init进程会定期调用wait()来清理任何已经结束的孤儿进程,因此孤儿进程通常不会成为僵尸进程。

  • 影响:孤儿进程通常不会对系统性能产生负面影响,因为它们会被init进程管理。

守护进程(Daemon Process)

  • 定义:守护进程是在后台运行的进程,它独立于控制终端,并且周期性地执行某种任务或等待处理某些事件。

  • 特点:守护进程通常在系统启动时启动,并在系统运行期间一直运行。它们不与任何终端关联,因此不会意外接收到用户输入。

  • 用途:守护进程用于各种系统级任务,如日志文件管理、系统监控、定时任务执行等。

了解这些进程状态和类型有助于进行有效的系统管理和故障排除。在设计长时间运行的应用程序时,确保正确管理子进程的生命周期是非常重要的,以避免资源泄露和性能问题。