大厂常问iOS面试题--组件化篇

1.组件化有什么好处?

  • 业务分层、解耦,使代码变得可维护;

  • 有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;

  • 便于各业务功能拆分、抽离,实现真正的功能复用;

  • 业务隔离,跨团队开发代码控制和版本风险控制的实现;

  • 模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;

  • 在维护好各级组件的情况下,随意组合满足不同客户需求;(只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App)

2.你是如何组件化解耦的?

  • 分层

    基础功能组件:按功能分库,不涉及产品业务需求,跟库Library类似,通过良好的接口拱上层业务组件调用;不写入产品定制逻辑,通过扩展接口完成定制;

    基础UI组件:各个业务模块依赖使用,但需要保持好定制扩展的设计

    业务组件:业务功能间相对独立,相互间没有Model共享的依赖;业务之间的页面调用只能通过UIBus进行跳转;业务之间的逻辑Action调用只能通过服务提供;

  • 中间件:target-action,url-block,protocol-class

3.为什么CTMediator方案优于基于Router的方案?

Router的缺点:

  • 在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

  • 在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。 在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。

  • 为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的。

CTMediator的优点:

  • 调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。

  • 组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

  • 方便传递各种类型的参数。

4.基于CTMediator的组件化方案,有哪些核心组成?

  • CTMediator中间件:集成就可以了

  • 模块Target_%@:模块的实现及提供对外的方法调用Action_methodName,需要传参数时,都统一以NSDictionary*的形式传入。

  • CTMediator+%@扩展:扩展里声明了模块业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。

大厂常问iOS面试题--性能优化篇

1.造成tableView卡顿的原因有哪些?

  • 1.最常用的就是cell的重用, 注册重用标识符

    如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell

    如果有很多数据的时候,就会堆积很多cell。

    如果重用cell,为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,如果没有再重新创建cell

  • 2.避免cell的重新布局

    cell的布局填充等操作 比较耗时,一般创建时就布局好

    如可以将cell单独放到一个自定义类,初始化时就布局好

  • 3.提前计算并缓存cell的属性及内容

    当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度

    而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell

  • 4.减少cell中控件的数量

    尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件,

    不适用的可以先隐藏

  • 5.不要使用ClearColor,无背景色,透明度也不要设置为0

    渲染耗时比较长

  • 6.使用局部更新

    如果只是更新某组的话,使用reloadSection进行局部更

  • 7.加载网络数据,下载图片,使用异步加载,并缓存

  • 8.少使用addView 给cell动态添加view

  • 9.按需加载cell,cell滚动很快时,只加载范围内的cell

  • 10.不要实现无用的代理方法,tableView只遵守两个协议

  • 11.缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可

  • 12.不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。

  • 13.预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;

  • 14.使用正确的数据结构来存储数据。

2.如何提升 tableview 的流畅度?

  • 本质上是降低 CPU、GPU 的工作,从这两个大的方面去提升性能。

    CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制

    GPU:纹理的渲染

  • 卡顿优化在 CPU 层面

    尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView

    不要频繁地调用 UIView 的相关属性,比如 frame、bounds、transform 等属性,尽量减少不必要的修改

    尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性

    Autolayout 会比直接设置 frame 消耗更多的 CPU 资源

    图片的 size 最好刚好跟 UIImageView 的 size 保持一致

    控制一下线程的最大并发数量

    尽量把耗时的操作放到子线程

    文本处理(尺寸计算、绘制)

    图片处理(解码、绘制)

  • 卡顿优化在 GPU层面

    尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示

    GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,所以纹理尽量不要超过这个尺寸

    尽量减少视图数量和层次

    减少透明的视图(alpha<1),不透明的就设置 opaque 为 YES

    尽量避免出现离屏渲染

  • iOS 保持界面流畅的技巧

    1.预排版,提前计算

    在接收到服务端返回的数据后,尽量将 CoreText 排版的结果、单个控件的高度、cell 整体的高度提前计算好,将其存储在模型的属性中。需要使用时,直接从模型中往外取,避免了计算的过程。

    尽量少用 UILabel,可以使用 CALayer 。避免使用 AutoLayout 的自动布局技术,采取纯代码的方式

    2.预渲染,提前绘制

    例如圆形的图标可以提前在,在接收到网络返回数据时,在后台线程进行处理,直接存储在模型数据里,回到主线程后直接调用就可以了

    避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。

    3.异步绘制

    4.全局并发线程

    5.高效的图片异步加载

