美文网首页
面试题难点底层逻辑

面试题难点底层逻辑

作者: Kevin_wzx | 来源:发表于2022-09-08 14:32 被阅读0次

目录

1.多线程管理
2.RunLoop
3.Runtime
4.内存管理
5.性能(内存)优化举例
6.App 编译与启动以及 App 启动如何优化
7.自动释放池 autoreleasepool
8.网络通信 Http、Https、TCP、UDP、IP
9.数据安全之 HTTPS 的加密方式和单双向认证的理解
10.推送、Socket
11.Block理解
12.锁有哪些,都怎么用,为什么用锁
13.几大设计模式
14.OC 底层之 KVC、KVO、Delegate、分类、扩展、通知
15.isa指针指向什么,讲一下这个指针?(属于Runtime相关面试题)
16.实例、类、元类三者的关联?
17.第三方库SD原理以及AFNet网络封装
18.iOS消息转发机制
19.数据存储和缓存
20.技巧使用:线上线下bug分析与日志、instrument、卡顿闪退监控
21.OC 与 Swift 比较及混合开发
22.跨平台 flutter、unitApp、组件化路由

1.多线程管理

https://www.cnblogs.com/kenshincui/p/3983982.html
https://www.jianshu.com/p/7649fad15cdb
https://www.jianshu.com/p/2c5840b96ff1
补充:https://www.jianshu.com/p/4f3c4cde8177
面试题:https://juejin.cn/post/7008445931116822535

  • 多线程的三种实现技术怎么选择?

1.NSThread:基于C语言,比较轻量化,使用简单。缺点是需要程序员自己去操作线程、管理线程的生命周期等,比较麻烦。

2.NSOperation:
1.基于OC语言,是面向对象的,不需要程序员管理线程
2.封装程度没有GCD高,所以相对比较自由;当需要进行自定义线程管理的时候可以使用这个,即需要去操作一些线程的就用这个
举例:比如一些暂停、重启等线程操作,比如用于自定义一些三方框架等对线程操作

3.GCD:
1.基于C语言,提供了强大的函数,API更简洁
2.以block的形式进行回调,代码更精简,封装性比较强(所以自由性比较弱)
举例:如果用于一些简单的操作,比如一些同步、单例等可以用这个

  • GCD的一些知识点?
  • 1.两种队列的区别:(有串行队列、并发队列;两者都符合FIFO即先进先出原则)

1.两者的执行顺序不同,以及开启的线程数不同。
2.串行是一次只有一个任务被执行;只开启一个线程,一个任务执行完毕后执行下一个任务。
3.并发是多个任务可以同时执行,可以开启多个线程同时执行任务。(注意:并发队列的并发功能只有在异步函数下才有效)

  • 2.执行任务的方式:同步、异步
    1.dispatch_sync同步函数会发生堵塞;堵塞在同步函数以下的函数,即堵塞在接下来的函数操作(因为FIFO原则,按顺序执行)
  • 3.GCD死锁
    死锁就是线程相互等待造成;通俗的解释:面试官说只要你告诉我死锁就让你进公司,面试者说只要你让我进公司就告诉你死锁。


    死锁
将主线程放到同步线程会出现死锁 死锁
  • 4.GCD通过信号量控制并发数
    注意:NSoperation可以直接设置并发数,就没有这么麻烦了
  • 5.GCD:2个核心就是任务和队列;在线程池里取出队列,把任务添加放进队列里面进行执行
  • 6.GCD:栅栏
    场景:有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作;可以通过栅栏实现,也可以通过线程组来实现。

栅栏实现:这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务,需要用到dispatch_barrier_async方法在两个操作组间形成栅栏

  • 7.使用GCD的时候如何让线程同步?目前就三种(6的栅栏当中就是一种)
    1.dispatch_group(线程组):
    2.dispatch_barrier(栅栏)
    3.dispatch_semaphore(信号量)

实际应用:在开发中我们需要等待某个网络回调完之后才执行后面的操作,即上面说的同步。具体例子比如做通讯录的时候需要判断权限,才能获取通讯录

其他问法:如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

  • 信号量特别说明(https://www.jianshu.com/p/04ca5470f212
    有三个函数:
    dispatch_semaphore_create 创建一个semaphore;(可以理解成信号总量)
    dispatch_semaphore_signal 发送一个信号;(信号总量+1)
    dispatch_semaphore_wait 等待信号;(当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1)

注意:信号量为0则阻塞线程,大于0则不会阻塞。我们通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。

// 创建队列组
    dispatch_group_t group = dispatch_group_create();   
// 创建信号量,并且设置值为10
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);   
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);   
    for (int i = 0; i < 100; i++)   
    {   // 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会
执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到
执行了Block的dispatch_semaphore_signal 才会继续执行

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);   
        dispatch_group_async(group, queue, ^{   
            NSLog(@"%i",i);   
            sleep(2);   
// 每次发送信号则semaphore会+1,
            dispatch_semaphore_signal(semaphore);   
        });   
    }   

上面的注释已经把如何通过控制信号量来控制线程同步解释的比较浅显了。关键就是这句:
// 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行

  • 1.NSOperation 实现多线程的使用步骤分为三步:
    1.创建操作:先将需要执行的操作封装到一个 NSOperation 对象中
    2.创建队列:创建 NSOperationQueue 对象
    3.将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中
    之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。
  • 2.相比GCD:
    NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象;比 GCD 更简单易用、代码可读性也更高。既然是基于 GCD 的更高一层的封装,那么GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)和队列(操作队列)的概念。
  • 3.操作(Operation)
    执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
  • 4.操作队列(Operation Queues)

1.注意这里的队列指操作队列,即用来存放操作的队列,不同于 GCD 中的调度队列 FIFO(先进先出)的原则。
解释:NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)

2.操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行

3.NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行

2.RunLoop

1.RunLoop 概念

字面意思 Run 表示运行,Loop 表示循环,结合在一起就是运行循环的意思;RunLoop 实际上是一个对象,它是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行。RunLoop 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。简单理解 RunLoop 就是在内部不停地做 do while 循环,当满足条件(比如有source)就激活 runloop,没有事件就休眠。

总结 Runloop 的基本作用:

  • 保持程序的持续运行
  • 处理APP中的各种事件(触摸、定时器、performSelector)
  • 节省cpu资源、提供程序的性能:该做事就做事,该休息就休息
    说明:有消息需要处理时立刻被唤醒,由内核态切换到用户态
    没有消息处理时休眠避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)

相关链接:
https://juejin.cn/post/7096034109524279309
https://www.jianshu.com/p/d260d18dd551

官方 RunLoop 模型图;从上图中可以看出,RunLoop 就是线程中的一个循环,RunLoop 会在循环中会不断检测,通过 Input sources(输入源)和 Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候让线程进行休息

2.RunLoop 与线程关系

RunLoop 和线程是息息相关一一对应,其映射关系是保存在一个全局的 Dictionary 里;线程的作用是用来执行特定的一个或多个任务,在默认情况下线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够不断地处理任务,并不退出,所以我们就有了 RunLoop。

