iOS 面试题 - 难点底层逻辑

目录

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.OC 与 Swift 比较及混合开发
19.跨平台 flutter、unitApp、RN 开发技能、组件化路由、UI 框架 QMUI_iOS、Hybrid、Weex、Qt
20.RAC(ReactiveCocoa) 与 RxSwift(ReactiveX for Swift)
21.技巧使用:线上线下 bug 分析与日志、instrument、卡顿闪退监控、App 加固
22.数据存储和缓存

面试题自省

1.多线程管理

1.什么是多线程?

多线程是指从软件或者硬件上实现多个线程并发执行的技术,能够同一时间执行多于一个线程进而提升整体处理性能。在 iOS 中每个进程启动后都会建立一个主线程(UI 线程),这个线程是其他线程的父线程。由于在 iOS 中除了主线程,其他子线程是独立于 Cocoa Touch 的,所以只有主线程可以更新 UI 界面(新版 iOS 中使用其他线程更新 UI 可能也能成功,但是不推荐)。

相关链接:
多线程面试题:https://juejin.cn/post/7008445931116822535

单线程与多线程对比
  • 多线程原理及说明

1.同一时间 CPU 只能处理1条线程,只有1条线程在执行工作。多线程并发(同时)执行,其实是 CPU 快速地在多条线程之间调度(切换),如果 CPU 调度线程的时间足够快就造成了多线程并发执行的假象。

2.充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“并行计算”状态弊端。

3.iOS 中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。常用的多线程开发有三种方式:NSThread、NSOperation、GCD。

注意:多线程并发,并不是 CPU 在同一时刻同时执行多个任务,只是 CPU 调度足够快造成的假象。

线程池原理
线程池原理
  • 多线程优缺点

优点:
1.能适当提高程序的执行效率;
2.能适当提高资源利用率(CPU、内存利用率);
3.线程上的任务执行完成之后,线程会自动销毁;
4.可以减少程序的响应时间、简化程序结构

缺点:
1.开启线程需要占用一定的内存空间(默认情况下主线程占用1M,子线程占用512KB);
2.开启(大量的)新线程,会占用消耗大量的内存空间和 CPU 时间,降低系统程序的运行性能;
3.线程越多,CPU 在调用线程上的开销就越大;
4.程序设计更加复杂,比如线程间的通信,多线程的数据共享等。

提问:如果线程非常非常多,会发生什么情况?
CPU 会在 N 多线程之间调度切换,会消耗大量的 CPU 资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。这个还可以考虑线程安全问题。

  • 线程间怎么通信?
  • 线程间的通信体现:一个线程传递数据给另一个线程
  • 在一个线程中执行完特定的任务后,转到另一个线程继续执行任务
  • 多线程的选择方案

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

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

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

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

多线程的选择方案

2.多线程相关的几个概念

  • 2.1主线程

一个程序运行后默认会开启一个线程称为主线程或 UI 线程,这个线程是其他线程的父线程。由于在 iOS 中除了主线程,其他子线程是独立于 Cocoa Touch 的,所以只有主线程可以更新 UI 界面(最新版 iOS 可能能成功但不推荐)。主线程一般用来刷新 UI 界面和处理 UI 事件(点击、滚动、拖拽等)。

注意:

1.不要将耗时操作(如网络请求)放到主线程中,耗时操作会卡住主线程,严重影响 UI 的流畅度,给用户一种卡的体验。
2.耗时操作一般都在子线程(后台线程、非主线程)处理操作后回到主线程中刷新 UI 界面。

为什么更新 UI 都要放在主线程:

为了安全和效率考虑;因为 UIKit 框架不是线程安全的框架,当在多个线程进行 UI 操作有可能出现资源抢夺导致问题。在子线程中是不能进行 UI 更新的,而可以更新的结果只是一个幻像:因为子线程代码执行完毕了又自动进入到了主线程,执行了子线程中的 UI 更新的函数栈,这中间的时间非常的短,就让大家误以为分线程可以更新 UI。如果子线程一直在运行,则子线程中的 UI 更新的函数栈,主线程无法获知即无法更新。其次因为开辟线程时会获取当前环境,如点击某个按钮,这个按钮响应的方法是开辟一个子线程,在子线程中对该按钮进行 UI 更新是能及时的,如换标题、换背景图,但意义不大。

