2021iOS高频(基础+底层)面试题汇总

目录

 

关键字copy,weak,assign,strong,nonatomic

Weak的底层实现原理

runtime理解

1.Category 的实现原理?

2.isa指针的理解,对象的isa指针指向哪里?isa指针有哪两种类型?

3.Objective-C 如何实现多重继承? 

 4.runtime 如何实现 weak 属性?

5.讲一下 OC 的消息机制

6、runtime中重要的数据结构:

7、runtime如何通过selector找到对应的IMP地址?

8.runtime具体应用 

 Runloop篇

1.Runloop 和线程的关系?

2.RunLoop的运行模式

3.runloop内部逻辑?

4.autoreleasePool 在何时被释放?

5.GCD 在Runloop中的使用?

6.AFNetworking 中如何运用 Runloop?

7.PerformSelector 的实现原理?

8.PerformSelector:afterDelay:这个方法在子线程中是否起作用?

9.事件响应的过程?

10.手势识别的过程?

11.CADispalyTimer和Timer哪个更精确

12.可以用Runloop实现什么功能?

多线程 

1.进程与线程

2.什么是多线程?

3.多线程的优点和缺点

4.多线程的 并行 和 并发 有什么区别?

5.iOS中实现多线程的几种方案,各自有什么特点?

6.多个网络请求完成后执行下一步

7.多个网络请求顺序执行后执行下一步

8.异步操作两组数据时, 执行完第一组之后, 才能执行第二组

9.多线程中的死锁?

10.GCD执行原理?

11. 什么时候选择使用GCD,什么时候选择NSOperation?

性能优化

CPU和GPU

离屏渲染

卡顿检测

耗电的主要来源

耗电优化

网络优化

定位优化

硬件检测优化

APP的启动

安装包瘦身


关键字copy,weak,assign,strong,nonatomic

assign:一般用来修饰基本的数据类型,包括基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char, 等等),为什么呢?assign声明的属性是不会增加引用计数的,也就是说声明的属性释放后,就没有了,即使其他对象用到了它,也无法留住它,只会crash。但是,即使被释放,指针却还在,成为了野指针,如果新的对象被分配到了这个内存地址上,又会crash,所以一般只用来声明基本的数据类型,因为它们会被分配到栈上,而栈会由系统自动处理,不会造成野指针。

retain: 与assign相对,我们要解决对象被其他对象引用后释放造成的问题,就要用retain来声明。retain声明后的对象会更改引用计数,那么每次被引用,引用计数都会+1,释放后就会-1,即使这个对象本身释放了,只要还有对象在引用它,就会持有,不会造成什么问题,只有当引用计数为0时,就被dealloc析构函数回收内存了。

copy: 最常见到copy声明的应该是NSString。copy与retain的区别在于retain的引用是拷贝指针地址,而copy是拷贝对象本身,也就是说retain是浅复制,copy是深复制,如果是浅复制,当修改对象值时,都会被修改,而深复制不会。之所以在NSString这类有可变类型的对象上使用,是因为它们有可能和对应的可变类型如NSMutableString之间进行赋值操作,为了防止内容被改变,使用copy去深复制一份。copy工作由copy方法执行,此属性只对那些实现了NSCopying协议的对象类型有效 。

以上三个可以在MRC中使用,但是weak和strong就只能在ARC中使用,也就是自动引用计数,这时就不能手动去进行retain、release等操作了,ARC会帮我们完成这些工作。

weak: weak其实类似于assign,叫弱引用,也是不增加引用计数。一般只有在防止循环引用时使用,比如父类引用了子类,子类又去引用父类。IBOutlet、Delegate一般用的就是weak,这是因为它们会在类外部被调用,防止循环引用。

strong: 相对的,strong就类似与retain了,叫强引用,会增加引用计数,类内部使用的属性一般都是strong修饰的,现在ARC已经基本替代了MRC,所以我们最常见的就是strong了。一般用于自定义的对象

nonatomic: 在修饰属性时,我们往往还会加一个nonatomic,这又是什么呢?它的名字叫非原子访问。对应的有atomic,是原子性的访问。我们知道,在使用多线程时为了避免在写操作时同时进行写导致问题,经常会对要写的对象进行加锁,也就是同一时刻只允许一个线程去操作它。如果一个属性是由atomic修饰的,那么系统就会进行线程保护,防止多个写操作同时进行。这有好处,但也有坏处,那就是消耗系统资源,所以对于iPhone这种小型设备,如果不是进行多线程的写操作,就可以使用nonatomic,取消线程保护,提高性能。

Weak的底层实现原理

runtime 会维护一个weak表,用于为维护指向对象的所有weak指针,weak是一个hash表,key为所指对象的指针,value为所指对象的指针地址数组

实现过程如下:

 2021iOS高频(基础+底层)面试题汇总

runtime理解

https://www.jianshu.com/p/ad97a1e91ba3

1.Category 的实现原理?

  • Category 实际上是 Category_t的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的Category,添加了同一个方法,执行的实际上是最后一个。

  • Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 Runtime ,Category 和原来的类才会合并到一起。

2.isa指针的理解,对象的isa指针指向哪里?isa指针有哪两种类型?

  • isa 等价于 is kind of

  • 实例对象的 isa 指向类对象

    类对象的 isa 指向元类对象

    元类对象的 isa 指向元类的基类

  • isa 有两种类型

    纯指针,指向内存地址

    NON_POINTER_ISA,除了内存地址,还存有一些其他信息

3.Objective-C 如何实现多重继承? 

Object-c的类没有多继承,只支持单继承,如果要实现多继承的话,可使用如下几种方式间接实现

  • 通过组合实现

    A和B组合,作为C类的组件

  • 通过协议实现

    C类实现A和B类的协议方法

  • 消息转发实现

    forwardInvocation:方法

 4.runtime 如何实现 weak 属性?

weak 此特质表明该属性定义了一种「非拥有关系」(nonowning relationship)。为这种属性设置新值时,设置方法既不持有新值(新指向的对象),也不释放旧值(原来指向的对象)。

runtime 对注册的类,会进行内存布局,从一个粗粒度的概念上来讲,这时候会有一个 hash 表,这是一个全局表,表中是用 weak 指向的对象内存地址作为 key,用所有指向该对象的 weak 指针表作为 value。当此对象的引用计数为 0 的时候会 dealloc,假如该对象内存地址是 a,那么就会以 a 为 key,在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil。

runtime 如何实现 weak 属性具体流程大致分为 3 步:

  • 1、初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。

  • 2、添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数,objc_storeWeak() 的作用是更新指针指向(指针可能原来指向着其他对象,这时候需要将该 weak 指针与旧对象解除绑定,会调用到 weak_unregister_no_lock),如果指针指向的新对象非空,则创建对应的弱引用表,将 weak 指针与新对象进行绑定,会调用到 weak_register_no_lock。在这个过程中,为了防止多线程中竞争冲突,会有一些锁的操作。

  • 3、释放时:调用 clearDeallocating 函数,clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

5.讲一下 OC 的消息机制

  • OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)

  • objc_msgSend底层有3大阶段,消息发送(当前类、父类中查找)、动态方法解析、消息转发

objc_msgSend它具体是如何发送消息:

  1.首先根据receiver对象的isa指针获取它对应的class

  2.优先在class的cache查找method方法,如果找不到,再到methodLists查找

  3.如果没有在class找到,再到super_class查找

  4.一旦找到method这个方法,就执行它实现的IMP。(若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。)

       5.如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX

6、runtime中重要的数据结构:

SEL  表示方法选择器。

Id      id是通用类型指针

Class 表示对象所属的类。

Method   表示类中的某个方法。

Ivar    表示类中的实例变量。

IMP本质上就是一个函数指针,指向方法的实现。

Cache  主要用来缓存。

7、runtime如何通过selector找到对应的IMP地址?

每一个类对象中都一个对象方法列表(对象方法缓存)

  • 类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)。

  • 方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现。

  • 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找。

  • 当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找。 

8.runtime具体应用 

  • 利用关联对象(AssociatedObject)给分类添加属性

  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)

  • 交换方法实现(交换系统的方法)

  • 利用消息转发机制解决方法找不到的异常问题

  • KVC 字典转模型

 Runloop篇

https://www.jianshu.com/p/85f14af8e7cf

Runloop就是一个运行循环,它保证了在没有任务的时候线程不退出,有任务的时候即使响应。Runloop跟线程,事件响应,手势识别,页面更新,定时器都有着紧密联系。