1.一个线程对应一个 RunLoop,每条线程都有唯一一个与之对应的 RunLoop 对象,RunLoop 和 Mode 是一对多,Mode 和 Source、Timer、Observer 也是一对多。主线程的 RunLoop 对象系统自动帮我们创建好了默认开启的,而子线程的 RunLoop 需要自己主动创建和维护, 默认关闭的,需要自己手动开启。
2.RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
3.RunLoop 并不保证线程安全;我们只能在当前线程内部操作当前线程的 RunLoop 对象,而不能在当前线程内部去操作其他线程的 RunLoop 对象方法。一个 RunLoop 对象中可能包含多个 Mode,且每次调用 RunLoop 的主函数时只能指定其中一个 Mode(CurrentMode),切换 Mode 需要重新指定一个 Mode,这个主要为了分隔开不同的 Source、Timer、Observer 让他们之间互不影响。当 RunLoop 运行在 Mode1 上时,是无法接受处理 Mode2 或 Mode3 上的 Source、Timer、Observer 事件的。

相关链接:
https://www.cnblogs.com/huangzs/p/7574260.html
https://www.jianshu.com/p/71d8bc0e2497

3. 主线程的 RunLoop 原理(比如 main.m 文件)

我们在启动一个 iOS 程序的时候,系统会调用创建项目时自动生成的 main.m 的文件。main.m 文件如下图所示,其中 UIApplicationMain 函数内部帮我们开启了主线程的 RunLoop。UIApplicationMain 内部拥有一个无限循环的代码,只要程序不退出/崩溃,它就一直循环,这个 main.m 的代码中主线程开启 RunLoop 的过程可以简单的理解为如下图2代码。其中 do while 循环 (while(为真),do{执行}),所以图2 running 一直为真所以一直执行,无限循环。return 0 为 main 函数的。

图1:main.m文件;UIApplicationMain 内部默认开启了主线程的 RunLoop,并执行了一段无限循环的代码(不是简单的 for 循环或 while 循环 图2:可看出程序一直在 do-while 循环中执行,所以 UIApplicationMain 函数一直没有返回,我们在运行程序之后程序不会马上退出,而是不断地接收处理消息以及等待休眠,会保持持续运行状态 C 语言 return 0 与 return 1

4.RunLoop 相关类

RunLoop 的数据结构,NSRunLoop(Foundation)是 CFRunLoop(CoreFoundation)的封装,提供了面向对象的 API。RunLoop 相关的主要涉及五个类:CFRunLoop:RunLoop 对象、CFRunLoopMode:运行模式、CFRunLoopSource:输入源/事件源、CFRunLoopTimer:定时源、CFRunLoopObserver:观察者。CFRunLoop 由 pthred(线程对象,说明 RunLoop 和线程是一一对应的)、currentMode(当前所处的运行模式)、 modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、 commonModelItems(Observer,Timer,Source 集合)构成。

说明: Core Foundation 是一组 C 语言接口,Foundation 用 Objective-C 封装了 Core Foundation 的 C 组件,并实现了额外了组件供开发人员使用,两种框架有所不同。

runloop的mode作用是什么?相关链接:
https://www.jianshu.com/p/730adaa296f4
https://www.cnblogs.com/haotianToch/p/6442860.html

RunLoop5个相关类 RunLoop相关类关系图 runloop的mode作用;其中kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步 Source/Timer/Observer 到多个 Mode 中的一种解决方案

5.RunLoop原理

猜想runloop内部是如何实现的? 相关链接:
https://blog.csdn.net/fengjun_1234/article/details/51930693?utm_source=jiancool
https://www.jianshu.com/p/66229ed12216

RunLoop运行逻辑图 RunLoop运行逻辑图-详细说明

6.RunLoop实战应用

相关链接:
https://juejin.cn/post/6844904090351353869
https://www.jianshu.com/p/b0b686cddca6

  • 1. NSTimer的使用

具体讲解:NSTimer是最常用的定时器创建方式,比较常用的创建方法有以下两种:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

两种方法创建的定时器区别在于以下点:
1.scheduledTimerWithTimeInterval 在主线程创建的定时器会在创建后自动将 timer 添加到主线程的 runloop 并启动,主线程的 runloopMode 为
NSDefaultRunLoopMode,但是在 ScrollView 滑动时执行的是
UITrackingRunLoopMode,NSDefaultRunLoopMode 被挂起,定时器失效,等到停止滑动才恢复;因此需要将 timer 分别加入 UITrackingRunLoopMode 和 NSDefaultRunLoopMode 中:

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop mainRunLoop] addTimer:timer forMode: UITrackingRunLoopMode];

或者直接添加到 NSRunLoopCommonModes 中:

[[NSRunLoop mainRunLoop] addTimer:timer forMode: NSRunLoopCommonModes];

也可新开一个子线程,主线程的 runloop 是自动开启的,但子线程的 runloop 需要手动开启,代码如下:

   __block NSInteger count = 0;
   [NSThread detachNewThreadWithBlock:^{
       _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
           count++;
           NSLog(@"第%li次", count);
      }];
      [[NSRunLoop currentRunLoop] run];
   }];
如果用 timerWithTimeInterval 创建则需要手动添加到 runloop 中;

2.timerWithTimeInterval 创建的定时器不会直接启动,而需要手动添加到 runloop 中;为防止出现滑动视图时定时器被挂起,可直接添加到 NSRunLoopCommonModes;

  • 2. ImageView 推迟显示(UI 任务分解)

有时候,我们会遇到这种情况:当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。怎么解决这个问题呢?这时候我们应该推迟图片的显示,也就是ImageView推迟显示图片。有下面两种方法。

方法1 - 利用 PerformSelector 设置当前线程的 RunLoop 的运行模式(在 UIScrollView 停止滚动时设置图片)

利用 performSelector 方法为 UIImageView 调用 setImage: 方法,并利用 inModes 将其设置为仅在 RunLoop 的 NSDefaultRunLoopMode 运行模式下对 imageView 进行图片设置。而当 UIScrollView 滑动时,处于 UITrackingMode,则不会设置图片。

setImage 操作必须在主线程执行,会包括图片解码和渲染两个阶段。频繁调用或者图片解码耗时,则很容易影响用户体验。通过以上方式可以很好地优化体验,另外对图片进行异步解码也是一个很好的优化思路,甚至可以将解码操作提前放到 runloop 空闲的时候去做。

代码如下:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];

方法2 - 监听 UIScrollView 的滚动

因为 UITableView 继承自 UIScrollView,所以我们可以通过监听 UIScrollView 的滚动,实现 UIScrollView 相关 delegate 即可,可以基于 runloop 的原理进行任务拆分,监听 runloop 的 BeforeWaiting 事件,每一次 runloop 循环加载一张图片,用 block 来包装一个 loadImageTask。

添加 runloop 的 observer 的方式如下:

typedef void(^BlockTask)(void);

/// 用于存储self对象本身
static void *ViewControllerSelf;

@property (nonatomic, strong) NSMutableArray<BlockTask> *loadImageTasks;

- (void)addRunloopObserver {
    /// runloop即将进入休眠时候,则会触发该callback;而每个runloop周期都有即将进入休眠的时机,所以用户滚动时callback会一直调用。
    /// 如果没有任何用户操作,则静止时runloop进入休眠,不会触发callback了。
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)self,
        &CFRetain,
        &CFRelease,
        NULL
    };
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &RunloopObserverCallBack, &context);
    
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    
    
    /// 而如果添加了这个timer,则用户停止滚动时,回调也会一直被调用。因为timer会唤醒runloop。
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.0001 repeats:YES block:^(NSTimer * _Nonnull timer) {
        /// nothing
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

/// 如何将loadImageTask的任务(需要该ViewController的实例对象)提供给该回调函数。
void RunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"RunloopObserverCallBack");
    /// 每次触发该回调,都从tasks中出列一个任务执行,即每次回调加载一张图片。
    
    /// 方法1,使用static变量存储self对象。
    ViewController *self = (__bridge ViewController *)ViewControllerSelf;
    
    /// 方法2,使用CFRunLoopObserverContext来传递self对象。
    if (self.loadImageTasks.count == 0) {
        return;
    }
    
    BlockTask task = self.loadImageTasks.firstObject;
    task();
    [self.loadImageTasks removeObjectAtIndex:0];
}
  • 3.后台常驻线程(即需要保持 runloop 一直运行)