相关链接:
https://www.cnblogs.com/itlover2013/p/15372008.html
http://www.wjhsh.net/8335IT-p-10373723.html

  • 2.2进程与线程

进程概念

进程:指系统上正在运行的程序,负责程序的内存分配,每一个进程都有自己独立的虚拟内存空间(一个程序运行的动态过程)。例如我们打开 QQ 是一个进程,打开 Xcode 也是一个进程。

进程
进程

线程概念

线程:指程序在执行过程中能够执行程序代码的一个执行单元,也就是线程是运行在程序中。线程是进程中一个独立执行的路径("控制/执行"单元),一个进程至少包含一条线程。

进程与线程的区别

1.进程是 CPU 资源分配的最小单位,线程是 CPU 资源调度的最小单位。
2.一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一条线程。
3.同一个进程内的线程共享进程资源。各个线程之间共享程序的内存空间,但是各个线程又拥有自己独立的内存空间。一个进程(程序)的所有任务都在线程中执行,注意1个线程中任务的执行是串行的。

进程和线程之间关系
  • 2.3串行和并行、并发、同步和异步

https://juejin.cn/post/6936486914568486943
https://blog.csdn.net/u010214802/article/details/90611509

串行概念

串行:一个线程执行多个任务,采取排队方式执行。

并行概念

并行:当系统有一个以上 CPU 时则线程的操作有可能非并发,当一个 CPU 执行一个线程时,另一个 CPU 可以执行另一个线程,两个线程互不抢占 CPU 资源可以同时进行,这种方式我们称之为并行(Parallel)。并行也就是多个线程分担多个任务,不同任务同时执行(比如:食堂窗口打饭)。

并发概念

并发:当有多个线程在操作时,如果系统只有一个 CPU 则它根本不可能真正同时进行一个以上的线程。它只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。

同步概念

同步:当前面一个任务未完成,后面的任务就会被阻塞。

异步概念

异步:不同的任务在不同的线程中执行,当子线程发生阻塞,主线程和其他线程不会受到影响(比如:食堂窗口打饭)。

相互之间联系与区别:

1.并行与并发区别:两个既相似又有区别,并行是指两个或者多个事件在同一时刻发生,其关键是你在同时处理多个任务。而并发是指两个或多个事件在同一时间间隔内发生,其关键是你有处理多个任务的能力,但不是同时是交替执行。两者最关键的点就是:是否是同时。

2.并行和串行:指的是任务的执行方式;串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行,异步是多个任务并行的前提条件。

3.同步和异步:指的是能否开启新的线程;同步不能开启新的线程,异步可以。异步强调解决阻塞问题,可以开辟新的线程执行任务。

并发、并行、串行 三者区别
并发、并行、串行 三者区别

3.多线程的使用场景

多线程的使用场景

4.多线程不安全问题

多线程为什么不安全?

1.在资源共享时,一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。
2.当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。

几种解决方案:

同步函数替换异步函数
使用串行队列
使用栅栏函数
使用互斥锁
使用信号量

相关链接:
https://juejin.cn/post/6844903455065112589
https://blog.csdn.net/weixin_38633659/article/details/124821800
https://juejin.cn/post/6844904138623418376

怎么解决线程不安全 - 使用串行队列
怎么解决线程不安全 - 使用互斥锁
怎么解决线程不安全 - 使用栅栏函数

5.多线程常用的3种实现方法详解(NSThread、NSOperation、GCD)

三种方式是随着 iOS 的发展逐渐引入的,所以相比而言后者比前者更加简单易用,并且 GCD 也是目前苹果官方比较推荐的方式(它充分利用了多核处理器的运算性能)。

详解:https://www.jianshu.com/p/ff50edea2a38

3种实现方法对比说明:

1.NSThread 是封装程度最小最轻量级的,使用更灵活,但需要手动管理线程的生命周期、线程同步、线程加锁、睡眠以及唤醒等,线程开销大。

2.NSOperation 是基于 GCD 更高一层的封装,完全面向对象;比 GCD 更简单易用、代码可读性也更高。两者中的一些概念同样适用于二者比如操作相当于任务等。

3.在项目什么时候选择使用 GCD,什么时候选择 NSOperation?

  • 项目中使用 NSOperation 的优点是 NSOperation 是对线程的高度抽象,在项目中使用它会使项目的程序结构更好,子类化 NSOperation 的设计思路是具有面向对象的优点(复用、封装),使得实现是多线程支持而接口简单,建议在复杂项目中使用。
  • 项目中使用 GCD 的优点是 GCD 本身非常简单、易用,对于不复杂的多线程操作会节省代码量,而 Block 参数的使用,会让代码更为易读,建议在简单项目中使用。

4.NSOperation:是对 GCD 的封装、NSOperation:GCD block 中的任务、NSOperationQueue: GCD 中的队列。

3种实现方法对比
GCD 和 NSOperation 比较

2.RunLoop

1.RunLoop 概念

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

简单理解:https://mp.weixin.qq.com/s/rMYZ6DjQVKqtpQUTKiPizw

总结 Runloop 的基本作用:

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

相关链接:
https://blog.ibireme.com/2015/05/18/runloop/
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 函数。

相关链接:https://maimai.cn/article/detail?fid=1529112559&efid=p52bIxVDwJJ_50EaBYbqVw

objc_msgSend
objc_msgSend
  • 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点:实例、类、元类三者的关联讲解部分。

Object(对象)
Meta Class(元类)
实例对象(Object)、类(Class)、Meta Class(元类) 之间简单的指向关系
  • Method(方法)
Method(方法)
Method(方法)
  • 类缓存(objc_cache)

1.当 Objective-C 运行时通过跟踪它的 isa 指针检查对象时,它可以找到一个实现许多方法的对象。而你可能只调用它们的一小部分,并且每次查找时搜索所有选择器的类分派表没有意义。
2.所以类实现一个缓存,每当你搜索一个类分派表并找到相应的选择器,它把它放入它的缓存。当 objc_msgSend 查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
3.为了加速消息分发,系统会对方法和对应的地址进行缓存,就放在上述的 objc_cache,所以在实际运行中大部分常用的方法都是会被缓存起来的,Runtime 系统实际上非常快,接近直接执行内存地址的程序速度。

  • Category(objc_category)

Category 是表示一个指向分类的结构体的指针,其定义如下代码;其中:
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过 objc_setAssociatedObject 和 objc_getAssociatedObject 增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从下面的 category_t 的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

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

消息发送的流程
  • 详解一个对象方法如 [obj foo]

一个对象的方法像这样 [obj foo],编译器转成消息发送 objc_msgSend(obj, foo),Runtime 时执行的流程是这样的:
1.首先通过 obj 的 isa 指针找到它的 class ;
2.在 class 的 method list 找 foo ;
3.如果 class 中没到 foo,继续往它的 superclass 中找 ;
4.一旦找到 foo 这个函数,就去执行它的实现 IMP。
5.问题:但这种实现有个问题就是效率低;但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次 objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是 objc_class 中另一个重要成员 objc_cache 做的事情 - 再找到 foo 之后,把 foo 的 method_name 作为 key ,method_imp 作为 value 给存起来。当再次收到 foo 消息的时候,可以直接在 cache 里找到,避免去遍历 objc_method_list。从源代码可以看到objc_cache 是存在 objc_class 结构体中的。

  • 消息传递是怎么实现的呢?

1.系统首先找到消息的接收对象,然后通过对象的 isa 找到它的类。
2.在它的类中查找 method_list,是否有 selector 方法。
3.没有则查找父类的 method_list。
4.找到对应的 method,执行它的 IMP。
5.转发 IMP 的 return 值。

消息机制的基本原理
方法的本质
SEL 和 IMP 的关系
Objective-C 中调用方法的过程
  • Runtime 作用之发送消息举例
Runtime 作用之发送消息举例

4.Runtime 之消息转发

在讲3.Runtime 之消息机制的最后一步中我们提到:“若找不到对应的 selector,消息被转发或者临时向 recevier 添加这个 selector 对应的实现方法,否则就会发生崩溃“。进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行 doesNotRecognizeSelector: 方法报 unrecognized selector 错。所以当一个方法找不到的时候,Runtime 提供了:“ 消息动态解析、消息接受者重定向、消息重定向 ” 等三步处理消息(其他叫法:动态方法解析、备用接收者、完整消息转发),具体流程如下图。

具体理解:
https://www.jianshu.com/p/633e5d8386a8
https://juejin.cn/post/6844903586216804359

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

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

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

Runtime 之消息转发
Runtime 之消息转发
动态方法解析流程
动态方法解析流程
  • 通俗理解消息发送与消息转发:
通俗理解消息发送与消息转发
通俗理解消息发送与消息转发

5.Runtime 的实际应用场景理解

举例:

1.利用关联对象(AssociatedObject)给分类添加属性
2.遍历类的所有成员变量(修改 textfield 的占位文字颜色、KVC 字典转模型、自动归档解档)|访问私有变量(UITextFiled 的修改)
3.交换方法实现(交换系统的方法)
4.利用消息转发机制解决方法找不到的异常问题
5.动态添加属性、动态添加方法
6.替换 ViewController 的声明周期
7.解决集合类因索引的问题崩溃的问题
8.App 热修复
9.防止按钮重复高强度点击(属于交换系统方法-拦截)
10.全局更换控件初始效果
11.App 异常加载的展位图
12.全局修改 UINavigationBar 的 backButtonItem
......

Runtime 实际应用场景详解:https://www.jianshu.com/p/4ceec094e134

实际开发应用

6. Runtime 的总结

Runtime 基础
RunTime进阶
RunTime应用

7. Runtime 相关面试题

相关链接:
https://juejin.cn/post/6844903827913572360
https://juejin.cn/post/6996921591388979213
https://juejin.cn/post/6844904121531809806

Runtime 相关面试题

报 unrecognized selector 异常:https://www.jianshu.com/p/c7dedcd0b662

Runtime 相关面试题

4.内存管理

内存管理简述:iOS 内存管理一般指的是 OC 对象的内存管理,因为 OC 对象分配在堆内存,堆内存需要程序员自己去动态分配和回收。基础数据类型(非OC对象)则分配在栈内存中,超过作用域就会由系统检测回收。

说明:

1.__strong 与变量在 ARC 模式下,id 类型和 OC 对象的所有权修饰符默认是 __strong。当一个变量通过 __strong 修饰符来修饰,当该变量超出其所在作用域后,该变量就会被废弃,同时赋值给该变量的对象也会被释放。

2.用一句话概括即:在 iOS 中使用引用计数来管理 OC 对象的内存。具体就是当一个新对象被创建,它的引用计数默认是1,调用 retain 一次会让 OC 对象的引用计数+1,调用 release 一次会让 OC 对象的引用计数-1,当引用计数减为0,OC 对象就会销毁释放其占用的内存空间。

内存管理解析:https://www.jianshu.com/p/4b244af31bde

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 是自动管理内存机制,简单地说就是在编译时代码中会在合适的位置自动加入如 release、autorelease 和 retain 等,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了,程序员不用关心对象释放的问题。

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