深入了解推荐ibireme的这篇深入理解RunLoop 

1.Runloop 和线程的关系?

  • 一个线程对应一个 Runloop。

  • 主线程的默认就有了 Runloop。

  • 子线程的 Runloop 以懒加载的形式创建。

  • Runloop 存储在一个全局的可变字典里,线程是 key ,Runloop 是 value。

2.RunLoop的运行模式

  • RunLoop的运行模式共有5种,RunLoop只会运行在一个模式下,要切换模式,就要暂停当前模式,重写启动一个运行模式

    - kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
    - UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
    - kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
    - UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
    - GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
    
    

3.runloop内部逻辑?

  • 实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

    RunLoop

  • 内部逻辑:

    1. 通知 Observer 已经进入了 RunLoop

    2. 通知 Observer 即将处理 Timer

    3. 通知 Observer 即将处理非基于端口的输入源(即将处理 Source0)

    4. 处理那些准备好的非基于端口的输入源(处理 Source0)

    5. 如果基于端口的输入源准备就绪并等待处理,请立刻处理该事件。转到第 9 步(处理 Source1)

    6. 通知 Observer 线程即将休眠

    7. 将线程置于休眠状态,直到发生以下事件之一

      • 事件到达基于端口的输入源(port-based input sources)(也就是 Source0)

      • Timer 到时间执行

      • 外部手动唤醒

      • 为 RunLoop 设定的时间超时

    8. 通知 Observer 线程刚被唤醒(还没处理事件)

    9. 处理待处理事件

      • 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到第 2 步

      • 如果输入源被触发,处理该事件(文档上是 deliver the event)

      • 如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到第 2 步

4.autoreleasePool 在何时被释放?

  • App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

  • 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

5.GCD 在Runloop中的使用?

  • GCD由 子线程 返回到 主线程,只有在这种情况下才会触发 RunLoop。会触发 RunLoop 的 Source 1 事件。

6.AFNetworking 中如何运用 Runloop?

  • AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
    
    + (NSThread *)networkRequestThread {
        static NSThread *_networkRequestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
            [_networkRequestThread start];
        });
        return _networkRequestThread;
    }
    
    
  • RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

    - (void)start {
        [self.lock lock];
        if ([self isCancelled]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        } else if ([self isReady]) {
            self.state = AFOperationExecutingState;
            [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
        [self.lock unlock];
    }
    
    
  • 当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

7.PerformSelector 的实现原理?

  • 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

  • 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

8.PerformSelector:afterDelay:这个方法在子线程中是否起作用?

  • 不起作用,子线程默认没有 Runloop,也就没有 Timer。可以使用 GCD的dispatch_after来实现

9.事件响应的过程?

  • 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

  • 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

  • _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

10.手势识别的过程?

  • 当 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

  • 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调。

  • 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

11.CADispalyTimer和Timer哪个更精确

CADisplayLink 更精确

  • iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

  • NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

  • CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

12.可以用Runloop实现什么功能?

  • 检测卡顿
  • 线程保活
  • 性能优化,将一些耗时操作放到runloop wait的情况处理。

Runloop 开发中运用

AutoreleasePool、事件响应、手势识别、界面更新、定时器、PerformSelecter、GCD、网络请求AFNetworking(AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop)

滑动与图片刷新:当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

实际上 RunLoop 底层也会用到 GCD 的东西,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

 

多线程 

1.进程与线程

  • 进程:

    1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.

    2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.

    3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源

  • 线程

    1.程序执行流的最小单元,线程是进程中的一个实体.

    2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程

  • 进程和线程的关系

    1.线程是进程的执行单元,进程的所有任务都在线程中执行

    2.线程是 CPU 分配资源和调度的最小单位

    3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程

    4.同一个进程内的线程共享进程资源

2.什么是多线程?

  • 多线程的实现原理:事实上,同一时间内单核的CPU只能执行一个线程,多线程是CPU快速的在多个线程之间进行切换(调度),造成了多个线程同时执行的假象。

  • 如果是多核CPU就真的可以同时处理多个线程了。

  • 多线程的目的是为了同步完成多项任务,通过提高系统的资源利用率来提高系统的效率。

3.多线程的优点和缺点

  • 优点:

    能适当提高程序的执行效率

    能适当提高资源利用率(CPU、内存利用率)

  • 缺点:

    开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能

    线程越多,CPU在调度线程上的开销就越大

    程序设计更加复杂:比如线程之间的通信、多线程的数据共享

4.多线程的 并行 和 并发 有什么区别?

  • 并行:充分利用计算机的多核,在多个线程上同步进行

  • 并发:在一条线程上通过快速切换,让人感觉在同步进行

5.iOS中实现多线程的几种方案,各自有什么特点?

  • NSThread 面向对象的,需要程序员手动创建线程,但不需要手动销毁。子线程间通信很难。

  • GCD c语言,充分利用了设备的多核,自动管理线程生命周期。比NSOperation效率更高。

  • NSOperation 基于gcd封装,更加面向对象,比gcd多了一些功能。

6.多个网络请求完成后执行下一步

  • 使用GCD的dispatch_group_t

    创建一个dispatch_group_t

    每次网络请求前先dispatch_group_enter,请求回调后再dispatch_group_leave,enter和leave必须配合使用,有几次enter就要有几次leave,否则group会一直存在。

    当所有enter的block都leave后,会执行dispatch_group_notify的block。

NSString *str = @"http://xxxx.com/";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];

dispatch_group_t downloadGroup = dispatch_group_create();
for (int i=0; i<10; i++) {
    dispatch_group_enter(downloadGroup);

    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%d---%d",i,i);
        dispatch_group_leave(downloadGroup);
    }];
    [task resume];
}

dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

  • 使用GCD的信号量dispatch_semaphore_t
  • dispatch_semaphore信号量为基于计数器的一种多线程同步机制。如果semaphore计数大于等于1,计数-1,返回,程序继续运行。如果计数为0,则等待。dispatch_semaphore_signal(semaphore)为计数+1操作,dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)为设置等待时间,这里设置的等待时间是一直等待。

    创建semaphore为0,等待,等10个网络请求都完成了,dispatch_semaphore_signal(semaphore)为计数+1,然后计数-1返回

NSString *str = @"http://xxxx.com/";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {

    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%d---%d",i,i);
        count++;
        if (count==10) {
            dispatch_semaphore_signal(sem);
            count = 0;
        }
    }];
    [task resume];
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

7.多个网络请求顺序执行后执行下一步

  • 使用信号量semaphore

    每一次遍历,都让其dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER),这个时候线程会等待,阻塞当前线程,直到dispatch_semaphore_signal(sem)调用之后

NSString *str = @"http://www.jianshu.com/p/6930f335adba";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {

    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        NSLog(@"%d---%d",i,i);
        dispatch_semaphore_signal(sem);
    }];

    [task resume];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"end");
});

8.异步操作两组数据时, 执行完第一组之后, 才能执行第二组

  • 这里使用dispatch_barrier_async栅栏方法即可实现

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
    NSLog(@"第一次任务的主线程为: %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    NSLog(@"第二次任务的主线程为: %@", [NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"第一次任务, 第二次任务执行完毕, 继续执行");
});

dispatch_async(queue, ^{
    NSLog(@"第三次任务的主线程为: %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
    NSLog(@"第四次任务的主线程为: %@", [NSThread currentThread]);
});

9.多线程中的死锁?

死锁是由于多个线程(进程)在执行过程中,因为争夺资源而造成的互相等待现象,你可以理解为卡主了。产生死锁的必要条件有四个:

  • 互斥条件 : 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

  • 请求和保持条件 : 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  • 不可剥夺条件 : 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  • 环路等待条件 : 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

    最常见的就是 同步函数 + 主队列 的组合,本质是队列阻塞。

dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2");
});

NSLog(@"1");
// 什么也不会打印,直接报错

10.GCD执行原理?

  • GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护(看到这句话是不是很开心?) 而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。

  • 如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。

  • 如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。

  • 这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开5~8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3~5条最为合理。

11. 什么时候选择使用GCD,什么时候选择NSOperation

项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。

项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议在简单项目中使用。

性能优化

面试中常常问道性能优化的问题,其中有几个主要的

  • 你在项目中是怎么优化内存的?
  • 优化你是从哪几方面着手?
  • 列表卡顿的原因可能有哪些?你平时是怎么优化的?
  • 遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?

CPU和GPU