3.APP启动时间应从哪些方面优化?

App启动时间可以通过xcode提供的工具来度量,在Xcode的Product->Scheme–>Edit Scheme->Run->Auguments中,将环境变量DYLD_PRINT_STATISTICS设为YES,优化需以下方面入手

  • dylib loading time

    核心思想是减少dylibs的引用

    合并现有的dylibs(最好是6个以内)

    使用静态库

  • rebase/binding time

    核心思想是减少DATA块内的指针

    减少Object C元数据量,减少Objc类数量,减少实例变量和函数(与面向对象设计思想冲突)

    减少c++虚函数

    多使用Swift结构体(推荐使用swift)

  • ObjC setup time

    核心思想同上,这部分内容基本上在上一阶段优化过后就不会太过耗时

    initializer time

  • 使用initialize替代load方法

    减少使用c/c++的attribute((constructor));推荐使用dispatch_once() pthread_once() std:once()等方法

    推荐使用swift

    不要在初始化中调用dlopen()方法,因为加载过程是单线程,无锁,如果调用dlopen则会变成多线程,会开启锁的消耗,同时有可能死锁

    不要在初始化中创建线程

4.如何降低APP包的大小

降低包大小需要从两方面着手

  • 可执行文件

    编译器优化: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 检测未使用的代码:菜单栏 -> Code -> Inspect Code

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

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

    优化的方式可以对资源进行无损的压缩

    去除没有用到的资源

5.如何检测离屏渲染与优化

  • 检测,通过勾选Xcode的Debug->View Debugging–>Rendering->Run->Color Offscreen-Rendered Yellow项。

  • 优化,如阴影,在绘制时添加阴影的路径

6.怎么检测图层混合

1、模拟器debug中color blended layers红色区域表示图层发生了混合

2、Instrument-选中Core Animation-勾选Color Blended Layers

避免图层混合:

  • 确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明

  • 如无特殊需要,不要设置低于1的alpha值

  • 确保UIImage没有alpha通道

UILabel图层混合解决方法:

iOS8以后设置背景色为非透明色并且设置label.layer.masksToBounds=YES让label只会渲染她的实际size区域,就能解决UILabel的图层混合问题

iOS8 之前只要设置背景色为非透明的就行

为什么设置了背景色但是在iOS8上仍然出现了图层混合呢?

UILabel在iOS8前后的变化,在iOS8以前,UILabel使用的是CALayer作为底图层,而在iOS8开始,UILabel的底图层变成了_UILabelLayer,绘制文本也有所改变。在背景色的四周多了一圈透明的边,而这一圈透明的边明显超出了图层的矩形区域,设置图层的masksToBounds为YES时,图层将会沿着Bounds进行裁剪 图层混合问题解决了

7.日常如何检查内存泄露?

  • 目前我知道的方式有以下几种

    Memory Leaks

    Alloctions

    Analyse

    Debug Memory Graph

    MLeaksFinder

  • 泄露的内存主要有以下两种:

    Laek Memory 这种是忘记 Release 操作所泄露的内存。

    Abandon Memory 这种是循环引用,无法释放掉的内存。

大厂常问iOS面试题--多线程篇

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。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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)调用之后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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栅栏方法即可实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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占用的资源。

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

    1
    2
    3
    4
    5
    6
    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条最为合理。

大厂常问iOS面试题--内存管理篇

1.什么情况使用weak关键字,相比assign有什么不同?

  • 什么情况使用 weak 关键字?

    在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性

    自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。在下文也有论述:《IBOutlet连出来的视图属性为什么可以被设置成weak?》

  • 不同点:

    weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。

    assign 可以用非 OC 对象,而 weak 必须用于 OC 对象

2.如何让自己的类用copy修饰符?如何重写带copy关键字的setter?

  • 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。

    具体步骤:

    需声明该类遵从 NSCopying 协议

    实现 NSCopying 协议。该协议只有一个方法:

    1
    - (id)copyWithZone:(NSZone *)zone;

    注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是 “copyWithZone” 方法。

  • 重写带 copy 关键字的 setter,例如:

    1
    2
    3
    4
    - (void)setName:(NSString *)name {
    //[_name release];
    _name = [name copy];
    }

3.深拷贝与浅拷贝

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:

  • 当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;

  • 当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。

copy方法:如果是非可扩展类对象,则是浅拷贝。如果是可扩展类对象,则是深拷贝。

mutableCopy方法:无论是可扩展类对象还是不可扩展类对象,都是深拷贝。

4.@property的本质是什么?ivar、getter、setter是如何生成并添加到这个类中的

  • @property 的本质是实例变量(ivar)+存取方法(access method = getter + setter),即 @property = ivar + getter + setter;

    “属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

  • ivar、getter、setter 是自动合成这个类中的

    完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.

5.@protocol和category中如何使用@property

  • 在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性

  • category 使用 @property 也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:objc_setAssociatedObjectobjc_getAssociatedObject

6.简要说一下@autoreleasePool的数据结构??

简单说是双向链表,每张链表头尾相接,有 parent、child指针

每创建一个池子,会在首部创建一个 哨兵 对象,作为标记

最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。

7.BAD_ACCESS在什么情况下出现?

访问了悬垂指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环

8.使用CADisplayLink、NSTimer有什么注意点?

CADisplayLink、NSTimer会造成循环引用,可以使用YYWeakProxy或者为CADisplayLink、NSTimer添加block方法解决循环引用

9.iOS内存分区情况

  • 栈区(Stack)

    由编译器自动分配释放,存放函数的参数,局部变量的值等

    栈是向低地址扩展的数据结构,是一块连续的内存区域

  • 堆区(Heap)

    由程序员分配释放

    是向高地址扩展的数据结构,是不连续的内存区域

  • 全局区

    全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域

    程序结束后由系统释放

  • 常量区

    常量字符串就是放在这里的

    程序结束后由系统释放

  • 代码区

    存放函数体的二进制代码

  • 注:

    • 在 iOS 中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的

    • 系统使用一个链表来维护所有已经分配的内存空间(系统仅仅记录,并不管理具体的内容)

    • 变量使用结束后,需要释放内存,OC 中是判断引用计数是否为 0,如果是就说明没有任何变量使用该空间,那么系统将其回收

    • 当一个 app 启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)

10.iOS内存管理方式

  • Tagged Pointer(小对象)

    Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate

    Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free

    在内存读取上有着 3 倍的效率,创建时比以前快 106 倍

    objc_msgSend 能识别 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据

    使用 Tagged Pointer 后,指针内存储的数据变成了 Tag + Data,也就是将数据直接存储在了指针中

  • NONPOINTER_ISA (指针中存放与该对象内存相关的信息) 苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。

    isa 的结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // x86_64 架构
    struct {
    uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
    uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
    uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
    uintptr_t shiftcls : 44; // 存放着 Class、Meta-Class 对象的内存地址信息
    uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
    uintptr_t weakly_referenced : 1; // 是否被弱引用指向
    uintptr_t deallocating : 1; // 对象是否正在释放
    uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
    uintptr_t extra_rc : 8; // 引用计数能够用 8 个二进制位存储时,直接存储在这里
    };

    // arm64 架构
    struct {
    uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
    uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
    uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
    uintptr_t shiftcls : 33; // 存放着 Class、Meta-Class 对象的内存地址信息
    uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
    uintptr_t weakly_referenced : 1; // 是否被弱引用指向
    uintptr_t deallocating : 1; // 对象是否正在释放
    uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
    uintptr_t extra_rc : 19; // 引用计数能够用 19 个二进制位存储时,直接存储在这里
    };

    这里的 has_sidetable_rc 和 extra_rc,has_sidetable_rc 表明该指针是否引用了 sidetable 散列表,之所以有这个选项,是因为少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在 extra_rc 中,当其被存满时,才会存入相应的 SideTables 散列表中,SideTables 中有很多张 SideTable,每个 SideTable 也都是一个散列表,而引用计数表就包含在 SideTable 之中。

  • 散列表(引用计数表、弱引用表)

    引用计数要么存放在 isa 的 extra_rc 中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable 的结构中,它是一个散列表,也就是哈希表。而 SideTable 又包含在一个全局的 StripeMap 的哈希映射表中,这个表的名字叫 SideTables。

    当一个对象访问 SideTables 时:

    • 首先会取得对象的地址,将地址进行哈希运算,与 SideTables 中 SideTable 的个数取余,最后得到的结果就是该对象所要访问的 SideTable

    • 在取得的 SideTable 中的 RefcountMap 表中再进行一次哈希查找,找到该对象在引用计数表中对应的位置

    • 如果该位置存在对应的引用计数,则对其进行操作,如果没有对应的引用计数,则创建一个对应的 size_t 对象,其实就是一个 uint 类型的无符号整型

    弱引用表也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表 weak_entry_t,而 weak_entry_t 是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。

