分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
分类(Category)和扩展(Extension)是Objective-C和Swift编程语言中的两种机制,用于对现有类进行功能的扩展和组织。
区别:
- 分类是Objective-C中的机制,而扩展是Swift中的机制。
- 分类可以为现有类添加新的方法,但不能添加新的实例变量,而扩展可以添加新的方法、计算属性和实例变量。
- 分类可以在运行时动态地应用于类,而扩展在编译时就被应用于类。
用途:
- 分类和扩展都可以用来对现有类进行功能的扩展,使得我们可以在不修改原始类的情况下添加新的方法或属性。
- 分类和扩展可以用来组织代码,将相关的方法和属性分组放置在不同的分类或扩展中,提高代码的可读性和可维护性。
分类的局限性:
- 分类不能添加新的实例变量,只能添加方法。
- 分类中的方法实现会覆盖原始类中同名方法的实现,可能导致意外的行为。
- 分类不能重写原始类的方法,只能添加新的方法。
分类的结构体成员: 分类的结构体成员包括:
isa:指向类对象的指针。superclass:指向父类的指针。name:分类的名称。instanceMethods:实例方法列表。classMethods:类方法列表。protocols:遵循的协议列表。properties:属性列表。
这些成员用于描述分类的结构和定义的方法、属性以及遵循的协议。
讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
atomic的实现机制是通过使用自旋锁(spinlock)来保证setter和getter方法的线程安全。自旋锁是一种基于忙等待的锁机制,它会不断地检查锁的状态,直到获取到锁为止。在使用atomic修饰属性时,编译器会自动生成原子读写方法,并在内部使用自旋锁来保护属性的读写操作[1]。
然而,尽管atomic修饰的属性在单独的原子操作中是线程安全的,但在组合操作中无法保证绝对的线程安全。这是因为多线程环境下,即使使用了自旋锁,仍然存在竞态条件(race condition)的问题。
竞态条件是指多个线程对共享资源进行读写操作时,由于执行时序的不确定性,可能导致结果的不确定性或错误。举个例子来说,假设有两个线程同时对一个属性进行自增操作,每个线程执行10000次自增。由于自增操作不是原子操作,可能会出现以下情况:
- 线程A读取属性的值为0,然后自增为1;
- 线程B读取属性的值为0,然后自增为1;
- 线程A将自增后的值1写回属性;
- 线程B将自增后的值1写回属性;
最终的结果是属性的值只增加了1,而不是预期的2。这是因为两个线程在读取和写回属性值的过程中,没有进行同步,导致了数据的不一致性。
因此,为了解决这个问题,需要增加操作的颗粒度,将读取和写入操作合并为一个原子操作。这可以通过使用更高级别的锁机制,如互斥锁(mutex)或信号量(semaphore),来保证操作的原子性和线程安全。
综上所述,尽管atomic修饰的属性可以提供一定程度的线程安全性,但在多线程环境下仍然需要谨慎处理共享资源的读写操作,以避免竞态条件和数据不一致的问题。
Learn more:
- atomic的实现机制;为什么不能保证绝对的线程安全 - 简书
- iOS面试题(每日一更)2020.6.2_讲一下 atomic 的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)-CSDN博客
- atomic为何在多线程不是绝对的安全 - 掘金
被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
被weak修饰的对象在被释放时会发生以下情况:
- 被置为nil:当一个对象被释放时,所有指向该对象的weak指针会自动被置为nil。这是由Runtime自动处理的,无需手动操作[1]。
实现方式: 被weak修饰的对象在被释放时,是通过Runtime维护的weak表来实现的。weak表是一个哈希表,用于存储指向某个对象的所有weak指针。具体实现如下:
- 初始化时:Runtime会调用objc_initWeak函数,初始化一个新的weak指针并指向对象的地址[2]。
- 添加引用时:当有新的weak指针指向对象时,Runtime会调用objc_storeWeak()函数,更新指针的指向,并创建对应的弱引用表[2]。
- 释放时:当对象被释放时,Runtime会调用clearDeallocating函数。该函数会根据对象地址获取所有指向该对象的weak指针地址的数组,然后遍历这个数组将其中的数据设为nil,并从weak表中删除该entry,最后清理对象的记录[2]。
sideTable是Runtime中用于存储引用计数和weak引用的数据结构。它的结构如下:
struct SideTable {
spinlock_t slock; // 保证原子操作的自旋锁
RefcountMap refcnts; // 引用计数的哈希表
weak_table_t weak_table; // weak引用的全局哈希表
}
struct weak_table_t {
weak_entry_t *weak_entries; // 保存了所有指向指定对象的weak指针
size_t num_entries; // 存储空间
uintptr_t mask; // 参与判断引用计数的辅助量
uintptr_t max_hash_displacement; // hash key最大偏移值
};
Learn more:
- iOS -- 问题杂记 - 稀土掘金
- 被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么? - 简书
- 被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么? - 简书
关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
关联对象(Associated Object)是Objective-C运行时提供的一种机制,用于在运行时为已存在的类添加属性。关联对象的应用非常广泛,可以用于以下场景:
-
为分类(Category)添加属性:分类无法直接添加实例变量和存取方法,但通过关联对象可以为分类添加属性,实现类似于属性的功能。
-
在不改变源码的情况下增加实例变量:关联对象可以在运行时为已存在的类添加实例变量,而不需要修改类的源码。
-
与其他对象建立关联关系:关联对象可以用于建立对象之间的关联关系,例如在一个对象中关联另一个对象,以便在需要时可以方便地获取到关联的对象。
系统是如何管理关联对象的呢?Objective-C运行时使用一个全局的AssociationsManager来管理所有的关联对象[1]。AssociationsManager内部维护了一个AssociationsHashMap,用于存储所有的关联对象。这个AssociationsHashMap的key是对象的指针地址,value是另一个AssociationsHashMap,用于保存关联对象的键值对。
当对象被释放时,Objective-C运行时会自动处理关联对象的释放。在对象的dealloc方法中,会调用object_dispose函数来销毁对象。在object_dispose函数中,会依次执行以下操作[1]:
- 检查对象是否有关联对象,如果有,则调用_object_remove_assocations函数来移除关联对象。
- 执行C++析构函数,如果对象是一个C++对象。
- 释放对象的内存。
因此,不需要手动将所有的关联对象的指针置空,Objective-C运行时会自动处理关联对象的释放。
Learn more:
- 关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么? - 简书
- 关联对象 AssociatedObject 完全解析 - 面向信仰编程
- AssociatedObject关联对象原理实现 | Vanch's Blog
KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
KVO(Key-Value Observing)是一种用于监听对象属性值改变的机制。在iOS中,KVO的底层实现涉及到Runtime和动态子类的创建。
KVO的底层实现原理:
- 当一个对象使用KVO监听时,iOS系统会动态创建一个该对象的子类,并将该对象的isa指针指向这个新创建的子类。这个子类是通过Runtime动态生成的,命名规则为NSKVONotifyin_原类名。
- 在这个新创建的子类中,会重写被监听属性的setter方法。在重写的setter方法中,会调用父类的setter方法,然后再发送通知给监听者。
- 在调用父类的setter方法之前,会调用willChangeValueForKey方法,用于通知监听者属性值即将发生改变。
- 在调用父类的setter方法之后,会调用didChangeValueForKey方法,用于通知监听者属性值已经发生改变。
取消系统默认的KVO并手动触发KVO:
- 取消系统默认的KVO可以使用removeObserver:forKeyPath:方法,将监听者从被监听对象中移除。
- 手动触发KVO可以使用willChangeValueForKey:和didChangeValueForKey:方法。在改变属性值之前调用willChangeValueForKey:方法,然后在改变属性值之后调用didChangeValueForKey:方法。这样可以手动触发KVO通知,将改变的值传递给监听者。
给KVO的触发设定条件:
- 如果想要在改变的值符合某个条件时再触发KVO,可以在重写的setter方法中添加条件判断语句。只有当属性值满足条件时,才调用willChangeValueForKey:和didChangeValueForKey:方法,触发KVO通知。
Learn more:
Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
Autoreleasepool使用的数据结构是一个双向链表,其中每个节点都是一个AutoreleasePoolPage结构体[1]。AutoreleasePoolPage是一个C++实现的类,它是AutoreleasePool的核心数据结构[3]。
AutoreleasePoolPage结构体的主要作用是保存添加进AutoreleasePool的对象的内存地址[1]。每个AutoreleasePoolPage对象会开辟4096字节的内存空间,除了保存实例变量所占空间外,剩余的空间用来存储autorelease对象的地址[3]。AutoreleasePoolPage结构体中有一个id *next指针,它作为游标指向栈顶最新添加的autorelease对象的下一个位置[2]。
当一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,并将其与之前的AutoreleasePoolPage对象通过双向链表连接起来[3]。新的AutoreleasePoolPage对象的next指针被初始化在栈底的位置,然后继续向栈顶添加新的autorelease对象[2]。
AutoreleasePoolPage结构体的嵌套形式实现了AutoreleasePool的嵌套功能。每次进行objc_autoreleasePoolPush调用时,会向当前的AutoreleasePoolPage中添加一个哨兵对象,用于标记AutoreleasePool的边界[3]。当执行objc_autoreleasePoolPop操作时,会根据传入的哨兵对象地址找到哨兵对象所在的AutoreleasePoolPage,并在当前的AutoreleasePoolPage中将晚于哨兵对象插入的所有autorelease对象都发送一次release消息,然后将next指针回移至正确的位置[2]。
Learn more:
- iOS AutoreleasePool 原理原创 - CSDN博客
- iOS面试题:Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么? - 简书
- 从数据结构理解Autoreleasepool 原理 - 掘金
讲一下对象,类对象,元类,根元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
对象、类对象、元类、根元类的结构体组成及关联如下:
-
对象(Object)的结构体组成:
- isa指针:指向对象所属的类对象。
- 其他实例变量:存储对象的属性和状态信息。
-
类对象(Class Object)的结构体组成:
- isa指针:指向元类对象。
- superclass指针:指向父类对象。
- ivars:实例变量列表,保存类的所有实例变量。
- methodLists:方法列表,保存实例方法和类方法。
- protocols:协议列表,保存类遵循的协议。
- cache:方法缓存,用于提高方法查找的效率。
-
元类(Meta Class)的结构体组成:
- isa指针:指向根元类对象。
- superclass指针:指向父类的元类对象。
- methodLists:方法列表,保存类方法。
- cache:方法缓存,用于提高方法查找的效率。
-
根元类(Root Meta Class)的结构体组成:
- isa指针:指向自身。
- superclass指针:指向根类对象。
- methodLists:方法列表,保存类方法。
- cache:方法缓存,用于提高方法查找的效率。
关联关系:
- 对象和类对象之间的关联:对象的isa指针指向类对象,通过类对象可以访问到对象所属的类的属性和方法。
- 类对象和元类之间的关联:类对象的isa指针指向元类,通过元类可以访问到类对象的类方法。
- 元类和根元类之间的关联:元类的isa指针指向根元类,通过根元类可以访问到元类的类方法。
为什么对象方法没有保存在对象结构体里,而是保存在类对象的结构体里?
- 对象方法是属于类的,而不是属于对象的。类对象保存了类的方法列表,包括实例方法和类方法,而对象只需要保存自己的属性和状态信息即可。通过类对象,对象可以访问到类的方法。这样设计的好处是可以节省内存空间,因为多个对象可以共享同一个类对象的方法列表。
Learn more:
class_ro_t 和 class_rw_t 的区别?
class_ro_t 和 class_rw_t 是在 iOS 开发中用于表示类的两个结构体。它们的区别如下:
class_ro_t:
- class_ro_t 存储了当前类在编译期间就已经确定的属性、方法以及遵循的协议[3]。
- class_ro_t 是一个只读结构体,无法进行修改[3]。
- class_ro_t 结构体中的成员变量包括 flags、instanceStart、instanceSize、ivarLayout、name、baseMethodList、baseProtocols、ivars、weakIvarLayout 和 baseProperties[3]。
class_rw_t:
- class_rw_t 存储了类的属性、方法以及遵循的协议等信息[3]。
- class_rw_t 是在运行时生成的,它包含了 class_ro_t,并且更新了 data 部分,将其替换为 class_rw_t 结构体的地址[2]。
- class_rw_t 结构体中的成员变量包括 flags、version、ro、methods、properties、protocols、firstSubclass 和 nextSiblingClass[2]。
区别:
- class_ro_t 存放的是编译期间就确定的内容,而 class_rw_t 是在运行时才确定的[1]。
- class_rw_t 会先将 class_ro_t 的内容拷贝过去,然后再将当前类的分类的属性、方法等拷贝到其中[1]。
- class_rw_t 可以看作是 class_ro_t 的超集,实际访问类的方法、属性等也都是访问的 class_rw_t 中的内容[1]。
Learn more:
- iOS底层- 类的结构(下) - 稀土掘金
- class_ro_t 和 class_rw_t 的区别? - 简书
- iOS开发-class_ro_t和class_rw_t的区别_class_rw_t 和 class_ro_t 的区别-CSDN博客
iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?
在iOS中,内省(introspection)是指通过运行时机制来获取类、对象和方法等信息的能力。以下是几个常用的内省方法:
-
class方法:class方法是一个Objective-C中的类方法,用于获取一个对象所属的类。- 通过调用对象的
class方法,可以获取该对象的类对象。 - 例如:
[object class]会返回对象object所属的类对象。
-
objc_getClass函数:objc_getClass函数是一个C函数,用于获取指定类名对应的类对象。- 通过传入类名的字符串,可以获取对应的类对象。
- 例如:
Class cls = objc_getClass("ClassName")会返回类名为"ClassName"的类对象。
区别:
class方法是Objective-C中的一个类方法,用于获取对象所属的类对象。objc_getClass函数是一个C函数,用于根据类名获取对应的类对象。class方法是通过对象调用的,而objc_getClass函数是直接调用的。class方法可以获取对象的类对象,而objc_getClass函数可以根据类名获取对应的类对象。
需要注意的是,以上方法都是在运行时获取信息的,可以用于动态地获取类、对象和方法等信息,而不是在编译时就确定的静态信息。
一个int变量被__block修饰与否的区别?
一个int变量被__block修饰与否的区别在于其在Block内部的可变性。
当一个int变量被__block修饰时,它可以在Block内部被修改。这是因为__block修饰符会将变量从栈上移动到堆上,使得Block内部可以捕获并修改该变量的值。这种情况下,Block内部对该变量的修改会影响到Block外部的变量。
而当一个int变量没有被__block修饰时,它在Block内部是只读的,无法被修改。这是因为没有使用__block修饰符,变量仍然保留在栈上,Block只能访问变量的值,但无法修改它。
下面是一个示例来说明这两种情况的区别:
int main() {
__block int a = 10;
void (^block)(void) = ^{
a = 20; // 可以修改a的值
NSLog(@"Inside Block: %d", a);
};
block();
NSLog(@"Outside Block: %d", a);
return 0;
}
输出结果为:
Inside Block: 20
Outside Block: 20
在上述示例中,由于a被__block修饰,Block内部可以修改a的值,并且这个修改也影响到了Block外部的变量。因此,无论是在Block内部还是外部,a的值都被修改为20。
如果将__block修饰符移除,即将__block int a = 10;改为int a = 10;,那么在Block内部尝试修改a的值将会导致编译错误。
为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?
在使用__weak修饰符来避免循环引用的情况下,同时在Block内部使用__strong修饰符是为了确保在Block执行期间,被__weak修饰的对象不会被提前释放。
当一个对象被__weak修饰时,它会被弱引用,意味着当没有强引用指向该对象时,它会被自动设置为nil。这是为了避免循环引用,特别是在Block中持有对象的情况下。
然而,当我们在Block内部使用__weak修饰的对象时,由于该对象可能已经被释放,所以它的值可能为nil。为了在Block内部安全地使用该对象,我们需要在Block内部使用__strong修饰符来创建一个强引用,以确保在Block执行期间对象不会被提前释放。
下面是一个示例来说明这种情况:
__weak NSObject *weakObject = someObject;
void (^block)(void) = ^{
__strong NSObject *strongObject = weakObject;
if (strongObject) {
// 在Block内部使用strongObject,确保对象不会被提前释放
NSLog(@"%@", strongObject);
}
};
// 在Block执行之前,someObject被释放
block();
在上述示例中,我们使用__weak修饰符创建了一个对someObject的弱引用weakObject。然后,在Block内部使用__strong修饰符创建了一个强引用strongObject,以确保在Block执行期间对象不会被提前释放。通过检查strongObject是否为nil,我们可以安全地在Block内部使用该对象。
这种使用__weak和__strong修饰符的组合常用于解决Block中的循环引用问题,并确保在Block执行期间对象不会被提前释放。
RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
RunLoop是iOS和macOS中的一个重要概念,它是一个事件循环机制,用于处理事件、定时器和输入源等。它的主要作用是保持线程的活跃状态,同时处理来自系统和应用程序的事件。
RunLoop的内部工作机制如下:
- 当线程启动时,会自动创建一个RunLoop对象,并在RunLoop中运行。
- RunLoop会不断地从事件队列中获取事件,并将其分发给相应的处理器进行处理。
- RunLoop会根据事件的类型和优先级来决定事件的处理顺序。
- 如果没有事件需要处理,RunLoop会进入休眠状态,直到有新的事件到达或定时器触发。
- 定时器可以用来触发周期性的事件,例如定时执行某个任务。
- RunLoop还可以处理输入源,例如处理用户输入事件、网络请求等。
- 当线程即将退出时,RunLoop会被销毁。
RunLoop与线程和内存管理的关系如下:
- 线程:每个线程都有一个对应的RunLoop,它们是一一对应的关系。RunLoop的存在可以使线程保持活跃状态,避免线程的空闲浪费。
- 内存管理:RunLoop可以影响对象的内存管理。在RunLoop中,当没有事件需要处理时,线程会进入休眠状态,此时可以释放一些不必要的资源,以节省内存。而当有事件到达时,RunLoop会重新唤醒线程,需要使用的资源也会被重新加载。
总结来说,RunLoop的作用是保持线程的活跃状态,并处理来自系统和应用程序的事件。它的内部工作机制包括事件的分发和处理、定时器的触发、休眠和唤醒等。RunLoop与线程和内存管理密切相关,可以使线程保持活跃,同时也可以影响对象的内存管理。
Learn more:
- Run Loops
- Objective C++, how to use runloop in background thread? - Stack Overflow
- RunLoop Internals by Example. In my previous article I spoke about… | by Bharath Nadampalli | Medium
当涉及到需要处理事件、定时器和输入源的情况时,RunLoop是一个非常有用的工具。以下是一个实际应用场景的示例:
场景:开发一个聊天应用程序,需要使用RunLoop处理来自服务器的消息并保持用户界面更新。
解决方案:
- 创建一个自定义的ChatServer类,用于处理与服务器的通信。
- 配置ChatServer实例以监听传入的消息。
- 创建一个定时器,每隔一段时间向服务器发送一个“ping”消息以保持连接活跃。
- 将ChatServer实例和定时器添加到RunLoop中,以便它们可以被处理。
- 添加一个观察者到RunLoop中,以检查是否有新的消息到达,并通知主线程更新用户界面。
- 启动RunLoop以开始处理事件。
代码示例(Objective-C):
// 创建ChatServer实例并配置
ChatServer *chatServer = [[ChatServer alloc] init];
[chatServer listenForMessages];
// 创建定时器发送ping消息
NSTimer *pingTimer = [NSTimer scheduledTimerWithTimeInterval:30.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[chatServer sendPing];
}];
// 获取当前RunLoop并添加ChatServer和定时器
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:chatServer forMode:NSDefaultRunLoopMode];
[runLoop addTimer:pingTimer forMode:NSDefaultRunLoopMode];
// 添加观察者以检查新消息并更新UI
RunLoopObserver *observer = [[RunLoopObserver alloc] initWithEvents:@[@(kCFRunLoopBeforeWaiting), @(kCFRunLoopExit)] handler:^(RunLoopObserver * _Nonnull observer, RunLoopActivity activity) {
if ([chatServer hasIncomingMessages]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUI];
});
}
}];
[runLoop addObserver:observer forMode:NSDefaultRunLoopMode];
// 启动RunLoop
[runLoop run];
在这个示例中,我们使用RunLoop来处理来自服务器的消息并保持用户界面更新。通过将ChatServer实例和定时器添加到RunLoop中,我们可以确保服务器连接保持活跃,并及时处理传入的消息。同时,通过使用观察者,我们可以检查新消息并相应地更新用户界面。
请注意,这只是一个简单的示例,实际应用中可能还需要处理其他因素,如取消RunLoop、错误处理和正确的内存管理。
Learn more:
- iphone - What's the real point and benefit of having an Run Loop? - Stack Overflow
- How to use NSRunLoop - Part 2. Here’s a more complex examples that… | by Ruslan Dzhafarov | Medium
- iOS Run Loop, What Why When. Run Loop is a mechanism that allows… | by Prafulla Singh | Medium
哪些场景可以触发离屏渲染?
在iOS中,以下场景可能触发离屏渲染:
- 阴影(shadows):当一个layer添加了阴影效果时,会触发离屏渲染[1]。
- 圆角(cornerRadius):当一个layer设置了圆角并且需要剪切圆角以外的内容时,会触发离屏渲染[1]。
- 遮罩(mask):当一个layer使用了遮罩来裁剪内容时,会触发离屏渲染[1]。
- 组透明(allowsGroupOpacity):当一个layer设置了组透明度为YES,并且透明度不为1时,会触发离屏渲染[1]。
- 光栅化(shouldRasterize):当一个layer的shouldRasterize属性被设置为true时,会触发离屏渲染[1]。
这些场景中,离屏渲染的本质原因是需要将layer及其子layer的内容先渲染到一个独立的帧缓冲区中,然后再将最终结果显示到屏幕上。离屏渲染会增加CPU和GPU的负担,可能导致性能下降,因此在开发中需要注意优化[1]。
Learn more: