这一套大厂通用的底层面试题总结

2,017 阅读48分钟
  • 先简单自我介绍一下自己

对mrc和arc的理解

在iOS开发中,MRC(Manual Reference Counting)和ARC(Automatic Reference Counting)是两种内存管理技术。它们的主要区别在于对象的引用计数是由开发者手动管理还是由编译器自动管理。

  1. MRC(手动引用计数): 在MRC中,开发者需要手动管理对象的引用计数。当一个对象被引用时,需要调用retain方法增加其引用计数;当一个对象不再需要时,需要调用release方法减少其引用计数;当引用计数变为0时,对象将被销毁。这种手动管理引用计数的方式容易导致内存泄漏或野指针问题,需要开发者非常小心地管理内存。
  2. ARC(自动引用计数): ARC引入了编译器的自动内存管理技术,它在编译时根据代码的上下文自动插入retain、release和autorelease等方法调用。开发者不需要显式地调用这些方法来管理内存。ARC会在对象不再被引用时自动将其销毁,从而减少了内存泄漏和野指针的风险。ARC是默认启用的,可以通过在项目中设置来开启或关闭。

在使用ARC时,开发者仍然需要注意一些内存管理的问题:

  • 循环引用:如果两个对象相互引用,而没有其他对象引用它们,可能会导致循环引用而无法释放内存。为了解决这个问题,可以使用weakunowned修饰符来避免强引用循环。
  • Core Foundation对象:ARC只管理Objective-C对象的内存,对于Core Foundation对象(如CFArray、CFDictionary等),需要手动管理其内存,使用CFRetainCFRelease来增加和减少引用计数。

总结: MRC需要手动管理对象的引用计数,而ARC则由编译器自动管理。ARC简化了内存管理,并减少了内存泄漏和野指针的问题,但仍需要开发者注意避免循环引用和处理Core Foundation对象。在现代iOS开发中,大多数开发者使用ARC来管理内存。

谈谈对自动释放池的理解

自动释放池(Autorelease Pool)是一种用于管理内存的机制,它在iOS开发中起到重要的作用。自动释放池允许开发者将对象的释放操作推迟到稍后的时候,从而更好地管理内存的使用。

自动释放池的主要目的是延迟释放对象,避免在创建大量临时对象时频繁地手动释放。当一个对象被添加到自动释放池中时,它的引用计数并不会立即减少,而是等到自动释放池被销毁时才进行释放操作。这样可以减少频繁的内存管理操作,提高性能。

在iOS开发中,自动释放池由@autoreleasepool语法块来表示。通过在代码中创建多个自动释放池,可以灵活地控制对象的生命周期。在一个自动释放池中创建的对象,当该自动释放池结束时(代码块执行完毕),会自动调用对象的release方法进行释放。

使用自动释放池的场景包括:

  1. 循环中创建临时对象:当在循环中创建大量临时对象时,可以将这些对象添加到自动释放池中,以确保及时释放内存。
  2. 在多线程中处理对象:当在多线程环境中使用对象时,可以为每个线程创建独立的自动释放池,以避免线程之间的干扰。

需要注意的是,自动释放池并不会解决内存泄漏的问题。如果一个对象没有被添加到自动释放池中,或者没有及时释放,仍然可能导致内存泄漏。因此,需要在适当的时机手动管理对象的引用计数或者使用ARC来自动管理内存。

总结: 自动释放池是一种用于管理内存的机制,它可以延迟对象的释放操作,提高性能和简化内存管理。通过使用@autoreleasepool语法块,可以将对象添加到自动释放池中,并在自动释放池结束时自动释放对象。自动释放池在处理循环中的临时对象和多线程环境中的对象时非常有用,但仍需要开发者注意及时释放对象,以避免内存泄漏。

自动释放池在mrc和arc区别

在MRC(Manual Reference Counting)和ARC(Automatic Reference Counting)中,自动释放池的概念和用法有一些区别。

在MRC中:

  • 开发者需要手动管理内存,包括手动增加和减少对象的引用计数。
  • 自动释放池需要手动创建和释放,使用NSAutoreleasePool类来管理。
  • 对象可以通过调用autorelease方法将其添加到当前自动释放池中。当自动释放池被销毁时,池中的对象会被发送一次release消息,从而释放对象的内存。

示例代码:

objectiveCopy code
- (void)someMethod {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // 创建临时对象,将其添加到自动释放池中
    NSObject *obj = [[[NSObject alloc] init] autorelease];

    // 手动释放对象
    [obj release];

    [pool release]; // 手动释放自动释放池
}

在ARC中:

  • 开发者不需要手动管理内存,编译器会自动插入内存管理代码。
  • 自动释放池由编译器隐式地创建和释放,开发者无需手动操作。
  • autorelease关键字不再使用,而是由编译器根据代码的上下文自动插入释放代码。

示例代码:

objectiveCopy code
- (void)someMethod {
    @autoreleasepool {
        // 创建临时对象,自动加入当前自动释放池

        // 不需要手动释放对象,由ARC自动处理
    } // 当前自动释放池隐式地被销毁,池中的对象会被自动释放
}

总结: 在MRC中,自动释放池需要手动创建和释放,并使用NSAutoreleasePool类来管理。而在ARC中,自动释放池由编译器隐式地创建和释放,开发者无需手动操作。在ARC中,不再使用autorelease关键字,而是由编译器自动插入释放代码。ARC简化了内存管理过程,减少了手动内存管理的繁琐,但仍然需要注意循环引用等问题。

多层自动释放池嵌套的对象在哪一层释放

在多层自动释放池嵌套的情况下,对象的释放是在最内层的自动释放池被销毁时进行的。当最内层的自动释放池结束时,池中的对象会被释放。

考虑以下示例代码:

objectiveCopy code
- (void)someMethod {
    @autoreleasepool {
        // 最外层自动释放池

        // 创建对象A,加入最外层自动释放池

        @autoreleasepool {
            // 内层自动释放池

            // 创建对象B,加入内层自动释放池
        } // 内层自动释放池结束,对象B被释放

        // 创建对象C,加入最外层自动释放池
    } // 最外层自动释放池结束,对象A和C被释放
}

在这个例子中,对象A和C是在最外层自动释放池中创建并加入的,它们的生命周期与最外层自动释放池的生命周期一致,当最外层自动释放池结束时,对象A和C会被释放。

而对象B是在内层自动释放池中创建并加入的,它的生命周期与内层自动释放池的生命周期一致,当内层自动释放池结束时,对象B会被释放。

因此,在多层自动释放池嵌套的情况下,对象的释放是在其所属的最内层自动释放池结束时进行的。

对于block,理解,mrc和arc下有什么区别,使用注意事项

Block是一种匿名函数的概念,它可以在Objective-C和C语言中使用。它允许我们将代码块作为对象来传递和存储,并在需要的时候执行这些代码块。

在MRC(Manual Reference Counting)下:

  • 在MRC中,使用Block需要手动管理其内存。
  • 当一个Block被创建时,它会在栈上分配内存,如果需要在Block中引用外部的对象,需要手动将这些对象进行retain和release操作,以确保对象在Block执行期间不会被提前释放或销毁。
  • 当Block需要在长期存储或在异步操作中使用时,需要将Block进行copy操作,将其移动到堆上分配内存,以确保Block及其引用的对象能够正确地存活。

在ARC(Automatic Reference Counting)下:

  • 在ARC中,编译器会自动处理Block的内存管理,无需手动管理retain和release操作。
  • ARC会自动根据Block对外部对象的引用情况来决定是否在Block创建时将外部对象进行retain操作,并在Block销毁时自动进行release操作。
  • 不需要手动执行copy操作,因为ARC会根据需要自动将Block从栈上移动到堆上。

使用注意事项:

  1. 在MRC中使用Block时,需要注意对外部对象的手动内存管理,特别是避免在Block中持有强引用循环,即循环引用的情况。
  2. 在ARC中,由于自动进行内存管理,循环引用的问题有所缓解,但仍需要注意Block对外部对象的强引用,特别是在Block可能长期存储、在异步操作中使用或作为属性的情况下。
  3. 当需要在Block内部修改外部的局部变量时,在MRC下需要使用__block修饰符来修饰变量,在ARC下不需要。
  4. 在使用Block时,要考虑到Block可能会被延迟执行,因此需要注意解决对于外部对象的强引用问题,避免出现潜在的内存泄漏。

总结: 在MRC下,需要手动管理Block的内存,包括对外部对象的retain和release操作以及手动进行copy操作。而在ARC下,Block的内存管理由编译器自动处理,无需手动管理retain和release操作,也不需要手动进行copy操作。在使用Block时,需要注意对外部对象的引用和解决循环引用的问题,避免内存泄漏。

对于深拷贝和浅拷贝的理解

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是对象复制的两种方式,用于创建一个新的对象并将原始对象的值复制到新对象中。它们在iOS开发中具有不同的行为和效果。

浅拷贝(Shallow Copy):

  • 浅拷贝是指创建一个新的对象,该对象与原始对象共享相同的内存地址,但是对象内部的引用对象并没有被复制。也就是说,新对象和原始对象引用了相同的内部对象。
  • 当进行浅拷贝时,只复制对象的指针,而不复制对象本身。
  • 对于可变对象(Mutable Object),浅拷贝会导致两个对象之间的修改相互影响,因为它们共享相同的引用对象。

深拷贝(Deep Copy):

  • 深拷贝是指创建一个新的对象,并复制原始对象的所有内容,包括内部的引用对象。新对象和原始对象是完全独立的,它们具有不同的内存地址。
  • 深拷贝会递归地复制对象的所有属性和关联对象,以确保每个对象都是独立的副本。
  • 对于可变对象(Mutable Object),深拷贝会生成一个全新的副本,使得修改一个对象不会影响另一个对象。

在iOS开发中,可以使用以下方法进行对象的拷贝:

  • 对于不可变对象(Immutable Object),无论使用浅拷贝还是深拷贝,它们的效果是一样的,因为不可变对象本身是无法被修改的。
  • 对于可变对象(Mutable Object),可以使用copy方法进行浅拷贝,使用mutableCopy方法进行深拷贝。这些方法在Foundation框架中的类中都有定义。

需要注意的是,并非所有的类都支持拷贝操作,因此在进行拷贝之前需要确认对象是否实现了NSCopying协议。

总结: 浅拷贝是创建一个新对象,该对象与原始对象共享相同的内存地址,而深拷贝是创建一个全新的对象,并复制原始对象的所有内容,包括内部的引用对象。浅拷贝只复制指针,深拷贝递归地复制对象及其关联对象。对于可变对象,深拷贝生成一个独立的副本,而浅拷贝共享引用对象。在iOS开发中,可以使用copy方法进行浅拷贝,使用mutableCopy方法进行深拷贝。需要注意对象是否实现了NSCopying协议。

对于strong weak,atomic等等理解

在iOS开发中,我们使用strongweakatomic等关键词来修饰属性,用于定义对象的引用关系和访问控制。以下是对这些关键词的理解:

  1. strong
  • strong是默认的属性修饰符,用于指定对对象的强引用。
  • 当一个对象被声明为strong时,会增加对象的引用计数,保持对象在内存中,直到没有任何强引用指向它,引用计数为0时,对象被释放。
  • 通常在需要长期持有对象的情况下使用strong,例如父子对象之间的关系。
  1. weak
  • weak用于指定对对象的弱引用。
  • 当一个对象被声明为weak时,不会增加其引用计数。如果没有其他强引用指向该对象,对象会被释放,弱引用会自动被设置为nil
  • weak通常用于避免引起循环引用(retain cycle)的情况,比如在委托模式中,避免委托对象持有其委托的强引用。
  1. atomicnonatomic
  • atomicnonatomic用于指定属性的线程安全性。
  • atomic是默认的属性修饰符,表示对属性的读写操作是原子的,即在多线程环境中保证读取和写入操作的完整性,但性能较差。
  • nonatomic表示对属性的读写操作不是原子的,性能更好,但在多线程环境下可能存在数据竞争问题。
  • 通常情况下,使用nonatomic属性来获得更好的性能,除非在多线程环境下需要确保数据完整性。

需要注意的是,atomicnonatomic属性仅在使用对象属性时才相关,而对于基本数据类型(如intfloat等)则不适用。

总结:

  • strong用于指定对对象的强引用,保持对象在内存中。
  • weak用于指定对对象的弱引用,不增加对象的引用计数,自动设置为nil
  • atomic表示属性的读写操作是原子的(线程安全),nonatomic表示非原子操作(线程不安全)。
  • 在使用属性时,可以根据需求选择合适的修饰符,注意线程安全性和性能的权衡。

weak原理

在 iOS 中,weak 是一种特殊的属性修饰符,用于指定对对象的弱引用。相比于强引用(strong),弱引用不会增加对象的引用计数,也不会保持对象在内存中。当没有其他强引用指向对象时,对象会被释放,并且弱引用会自动被设置为 nil

weak 属性的实现原理依赖于 Objective-C 的运行时(Runtime)系统和引用计数(Reference Counting)机制的支持。

下面是 weak 属性的一般工作原理:

  1. 弱引用的底层数据结构是一个叫做 __weak 的内存表。它是一个散列表(hash table),用于存储所有当前的弱引用。
  2. 当一个对象被声明为 weak,运行时系统会自动将其添加到相应的 __weak 表中。
  3. 当对象的引用计数为零时,即没有强引用指向它,引用计数机制会将其内存释放。
  4. 在释放对象内存时,运行时系统会遍历 __weak 表,并将其中的所有弱引用设置为 nil。这样,避免了弱引用指向已经释放的对象。

需要注意的是,weak 属性只能用于 Objective-C 对象,而不能用于纯 C 类型、基本数据类型或结构体等。此外,weak 属性也不能修饰 IBOutlet,因为 IBOutlet 是在 xib 或者 Storyboard 中通过 Interface Builder 进行连接的,而不是通过代码来创建的。

总结:

  • weak 是一种特殊的属性修饰符,用于指定对对象的弱引用。
  • 弱引用不会增加对象的引用计数,也不会保持对象在内存中。
  • weak 属性的实现依赖于 Objective-C 运行时系统和引用计数机制的支持。
  • 弱引用底层使用 __weak 表来存储弱引用对象,当对象被释放时,相关的弱引用会被自动设置为 nil
  • weak 属性只能用于 Objective-C 对象,不能用于纯 C 类型、基本数据类型或结构体等。

如果属性完全不加修饰词入weak,atomic,系统会怎么处理

如果在 iOS 中声明属性时完全没有添加修饰符(不加 weakatomic等),则属性的默认行为将根据其所在的上下文来确定。默认情况下,属性是具有强引用(strong)和原子性(atomic)的。

具体而言:

  • 引用修饰符默认为 strong:如果属性没有显式指定引用修饰符,那么默认情况下,属性会被视为具有强引用。这意味着在设置属性时,引用计数会增加,并且对象会在没有任何强引用时被保持在内存中。对于可变对象,这可能导致循环引用的问题,需要小心处理。
  • 原子性修饰符默认为 atomic:如果属性没有指定原子性修饰符,那么属性会默认具有原子性。这意味着对属性的读取和写入操作会进行加锁,以确保在多线程环境下的线程安全性。然而,原子性操作会带来一定的性能开销。

需要注意的是,默认的属性行为可能不适用于所有情况,因此在声明属性时最好根据具体的需求显式地添加适当的修饰符,以明确属性的引用关系和访问控制。

总结:

  • 如果在 iOS 中声明属性时完全没有添加修饰符,属性的默认行为是具有强引用(strong)和原子性(atomic)。
  • 默认情况下,属性被视为具有强引用,引用计数会增加,并且对象会在没有任何强引用时被保持在内存中。
  • 默认情况下,属性具有原子性,对属性的读取和写入操作会进行加锁以确保线程安全性。
  • 最好根据具体需求显式地添加适当的修饰符,以明确属性的引用关系和访问控制。

简述下block的实现

Block是一种闭包(Closure)的实现,它可以捕获和保存其周围上下文中的变量和常量,并作为一段可执行的代码块进行传递和调用。在iOS开发中,Block常用于实现回调、异步操作、线程间通信等场景。

Block的实现涉及以下几个关键部分:

  1. 闭包的定义:Block使用^符号进行定义,类似于匿名函数。例如,^{ /* block body */ }表示一个没有参数和返回值的Block。

  2. Block结构:Block是一个封装了可执行代码和相关上下文的数据结构。它包含三个主要部分:

    • isa:指向Block对象的类的指针,用于表示Block对象的类型和元信息。
    • flags:标志位,用于存储Block的相关信息,比如是否是全局Block、是否捕获了外部变量等。
    • invoke:指向实际执行的函数指针,用于执行Block的代码。
  3. Block的截获变量:Block可以截获周围上下文中的变量和常量,使得这些变量和常量在Block内部可访问。被截获的变量和常量会被复制到Block的数据结构中,并在Block执行时使用。

  4. Block的类型:Block有三种类型:

    • 全局Block(Global Block):没有访问外部变量的Block,被存储在全局数据区,其生命周期和应用程序的生命周期一样长。
    • 栈上的Block(Stack Block):具有自动变量截获的Block,其生命周期与所在的函数或代码块相关联。
    • 堆上的Block(Heap Block):通过copy操作从栈上复制到堆上的Block,需要手动管理内存。
  5. Block的复制和捕获:当一个Block被复制到堆上时,会自动对其内部使用到的外部变量进行引用计数的增加。这样可以确保Block在使用外部变量时,保持正确的引用关系和内存管理。