我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。那么怎么做呢?添加一条用于常驻内存的强引用的子线程,在该线程的 RunLoop下添加一个 Sources,开启 RunLoop,注意子线程中开启 runloop 需要使用到 autoreleasepool,不会内存泄露。

具体怎么创建一个常驻线程以及具体代码如下:
1.为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
2.向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
3.启动该RunLoop

  @autoreleasepool {

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

        [runLoop run];
    }
线程的runloop一直运行的前提条件就是:必须有一个Mode Item,即Source、Timer、Observer之一,详解如图 AFNetworking 2.x的常驻线程 推荐方式
  • 4.怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?(滑动体验)

当我们在子线程请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。我们就可以将更新 UI 事件放在主线程的 NSDefaultRunLoopMode 上执行即可,这样就会等用户不再滑动页面,主线程 RunLoop 由 UITrackingRunLoopMode 切换到 NSDefaultRunLoopMode 时再去更新 UI。

[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
  • 5. PerformSelector 相关

PerformSelector 的实现原理?

这类方法的本质其实就是使用NSTimer。

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

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

3.对于上面2种方法如果对应线程没有 RunLoop 就会失效 ,所以对于子线程,只能使用 dispatch_after 来做到延时操作,因为 GCD 启动子线程,内部其实用到了 runloop。

讲解:

可以利用 runloopMode,如仅在 Default Mode 下设置 UIImageView 的图片,以免 UIScrollView 的滚动受到影响, 比如微博,滑动停止时候,图片一个个展示出来;performSelector:withObject:afterDelay:inModes: 方法可以在指定 runloopMode 中执行任务,如仅在 DefaultMode 下给 UIImageView 设置图片,则 UIScrollView 滚动时,设置图片的任务不会执行,以保证滚动的流畅性,一旦停止处理 DefaultMode 再进行图片设置;可以使用 cancelPreviousPerformRequestsWithTarget: 和 cancelPreviousPerformRequestsWithTarget:selector:object: 来将正在排队的任务取消。

PerformSelector:afterDelay: 这个方法在子线程中是否起作用?为什么?怎么解决?

不起作用,子线程默认没有开启 Runloop,也就 Timer 不起作用。
解决的办法是可以使用 GCD 延时操作 dispatch_after 来实现。关于 dispatch_after 相关链接:https://www.jianshu.com/p/6ebf672e203fhttps://www.cnblogs.com/tomandhua/p/5711368.html

GCD GCD GCD

PerformSelector 相关 - 易错代码分析
代码1:

NSLog(@"1");

dispatch_async(dispatch_get_global_queue(0, 0), ^{

    NSLog(@"2");

    [self performSelector:@selector(test) withObject:nil afterDelay:10];

    NSLog(@"3");
});

NSLog(@"4");

- (void)test {
    NSLog(@"5");
}

打印结果:1423,test 方法并不会执行。
原因分析:如果是带 afterDelay 的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的 RunLoop 中。由于当前线程没有开启 RunLoop,所以该方法会失效。

代码2:(修改:增加 [[NSRunLoop currentRunLoop] run];)

NSLog(@"1");

dispatch_async(dispatch_get_global_queue(0, 0), ^{

    NSLog(@"2");

    [[NSRunLoop currentRunLoop] run];
    [self performSelector:@selector(test) withObject:nil afterDelay:10];

    NSLog(@"3");
});

NSLog(@"4");

- (void)test {
    NSLog(@"5");
}

打印结果:1423,test 方法并不会执行。
原因分析:如果 RunLoop 的 mode 中一个 item 都没有,RunLoop 会退出。即在调用 RunLoop 的 run 方法后,由于其 mode 中没有添加任何 item 去维持 RunLoop 的时间循环,RunLoop 随即还是会退出。所以我们自己启动 RunLoop,一定要在添加 item 后。

代码3:(修改:调换 RunLoop 启动顺序)

NSLog(@"1");

dispatch_async(dispatch_get_global_queue(0, 0), ^{

    NSLog(@"2");

    [self performSelector:@selector(test) withObject:nil afterDelay:10];
    [[NSRunLoop currentRunLoop] run];

    NSLog(@"3");
});

NSLog(@"4");

- (void)test {
    NSLog(@"5");
}

打印结果:14253,test方法会执行。

  • 6.页面渲染,异步绘制
    页面渲染,异步绘制

7.RunLoop 其他举例理解:在下拉刷新表格时如何让上面的轮播图也继续自动轮播

讲解1:

主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性,DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,不会影响到滑动操作(滑动时定时器不工作了)。如果想让定(计)时器继续工作,就切换Mode,也就是滑动与不滑动取决于你timer的runloop的mode是什么模式的(kCFRunLoopDefaultMode::App的默认 Mode,通常主线程是在这个 Mode 下运行的;)。我们可以把 Timer 同时添加到 UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 上,那么如何把 timer 同时添加到多个 mode 上呢?就要用到 NSRunLoopCommonModes 了。
https://www.jianshu.com/p/2ca582f14100

讲解2:

解决UITableView上计时器(Timer)的滑动问题时,要想计时器(Timer)不因UITableView的滑动而停止工作,就得探讨一下RunLoop了。 RunLoop本质和它的意思一样是运行着的循环,更确切的说是线程中的循环。它用来接受循环中的事件和安排线程工作,并在没有工作时让线程进入睡眠状态。所以根据RunLoop的定义,当Timer被滑动过了,误以为没有工作,让它进入睡眠状态了。怎样来避免这种情况呢?我们可以先来了解 RunLoop 的几种模式。RunLoop有Default模式、Connection模式、Modal模式、Event tracking模式和Common模式(具体模式的含义在http://www.cnblogs.com/fmdxiangdui/p/6164350.html介绍)。在Cocoa应用程序中,默认情况下 Common Modes 包含 default modes、modal modes、event Tracking modes。可使用 CFRunLoopAddCommonMode 方法向 Common Modes 中添加自定义modes,因此我们需要把计时器的 RunLoop的Mode 调整为 Common 模式。具体的操作如下:

//将定时器添加到runloop中
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes ];
[[NSRunLoop currentRunLoop] run];//这个地方创建的 NSRunLoop 就属于子线程,手动开启 Runloop

提问:

默认线程中,开启一个定时器,将定时器加入NSRunLoopCommonModes,然后这时界面有一个滑动视图在滚动,请问定时器是否能正常工作?如果正常工作,请问定时器回调方法里的线程是主线程还是子线程?如果不能正常工作,请说出原因?

答:可以工作,在主线程。在默认线程里,开启定时器相当于在默认的 runloop 中加入了一个事件源,如果这时有滑动视图,也就是从默认的 runloop 切换到跟踪模式,系统为了快速响应对屏幕操作的事件,会关闭定时器这样的运算。加入到 NSRunLoopCommonModes 下,相当于告诉系统,在跟踪模式下也需要处理默认 runloop 事件,以下举例代码可以实践验证。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:scrollView];
    scrollView.contentSize = CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT*2);
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, SCREEN_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT)];
    view.backgroundColor = UIColor.redColor;
    [scrollView addSubview:view];
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%@",[NSThread currentThread]);
    } repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