11.循环引用

1. 概述

iOS内存中的分区有:堆、栈、静态区。其中,栈和静态区是操作系统自己管理回收,不会造成循环引用。在堆中的相互引用无法回收,有可能造成循环引用。

循环引用的实质:多个对象相互之间有强引用,不能施放让系统回收。

解决循环引用一般是将 strong 引用改为 weak 引用。

2. 循环引用场景分析及解决方法

1)父类与子类

如:在使用UITableView 的时候,将 UITableView 给 Cell 使用,cell 中的 strong 引用会造成循环引用。

1
2
3
4
5
6
7
8
9
10
11
// controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TestTableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"UITableViewCellId" forIndexPath:indexPath];
cell.tableView = tableView;
return cell;
}

// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, strong) UITableView *tableView; // strong 造成循环引用
@end

解决:strong 改为 weak

1
2
3
4
// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, weak) UITableView *tableView; // strong 改为 weak
@end

2)block

block在copy时都会对block内部用到的对象进行强引用的。

1
2
3
self.testObject.testCircleBlock = ^{
[self doSomething];
};

self将block作为自己的属性变量,而在block的方法体里面又引用了 self 本身,此时就很简单的形成了一个循环引用。

应该将 self 改为弱引用

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.testObject.testCircleBlock = ^{
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};

在 ARC 中,在被拷贝的 block 中无论是直接引用 self 还是通过引用 self 的成员变量间接引用 self,该 block 都会 retain self。

  • 快速定义宏
1
2
3
4
5
// weak obj
/#define WEAK_OBJ(type) __weak typeof(type) weak##type = type;

// strong obj
/#define STRONG_OBJ(type) __strong typeof(type) str##type = weak##type;

3)Delegate

delegate 属性的声明如下:

1
@property (nonatomic, weak) id <TestDelegate> delegate;

如果将 weak 改为 strong,则会造成循环引用

1
2
3
4
5
6
7
8
9
// self -> AViewController
BViewController *bVc = [BViewController new];
bVc = self;
[self.navigationController pushViewController: bVc animated:YES];

// 假如是 strong 的情况
// bVc.delegate ===> AViewController (也就是 A 的引用计数 + 1)
// AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用计数 + 1
// 导致: AViewController <======> Delegate ,也就循环引用啦

4)NSTimer

NSTimer 的 target 对传入的参数都是强引用(即使是 weak 对象)

解决办法: 《Effective Objective-C 》中的52条方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#import <Foundation/Foundation.h>

@interface NSTimer (YPQBlocksSupport)

+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;

@end

#import "NSTimer+YPQBlocksSupport.h"

@implementation NSTimer (YPQBlocksSupport)

+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ypq_blockInvoke:) userInfo:[block copy]
repeats:repeats];
}

- (void)ypq_blockInvoke:(NSTimer *)timer
{
void (^block)() = timer.userInfo;
if(block)
{
block();
}
}

@end

使用方式:

1
2
3
4
5
6
7
__weak ViewController * weakSelf = self;
[NSTimer ypq_scheduledTimeWithTimeInterval:4.0f
block:^{
ViewController * strongSelf = weakSelf;
[strongSelf afterThreeSecondBeginAction];
}
repeats:YES];

计时器保留其目标对象,反复执行任务导致的循环,确实要注意,另外在dealloc的时候,不要忘了调用计时器中的 invalidate方法。

大厂常问iOS面试题--Runtime篇

1.什么是Runtime

在 Objective-C 中,Runtime 是一套 C 语言的 API,用于在运行时操作对象、类和元类。通过 Objective-C Runtime,开发者可以在程序运行时动态地创建类、修改类的方法、获取类的属性信息、发送消息等。这使得 Objective-C 具有了诸多动态语言的特性,如动态绑定、消息转发、方法调用等。

2.Category 的实现原理?

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

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

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

  • isa 等价于 is kind of

    实例对象的 isa 指向类对象

    类对象的 isa 指向元类对象

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

  • isa 有两种类型

    纯指针,指向内存地址

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

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

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

  • 通过组合实现

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

  • 通过协议实现

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

  • 消息转发实现

    forwardInvocation:方法