总结: Block是一种闭包的实现,可以捕获和保存上下文中的变量和常量,并作为可执行的代码块进行传递和调用。它包含Block结构、截获变量、类型和复制等关键部分。Block的实现可以用于实现回调、异步操作、线程间通信等场景,提供了一种便捷的方式来处理代码块的封装和传递。

描述下IM系统如何保证消息不丢

IM(即时通讯)系统中确保消息不丢失是非常重要的,这涉及到系统的可靠性和消息的可达性。下面是一些常见的方法和技术来保证消息不丢失:

  1. 可靠的传输协议:IM系统通常使用可靠的传输协议,如TCP(传输控制协议),而不是不可靠的协议如UDP(用户数据报协议)。TCP使用确认、重传等机制来保证数据的可靠传输,确保消息不会在传输过程中丢失。
  2. 消息队列:IM系统通常使用消息队列来存储和传递消息。当发送方发送一条消息时,消息会先被放入消息队列中,然后由接收方从队列中取出并处理。通过消息队列,即使接收方当前不在线,消息也能被安全地存储,直到接收方上线并准备好接收消息。
  3. 消息确认机制:IM系统可以引入消息确认机制。当发送方发送一条消息后,接收方会向发送方发送一个确认消息,告知消息已经收到。如果发送方在一定时间内没有收到确认消息,可以进行重传,确保消息的可达性。
  4. 消息持久化:IM系统通常会将消息进行持久化存储,以防止消息在系统故障或重启后丢失。通过将消息写入数据库或者磁盘文件中,可以确保即使系统发生故障,消息仍然可用。
  5. 冗余备份:为了提高系统的可靠性,IM系统通常会进行冗余备份。这意味着消息会在多个服务器上进行备份存储,以防止单点故障导致消息丢失。通过备份机制,即使某个服务器出现问题,消息仍然可以从其他备份服务器中恢复。
  6. 异常处理和重试机制:当消息传输或处理过程中发生异常情况时,IM系统应具备相应的异常处理和重试机制。例如,网络中断、服务器故障等情况下,系统应该能够自动检测并尝试重新发送消息,以确保消息的可达性。

需要注意的是,尽管采取了上述的措施,但在复杂的分布式系统中,完全杜绝消息丢失是非常困难的。因此,IM系统在设计时应该综合考虑系统的可靠性和性能,并在可接受的范围内尽量减少消息丢失的可能性。

IM数据库如何设计表

设计IM系统的数据库表结构需要考虑到消息的发送、接收、存储和查询等方面。以下是一个简单的示例,展示了IM系统可能涉及的一些表以及其字段:

  1. User(用户表):

    • UserID:用户ID
    • Username:用户名
    • Password:密码
    • FullName:用户全名
    • ...(其他用户信息字段)
  2. Conversation(会话表):

    • ConversationID:会话ID
    • Name:会话名称
    • CreatedBy:创建者UserID
    • CreatedAt:创建时间
    • ...(其他会话信息字段)
  3. Participant(参与者表):

    • ParticipantID:参与者ID
    • UserID:用户ID
    • ConversationID:会话ID
  4. Message(消息表):

    • MessageID:消息ID
    • ConversationID:会话ID
    • SenderID:发送者UserID
    • ReceiverID:接收者UserID
    • Content:消息内容
    • SentAt:发送时间
    • ...(其他消息信息字段)

这些是一个简单的示例表结构,实际上在设计IM系统的数据库表时,还需要考虑到更多的因素,例如消息状态(已读、未读)、附件(图片、文件)存储等。另外,根据具体需求,还可以进行表的拆分和优化,以提高数据库的性能和可扩展性。

此外,还需要根据具体的数据库管理系统(如MySQL、PostgreSQL)选择合适的数据类型、索引、约束等来支持数据的高效存储和查询。在设计数据库表时,要考虑到数据的一致性、完整性和安全性,以及系统的性能要求和扩展性要求。

C++引用和指针有什么区别

C++中,引用(Reference)和指针(Pointer)都可以用于间接访问对象,但它们在语义和使用上有一些重要的区别:

  1. 定义和初始化:引用必须在定义时进行初始化,并且一旦绑定到一个对象后,就不能再重新绑定到其他对象上。而指针可以在定义时不进行初始化,也可以在之后重新指向其他对象。
  2. 空值(Null value):指针可以具有空值(nullptr),表示指向无效对象或没有指向任何对象。引用不能有空值,必须始终引用有效的对象。
  3. 内存地址和解引用:指针存储了一个对象的内存地址,可以通过解引用操作符(*)来访问所指向的对象。而引用不存储内存地址,它只是目标对象的一个别名,可以直接使用引用来访问目标对象。
  4. 重新赋值:指针可以通过赋值操作符(=)重新指向其他对象,可以修改指针所指向的对象。而引用一旦绑定到一个对象后,就不能再重新绑定到其他对象上,相当于一个别名,无法修改引用所指向的对象。
  5. 空间需求:引用不会占用额外的内存空间,它只是对象的别名。而指针需要占用额外的内存空间来存储指针变量的值。
  6. 函数参数传递:引用可以用作函数的参数,通过引用传递可以直接修改函数外部的对象。指针也可以用作函数的参数,但需要通过指针解引用来访问和修改对象。

总的来说,引用更加简单和安全,而指针更加灵活和具有更多的操作控制。在选择使用引用还是指针时,需要根据具体的需求和情况来决定哪种方式更适合。

C++中,引用(Reference)和指针(Pointer)都可以用于间接访问对象,但它们在语义和使用上有一些重要的区别:

  1. 定义和初始化:引用必须在定义时进行初始化,并且一旦绑定到一个对象后,就不能再重新绑定到其他对象上。而指针可以在定义时不进行初始化,也可以在之后重新指向其他对象。
  2. 空值(Null value):指针可以具有空值(nullptr),表示指向无效对象或没有指向任何对象。引用不能有空值,必须始终引用有效的对象。
  3. 内存地址和解引用:指针存储了一个对象的内存地址,可以通过解引用操作符(*)来访问所指向的对象。而引用不存储内存地址,它只是目标对象的一个别名,可以直接使用引用来访问目标对象。
  4. 重新赋值:指针可以通过赋值操作符(=)重新指向其他对象,可以修改指针所指向的对象。而引用一旦绑定到一个对象后,就不能再重新绑定到其他对象上,相当于一个别名,无法修改引用所指向的对象。
  5. 空间需求:引用不会占用额外的内存空间,它只是对象的别名。而指针需要占用额外的内存空间来存储指针变量的值。
  6. 函数参数传递:引用可以用作函数的参数,通过引用传递可以直接修改函数外部的对象。指针也可以用作函数的参数,但需要通过指针解引用来访问和修改对象。