3.Runtime(运行时)

1.什么是 Runtime ?

  • 背景:

1.对于程序的运行需要将源代码转换为可执行的程序,这个过程需要经过3个步骤:编译、链接、运行。

  1. C 语言是一门静态类语言,在编译阶段就已经确定了所有变量的数据类型和确定好了要调用的函数以及函数的实现;OC 是一门动态语言,在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数,只有在运行时才检查变量的数据类型以及根据函数名查找要调用的具体函数,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态的创建类和对象、进行消息传递和转发。
  2. OC 把一些决定性的工作从编译阶段、链接阶段推迟到运行时阶段 的机制使得 OC 变得更加灵活,我们可以在程序运行的时候,动态的去修改一个方法的实现,这也为大为流行的『热更新』提供了可能性,Runtime 是 OC 实现面向对象和运行时(动态)机制的基础。

4.理解 OC 的 Runtime 机制可以帮我们更好的了解这个语言,还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime 要先了解它的核心 - 消息传递机制(Messaging)。

5.高级编程语言想要成为可执行文件需要:“ 先编译为:汇编语言 -> 再汇编为:机器语言 ”,机器语言也是计算机能够识别的唯一语言,但是 OC 并不能直接编译为汇编语言,而是要先转写为纯 C 语言再进行编译和汇编的操作,从 OC 到 C 语言的过渡就是由 Runtime 来实现的。然而我们使用 OC 进行面向对象开发,而 C 语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

  • 概念理解:

Runtime 是一个库,是一个由一系列函数和数据结构组成具有公共接口的动态共享库,这个库使我们可以在程序运行时动态的创建对象、检查对象,修改类和对象的方法;Runtime 是由 C 和 C++、汇编实现的一套 API,为 OC 语言加入了面向对象、运行时的功能,它将数据类型的确定由编译时推迟到了运行时。平时编写的 OC 代码,在程序运行过程中最终会转换成纯 C 语言代码再进行编译和汇编的操作,从 OC 到 C 语言的过渡就是由 Runtime 来实现的。

  • 注意:

OC 的动态性全都是 Runtime 支持的,OC 类的类型和数据变量的类型都是在运行时确定的,而不是在编译时确定。运行时(Runtime)特性,我们可以动态的添加方法,或者替换方法。

相关链接:
快速上手runtime:https://www.jianshu.com/p/e071206103a4
runtime 系统的知识:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

2.Runtime 中涉及的几个概念

  • objc_msgSend

所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector); 是 [receiver selector]; 对应的 C 函数。

  • Class(类)

1.在 objc/runtime.h 中,Class 被定义为指向 objc_class 结构体的指针,objc_class 结构体的数据结构如下代码。
2.从中可以看出 objc_class 结构体定义了很多变量:自身的所有实例变量(ivars)、所有方法定义(methodLists)、遵守的协议列表(protocols)等,objc_class 结构体存放的数据称为元数据(metadata)。
3.objc_class 结构体的第一个成员变量是 isa 指针,isa 指针保存的是所属类的结构体的实例的指针,这里保存的就是 objc_class 结构体的实例指针,而实例换个名字就是对象。换句话说 Class 的本质其实就是一个对象,我们称之为类对象。

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa;                                          // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父类的指针
    const char * _Nonnull name;                                  // 类的名字
    long version;                                                // 类的版本信息,默认为 0
    long info;                                                   // 类的信息,供运行期使用的一些位标识
    long instance_size;                                          // 该类的实例变量大小;
    struct objc_ivar_list * _Nullable ivars;                     // 该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
    struct objc_cache * _Nonnull cache;                          // 方法缓存
    struct objc_protocol_list * _Nullable protocols;             // 遵守的协议列表
#endif

};
  • Object(实例/对象)、Meta Class(元类)

对于实例对象(Object)、类(Class)、Meta Class(元类) 以及他们之间的关系可以参考本篇第16点:实例、类、元类三者的关联讲解部分。

  • Method(方法)

3.Runtime 之消息机制即消息发送(传递)

消息机制的基本原理 方法的本质 SEL 和 IMP 的关系 Runtime 作用之发送消息举例

4.Runtime 之消息转发

在讲2.Runtime 之消息机制的最后一步中我们提到:若找不到对应的 selector,消息被转发或者临时向 recevier 添加这个 selector 对应的实现方法,否则就会发生崩溃。当一个方法找不到的时候,Runtime 提供了:“ 消息动态解析、消息接受者重定向、消息重定向 ”等三步处理消息,具体流程如下图。

具体理解:https://www.jianshu.com/p/633e5d8386a8

Runtime 之消息转发

5.Runtime 的应用场景理解

  • 1.weak释放nil的过程

Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。(Hash表你理解成字典就行)

  • 2.iOS消息转发机制原理?(Runtime相关)

https://www.jianshu.com/p/45db86af7b60

基本分为三个步骤:1.动态方法解析 2.备用接受者 3.完整转发

  • 动态方法解析
    对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)进行判断。如果YES则能接受消息,NO不能接受消息,进入第二步

  • 备用接受者
    动态方法解析无法处理消息,则会走备用接受者。这个备用接受者只能是一个新的对象,不能是self本身,否则就会出现无限循环。如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果

  • 完整消息转发
    如果第2步返回self或者nil,则说明没有可以响应的目标,则进入第三步走完整消息转发

  • 3.runtime的实际应用举例

例子1:友盟统计埋点时候交换系统方法
友盟的方法照着文档写就可以;runtime那个就是交换系统的方法达到无侵入的埋点。主要看要交换的方法,比如最常见的vc的那几个周期方法。无侵入的意思就是不要到处都去写。

例子2:修改重写KVC的undefinedKey方法
KVC键值对的时候key找不到vaue的时候就会调用undefinedKey方法,就无法字典转模型,这个时候可以利用runtime修改重写这个undefinedKey方法解决奔溃,就可以继续使用KVC进行字典转模型来使用。

4.内存管理

5.性能(内存)优化举例

iOS 保持界面流畅的技巧:https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
iOS 性能优化之内存优化:https://www.jianshu.com/p/8662b2efbb23
iOS 性能优化知识梳理:https://zhuanlan.zhihu.com/p/356426277
iOS 性能优化方案总结:https://www.jianshu.com/p/fdb2d167a6bb

总结下来,主要有下面几方面原因导致内存占用高:
1.使用了不合理的API

2.网络下载的图片过大
3.第三方库的缓存机制
4.Masonry布局框架
5.没必要常驻内存的对象,实现为常驻内存
6.数据模型中冗余的字段
7.内存泄漏

1.图片相关:

图片加载显示的大致过程:
1.加载:从磁盘读取图片并加载到内存。(data buffer)
2.解码:CPU对图片进行解码,获取图片原始数据。(image buffer)
3.渲染:渲染图片,生成 frame buffer,显示硬件会从 frame buffer 中读取显示到屏幕上。

注意:png等图片都是压缩的需要解码; 一般我们使用的图像是JPG/PNG,这些图像数据不是位图,而是经过编码压缩后的数据,使用它渲染到屏幕之前需要进行解码转成位图数据,这个解码操作是比较耗时的,并且没有GPU硬解码,只能通过CPU,iOS默认会在主线程对图像进行解码。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。