补充说明:

  • ARC 这个机制苹果比较推荐,这一机制使得开发者无需键入 retain 和 release,这不仅能够降低程序崩溃和内存泄露的风险,而且可以减少开发者的工作量,能够大幅度提升程序的流畅性和可预测性。ARC 不能在 iOS5 之前的系统中使用,ARC 不适用于 Core Foundation 框架中,仍然需要手动管理内存。

  • GC (Garbage Collector, 垃圾回收机制)
    GC:在 OC2.0 之后,内存管理出现了类似于 JAVA 和 C# 的内存垃圾收集技术,但与 ARC 完全不同,GC 是后台有一个线程负责监察已经不再使用的对象,然后将它释放。由于后台一直有一个线程在运行,因此会严重影响性能,这也是 Android(安卓)手机一直让人不爽的原因。CG 技术不能应用于 iOS 开发,只能应用于 Mac OS X 开发。注意:iOS 是手机端开发;MAC OS X 是电脑端开发。

  • MRC 和 ARC 下的混编
    在 ARC 的项目中,对 MRC 的文件可以添加编译选项 -fno-objc-arc 的标识;
    在 MRC 的项目中,对 ARC 的文件可以添加编译选项 -fobjc-arc 的标识;
    步骤:Build Phases -> Compile Soueces

相关链接:
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

详解:https://www.jianshu.com/p/000b42272d67

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

详解:https://www.jianshu.com/p/3ee0bdc8ad70

11.Block 理解

block 本质上是一个 OC 对象,内部有一个 isa 指针,是封装了函数调用以及函数调用环境的 OC 对象。

说明:

1.带有自动变量(局部变量)的匿名函数叫做 block,又叫做匿名函数、代码块。用函数式编程进行了保存,底层是有名的。在底层的 __main__block__impl__0 上有指出 block 是 isa 指针对象。

2.为什么 block 可以捕获外部变量:因为 __main__block__impl__0 会自动生成相应属性。

3.为什么内部属性没法修改(没有用 __block 修饰):因为内部的属性是拷贝过去的,是浅拷贝,只拷贝了对象值,未拷贝指针,因此你内部修改没法改变外部变量。可以理解为内部的变量只是在变量空间中的临时变量作出了改变,并没有影响外部,所以会造成代码歧义,比如内部是18外面是19。用 __block 修饰的变量在拷贝的时候会把指针一起拷贝进去。

4.block 有三种类型:全局静态 block、栈区 block、堆区 block。
NSGolobalBlock - 全局静态 block:不使用外部变量的 block 或者只使用静态变量和全局变量。
NSMallocblock - 堆区 block:使用外部变量且赋值的强引用,出了作用域会释放 { } 才会持有对象。
NSStackBlock - 栈区 block:使用外部变量但未进行 copy 或者引用的 weak 变量,系统决定释放。

5.为什么 __weak 可以解决循环引用:__weak 使用之后会调用 __objc_initWeak 函数和 storeWeak 函数等,将要指向的对象地址(id *location)以及指针(objc_object *newObj)传入 storeWeak 函数,通过 hash 表映射,location 作为 key,newObj 作为 value,存储在 weakTable 中进行维护。weak 指针指向对象,不会让对象的引用计数增加,所以 block 内部就不会持有 self 对象,破解循环引用。

6.为什么 __strong 不会造成循环引用呢:因为 __strong 是在 block 内部创建的,属于局部变量,强引用的也是 weak 指针(因为外部是用 __weak 修饰的,所以内部就算是 __strong 修饰也没关系),在 block 执行完成后,strongSelf 变量就会被释放,这个临时的“循环引用”就会被打破。

7.为什么异步时需要使用 __strong 再对对象重新声明:如果在异步执行的过程中 self 被释放会调用 objc_clear_deallocating(id obj) 方法,下一步调用 clearDeallocating 方法,紧接着调用 weak_clear_no_lock(weak_table_t *weak_table, id referent)函数,在 hash 表中根据 key(对象地址)寻找 weak 指针的数组,遍历置空。所以为了避免 self 被释放掉导致后续流程无法走通,使用 __strong 强引用弱指针,执行过程中 weak 指针就不会被释放,在执行完成之后再释放置空。

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

  • 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、分类、扩展、通知

详解:https://www.jianshu.com/p/5f908920455e

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

实例(对象)、类、元类之间的关系
isa指针理解

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.OC 与 Swift 比较及混合开发