总的来说,引用更加简单和安全,而指针更加灵活和具有更多的操作控制。在选择使用引用还是指针时,需要根据具体的需求和情况来决定哪种方式更适合。

assign 和weak的区别

ChatGPT

在 Objective-C 中,assignweak 是两种不同的属性修饰符,用于定义对象属性的内存管理语义。它们在内存管理和对象生命周期管理方面有以下区别:

  1. assign 属性修饰符:

    • assign 通常用于基本数据类型(如NSIntegerCGFloat等)和 C 数据类型的属性,以及在 MRC(手动引用计数)环境下用于对象类型的属性。
    • assign 意味着属性的赋值操作仅仅是简单地赋值给实例变量,不会进行引用计数的增减。当赋值对象释放后,属性仍然会持有之前的值,这可能导致悬垂指针问题。
    • 当被赋值的对象释放后,属性值仍然指向该对象的内存地址,即使对象已经被释放或销毁,也没有设置为 nil
  2. weak 属性修饰符:

    • weak 用于对象类型的属性,在 ARC(自动引用计数)环境下使用。
    • weak 属性不会增加对象的引用计数,也不会阻止对象的释放。当被 weak 修饰的对象释放后,属性会自动设置为 nil
    • weak 属性避免了悬垂指针问题,因为当对象被释放后,属性会自动设置为 nil,避免了访问已释放对象的问题。

总结:

  • assign 用于基本数据类型、C 数据类型和 MRC 环境下的对象类型,赋值操作不进行引用计数的增减,可能导致悬垂指针问题。
  • weak 用于 ARC 环境下的对象类型,不增加对象的引用计数,属性值会自动设置为 nil,避免了悬垂指针问题。

在使用属性修饰符时,需要根据具体的需求和对象的生命周期来选择合适的修饰符,以确保正确的内存管理和避免悬垂指针问题。

block 的实现原理

ChatGPT

Block是Objective-C和Swift语言中的一种闭包(Closure)类型,它可以封装一段代码块,并且可以在稍后的时间点执行该代码块。下面是Block的简要实现原理:

在编译时:

  1. 当编译器遇到Block时,它会将Block内部的代码封装成一个结构体,这个结构体中包含了Block所需执行的代码块以及所捕获的外部变量。
  2. 编译器会将该结构体作为一个对象分配在栈上或堆上,取决于Block的使用场景。

在运行时:

  1. 当程序执行到Block定义的地方时,栈上的Block会被复制到堆上,确保Block在栈上定义的变量在Block执行时仍然有效。
  2. 如果Block内部使用了外部变量,编译器会自动将这些外部变量进行捕获(Capture),使得Block内部可以访问和修改这些变量的值。
  3. 捕获的外部变量会被复制到Block的结构体中,并且对于引用类型的变量,会进行自动的引用计数管理。

Block捕获外部变量的方式分为三种:

  • __block 修饰符:用于在Block内部修改被捕获的外部变量,并且在Block外部也可以看到变化。
  • 自动捕获:对于被Block内部引用的外部变量,编译器会自动判断是按值捕获(const修饰)还是按引用捕获(strong修饰)。
  • 静态变量捕获:Block内部可以直接访问函数内的静态变量。

总结: Block的实现原理涉及编译时和运行时的过程。在编译时,Block会被封装成一个结构体,同时对外部变量进行捕获。在运行时,栈上的Block会被复制到堆上,保证Block在执行时能够访问正确的外部变量。Block的实现机制使得它成为了一种强大的工具,可以在代码中创建和传递一段可执行的逻辑。

weak的底层实现

ChatGPT

在 Objective-C 中,weak 是一种修饰符,用于声明一个弱引用的对象。它的底层实现是通过 Objective-C Runtime 提供的函数 objc_loadWeak()objc_storeWeak() 来实现的。

  1. 弱引用的创建:

    • 当使用 weak 修饰一个对象时,编译器会在编译时期生成一个指向对象的弱指针(weak pointer)。
    • 在运行时,当对象被赋值给弱指针时,objc_storeWeak() 函数会被调用。
    • objc_storeWeak() 函数会将对象添加到 AutoreleasePool 中,并将对象的地址存储在一个哈希表(weak_table)中。
  2. 弱引用的维护:

    • 当对象的引用计数为 0 时,即没有强引用指向它时,Objective-C Runtime 会自动将该对象释放。
    • 在对象释放后,Objective-C Runtime 会遍历 weak_table 中存储的弱引用指针,并将对应的指针设置为 nil,避免出现悬垂指针。
    • 这个遍历和设置为 nil 的过程通常是在下一次自动释放池(Autorelease Pool)的释放时执行。

总结:

  • weak 的底层实现是通过 objc_loadWeak()objc_storeWeak() 函数来完成的。
  • 当对象被赋值给 weak 引用时,使用 objc_storeWeak() 将对象添加到 AutoreleasePool 并存储在 weak_table 中。
  • 当对象被释放后,Objective-C Runtime 会遍历 weak_table 并将对应的弱引用指针设置为 nil
  • 这个遍历和设置为 nil 的过程通常是在下一次自动释放池的释放时执行。

runloop source0 那些事件

ChatGPT