相关链接:
https://blog.csdn.net/olsQ93038o99S/article/details/121058817
http://t.zoukankan.com/ldnh-p-5270135.html
https://blog.csdn.net/houwenjie11/article/details/52983072
https://www.jianshu.com/p/3b57f60ce4a9

1.1 图片加载方式有imageNamed和imageWithContentsOfFile;仅使用一次或是使用频率很低的大图片资源,应该使用后者,不会缓存,再没有被引用会清除。

1.2 在没有必要的情况下,使用了-[UIColor colorWithPatternImage:]这个方法,比如将label的背景色设定为一个图片会用到。这个方法会引用到一个加载到内存中的图片,然后又会在内存中创建出另一个图像,而图像的内存占用是很大的。

1.3 如果用于显示图片的视图很小,而下载的图片很大,那么我们应该对图片进行缩放处理,然后将缩放后的图片保存到SDWebImage的内存缓存中加载网络数据下载图片时,使用异步加载并缓存

iOS图片存放的3种方式:https://juejin.im/post/6844903978262773774

图像显示原理

2.数据模型相关(预排版):

2.1 预排版:当获取到 API JSON 数据后,我会把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。

对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。为了达到最高性能,你可能需要牺牲一些开发速度,不要用 Autolayout 等技术,少用 UILabel 等文本控件。但如果你对性能的要求并不那么高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。这里有个来自百度知道团队的开源项目可以很方便的帮你实现这一点:FDTemplateLayoutCell

简单举例:比如通过子线层先获取计算数据再计算高度(如AFNetWorking加载数据就是算子线层,GCD里面计算数据高度比如字符串算Label高度)。在用 tableView 和 UICollectionView 显示内容时,有时会出现复杂布局的Cell,为了优化性能,我们可以把布局数据计算好,就是常规的在Model层请求数据后提前将cell高度算好,在加载时直接显示,很好的优化性能增强用户体验。

相关链接:http://t.zoukankan.com/alan12138-p-11679350.html

2.2 去除冗余的字段:对于从服务端返回的数据,解析为模型时,随着版本的迭代,可能有一些字段已经不再使用了。如果这样的模型对象会生成很多,那么对于模型中的冗余字段进行清理,也可以节省一定数量的内存占用。

3.内存泄漏:

内存泄漏会导致应用的内存占用一直升高,需要规范防止循环引用的发生。基于此,在项目中引入ReactiveObjC中的两个牛X的宏,@weakify, @strongify,并遵循以下写法规范:在block外部使用@weakify(self),可以一次定义多个weak引用。
在block内部的开头使用@strongify(self),可以一次定义多个strong引用。

4.UITableviewCell:(UITableview的卡顿内存优化?)

4.1 重用单元格,cell的重用, 注册重用标识符

4.2 避免cell的重新布局(比较耗时)减少视图数目等方式对表格视图进行优化

4.3通过使用不透明的视图提高渲染速度;不要使用ClearColor,无背景色,透明度也不要设置为0(渲染耗时比较长)

4.4使用局部更新,尽量避免全局更新

4.5 尽量不要切圆角操作,耗性能,可以通过贝泽尔曲线绘制

5.尽量避免出现离屏渲染

离屏渲染:指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区(离屏缓存区)进行渲染操作。等所有数据都在离屏渲染区完成渲染后才会提交到帧缓存区,然后再被显示。

离屏渲染存在的性能问题:(多耗费:空间、时间、性能)
1.相比于正常的渲染流程,离屏渲染需要额外创建一个缓冲区,需要多耗费一些空间
2.触发离屏渲染后,需要先从 Frame Buffer 切换到 Off-Screen Buffer ,渲染完毕后再切换回 Frame Buffer ,这一过程需是比较耗费性能的,因为要来回切换上下文;
3.数据由 Off-Screen Buffer 取出,再存入 Frame Buffer 也需要耗费时间,这样增加了掉帧的可能性;

为何要使用离屏渲染:
1.有些后续经常用到的图层数据,可以先缓存在离屏缓存,用到时直接复用。
2.存在一些特殊效果,正常流程无法完成,必须使用离屏渲染,比如圆角、阴影和遮罩、高斯模糊、半透明图层混合等正常的渲染流程采用油画算法由远及近的渲染图层,当一个图层显示到屏幕上后,帧缓冲区会立即删除这一图层的数据。

相关链接:
https://cloud.tencent.com/developer/article/1867634
https://tech.souyunku.com/?p=33686
https://www.jianshu.com/p/6d05a7627645

图形渲染管线 图像渲染流程-屏幕显示,CPU计算显示内容–>GPU渲染–>渲染结果放到帧缓冲区(iOS是双缓冲)–>视频控制器按照VSync信号逐行读取帧缓冲区数据,传递给显示器显示 离屏渲染流程

6.App 编译与启动以及 App 启动如何优化

1.App的编译

编译:就是编译器帮你把源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。比如Java只有JVM识别的字节码,C#中只有CLR能识别的MSIL。另外还有啥链接器、汇编器,为了了便于理解我们可以统称为编译器)

iOS App编译:在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM(LLVM 作用就是提供一个广泛的工具,可以将任何高级语言的代码编译为任何架构的 CPU 都可以运行的机器代码。它将整个编译过程分类了三个模块:前端、公用优化器、后端)

iOS系统在编译过程做了什么事情:

  • 预编译(处理):编译器Clang首先预处理我们代码,做一些比如将宏替换到代码中、删除注释、处理预编译命令等工作。

  • 词法分析:这个时候编译就开始了,词法分析器读入预处理过的代码,将其中的字符进行词法单元处理;主要为了在下一步生成语法树做基础工作。

  • 编译-语法分析:将词法单元抽象生成一个语法树;主要为了后面的静态分析。

  • 静态分析 | 中间代码生成:将源代码转化为抽象语法树,编译器进行遍历整个树来做静态分析,在静态分析结束后,编译器会生成一种比较接近机器码的中间代码IR,也是整个编译链接系统的中间产物。

  • 生成汇编代码:LLVM根据优化策略对IR进行一些优化,优化完成后调用汇编生成器将IR转化成汇编代码。此时生成产物就是.o文件(二进制文件)。

  • 链接生成一个可执行文件:链接其实就是一个打包的过程,将编译出的所有.o文件和一些如dylib、.a、tbd文件链接起来(即将所有的目标文件链接),一起合并生成一个Mach-o文件,到这里编译过程全部结束,可执行文件mach-o已生成。

补充:
1.常见的类型检查、语法错误、方法未定义等都是在静态分析中发现并处理的,静态分析能做的事情还有非常多。
2.IR是在iOS编译系统中,前端Clang和后端LLVM的分界点。Clang的任务在生成IR后结束,将IR交付给LLVM后LLVM开始工作。
3.在生成二进制文件后,我们可以通过二进制重排的方式对我们的编译产物进行更进一步的优化,已达到缩小编译产物大小、优化启动速度等目的。

相关链接:
https://www.jianshu.com/p/d41c60b8c930/
https://www.cnblogs.com/wjw-blog/p/12802232.html
http://www.icodebang.com/article/268311
https://blog.csdn.net/lefex/article/details/105592182
https://mp.weixin.qq.com/s/lVqrsms0XjjnHv2RSpw3cQ

App编译

2.App的启动(启动类型)

1.冷启动:进程没有在运行,也没有在后台。这个时候点击桌面应用图标,加载并创建应用进程,直到App内容显示完毕。也就是指 app 被后台杀死后,在这个状态打开 app,这种启动方式叫做冷启动。

