以下内容是本人在刷题过程中整理的一些题目, 答案或者来自原帖, 或者来自GPT,不一定保证正确,各位看官仔细斟酌
NSString 如何计算字符个数?
ChatGPT
NSString的length属性是基于UTF-16编码实现的。在UTF-16编码中,一个Unicode字符可能占用一个或两个16位的代码单元(即一个或两个UTF-16码元)。因此,NSString的length属> 性返回的是字符串中代码单元的数量,而不是字符的实际数量。对于大多数情况下的常规字符,一个Unicode字符对应一个UTF-16码元,因此length属性返回的值即为字符串中的字符数。但对于一些特殊字符,比如表情符号和一些辅助字符,一个Unicode字符可能会占用两个UTF-16码元,这时length属性返回的值可能会大于字符实际的个数。
因此,虽然length属性可以在大多数情况下准确地计算字符串中的字符个数,但在处理包含特殊字符的字符串时,需要注意到这种编码差异可能导致的计数不准确的情况。
block为什么要是用copy修饰?
ChatGPT
在iOS中,当你在一个Block内部访问了外部的对象(例如局部变量或者对象属性),这个Block会将这些外部对象进行捕获,以便在Block执行时可以访问它们。默认情况下,Block会在栈上创建,这意味着它们可以直接访问外部的变量。然而,如果这个Block在创建后,还会被传递到异步执行的任务中(比如在网络请求的回调中使用Block),那么当任务执行时,这个Block所捕获的外部对象可能已经超出了其作用域,导致出现野指针错误或者内存泄漏。因为栈上的Block会随着其作用域的结束而被销毁,而异步任务可能在Block销毁之后执行,这就会造成问题。
为了解决这个问题,可以使用copy关键字来将Block从栈上复制到堆上。通过使用copy修饰符,Block会被复制到堆上,从而保证在异步任务执行时,Block所捕获的外部对象依然存在。即使原始Block的栈上实例被销毁,复制后的Block在堆上仍然可以继续使用,并且能够正确访问外部的对象。
因此,在iOS开发中,当你需要在Block内部访问外部的对象,并且这个Block可能会被传递到异步任务中时,建议使用copy修饰符来确保代码的正确性和安全性。
分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
分类(Category)和扩展(Extension)是Objective-C和Swift中的两种不同的方式来扩展类的功能。它们有一些相似之处,但也有一些区别。
分类(Category):
在Objective-C中,分类是一种将类的实现分散到多个文件中的方式。通过分类,可以为已有的类添加新的方法,而无需修改原始类的实现。分类可以用来实现以下功能:添加新方法: 可以向已有的类中添加新的实例方法、类方法或协议。
分组代码: 可以将一个类的实现拆分到多个文件中,使代码更加组织有序。
分类的局限性包括:无法添加实例变量: 分类中无法添加新的实例变量。
方法覆盖: 如果在分类中定义了与原始类相同的方法,会覆盖原始类的实现。
分类的结构体里面一般包括以下成员:方法声明和实现: 在分类的实现文件中定义新增的方法。
头文件声明: 在分类的头文件中声明新增的方法,以便在其他地方使用。扩展(Extension):
在Objective-C中,扩展是一种特殊的匿名分类,它允许在类的实现文件中添加额外的方法、属性和成员变量,但是这些扩展只能在当前类的实现文件中使用,无法在其他文件中引用。扩展可以用来实现以下功能:添加私有方法和属性: 可以在类的实现文件中添加私有的方法和属性,使其只能在当前类的实现中访问。
遵循协议: 可以在扩展中实现某些协议,以隐藏实现细节并将其与类的接口分离。
扩展的结构体里面一般包括以下成员:方法声明和实现: 在扩展的实现文件中定义新增的方法。
私有属性声明和实现: 在扩展的实现文件中添加新增的私有属性和实现。
在Swift中,扩展的概念更加强大,它可以用来实现类似于Objective-C中分类和扩展的功能,但更加灵活和统一。在Swift中,扩展可以用来添加新的方法、计算属性、下标、协议实现等功能,而且可以在任何地方使用,不受限于类的实现文件。因此,Swift中的扩展更加推荐使用。
讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
atomic是Objective-C中用于声明属性的一个修饰符,用来指定该属性在多线程环境下的行为。当使用atomic修饰符时,编译器会自动生成线程安全的访问方法,以确保在多个线程同时访问属性时不会发生数据竞争问题。其实现机制通常是通过在访问方法中使用锁机制(如@synchronized)来确保属性的读取和写入操作的原子性。
然而,尽管atomic可以提供一定程度上的线程安全性,但它并不能保证绝对的线程安全。这是因为即使使用了锁机制,仍然存在一些情况会导致数据竞争和不一致的问题。
一个典型的场景是:假设有两个线程同时对一个对象的属性进行读取和写入操作,而这两个操作并不是原子性的。在atomic修饰下,编译器会为属性的访问方法加上锁,以保证同一时间只有一个线程能够执行属性的读取或写入操作。然而,如果其中一个线程正在执行写入操作时,另一个线程同时进行读取操作,由于读取操作并不受锁的保护,它可能会读取到属性的中间状态,导致数据不一致性。
因此,尽管atomic修饰符可以提供一定程度上的线程安全性,但在一些特定的并发场景下,仍然需要开发者自己实现额外的线程同步机制来确保数据的完整性和一致性。另外,值得注意的是,使用atomic修饰符会带来一定的性能开销,因为每次访问属性都需要进行锁的获取和释放操作,所以在某些情况下,使用atomic并不是最优的选择。
- 为什么读取操作不受到锁的保护?(上一题的引申)
atomic修饰符所提供的线程安全保证主要是针对属性的写入操作的。读取操作通常是不会改变属性的值的,因此不需要额外的保护。如果每次读取操作都需要获取锁,将会带来性能的损耗,并且会降低并发访问的效率。
被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
当一个对象被weak修饰时,它的引用计数不会增加,因此并不会影响对象的生命周期。当被weak修饰的对象所指向的对象被释放时,该weak指针会被自动置为nil,以防止出现野指针访问。
weak指针的实现通常依赖于Objective-C的Runtime机制中的side table。side table是一个哈希表,用于存储weak指针的引用信息。每当一个对象被设置为weak指针所引用的目标时,Runtime会将这个weak指针的地址作为key,被引用对象的地址作为value,存储到side table中。当被引用的对象被释放时,Runtime会遍历side table,将所有指向该对象的weak指针的值置为nil。
下面是side table的简化结构示意图:
+---------------------------+
| Bucket 0 |
+---------------------------+
| Weak Pointer Address 1 |
| Weak Pointer Address 2 |
| ... |
+---------------------------+
| Bucket 1 |
+---------------------------+
| Weak Pointer Address 3 |
| Weak Pointer Address 4 |
| ... |
+---------------------------+
| ... |
+---------------------------+
side table是一个哈希表,它由多个桶(bucket)组成,每个桶存储一组weak指针的引用信息。每个桶中存储了多个weak指针的地址,这些weak指针都指向同一个对象。当对象被释放时,Runtime会遍历side table中的每个桶,将所有指向该对象的weak指针的值置为nil。
这样一来,被weak修饰的对象的引用计数不会增加,但在其所指向的对象被释放时,所有指向该对象的weak指针都会被自动置为nil,以避免野指针访问。
关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
关联对象(Associated Objects)是Objective-C中的一种机制,允许开发者向现有的类动态添加额外的实例变量。关联对象的主要应用包括:
为现有的类添加属性:通过关联对象,可以在不修改原始类的情况下,为其添加新的属性。
在分类中添加属性:由于分类无法直接添加实例变量,可以使用关联对象来实现在分类中添加属性的功能。
实现代理模式:可以将代理对象关联到另一个对象上,从而实现代理模式。
系统是如何管理关联对象的呢?在Objective-C的Runtime中,每个类都有一个关联对象的哈希表(Associative References Table)。当通过objc_setAssociatedObject函数将一个对象关联到另一个对象时,Runtime会将这个关联对象的地址作为key,被关联对象的地址作为value,存储到关联对象的哈希表中。当通过objc_getAssociatedObject函数获取关联对象时,Runtime会根据key从哈希表中找到对应的关联对象。关联对象的释放通常是自动管理的。当一个对象被释放时,Runtime会自动遍历其关联对象的哈希表,将所有关联对象的指针置为nil,以避免出现野指针访问。因此,开发者不需要手动将关联对象的指针置空。需要注意的是,由于ARC机制的存在,当对象的引用计数为0时,对象会被释放,而关联对象的指针也会被置为nil,这样可以确保在释放对象后不会出现野指针访问的情况。
KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
KVO(Key-Value Observing)是Objective-C中的一种观察者模式,用于监听对象属性值的变化。它的底层实现依赖于Objective-C的Runtime机制。
底层实现原理:
当你添加一个观察者(Observer)时,Runtime会动态创建一个继承自被观察对象类的中间类。
这个中间类会重写被观察属性的setter方法,以在属性值发生变化时发送通知。
在setter方法中,会调用willChangeValueForKey:和didChangeValueForKey:方法,通知观察者属性值将要改变和已经改变。
- 取消系统默认的KVO
// 关闭自动通知机制
[MyClass setAutomaticallyNotifiesObserversForKey:@"propertyName"];
// 修改属性值,此时不会触发 KVO
object.propertyName = newValue;
// 打开自动通知机制
[MyClass setAutomaticallyNotifiesObserversForKey:@"propertyName"];
- 手动触发
// 移除观察者
[object removeObserver:self forKeyPath:@"propertyName"];
// 重新实现属性的 setter 方法
- (void)setPropertyName:(NSString *)propertyName {
// 自定义属性设置逻辑
_propertyName = propertyName;
// 手动触发 KVO 通知,如果需要的话
[self willChangeValueForKey:@"propertyName"];
[self didChangeValueForKey:@"propertyName"];
}
讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
在 Objective-C 中,对象、类对象、元类以及根元类(Root Metaclass)是组成类和对象系统的核心组件。它们之间的关系是相互关联的,而这种关联是通过指针来实现的。
对象(Object):
对象代表了内存中实际的实例,存储了实例变量的值以及指向其类对象的指针。类对象(Class Object):
类对象代表了类本身,存储了类的实例方法列表、属性列表等信息,并且包含指向元类的指针。元类(Metaclass):
元类是类对象的类,它存储了类方法列表、协议列表等信息,并且包含指向根元类的指针。根元类(Root Metaclass):
根元类是所有元类的父类,它存储了所有类共享的类方法列表等信息。
- 这些结构体之间的关系可以通过下图来说明:
+--------------+ isa +--------------+ isa +--------------+ isa +--------------+
| Object | --------> | Class Object| --------> | Metaclass | --------> | Root Metaclass |
+--------------+ +--------------+ +--------------+ +--------------+
对象的方法并不是直接存储在对象结构体中的,而是存储在类对象的方法列表中。这是因为对象的方法通常是类共享的,所有同一个类的所有实例都共享相同的方法列表。因此,将方法存储在类对象中可以节省内存,同时也方便了方法的查找和共享。
另外,类对象中还包含了指向其元类的指针。通过这个指针,类对象可以访问元类中的类方法列表,实现类方法的调用。元类中也包含了指向根元类的指针,形成了一个链式结构,用于查找和调用类方法。
总的来说,对象、类对象、元类以及根元类之间通过指针相互关联,共同构成了 Objective-C 中的类和对象系统,实现了方法的共享和继承。
class_ro_t 和 class_rw_t 的区别?
class_ro_t 和 class_rw_t 是 Objective-C 中用于描述类的结构体,它们在运行时扮演了不同的角色,具有不同的属性和功能。
class_ro_t(Class RO Data,只读数据)
class_ro_t 结构体存储了类的只读数据,包括类的实例变量、属性、方法列表等静态信息。这些信息在类加载时就已经确定,并且在整个程序运行过程中保持不变。
具体属性包括:
const char *name:类名。
const struct class *superclass:指向父类的指针。
uint32_t method_count:方法列表中方法的数量。
const struct method_list_t *method_list:指向方法列表的指针。
uint32_t ivar_count:实例变量列表中变量的数量。
const struct ivar_list_t *ivar_list:指向实例变量列表的指针。
...class_rw_t(Class RW Data,读写数据)
class_rw_t 结构体存储了类的读写数据,包括类的可变信息,例如在运行时动态添加的方法、属性等。
具体属性包括:
uint32_t flags:标志位,描述类的一些特性,例如是否支持 ARC、是否是元类等。
uint32_t version:类的版本号。
const struct method_list_t *methods:指向方法列表的指针。
const struct property_list_t *properties:指向属性列表的指针。
const struct protocol_list_t *protocols:指向协议列表的指针。
...区别:
数据内容: class_ro_t 存储了类的只读数据,包括静态信息;而 class_rw_t 存储了类的读写数据,包括动态信息。
可变性: class_ro_t 中的数据在类加载后不可更改,是只读的;而 class_rw_t 中的数据可以在运行时动态地添加或修改,是可读写的。
使用场景: class_ro_t 通常用于描述类的静态信息,如在编译时确定的方法列表、属性列表等;而 class_rw_t 用于描述类的动态信息,如在运行时动态添加的方法、属性等。总的来说,class_ro_t 和 class_rw_t 分别描述了类的静态信息和动态信息,它们在运行时起到了不同的作用,共同构成了 Objective-C 类的完整描述。
Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
Autorelease Pool(自动释放池)是 Objective-C 中用来管理自动释放对象的机制。在运行过程中,当你创建一个自动释放对象时,这个对象会被添加到当前的自动释放池中,当自动释放池被销毁时,池中的所有对象都会被释放。
AutoreleasePoolPage 是用来实现自动释放池的数据结构。它是一个链表节点,用于管理自动释放池中的对象。每个 AutoreleasePoolPage 包含一个数组,用于存储自动释放对象的指针。当数组满了之后,会通过指针的形式连接到下一个 AutoreleasePoolPage,形成链表结构。
AutoreleasePoolPage 结构体通常包括以下成员:
id *next:指向下一个 AutoreleasePoolPage 的指针,形成链表结构。
uint32_t const capacity:当前页的容量,即可以存储的自动释放对象的数量上限。
uint32_t count:当前页已经存储的自动释放对象的数量。
struct _opaque_data:用于存储自动释放对象的数组。
这样,当你创建一个自动释放池时,实际上是在内存中创建了一个 AutoreleasePoolPage 结构体,并将其作为栈顶的自动释放池。每次向自动释放池中添加对象时,都是将对象的指针添加到当前页的数组中,当当前页的数组满了之后,会创建一个新的 AutoreleasePoolPage 并将其链接到链表中。当自动释放池被销毁时,会依次释放链表中的每个 AutoreleasePoolPage,从而释放池中的所有对象。总的来说,AutoreleasePoolPage 是用来实现自动释放池的数据结构,它通过链表的形式管理自动释放对象,并在适当的时候进行内存的回收和释放。
iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?
在 iOS 中,内省(introspection)是指在运行时获取对象、类、协议等信息的能力。以下是 iOS 中常用的内省方法:
[NSObject isKindOfClass:]: 用于判断一个对象是否是指定类或其子类的实例。
[NSObject isMemberOfClass:]: 用于判断一个对象是否是指定类的实例(不包括其子类)。
[NSObject respondsToSelector:]: 用于判断一个对象是否响应指定的方法。
[NSObject conformsToProtocol:]: 用于判断一个类是否遵循指定的协议。
这些方法可以帮助我们在运行时动态地获取对象的信息,从而进行灵活的编程。关于 class 方法和 objc_getClass 函数的区别如下:
class 方法: 是 NSObject 类的一个实例方法,用于获取对象的类对象。例如,[obj class] 会返回 obj 的类对象。
objc_getClass 函数: 是 Objective-C Runtime 中的一个函数,用于获取指定类的类对象。它接受一个类名的 C 字符串作为参数,返回对应的类对象。例如,objc_getClass("ClassName") 会返回名为 "ClassName" 的类的类对象。
主要的区别在于使用方式和返回值的类型。class 方法是一个对象的实例方法,用于获取该对象的类对象,而 objc_getClass 函数是一个 C 函数,用于直接获取指定类的类对象。
一个int变量被__block修饰与否的区别?
在 Objective-C 中,__block 修饰符通常用于在 Block 内部修改局部变量的值,并且使得变量的值在 Block 内外保持一致。对于基本数据类型如 int,__block 修饰符的使用与否会有以下区别:
- 被 __block 修饰的变量:
- 可以在 Block 内部被修改,并且修改后的值会影响到外部变量的值。
在 Block 内部和外部是同一个变量,即共享同一个内存地址。- 在 Block 内部和外部是同一个变量,即共享同一个内存地址。
__block int x = 10;
void (^block)(void) = ^{
x = 20; // 修改 x 的值
};
block();
NSLog(@"x = %d", x); // 输出 x = 202.未被 __block 修饰的变量:
- 不能在 Block 内部被修改,否则会编译错误。
- 在 Block 内部和外部是不同的变量,即 Block 内部创建了一个变量的拷贝。
int x = 10;
void (^block)(void) = ^{
// x = 20; // 编译错误,不能修改未被 __block 修饰的变量
};
block();
NSLog(@"x = %d", x); // 输出 x = 10
为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?
在 Objective-C 中,经常会在 Block 内部引用外部的对象,为了避免循环引用(retain cycle),通常会使用 __weak 修饰符来修饰外部的强引用对象,以避免强引用循环导致内存泄漏。
使用 __weak 修饰符可以解决循环引用的问题,但同时也会带来一个新的问题,就是在 Block 内部使用被 __weak 修饰的对象时,由于对象是弱引用,可能会在 Block 执行期间被释放,导致访问悬空对象而产生异常。
为了解决这个问题,通常会在 Block 内部使用 __strong 修饰符来修饰被 __weak 修饰的对象。这样做的原因是,__strong 修饰符可以使得在 Block 执行期间弱引用对象保持有效,从而避免访问悬空对象的问题。
总的来说,在 Block 内部使用 __weak 修饰外部对象是为了避免循环引用,而在使用外部对象时需要使用 __strong 修饰符来确保对象在 Block 执行期间保持有效。这种组合的使用方式可以有效地解决循环引用和访问悬空对象的问题。
RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
RunLoop(运行循环)是 iOS 和 macOS 中的一种事件处理机制,它主要用于处理各种输入源(包括事件、定时器、消息等)并管理线程的消息循环。RunLoop 的作用包括但不限于以下几点:
处理事件: RunLoop 可以处理用户输入、定时器、网络请求等各种事件,保证这些事件可以及时得到处理。
节省 CPU 资源: RunLoop 可以使线程进入休眠状态,直到有事件需要处理时才唤醒线程,从而节省 CPU 资源,提高性能。
管理定时器: RunLoop 可以管理定时器,使得定时器可以按照预定的时间间隔触发。
线程间通信: RunLoop 可以在多线程编程中进行线程间的通信,使得不同线程之间可以传递消息、共享数据。RunLoop 的内部工作机制如下:
RunLoop 启动: 当一个线程需要执行事件循环时,会创建一个对应的 RunLoop 对象,并调用 run 或 runUntilDate: 方法启动 RunLoop。
进入事件循环: 一旦 RunLoop 启动,它会进入一个事件循环(Event Loop)中,不断地处理各种输入源和定时器事件,直到被手动停止或者没有事件需要处理时才退出循环。
处理事件: 在事件循环中,RunLoop 会不断地从输入源中获取事件,并调用相应的处理方法来处理这些事件,比如响应用户输入、执行定时器任务等。
休眠和唤醒: 如果没有事件需要处理,RunLoop 会使线程进入休眠状态,直到有新的事件到来时才唤醒线程继续处理事件。
定时器管理: RunLoop 会管理定时器,根据定时器设定的时间间隔触发定时器任务,并将任务添加到事件队列中等待处理。RunLoop 在线程和内存管理中的作用体现在以下几个方面:
线程管理: RunLoop 可以管理线程的事件循环,使得线程可以在需要时进行事件处理,而在空闲时保持休眠状态,从而节省 CPU 资源,提高性能。
内存管理: RunLoop 可以处理各种输入源的事件,包括定时器、网络请求等,可以在事件到来时触发相应的处理方法,从而及时释放不再需要的内存资源,避免内存泄漏和内存过大的问题。
什么是离屏渲染, 什么场景会触发离屏渲染?
离屏渲染(Offscreen Rendering)是指将图像渲染到屏幕之外的缓冲区中,然后再将渲染好的图像复制到屏幕上显示的过程。离屏渲染通常会带来额外的性能消耗,因此在开发中需要尽量避免过度使用离屏渲染。
以下是一些可能触发离屏渲染的场景:
- 图层属性的混合(Blending): 当一个图层的 opacity、corner radius、shadow、mask、group opacity 等属性发生改变时,如果该图层需要与其下层图层进行混合,就会触发离屏渲染。
- 图层的遮罩(Masking): 当一个图层需要应用遮罩(例如使用 CAShapeLayer 或 CALayer 的 mask 属性)时,就会触发离屏渲染。
- 图层的剪裁(Clipping): 当一个图层需要进行剪裁(例如使用 CALayer 的 cornerRadius 属性进行圆角裁剪),就会触发离屏渲染。
- OpenGL 或 Core Graphics 绘制: 当使用 OpenGL 或 Core Graphics 直接绘制图形时,也可能会触发离屏渲染。
- 模糊效果(Blur): 当一个图层需要应用模糊效果(例如使用 UIVisualEffectView 或 UIBlurEffect)时,就会触发离屏渲染。
- 渲染到纹理(Render to Texture): 当一个图层需要将内容渲染到纹理中,然后再将纹理渲染到屏幕上时,就会触发离屏渲染。
需要注意的是,虽然离屏渲染可以实现一些复杂的效果,但过度使用离屏渲染会带来性能上的损耗,可能导致应用的卡顿和耗电量增加。因此,在开发中应尽量避免过度使用会触发离屏渲染的场景,以提高应用的性能和用户体验。