在 Run Loop 中,Source 0 是一种基于事件的输入源(Event-based input source),它用于处理特定类型的事件。Source 0 主要处理以下类型的事件:

  1. 应用程序的内部事件:这包括自定义事件,例如应用程序内部通信、线程间通信等。通过向 Run Loop 中添加自定义的 Source 0 来处理这些事件。
  2. 系统级事件:这包括操作系统或其他框架发送的事件,例如系统通知、文件系统变化、端口通信等。通过向 Run Loop 中添加系统级的 Source 0 来处理这些事件。

添加 Source 0 到 Run Loop 后,当相应的事件发生时,Run Loop 会触发与该 Source 0 相关联的处理器(Handler)来处理事件。处理器可以是一个函数、一个 Block 或者一个对象的方法。

需要注意的是,Source 0 是基于事件的输入源,与 Source 1 和 Source 2 不同。Source 1 是基于端口的输入源,用于处理端口通信事件,而 Source 2 是基于定时器的输入源,用于处理定时器事件。

通过适当地配置和添加 Source 0 到 Run Loop,我们可以监听并处理特定类型的事件,从而实现异步处理、事件驱动等功能。

什么是死锁

死锁(Deadlock)是多线程或多进程并发编程中的一种情况,指的是两个或多个进程(线程)彼此持有对方所需的资源,导致它们都无法继续执行,陷入无限等待的状态。

死锁通常涉及多个资源和多个进程(线程),并满足以下四个必要条件:

  1. 互斥条件(Mutual Exclusion):至少有一个资源同时只能被一个进程(线程)占用,即当一个进程(线程)访问该资源时,其他进程(线程)无法同时访问。
  2. 请求和保持条件(Hold and Wait):进程(线程)在持有至少一个资源的同时,还可以请求其他进程(线程)所持有的资源,而不释放自己已经持有的资源。
  3. 不可剥夺条件(No Preemption):已经分配给一个进程(线程)的资源不能被强制性地剥夺,只能由进程(线程)自己释放。
  4. 环路等待条件(Circular Wait):存在一个进程(线程)的资源等待链,使得每个进程(线程)都在等待下一个进程(线程)所持有的资源。

当这四个条件同时满足时,就会导致死锁的发生。在死锁状态下,参与者无法继续执行,系统无法继续进行,只能通过干预或重启来解决死锁问题。

为避免死锁的发生,可以采取一些预防和解决策略,如合理地分配资源、避免循环等待、引入资源的优先级等。同时,良好的设计和并发编程实践也可以减少死锁的可能性。

自动释放池的原理

ChatGPT

自动释放池(Autorelease Pool)是在 Objective-C 中用于管理内存释放的机制,它允许对象在稍后的时间点自动释放,而不需要手动调用 release 方法。

自动释放池的原理如下:

  1. 创建自动释放池:

    • 在每个线程中,当进入一个新的事件循环(例如 Run Loop)或方法调用栈时,会创建一个自动释放池。
    • 自动释放池通过一个栈结构(通常是一个链表)来管理。
  2. 对象的自动释放:

    • 当对象被发送一个 autorelease 消息时,它会被添加到当前自动释放池中。
    • autorelease 方法会将对象注册到自动释放池中,而不是立即释放对象。
  3. 自动释放池的释放:

    • 当自动释放池被释放时,会逆序地向其中的每个对象发送一条 release 消息,使这些对象被释放。
    • 释放操作通常在事件循环结束或方法调用栈退出时执行。

通过自动释放池,可以推迟对象的释放操作,从而减少了手动管理内存的复杂性。自动释放池的作用是确保在适当的时机释放对象,避免内存泄漏。

需要注意的是,自动释放池仅仅是对 autorelease 方法的管理机制,实际的内存管理仍然依赖于引用计数。当对象被添加到自动释放池后,它的引用计数会增加,当自动释放池被释放时,会减少对象的引用计数。如果对象的引用计数降为0,则对象会被销毁并释放内存。

iOS 内存的几大区域

ChatGPT

在 iOS 应用程序的内存管理中,常见的几大区域包括:

  1. 栈(Stack):

    • 栈是一块内存区域,用于存储局部变量、方法参数、函数调用等信息。
    • 栈上的内存分配和释放是由系统自动管理的,遵循后进先出(LIFO)的原则。
  2. 堆(Heap):

    • 堆是一块动态分配的内存区域,用于存储动态分配的对象、数据结构等。
    • 堆上的内存分配和释放需要手动管理,通过调用 allocinitnewmalloc 等方法来分配内存,并通过 releasedealloc 方法来释放内存。
  3. 全局区(Global Area):

    • 全局区是用于存储全局变量、静态变量、常量等的内存区域。
    • 全局区的内存分配和释放由系统自动管理,这些变量在整个应用程序生命周期内都是有效的。
  4. 常量区(Constant Area):

    • 常量区用于存储字符串常量、静态常量等的内存区域。
    • 常量区的内存分配和释放由系统自动管理,这些常量在整个应用程序生命周期内都是有效的。
  5. 代码区(Code Area):

    • 代码区存储应用程序的可执行代码,包括方法、函数等的指令。
    • 代码区的内存是只读的,不可修改。

需要注意的是,这些内存区域的管理方式和生命周期是不同的。栈和全局区的内存管理由系统自动处理,而堆和常量区的内存需要手动进行管理。合理地分配和释放内存,避免内存泄漏和野指针等问题,是开发 iOS 应用程序时需要注意的重要方面。

多线程的理解

ChatGPT

多线程是一种并发编程的技术,它允许在一个程序中同时执行多个线程,每个线程独立运行,并可以并发地执行不同的任务。

在多线程编程中,一个程序可以创建和管理多个线程,每个线程都有自己的代码执行路径和程序计数器,可以独立地执行任务。多线程可以提高程序的并发性和响应性,允许同时处理多个任务,从而提高程序的性能和效率。