2.热启动:这个时候的App是还在运行的,比如你刚刚打开的应用,然后摁下Home键回到了桌面,然后你又点击图标启动了App。也就是指 app 没有被后台杀死,仍然在后台运行,通常我们再次去打开这个 app,这种启动方式叫热启动。

3.举例:(待验证)
例子1:xcode连着手机第一次编译运行成功App安装到手机上显示App内容这个属于冷启动;如果这个时候再次第二次编译运行App跑起来了显示App内容这个属于热启动。
例子2:苹果商店下载好了App,这个时候第一次点击App启动属于冷启动;如果没有退出App只是挂起后台回到手机桌面或者使用其他应用,这个时候再切换回来使用App这个属于热启动。

4.冷启动与热启动的区别:
1.冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
2.热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application。
3.总结:冷启动需要重新创建进程,加载一些资源(可执行文件,动态库加载等等),之后app会去渲染首页,初始化其他业务模块,读取一些配置文件等等。热启动因为进程被保留,只需要拉取数据,绘制页面就行了,启动速度比较快,消耗的资源也相对较少。

3.App的启动过程做了什么事情

热启动通常情况下都是没什么问题的,启动起来也都比较快,主要优化的目标还是冷启动。我们先了解一个冷启动的过程中,系统都做了什么:可分为 pre-main 阶段和 main() 阶段。pre-main 阶段为 main 函数执行之前所做的操作,main 阶段为 main 函数到首页展示阶段。

1.premain阶段

  • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
  • 加载动态链接库加载器dyld(dynamic loader)
  • 定位内部、外部指针引用,例如字符串、函数等
  • 加载类扩展(Category)中的方法
  • C++静态对象加载、调用ObjC的 +load 函数
  • 执行声明为attribute((constructor))的C函数

2.main阶段(程序执行)

  • 调用main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching

补充-APP的入口即main函数的几个参数解读:https://blog.csdn.net/watertekhqx/article/details/71411562

App启动 iOS程序生命周期图

4.App的编译与启动的区别

iOS编译与app启动:https://www.jianshu.com/p/65901441903e
iOS App-从编译到运行:http://www.icodebang.com/article/268311

5.App的启动优化

主要从三个方面来做了启动时间的优化,main之后的耗时方法优化、premain的+load方法优化、二进制重排优化premain时间。

相关链接:
https://juejin.cn/post/6861917375382929415
https://juejin.cn/post/6844904165773328392
https://juejin.cn/post/6844904138048831496
https://juejin.cn/post/6844903490792194062
https://mp.weixin.qq.com/s/h3vB_zEJBAHCfGmD5EkMcw

premain阶段优化:
1.删减无用的类方法
2.减少+load操作
3.减少attribute((constructor))的C函数
4.减少启动加载的动态库

main阶段优化:
1.将启动时非必要的操作延迟到首页显示之后加载
2.统计并优化耗时的方法
3.对于一些可以放在子线程的操作可以尽量不占用主线程

补充说明:
二进制重排:生成汇编代码的时候即生成.o二进制文件后,我们可以通过二进制重排的方式对我们的编译产物进行更进一步的优化,已达到缩小编译产物大小、优化启动速度等目的。

App的启动优化1 App的启动优化2 App的启动优化2 卡顿优化监控 耗电优化监控 网络优化监控

7.自动释放池 autoreleasepool

1.相关概念

1.OC中的一种内存自动回收机制,它可以延迟加入 AutoreleasePool 中的变量 release 的时机。

2.当创建一个对象,在正常情况下,变量会在超出其作用域时立即 release ,如果将其加入到自动释放池中,这个对象并不会立即释放,而会等到 runloop 休眠 / 超出 autoreleasepool 作用域之后进行释放。

3.自动释放池是什么时候创建的?什么时候销毁的?
创建:运行循环检测到事件并启动后,就会创建自动释放池
销毁:一次完整的运行循环结束之前,会被销毁

4.实际测试结果,是运行循环放在内部的速度更快;日常开发中,如果遇到局部代码内存峰值很高,可以引入运行循环及时释放延迟释放对象。

5.所谓自动释放池,指它是一个存放对象的容器(集合),而自动释放池会保证延迟销毁该池中所有的对象。出于自动释放池的考虑,所有的对象都应该添加到自动释放池中,这样可以让自动释放池在销毁之前,先销毁池中的所有对象。

6.自动释放池创建与销毁过程:程序启动到加载完成,主线程对应的 Runloop 处于休眠状态,直到用户点击交互唤醒 Runloop。用户每次交互都会启动一次 Runloop 用来处理用户的点击、交互事件。Runloop 被唤醒后,会自动创建 AutoReleasePool,并将所有延迟释放的对象添加到 AutoReleasePool。在一次完整的 Runloop 执行结束前,会自动向 AutoReleasePool 中的对象发送release消息,然后销毁 AutoReleasePool

7.与RunLoop关联理解(自动释放池不会内存泄露):就是说 AutoreleasePool 创建是在一个 RunLoop 事件开始之前(push),AutoreleasePool 释放是在一个 RunLoop 事件即将结束之前(pop)。 AutoreleasePool 里的 Autorelease 对象的加入是在 RunLoop 事件中,AutoreleasePool 里的 Autorelease 对象的释放是在 AutoreleasePool 释放时。

自动释放池不会内存泄露

相关链接:
https://juejin.cn/post/7010726670181253127
https://www.cnblogs.com/SNMX/p/16434707.html
https://juejin.cn/post/6900043544304713735
https://juejin.cn/post/6844904039239548941

2.相关总结

1.自动释放池的销毁和其他普通对象相同,只要其引用计数为0,系统就会自动销毁自动释放池对象。系统会在调用 NSAotoreleasePool 的 dealloc 方法时回收该池中的所有对象。

2.@autoreleasepool,就是把在它作用域(就是"{}")中的代码,先 push 进去,然后等这些代码都干完活了,再把他们 pop 出去。

3.autoreleasepool 由许许多多的 AutoreleasePoolPage 组成,自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的。当一个AutoreleasePoolPage装满之后,就会创建新的AutoreleasePoolPage,两个Page之间用parent/child互相关联,从而证明双向链表的说法。

4.当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中。调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息。

5.在AutoreleasePoolPage 自身变量的56个字节之后,当 push 对象进 page 时,会先push一个边界符进去 POOL_BOUNDARY。这个边界符也占8个字节。

6.要搞清两个概念,一个是 autoreleasepool,另外一个是 AutoreleasePoolPage,我们应该很清楚的知道 AutoreleasePoolPage 是一个双向链表,为什么要设置多张 Page,其实系统还是为了节省空间,类似我们的内存分页的思想。通过 autpreleasePoolPush 设置哨兵 nil 也就是 begain,里面有个 next 指针指定了下个 obj 的位置,也就是 end。那么在调用 autoreleasePoolPop 的时候其实就是释放 end - begain 的空间,同时给这里的每个对象都发送一个 release方法。

注意:
1.autorelease 方法不会改变对象的引用计数,只是将该对象添加到自动释放池中,该方法会返回调用该方法的对象本身。
2.自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程。

自动释放池-英文 自动释放池-中文 @autoreleasepool 代码

3.MRC、ARC、自动释放池区别