相关链接:
https://zhuanlan.zhihu.com/p/518667958
https://juejin.cn/post/6844903457824964622
https://juejin.cn/post/7146890801207836703

OC 和 Swift 共同点 - 联系

1.Swift 和 OC 共用一套运行时环境,Swift 的类型可以桥接到 OC,OC 也可以桥接到 Swift,两者能够互相引用混合编程。
2.OC 之前积累的很多类库在 Swift 中大部分依然可以直接使用,不过语法是有变化的。
3.OC 出现过的绝大多数概念,比如引用计数、ARC、属性、协议、接口、初始化、扩展类、命名参数、匿名函数等,在 Swift 中继续有效(可能最多换个术语)。Swift 大多数概念与 OC 一样。当然 Swift 也多出了一些新兴概念,这些在 OC 中是没有的,比如范型、元组等。

Swift 优点

1.Swift 容易阅读,语法和文件结构简易化
2.Swift 更易于维护,文件分离后结构更清晰
3.Swift 更加安全,它是类型安全的语言
4.Swift 代码更少,简洁的语法,可以省去大量冗余代码
5.Swift 速度更快,运算性能更高
6.苹果公司主推的开发语言

Swift 缺点

1.版本相对不稳定,版本更新频繁,每次更新都有些许变动,需要开发者重新学习。
2.社区的开源项目偏少,毕竟 OC 独大好多年,很多优秀的类库都不支持 Swift,不过这种状况正在改变,现在有好多优秀的 Swift 的开源类库了。
3.偶尔开发中遇到的一些问题,很难查找到相关资料,这是一个弊端。
4.纯 Swift 的运行时和 OC 有本质区别,一些 OC 中运行时的强大功能,在纯 Swift 中变无效了。
5.对于不支持 Swift 的一些第三方类库,如果非得使用只能混合编程,利用桥接文件实现。

OC 和 Swift 细节使用区别

1.Swift 不分 .h 和 .m 文件 ,一个类只有 .swift 一个文件,所以整体的文件数量比起 OC 有一定减少。
2.Swift 句尾不需要分号 ,除非你想在一行中写三行代码就加分号隔开。
3.Swift 数据类型都会自动判断 , 只区分变量 var 和常量 let。
4.强制类型转换格式不同,OC强转:(int)a Swift 强转:Int(a)。
5.关于 BOOL 类型更加严格 ,Swift 不再是 OC 的非0就是真,而是 true 才是真 false 才是假
6、Swift 的循环语句中必须加 {},就算只有一行代码也必须要加
7、Swift 的 switch 语句后面可以跟各种数据类型了 ,如 Int、字符串都行,并且里面不用写 break(OC好像不能字符串)。
8、Swift if 后的括号可以省略:if a>b {},而 OC 里 if后面必须写括号。
9、Swift 打印用 print("") 打印变量时可以 print("(value)"),不用像 OC 那样记很多 %@,d% 等。
10、Swift 的【Any】可以代表任何类型的值,无论是类、枚举、结构体还是任何其他 Swift 类型,这个对应 OC 中的【id】类型。

OC 和 Swift 细节使用区别

OC 和 Swift 混编

OC 和 Swift 两者混编需要通过桥接文件进行混合编程。

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

19.跨平台 flutter、unitApp、RN 开发技能、组件化路由、UI 框架 QMUI_iOS、Hybrid、Weex、Qt

flutter

官网:https://flutter.dev/
flutter 实战·第二版:https://book.flutterchina.club/chapter2/flutter_widget_intro.html#_2-2-2-widget-https://flutterchina.club/
flutter 开发文档环境配置 - 中文版:https://flutter.cn/docs/get-started/install/macos
flutter SDK 下载:https://docs.flutter.dev/development/tools/sdk/releases?tab=macos
DoraemonKit(一款功能齐全的客户端 iOS 、Android、微信小程序、Flutter 等研发助手):https://github.com/didi/DoKit

Dart(flutter 开发语言):https://www.dartcn.com/