以下是一些关键概念和要点,用于理解多线程的概念:

  1. 线程(Thread):线程是操作系统调度的最小执行单位,每个线程都有自己的程序计数器和堆栈。
  2. 并发(Concurrency):并发是指多个任务可以同时执行的能力。
  3. 并行(Parallelism):并行是指多个任务实际上在同一时刻同时执行。
  4. 线程安全(Thread-Safety):线程安全是指多个线程并发访问共享资源时,不会产生不正确的结果。
  5. 互斥锁(Mutex):互斥锁是一种同步机制,用于保护共享资源,确保在任意时刻只有一个线程可以访问该资源。
  6. 条件变量(Condition Variable):条件变量用于线程之间的通信和同步,允许线程等待特定条件的发生。
  7. 死锁(Deadlock):死锁是多线程编程中的一种情况,指的是多个线程相互等待对方所持有的资源,导致所有线程都无法继续执行。
  8. 线程池(Thread Pool):线程池是一种预先创建的线程集合,可以重复使用来执行多个任务,避免频繁地创建和销毁线程的开销。

多线程编程在实践中具有一定的挑战性,包括线程安全、竞态条件、死锁等问题。因此,在编写多线程代码时,需要小心处理共享资源、加锁机制、同步机制以及线程之间的通信,以确保程序的正确性和可靠性。

分类的底层 实现原理

ChatGPT

在 iOS 中,分类(Category)是一种扩展现有类的机制,允许在不修改原始类的情况下添加新的方法、属性和协议。分类的底层实现原理涉及 Objective-C 的运行时机制。

分类的底层实现原理如下:

  1. 编译阶段:

    • 在编译时,Objective-C 编译器会将分类的声明和实现合并到原始类的代码中,形成一个合并后的类。
  2. 运行时阶段:

    • 在运行时,Objective-C 运行时系统会将分类的方法列表合并到原始类的方法列表中。
    • 当调用一个分类中的方法时,运行时系统会按照方法查找的顺序先在原始类的方法列表中查找,如果找不到则在分类的方法列表中继续查找。
  3. 方法冲突解决:

    • 如果一个方法在原始类和分类中都存在,运行时系统会根据一定的规则来决定使用哪个方法。
    • 通常情况下,最后编译的分类会覆盖之前编译的分类的方法,而最后编译的分类和原始类之间的方法覆盖原则是不确定的。

需要注意的是,分类不能添加实例变量,只能添加方法、属性和协议。此外,分类可以为现有的类添加新的方法实现,但不能替换原有方法的实现。

通过分类,我们可以在不修改原始类的情况下为现有类添加新的功能,使代码更加模块化和可扩展。分类在实际开发中被广泛应用于为系统类或第三方库类添加额外的功能或修复bug,同时也可以用于将大型类分解为更小的逻辑单元。

load 的底层 实现

load 方法是 Objective-C 运行时机制中的一个特殊方法,它会在类被加载到内存时自动调用,而不需要手动触发。load 方法的底层实现涉及到 Objective-C 运行时的初始化和类加载的过程。

load 方法的底层实现原理如下:

  1. 类加载:

    • 当一个类第一次被引用或者程序启动时,Objective-C 运行时会将该类从可执行文件中加载到内存中。
    • 类的加载包括分配内存、设置类的元数据、连接父类和子类、处理类的实例变量、方法列表等。
  2. load 方法的查找和调用:

    • 在类加载的过程中,Objective-C 运行时会遍历所有已加载的类,并检查是否实现了 load 方法。
    • 如果一个类实现了 load 方法,运行时会在类加载时自动调用该方法。
    • load 方法的调用是在类加载过程中的特定时间点,早于 main 函数执行和其他类方法的调用。
  3. load 方法的执行顺序:

    • load 方法的执行顺序是不确定的,它会在类被加载时按照编译时的顺序调用。
    • 父类的 load 方法会在子类的 load 方法之前调用。
    • 在继承关系中,父类的 load 方法先于子类的 load 方法执行。

需要注意的是,load 方法是在类加载时自动调用的,而不是在类的实例被创建时调用。它适用于在类加载阶段执行一些静态的初始化操作,例如注册类、方法交换、动态添加方法等。由于 load 方法是在类加载时调用的,因此不需要手动触发或通过类的实例来调用。

load 方法的使用需要谨慎,应该遵循一些规范和最佳实践。例如,避免在 load 方法中进行过重的计算或复杂的操作,以免影响应用程序的启动性能。此外,还应注意处理好 load 方法的执行顺序,避免出现意料之外的依赖关系。

分类的底层

ChatGPT

分类(Category)是 Objective-C 的一种特性,它允许在不修改原始类的情况下为类添加方法、属性和协议。分类的底层实现涉及 Objective-C 的运行时机制。

分类的底层实现原理如下:

  1. 编译阶段:

    • 在编译时,Objective-C 编译器将分类的声明和实现与原始类的代码分开存储在两个文件中。分类的声明通常在 .h 文件中,而分类的实现在 .m 文件中。
  2. 运行时阶段:

    • 在运行时,Objective-C 运行时系统会将分类的方法列表合并到原始类的方法列表中,形成一个完整的方法列表。
    • 运行时系统通过类对象和元类对象来管理类的方法列表。当调用一个方法时,运行时系统会在类的方法列表中查找并执行对应的方法。
  3. 方法冲突解决:

    • 如果一个方法在原始类和分类中都存在,运行时系统会根据一定的规则来解决方法冲突。
    • 通常情况下,最后编译的分类会覆盖之前编译的分类和原始类的方法。因此,最后编译的分类中的方法会在方法调用时被执行。

需要注意的是,分类不能添加实例变量,因为它们的结构在编译时已经确定。只能通过分类添加方法、属性和协议。

通过分类,我们可以为现有的类添加新的方法、属性和协议,扩展类的功能,而无需修改原始类的代码。分类在代码重用和模块化方面具有很大的优势,可以将类的功能划分为多个独立的模块,使代码更加清晰、可读和易于维护。

iOS如何 进行全埋点,有什么方案

ChatGPT