1.自动释放池机制是为了延时释放对象,他的概念看上去很像ARC,但实际更类似于C语言中自动变量的特性。(自动变量:在超出变量作用域后将被废弃;自动释放池:在超出释放池生命周期后,向其管理的对象实例发送 release 消息)

2.MAC 与 ARC 是 OC 的内存管理机制;其中 MAC 是手动管理内存机制,需要手动的通过retain去为对象获取内存,并用 release 释放内存。ARC 是自动管理内存机制,简单地说就是代码中自动加入了 retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

3.从 MRC 到 ARC 的变化,就取决于 @autoreleasepool。其中一些相关名词 autoreleasepool、AutoreleasePoolPage、@autoreleasepool、autorelease 等可以通过这些相关阅读理解。

相关链接:
https://www.jianshu.com/p/7bd2f85f03dc
https://blog.csdn.net/ZCMUCZX/article/details/75043236
https://www.jianshu.com/p/48665652e4e4
https://juejin.cn/post/6844903498107076616

MRC环境中下使用自动释放池 ARC环境中下使用自动释放池1 ARC环境中下使用自动释放池2 iOS MRC与ARC的混合编程 iOS MRC与ARC的混合编程

8.网络通信 Http、Https、TCP、UDP、IP

3次握手

9.数据安全之 HTTPS 的加密方式和单双向认证的理解

  • 几种常用的数据加密方式

1.常见的数据加密方式有对称加密和非对称加密,还有就是 MD5 的不可逆加密
2.对称加密常用的有 AES 对称加密,可用于网络请求的内容加密;还有DES
3.非对称加密常用的有 RSA,还有DSA

补充1:解释对称加密;如AES 加密和解密效率高,双方必须使用同一个秘钥,如果不考虑秘钥被偷窃,那么AES 是很安全的

补充2:解释非对称加密;
概念:如果B只给A传加密数据,那么B需要A给的公钥,B用这个公钥进行加密,A用自己对应的私钥解密即可。

场景问题:公钥是公开的,大家都可以给A传数据,A都能用自己的私钥解开(因为大家都是用对应且公开的公钥加密的),那么A就不晓得到底哪个才会B发送的,所以就有了签名。签名就是用私钥签名(说白了就是用私钥加密,只有公钥才能解开)。为了让A知道是B发送的,所以B需要给A自己的公钥(这个公钥不是上面说的公钥,而是B提供的另一套公私-钥匙)

补充3:Base64 只是编码,不是加密;Base64的意思就是:考虑到多语言原因,有的特殊字符传输不兼容,因为很多都是只支持ASSIC码,那么特殊字符就会找不到对应的ASSIC码,所以采用BASE64 可以叫全天下所有字符用 64中字符表示,而且这64种字符都在 ASSIC 中,所以在网络传输中很流行。

Base64特点:
1.Base64这个算法是编码,算法可逆,解码方便,不用于私密信息通信;
2.虽然解码方便,但毕竟编码了,肉眼还是不能直接看出原始内容;
3.加密后的字符串只有[0-9a-zA-Z+/=],不可打印字符(包括转移字符)也可传输。

  • HTTPS的加密

HTTPS 中的加密过程用到了非对称加密进行密钥的传递,然后再用对称加密就行数据传输。非对称加密是很耗时的一种加密方式,但是比较安全,对称相对没有那么安全,但是效率高。所以兼顾安全与效率,采用了两者结合。
1.服务器端的公钥和私钥,用来进行:非对称加密
2.客户端生成的随机密钥,用来进行:对称加密,HTTPS的对称加密就是加密的实际的数据

小结:
1.对称加密的意思是,大家拿到一样的秘钥进行加密解密。非对称加密的意思是,需要公钥和私钥才能进行加解密。
2.非对称加密可以理解成开始的时候是非对称加密获取对称加密的秘钥。然后拿到对称加密的秘钥之后,再用这个秘钥进行对称加密。因为非对称加密需要的时间更长,长时间的交互数据是不可取的。所以非对称加密只是在一开始拿到对称加密的秘钥用到了,后续的数据交互还是用到了更快捷的对称加密。
3.单向和双向的区别在于,单向的服务器端是不会验证客户端的,并且在给服务器的加密方式也是没有加密的。

图解:

单向认证 双向认证 https加密、解密、验证及数据传输过程

补充1:APP 的安全性主要采用的 HTTPS 双向认证,通过 AFNetworking 的 AFSecurityPolicy 类实现对通道的双向验证,达到对数据交互的安全加 密,之前是采用的 RSA 非对称加密,直接采用的内容加密。(你们项目为什么会用双向认证,你们的证书放到哪的?)

https://blog.csdn.net/superviser3000/article/details/80812263
当时是做贷款的APP,最开始是用的内容加密,因为用的非对称加密,比较的慢,所以才换成了https双向认证;证书是放到服务器的。AFNetworking 的 AFSecurityPolicy 类实现对通道的双向验证这个你就说,放在封装好的几个回调里面,具体的方法名字忘记了

补充2:通过 NSURLProtocol 拦截前端的网络请求,然后通过 HTTPS 双向认证进行加密,这个怎么理解啊?

NSURLProtocol 拦截前端的网络请求这个类的用法就是,先注册然后再方法里面筛选你想拦截的请求。

10.推送、Socket

11.Block理解

  • block本质全面解析

1.原理:https://juejin.cn/post/6844904201970008072
2.变量捕获:https://juejin.cn/post/6844904202003562509
3.类型:https://juejin.cn/post/6844904202041294861
4.copy和strong:https://juejin.cn/post/6844904202850811912
5.捕获的变量何时销毁:https://juejin.cn/post/6844904202909532173
6.block内修改变量的值:https://juejin.cn/post/6845166890738794503
7.实际场景应用:https://juejin.cn/post/6844903597214285837

  • block在内存管理上的特点

https://zhidao.baidu.com/question/555374149788762652.html

  • 在block内如何修改block外部变量?

https://www.jianshu.com/p/a1c8532e172d
https://juejin.cn/post/6845166890738794503

__block修改block外部变量 block仅仅使用局部变量的内存地址而没有修改,这个不需要__block
  • 使用block时什么情况会发生引用循环,如何解决?

一个对象中强引用了block,在block中又强引用了该对象,就会产生循环引用。
解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用。
id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏。id __block weakSelf = self;或者将其中一方强制制空xxx = nil。
检测代码中是否存在循环引用问题,可使用 Facebook 开源的一个检测工具FBRetainCycleDetector

  • 代理和block怎么选择使用?

网络回调对Block和Delegate的对比:https://juejin.cn/post/6844903582601314312
代理和block:https://juejin.cn/post/6844903428229971976
iOS 页面间五种传值(属性,代理 , block,单例,通知):https://juejin.cn/post/6844903440389242888
Block那些事:https://juejin.cn/post/6878108359979958285

代理和block怎么选择 代理和block怎么选择
  • block与__block与__weak与__storng这些啥关系、用法

__block是修饰在block中需要改变的变量,一般的变量就是放在栈上面的,__block相当于将变量从栈拷贝到堆上面。如果不声明的话,编译器会报错的。__weak是将修饰的对象弱引用,一般用在循环引用的时候。破坏掉循环的闭环。__storong是在__weak使用之后在block里面对weak修饰的对象进行一个强引用,这个强引用是防止在block里面被提前释放,但是并不会引发循环引用。

12.锁有哪些,都怎么用,为什么用锁

锁:是保证线程安全常见的同步工具;锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