5.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 表中删除,最后清理对象的记录。

6.讲一下 OC 的消息机制

Objective-C 的消息机制是其动态消息传递的核心特性,它是 Objective-C 语言中实现方法调用的机制。在 Objective-C 中,对象之间的通信是通过向对象发送消息来实现的,而不是像其他语言那样直接调用方法。

消息机制的基本原理如下:

  • 1、方法调用:在 Objective-C 中,调用对象的方法实际上是向对象发送一个消息。消息由方法名和对应的参数组成。

  • 2、动态绑定:在编译时,编译器并不会确定对象接收到消息时应该调用的具体方法,而是在运行时根据对象的类和方法选择器(selector)来动态确定调用的方法。这种动态确定方法的过程称为动态绑定。

  • 3、消息传递:当一个对象接收到消息时,Objective-C 运行时会根据消息的方法选择器(selector)在对象所属的类的方法列表(method list)中查找相应的方法实现。如果找到了匹配的方法实现,则调用该方法;如果找不到匹配的方法实现,则会执行消息转发(message forwarding)过程。

  • 4、消息转发:当运行时无法在类的方法列表中找到与消息相匹配的方法实现时,Objective-C 运行时会调用对象的 forwardingTargetForSelector: 方法,尝试将消息转发给另一个对象处理。如果该方法返回一个有效的对象,则将消息转发给该对象;如果返回 nil,则会继续执行下一步的消息转发机制。

  • 5、动态方法解析:如果 forwardingTargetForSelector: 方法没有返回有效的对象,Objective-C 运行时会调用对象的 resolveInstanceMethod:resolveClassMethod: 方法,尝试动态地添加方法实现。如果成功添加了方法实现,则会重新执行消息传递过程;如果仍然无法找到相应的方法实现,则会执行最后一步的消息转发机制。

  • 6、完整的消息转发:如果动态方法解析仍然无法找到相应的方法实现,则 Objective-C 运行时会调用对象的 forwardInvocation: 方法,将原始的未知消息封装成一个 NSInvocation 对象,并传递给 forwardInvocation: 方法进行处理。开发者可以在该方法中自定义处理未知消息的逻辑,例如将消息转发给其他对象进行处理。

7.runtime具体应用

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

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

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

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

  • KVC 字典转模型

8.runtime如何通过selector找到对应的IMP地址?

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

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

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

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

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

9.简述下Objective-C中调用方法的过程

Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:

  • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类

  • 然后在该类中的方法列表以及其父类方法列表中寻找方法运行

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

  • 但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会,这三次拯救程序奔溃的说明见问题《什么时候会报unrecognized selector的异常》中的说明。

10.load和initialize的区别

两者都会自动调用父类的,不需要super操作,且仅会调用一次(不包括外部显示调用).

  • load和initialize方法都会在实例化对象之前调用,以main函数为分水岭,前者在main函数之前调用,后者在之后调用。这两个方法会被自动调用,不能手动调用它们。

  • load和initialize方法都不用显示的调用父类的方法而是自动调用,即使子类没有initialize方法也会调用父类的方法,而load方法则不会调用父类。

  • load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。

  • load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。

11.怎么理解Objective-C是动态运行时语言。

  • 主要是将数据类型的确定由编译时,推迟到了运行时。这个问题其实浅涉及到两个概念,运行时和多态。

  • 简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。

  • 多态:不同对象以自己的方式响应相同的消息的能力叫做多态。

  • 意思就是假设生物类(life)都拥有一个相同的方法-eat;那人类属于生物,猪也属于生物,都继承了life后,实现各自的eat,但是调用是我们只需调用各自的eat方法。也就是不同的对象以自己的方式响应了相同的消 息(响应了eat这个选择器)。因此也可以说,运行时机制是多态的基础.

大厂常问iOS面试题--Runloop篇

1.Runloop 和线程的关系?

  • 一个线程对应一个 Runloop。

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

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

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

2.RunLoop的运行模式

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

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

3.runloop内部逻辑?

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

  • 内部逻辑:

    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    + (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 不至于退出,并没有用于实际的发送消息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    - (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.CADisplayLink和Timer哪个更精确

CADisplayLink 更精确

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

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

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

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

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

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