相关链接:
https://juejin.cn/post/6844903977901883406
https://juejin.cn/post/6844904110995554318
https://mp.weixin.qq.com/s/nkjPIgNRazW56bHuh0PEXQ

flutter

unitApp

官网:https://uniapp.dcloud.net.cn/
相关链接:https://www.jianshu.com/p/1ec090474b9c

RN 开发技能

相关链接:
https://juejin.cn/post/6844903606819225607
https://juejin.cn/post/6844903911032094728

组件化路由

组件化路由框架 wisdomNeighbor - demo:https://github.com/AZlinli/wisdomNeighbor
在现有工程中实施基于 CTMediator 的组件化方案:https://casatwy.com/modulization_in_action.html
有赞移动 iOS 组件化(模块化)架构设计实践:https://tech.youzan.com/you-zan-ioszu-jian-hua-jia-gou-she-ji-shi-jian/
补充-iOS 列表界面如何优雅实现模块化与动态化:https://www.jianshu.com/p/f0a74d5744b8

相关链接:
https://juejin.im/post/6847902224744448014
https://juejin.cn/post/6844903582739726350
https://juejin.cn/post/6844903902467342349

UI 框架 QMUI_iOS

官网:https://qmuiteam.com/ios/
demo:https://github.com/Tencent/QMUI_iOS

Qt Mobile

Hybrid 技术简介

20.RAC(ReactiveCocoa) 与 RxSwift(ReactiveX for Swift)

RAC(ReactiveCocoa/)

https://www.jianshu.com/p/bfb8f1e2766e
http://www.jianshu.com/p/50dc8b184864

RxSwift(ReactiveX for Swift)

https://beeth0ven.github.io/RxSwift-Chinese-Documentation/

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

线上线下 bug 分析与日志

https://juejin.cn/post/6844903745768128525
https://blog.csdn.net/m0_67695717/article/details/124984572
https://www.jianshu.com/p/839555c23f99

instrument

https://juejin.cn/post/6865102561507672077
https://blog.csdn.net/weixin_41963895/article/details/107231347
http://t.zoukankan.com/ljcgood66-p-6607396.html

instrument

卡顿闪退监控

卡顿和普通的 bug 不一样,卡顿的发生通常和机型、系统、用户操作路径有密切关系。因此当用户上报卡顿时,开发和测试人员很难复现用户的问题。因此要求开发人员在代码中监控卡顿,当卡顿发生时,能立马监听到且知道当前程序在做什么,并将日志上传,这样开发人员才能解决问题。

https://mp.weixin.qq.com/s/AXvi9eVtnd3ozy794HSJqw
https://www.jianshu.com/p/163c5e668af5
https://it.sohu.com/a/540162644_121207965
https://juejin.cn/post/6844904004053368846

App 加固

市面上有很多 App 加固的第三方工具在这里不做介绍。

https://www.jianshu.com/p/2410fe5c6b96
https://zhuanlan.zhihu.com/p/359199197
https://juejin.cn/post/6844903494969737229

22.数据存储和缓存

https://www.jianshu.com/p/1757d76c16ef

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 高频问题:OOM: 监控可以用didReceiveMemoryWarning 也可以类似flex ,通过mallo...
    咸鱼有只喵阅读 2,518评论 2 31
  • 目录 1.内存区域解析2.什么是引用计数(retainCount)3.什么是指针和地址4.内存泄漏、野指针、空指针...
    Kevin_wzx阅读 994评论 0 13
  • 得物 KVO willchangevalue什么时候调用 键值观察通知依赖于 NSObject 的两个方法: w...
    芒果不可思议阅读 740评论 0 1
  • 内存管理 什么情况使用weak关键字,相比assign有什么不同?[https://juejin.cn/post/...
    Lingday阅读 331评论 0 0
  • 序 曾几何时,特别喜欢看、收集别人分享的面试真题,直到看到图中这个学习方法,若有所思。在百度三面被挂掉之后,沉下心...
    强子ly阅读 12,657评论 10 176