锁有哪些:锁的分类方式可以根据锁的状态、锁的特性等进行不同的分类;很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。关于锁的分类,可以参考
Java中的锁分类 看一下。大概有:自旋锁、互斥锁、递归锁等等。

为什么用锁:用来保护线程安全的工具;简单比如有时候加载数据列表相关进行上锁,为了防止下拉加载更多的时候没有数据等问题,防止数组越界闪退。

相关链接:http://zenonhuang.me/2018/03/08/technology/2018-03-01-LockForiOS/

13.几大设计模式

1.MVC模式
2.代理模式
3.观察者模式
4.单例模式
5.策略模式
6.简单工厂模式

相关链接:https://cloud.tencent.com/developer/article/1781975

14.OC 底层之 KVC、KVO、Delegate、分类、扩展、通知

  • 分类Category底层的实现原理

通过探索Category底层原理回答以下问题
1.Category是否可以添加方法、属性、成员变量?Category是否可以遵守Protocol?
2.Category的本质是什么,在底层是怎么存储的?
3.Category的实现原理是什么,Catagory中的方法是如何调用到的?
4.Category中是否有Load方法,load方法是什么时候调用的?
5.Category中 load、initialize的区别?

相关链接:https://www.jianshu.com/p/ecc9873a3d8e

15.isa指针指向什么,讲一下这个指针?(属于Runtime相关面试题)

一个objc对象的isa的指针指向他的类对象,从而可以找到对象上的方法。图中实线是 super_class指针,虚线是isa指针。
1.Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
2.每个Class都有一个isa指针指向唯一的Meta class
3.Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
4.每个Meta class的isa指针都指向Root class (meta)。

相关链接:https://juejin.cn/post/6844903942564872206

实例(对象)、类、元类之间的关系

16.实例、类、元类三者的关联?

isa指针:
OC中任何类的定义都是对象,任何对象都有isa指针,isa是一个Class类型的指针。
实例的isa指针,指向类;
类的isa指针,指向元类;
元类的isa指针,指向根元类;
父元类的isa指针,也指向根元类;
根元类的isa指针,指向它自己。

superClass:
类的superClass指向父类;
父类的superClass指向根类;
根类的superClass指向nil;
元类的superClass指向父元类;
父元类的superClass指向根元类;
根元类的superClass指向根类。

分类不能添加实例变量的原因:
分类结构体不包含实例变量数组,分类是在依赖runtime加载的。而此时类的内存分布已经确定,若此时再修改分布情况,对编程性语言是重大的消极影响是不允许的。

发送消息的查找过程:
沿着isa指针的方向查找

相关链接:
https://www.jianshu.com/p/ffb021a4b97c
https://www.jianshu.com/p/eb79616e05c4
https://blog.csdn.net/Margaret_MO/article/details/112248524
https://blog.csdn.net/m0_46110288/article/details/114643943

实例(对象)、类、元类、基类之间的关联
  • 类方法与对象方法(实例方法)介绍及区别

类的方法和对象的方法存放在哪里?(可以关联 isa 指针理解)
1.类方法:存储在元类中,由元类实例化出来的。
2.对象方法:存储在类中,由类实例化出来的。
3.如此设计的好处:
3.1两者方法的调用都可以理解为消息发送,可通过指定的类或对象查找对应的消息
3.2类的一切信息存储在元类中,对象的一切信息存储在类中,易区分

相关链接:
https://www.jianshu.com/p/21997f7f1e95
https://blog.csdn.net/lianai911/article/details/103400835

17.第三方库SD原理以及AFNet网络封装

1.SDWebImage的实现原理:

SDWebImage的实现原理(给UIImageView加载图片的逻辑;sd_setImageWithURL:placeholderImage:)
1.先在SDWebImageCache中寻找图片是否有对应的缓存,以url作为数据的索引先从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过就会缓存下来),找到直接使用。
2.如果缓存未找到,就会通过MD5处理过的key在磁盘中查询对应的数据,找到了就会把磁盘中的数据加载到内存并将图片显示出来。(即从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中)
3.如果内存和磁盘中都没有找到则向远程服务器请求下载图片,下载后会存到缓存中并写入磁盘。(即从网络上获取使用,缓存到内存,缓存到沙盒)
注意:整个获取图片的过程都在子线程中执行,获取图片后回到主线程将图片展示出来

2.AFNet网络封装:

二次封装;
1.首先创建一个基础类,用于转发AFN的block。将自己所需的基础配置写好,这里可以设计post、get、上传和下载的通用接口,这样在其他地方就可以直接用了。
2.写一个协议将成功和失败的回调转发出来,然后在实现了协议的地方就可以回调,这样可以把网络操作集中起来。
3.对于一些特定的网络请求可以调用前面的基础类,实现更高程度的封装

  • 你怎么理解AFNetWorking的的大概实现流程?

http://blog.cnbang.net/tech/2320/

18.iOS消息转发机制

提问:什么时候会报unrecognized selector的异常?(考iOS消息转发机制-runtime)
1.对象未实现该方法;2.对象已经被释放
比如当调用对象的某个方法时,该对象未实现此方法,就会出现这种情况。

iOS有哪些机制来避免走到这一步?看下图
(objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector))
https://www.jianshu.com/p/c7dedcd0b662
https://www.jianshu.com/p/fdd8f5225f0c

runtime的消息机制可以避免走到这一步

19.数据存储和缓存

20.技巧使用:线上线下bug分析与日志、instrument、卡顿闪退监控

21.OC 与 Swift 比较及混合开发

22.跨平台 flutter、unitApp、组件化路由

相关文章

  • 面试题难点底层逻辑

    目录 1.多线程管理2.RunLoop3.Runtime(运行时)4.内存管理5.性能(内存)优化举例6.App ...

  • 面试题难点底层逻辑

    目录 1.多线程管理2.RunLoop3.Runtime4.内存管理5.性能(内存)优化举例6.App 编译与启动...

  • 探寻RunLoop的本质

    iOS底层原理总结 - RunLoop 面试题 讲讲 RunLoop,项目中有用到吗? RunLoop内部实现逻辑...

  • 如何掌握思维模型?

    思维模型是我们认识这个世界最底层的逻辑,如何掌握思维模型是一个难点。但难点我们不怕,我们是有方法论的人,有方法就有...

  • 你知道自己的底层逻辑吗?

    最近对底层逻辑比较感兴趣。那什么是底层逻辑呢?《底层逻辑》这本书里给出了这样的定义:底层逻辑就是从事物的底层、本质...

  • 来自底层的逻辑

    现在都在讲底层逻辑,底层逻辑,到底什么是底层逻辑呢? 百度百科给出这样的解释,“底层逻辑,指从事物的底层、本质出发...

  • 【底层逻辑】是什么?

    【底层逻辑】是什么? 底层逻辑基础 思考问题的本质、原始需求 就是底层逻辑 事物背后的基础规律 底层逻辑是一切事物...

  • 什么是底层逻辑?

    什么是底层逻辑?底层逻辑就是万千现象背后的那个底层规律。底层逻辑有4个特点,即抽象、至简、源头、通用。 ...

  • 【读书笔记】1

    《底层逻辑》刘润 事物间的共同点就是底层逻辑,只有不同之中的相同之处,变化背后不变的东西才是底层逻辑,只有底层逻辑...

  • iOS底层面试题--RunLoop

    什么是RunLoop? iOS底层面试题--RunLoop RunLoop面试题分析

网友评论

      本文标题:面试题难点底层逻辑

      本文链接:https://www.haomeiwen.com/subject/meqonrtx.html