全埋点(Full-Stack Monitoring)是一种在移动应用中进行全面数据采集和监控的技术,用于收集用户行为、性能指标和错误信息等数据。在 iOS 应用中实现全埋点可以通过以下方案:

  1. 手动埋点:

    • 手动埋点是最常见的全埋点实现方式。在关键的用户操作点、界面切换、网络请求等位置,手动插入代码来记录相应的事件和数据。
    • 优点是灵活性高,可以根据具体需求选择埋点位置和要采集的数据。缺点是需要手动添加埋点代码,工作量较大,容易遗漏和错误。
  2. AOP(Aspect-Oriented Programming):

    • AOP 是一种编程思想,可以通过动态代理和代码注入的方式实现全埋点。
    • 在 iOS 中,使用工具如 Aspects、JSPatch、fishhook 等,可以通过 AOP 技术在方法调用前后插入代码,实现全埋点的数据采集。
    • AOP 的优点是相对于手动埋点,可以更方便地集成和管理全埋点逻辑,减少代码侵入性。但需要注意使用 AOP 工具的稳定性和性能影响。
  3. 自动埋点:

    • 自动埋点是一种在应用层面自动收集用户行为和事件的方式,无需手动插入埋点代码。
    • 在 iOS 中,可以通过 Method Swizzling、消息转发等技术来实现自动埋点。
    • 自动埋点的优点是减少手动埋点的工作量,提高埋点的全面性和准确性。缺点是可能会增加运行时的性能开销,需要注意性能优化和异常处理。

无论采用哪种方案,进行全埋点时需要注意以下事项:

  • 确定需要采集的数据,包括用户行为、性能指标、错误信息等。
  • 合理选择埋点位置,覆盖关键的用户操作点和关键路径。
  • 采集的数据需要经过合适的加密和安全处理,保护用户隐私和敏感信息。
  • 对于数据量较大的情况,要考虑合理的数据存储和传输方案,避免对应用性能和用户体验造成过大的影响。
  • 进行全埋点时需要与法律法规和用户隐私保护政策保持一致,确保数据采集和使用的合法性和合规性。

综上所述,全埋点是一项复杂的任务,需要结合具体应用场景和需求选择合适的方案,并进行合理的数据处理和安全保护。

类的结构

ChatGPT

在 iOS 中,一个类的结构由以下几个主要组成部分组成:

  1. 实例变量(Instance Variables):

    • 实例变量是类的成员变量,用于存储对象的状态和数据。
    • 它们可以在类的任意方法中访问和修改。
    • 实例变量的声明通常在类的接口(.h 文件)中,但实际的存储通常在类的实现(.m 文件)中。
  2. 属性(Properties):

    • 属性是对实例变量的封装,提供了对实例变量的访问和设置的接口。
    • 通过属性,可以指定实例变量的访问修饰符(如读写权限)和内存管理方式(如 strong、weak、copy 等)。
    • 属性的声明通常在类的接口(.h 文件)中,并且可以使用 @property@synthesize 关键字自动生成存取方法的实现。
  3. 方法(Methods):

    • 方法是类中定义的函数,用于实现类的行为和功能。
    • 一个方法由方法名、参数列表、返回类型和方法体组成。
    • 方法可以分为实例方法(instance methods)和类方法(class methods)两种类型,分别用于操作实例对象和类本身。
  4. 初始化方法(Initializers):

    • 初始化方法是用于创建和初始化类的实例对象的特殊方法。
    • 初始化方法的命名约定是以 init 开头,并可以有不同的初始化方式和参数组合。
    • 初始化方法在对象创建后自动调用,用于完成对象的初始化工作,并返回初始化后的对象。
  5. 协议(Protocols):

    • 协议是一组方法声明,用于定义类或对象应该实现的特定行为。
    • 类可以采用(adopt)一个或多个协议,并实现协议中定义的方法。
    • 通过采用协议,可以实现多态性、代码复用和接口规范等功能。

以上是一个类的基本结构组成部分,通过这些组成部分可以定义类的属性、行为和初始化方式,并与其他类进行交互和继承。每个类都有其特定的结构,通过定义和实现这些组成部分,可以创建具有特定功能和行为的对象。

block 里面开启定时器 需要注意哪些 ?

ChatGPT

在 Block 中开启定时器时,需要注意以下几点:

  1. 循环引用(Retain Cycle):

    • 当 Block 中持有定时器对象时,需要注意避免循环引用的问题,以免造成内存泄漏。
    • 如果 Block 内部引用了定时器对象并且定时器对象又强引用了该 Block,就会形成循环引用。
    • 为了避免循环引用,可以使用 __weak 关键字来声明一个弱引用的变量,然后在 Block 内部引用该弱引用变量。
  2. 定时器生命周期管理:

    • 需要注意在 Block 中合理管理定时器的生命周期,确保定时器能够正确地开始和停止。
    • 在开启定时器前,应该保证定时器对象是有效的,并且在适当的时机停止或释放定时器。
  3. Block 内部的强引用:

    • Block 内部默认会强引用它所捕获的变量,包括局部变量和对象。
    • 如果在 Block 内部使用了对象属性或全局变量,需要注意对象的内存管理,避免出现循环引用或内存泄漏。
  4. 主线程操作:

    • 如果 Block 中的定时器需要执行 UI 相关的操作,需要确保定时器回调在主线程中执行,以避免界面更新的问题。
    • 可以使用 GCD(Grand Central Dispatch)或 performSelectorOnMainThread:withObject:waitUntilDone: 方法来将定时器回调放到主线程执行。
  5. 定时器精度:

    • 定时器的精度取决于定时器的类型和调度方式。在选择定时器类型和设置定时器的间隔时,需要根据具体需求和性能要求进行调整。

总之,在 Block 中开启定时器时,需要注意循环引用、定时器生命周期管理、强引用、主线程操作和定时器精度等方面的问题,以确保代码的正确性、性能和内存管理。

Http协议30x的错误是什么
谈谈你懂runloop得理解:由浅入深
谈谈对多线程理解:由浅入深
谈谈category和extension区别,系统如何底层实现category
谈谈消息转发机制实现
谈谈事件响应链,如何响应view之外的事件
界面性能优化