在屏幕成像的过程中,CPU和GPU起着至关重要的作用

  • CPU(Central Processing Unit,*处理器)
  • 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
  • GPU(Graphics Processing Unit,图形处理器)
  • 纹理的渲染 

     2021iOS高频(基础+底层)面试题汇总
  • 在iOS中是双缓冲机制,有前帧缓存、后帧缓

 2021iOS高频(基础+底层)面试题汇总

 2021iOS高频(基础+底层)面试题汇总

卡顿解决的主要思路

  • 尽可能减少CPU、GPU资源消耗
  • 按照60FPS的刷帧率,每隔16ms就会有一次VSync信号
  • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
  • 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
  • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
  • Autolayout会比直接设置frame消耗更多的CPU资源
  • 图片的size最好刚好跟UIImageView的size保持一致
  • 控制一下线程的最大并发数量
  • 尽量把耗时的操作放到子线程
  • 文本处理(尺寸计算、绘制)
  • 图片处理(解码、绘制)
  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
  • 尽量减少视图数量和层次
  • 减少透明的视图(alpha<1),不透明的就设置opaque为YES
  • 尽量避免出现离屏渲染

离屏渲染

在OpenGL中,GPU有2种渲染方式:

  • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
  • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作

离屏渲染消耗性能的原因

  • 需要创建新的缓冲区
  • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
    #######哪些操作会触发离屏渲染?
  • 光栅化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
  • 考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
  • 阴影,layer.shadowXXX
  • 如果设置了layer.shadowPath就不会产生离屏渲染

卡顿检测

  • 平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
  • 可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的

耗电的主要来源

  • CPU处理,Processing
  • 网络,Networking
  • 定位,Location
  • 图像,Graphics

耗电优化

  • 尽可能降低CPU、GPU功耗
  • 少用定时器
  • 优化I/O操作
  • 尽量不要频繁写入小数据,最好批量一次性写入
  • 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
  • 数据量比较大的,建议使用数据库(比如SQLite、CoreData)

网络优化

  • 减少、压缩网络数据
  • 如果多次请求的结果是相同的,尽量使用缓存
  • 使用断点续传,否则网络不稳定时可能多次传输相同的内容
  • 网络不可用时,不要尝试执行网络请求
  • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
  • 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

定位优化

  • 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
  • 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
  • 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
  • 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
  • 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:

硬件检测优化

用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

APP的启动

  • APP的启动可以分为2种
  • 冷启动(Cold Launch):从零开始启动APP
  • 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP

APP启动时间的优化,主要是针对冷启动进行优化
通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1

  • APP的冷启动可以概括为3大阶段
  • dyld
  • runtime
  • main

     2021iOS高频(基础+底层)面试题汇总

     

APP的启动 - dyld

dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

  • 启动APP时,dyld所做的事情有

  • 装载APP的可执行文件,同时会递归加载所有依赖的动态库

  • 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理

APP的启动 - runtime

  • 启动APP时,runtime所做的事情有

  • 调用map_images进行可执行文件内容的解析和处理

  • 在load_images中调用call_load_methods,调用所有Class和Category的+load方法

  • 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)

  • 调用C++静态初始化器和attribute((constructor))修饰的函数

  • 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理

总结一下

  • APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库

  • 并由runtime负责加载成objc定义的结构

  • 所有初始化工作结束后,dyld就会调用main函数

  • 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

APP的启动优化

  • 按照不同的阶段

  • dyld

  • 减少动态库、合并一些动态库(定期清理不必要的动态库)

  • 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)

  • 减少C++虚函数数量

  • Swift尽量使用struct

  • runtime

  • 用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、ObjC的+load

  • main

  • 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中

  • 按需加载

安装包瘦身

  • 安装包(IPA)主要由可执行文件、资源组成

  • 资源(图片、音频、视频等)

  • 采取无损压缩

  • 去除没有用到的资源: https://github.com/tinymind/LSUnusedResources

  • 可执行文件瘦身

  • 编译器优化

  • Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES

  • 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions

  • 利用AppCode(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code

  • 编写LLVM插件检测出重复代码、未被调用的代码

LinkMap

 

 

上一篇:iOS中的系统目录(Documents、tmp、Library)、RunLoop的一些知识点


下一篇:Runloop源码