2018年 iOS 面试知识梳理

#import和#include和@class

include会造成头文件循环引用的问题
import 解决了 include 重复引用的问题
@class是为了编译的效率,如果a->b,b->c ,底层a的头文件改变的话,不使用@class,整个链都要重新编译
@class也是解决了重复引用问题

RunLoop

我的总结


多线程

线程
每个进程都可以包含多个线程,它们可以同时运行。如果只有一个处理器核心,操作系统在所有线程之间切换。如果拥有多个核心,线程就分散到各个核心执行。

线程安全的问题。Foundation 框架通常被认为是线程安全的,而UIKit 框架被认为是非线程安全的。处理UIKit 对象的所有方法都应从相同线程执行,该线程就是主线程main thread。

不可变对象通常是安全的,可变对象通常不是线程安全的。

每个线程都维护着自己的堆栈 NSAutoreleasePool 对象。

每个线程都有一个运行循环,并且只有一个。非主线程必须自己运行运行循环。如果分离的线程没有进入运行循环,则只要分离的方法完成执行,线程就会退出。

thread
这是一套在很多操作系统都通用的多线程api,基于 c 语言。需要手动处理线程的各个状态的转换。
NSThread
这个是苹果封装的,面向对象的。可以直接操控线程对象,非常方便。但是它的生命周期还是需要手动管理,可以获取当前线程类,可以知道当前线程的各种属性,用于调试很方便。

可用功能:启动线程,取消线程,暂停线程,设置和获取线程名字,获取当前线程信息。
GCD

  • 概念
    将长期运行的任务拆分成多个工作单元,并将这些单元添加到队列中
    系统为我们管理这些队列,为我们在多个线程执行工作单元,自动管理线程的生命周期(创建,调度,销毁)
    在 C 接口中添加了一些优秀的概念,比如工作单元、自动化后台进程、自动线程管理

  • GCD 中两个重要概念:任务和队列
    任务 有两种执行方式:
    同步执行:dispatch_sync();不会开启新线程
    异步执行:dispatch_async(); 会开启新线程
    异步执行 任务不是马上执行,而是将所有任务添加到队列之后才开始异步执行
    参数为队列和任务block;

同步和异步的主要区别在于会不会阻塞当前线程。
同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
  • 另一个重要概念是队列。严格遵守FIFO(先进先出)原则
    队列的创建:dispatch_queue_creat
需要传入俩个参数,第一个是唯一标识符(this parameter is optional and may be NULL),
第二个用来识别串行队列还是并行队列,
`DISPATCH_QUEUE_SERIAL`表示串行
`DISPATCH_QUEUE_CONCURRENT`表示并行。

并行队列(Concurrent Dispatch Queue):可以让多个任务并行(同时)执行(自动开启多个线程同时执行任务)
串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
并行功能只有在异步(dispatch_async)函数下才有效

并行队列可以使用dispath_get_global_queue创建全局并行队列。
dispatch_get_global_queue()函数抓取一个已经存在并始终可用的全局队列。接受俩个参数,第一个指定优先级,第二个始终为0.

串行队列 异步执行 会开启一条新线程,但是任务还是串行,所以任务还是一个一个执行。
  • 还有一种特殊的队列 主队列
    dispatch_get_main_queue所有放在主队列的任务,都会放在主线程执行
  • 在主线程 主队列 + 同步执行 会互等卡住不可行
  • 在其他线程中使用 主队列+同步执行。在主线程一个一个执行
  • 主队列+异步执行 所有任务添加到队列之后才开始执行
  • 一般在 主线程 进行 UI 刷新,把耗时操作放在其他线程。

GCD的其他用法

  • GCD延时执行方法 dispatch_after()
  • GCD一次性代码 dispatch_once(),创建单例
  • GCD 的快速迭代方法 dispatch_apply() ,可以同时遍历 百度过的地址
    这个方法也是会阻塞线程的
  • GCD 的队列组 dispatch_group() ,dispatch_group_notify()在组中所有代码块运行完成时执行

GCD 的应用

  • question:如何用 GCD 实现线程依赖?

  • question:如何用 GCD 实现 A 任务 B任务并发执行,都执行完成之后执行 C 任务,待 C 任务完成再并发执行 D 、E 任务?

  • question:如何用 GCD 实现最大并发数?


    2018年 iOS 面试知识梳理_第1张图片
    占位

answer one:使用dispatch_group_enterdispatch_group_leave
demo:

    dispatch_group_t group =  dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_enter(group);
    NSLog(@"group did start");
    dispatch_block_t block1 = dispatch_block_create(0, ^{

        sleep(1);
        NSLog(@"block1 finish");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_block_t block2 = dispatch_block_create(0, ^{
       
        sleep(3);
        NSLog(@"bolck2 finish");
        dispatch_group_leave(group);
    });
    dispatch_async(queue, block1);
    dispatch_async(queue, block2);

    dispatch_notify(group,  dispatch_get_main_queue(), ^{
       
        NSLog(@"group did finished");
    });
    //打印结果
    //group did start
    //block1 finish
    //bolck2 finish
    //group did finished

answer two: 使用dispatch_barrier_(a)sync
demo:


    dispatch_block_t blockC = ^{
        for (NSInteger i = 0; i < 5000000000; i++) {
            
            if (i == 5000) {
                NSLog(@"point == 5000");
            }
            if (i == 6000) {
                NSLog(@"point == 6000");
            }
            if (i == 7000) {
                NSLog(@"point == 7000");
            }
        }
        NSLog(@"blockC did finish");
    };
    
    //试试使用注释的队列会发生什么
    //dispatch_queue_t queue = dispatch_get_global_queue(0, 
0);
    dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{ NSLog(@"blockA did finish");});
    dispatch_async(queue, ^{ NSLog(@"blockB did finish");});
    //这里有一个同步和异步的概念,想想会有什么区别?
    dispatch_barrier_sync(queue, blockC);
    dispatch_async(queue, ^{ NSLog(@"blockC did finish");});
    dispatch_async(queue, ^{ NSLog(@"blockD did finish");});
//打印结果
 11:03:32.998479+0800  blockA did finish
 11:03:32.998479+0800  blockB did finish
 11:03:32.998706+0800  point == 5000
 11:03:32.998831+0800  point == 6000
 11:03:32.998941+0800  point == 7000
 11:03:46.751058+0800  blockC did finish
 11:03:46.751281+0800  blockC did finish
 11:03:46.751322+0800  blockD did finish

answer three : 使用 semaphore

dispatch_semaphore_create   创建一个semaphore
dispatch_semaphore_signal   发送一个信号
dispatch_semaphore_wait    等待信号

demo:

    dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t semphore = dispatch_semaphore_create(2);
    for (NSInteger i = 0; i < 10; i++) {
        
        dispatch_async(queue, ^{
            
            dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task %ld",i);
            sleep(2);
            NSLog(@"complete task %ld",i);
            
            dispatch_semaphore_signal(semphore);
        });
    }

NSOprationQueue 与 GCD 的区别

NSOperation 用 GCD 构建封装的,是 GCD 的高级抽象。

GCD 仅仅支持 FIFO 队列,而NSOperation 中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。

GCD 不支持异步操作之间的依赖关系设置。

NSOperation 支持 KVO ,意味着我们可以观察任务的执行状态。

我们能够对 NSOperation 进行继承,在这之上添加成员变量和成员方法,提高整个代码的复用性。
  • 性能
    GCD 更接近底层,OP 是高级抽象,所以GCD 在追求底层操作来说,是速度最快的。
    如果异步操作需要更多的被交互和 UI 呈现,OP是好的选择。
    底层代码中,任务之间不太相互依赖,而需要更高的并发能力。GCD 则更有优势。

frame 和 bounds

问题

  • bounds 变了,frame会变吗?那什么会变?

  • View 在 transform 的时候 bounds 会变吗?frame会变吗?

带着问题看下面的代码

代码如下

    _superView.frame = CGRectMake(100, 200, 200, 200);
    //bounds 属性是用来让子类view 在确定 frame 的时候作参考的坐标。能够影响自身frame的是父类的bounds属性。
    self.boundsButton.frame = CGRectMake(0, 0, 100, 100);
    
    [UIView animateWithDuration:2 animations:^{
        
        _superView.bounds = CGRectMake(-100, -100, 200, 200);
    }completion:^(BOOL finished) {
       
        [UIView transitionWithView:_boundsButton duration:2 options:0 animations:^{
            _boundsButton.transform = CGAffineTransformMakeRotation(M_PI_4);
        } completion:^(BOOL finished) {
            
            NSLog(@"bounds = %@",NSStringFromCGRect(_boundsButton.bounds));
            // 打印结果:bounds = {{0, 0}, {100, 100}}

            UILabel *frameLabel = [[UILabel alloc] initWithFrame:_boundsButton.frame];
            
            [_superView addSubview:frameLabel];
            frameLabel.backgroundColor = [UIColor blueColor];
            frameLabel.alpha = 0.3f;
            NSLog(@"frame = %@",NSStringFromCGRect(_boundsButton.frame));
           //打印结果: frame = {{-20.710678118654748, -20.710678118654755}, {141.42135623730951, 141.42135623730951}}
        }];
    }];

运行效果

2018年 iOS 面试知识梳理_第2张图片
frame 和 bounds.gif

代码解析

  • 第一步改变 superView 的bounds ,使自己的坐标原点变为(-100,-100),导致了坐标(0,0)点到了右下方,所以子view 就向右下方移动

  • 第二步对子view做了旋转操作。可以看到子view 的bounds没有变化,但是frame发生了变化

代码结论

  • 1.frame中的 x、y 是代表一个view的左上角的坐标

  • 2.frame中的 size 是代表一个view的占有区域
    改变其值是左上角不动,从右下角进行拉伸

  • 3.bounds 中x、y 是用来给子view的frame的 x、y做为参考坐标的

  • 4.bounds中的size是代表一个view的边界长短
    改变其值是从中心进行缩放

坐标转换 API

- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;

这段 API 是从左向右读代码,转换在调用 view 中的 point 坐标在toView 中的坐标

- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;

这段 API 是从右往左读,在 fromView 中的 point 坐标转换到调用 view 中的坐标

//这和上面的是雷同的哈
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

点击事件如何响应

view 的响应都会通过一个方法判断当前view是否可以响应点击,返回的 view 就是那个可以响应的子 view。返回nil,就是不可响应。事件响应的具体移步这里

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 

需求:子 view 在超出父 View 坐标范围的情况下,仍然可以响应点击

思考:不可以响应的原因是因为默认实现方法中的点击的 point 不在父 view 上

解决办法:判断 point 在超出父View 范围并且在子 view 范围内的情况下返回子 View

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //当前 view 的父类调用此方法返回的就是当前 view
    UIView *view = [super hitTest:point withEvent:event];
     //说明 point 不在当前 view 上面
    if (view == nil) {
        UIView *subview = (UIView *)self.subviews[0];
        //这段代码不理解看上面
        CGPoint newPoint = [self convertPoint:point toView:subview];
        CGRect rect = subview.bounds;
        if (CGRectContainsPoint(rect, newPoint)) {
            return subview ;
        }
    }
    return view;
}

UIView 和 CALayer 的关系

  • 1.UIView 可以响应点击事件,而Layer 不可以
    由他们的继承关系决定
    UIView 继承自 UIResponder
    CAlayer继承自 NSObject

  • UIView的frame是由 layer 决定的
    layer 的frame 是由它的 anchorPoint 、position、bounds、transform决定的

  • UIView做为layer代理,在layer生成之后显示出来
    各司其职:View 负责对显示内容管理,layer对内容绘制

  • 他们的区别就是策略和机制的区别
    layer就是机制,不经常变动的那一部分
    view就是策略,用来做各种管理,可以变化的


block

在了解block之前,先了解
C语言函数中可能使用的变量类型

  • 函数参数
  • 自动变量(局部变量)
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

block 的类型
三种类型:
全局区
栈区
堆区

block为什么用copy修饰?
为了使block在栈区创建之后复制在堆区,不被释放

block strong 和copy 修饰一样吗?
一样的,ARC 做的事情是一样的

什么时候栈上的block会被复制到堆?

1.调用block 的copy 函数
2.block 作为函数返回
3.将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
4.方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时

不加__block的情况下,block 内的变量都不可以被改变吗?

有几种特殊情况,可以直接改变存储在这些特殊区域的变量
静态变量(局部)
静态全局变量
全局变量

第一种变量是直接指针引用,后两种变量的使用是没有被捕获到block体里面的,不经过block直接获取。

这里要注意一种情况,说的修改是整个变量的赋值操作,但是变更对象的操作是允许的,比如在不加__block的情况下给可变数组添加元素是被允许的。

我的理解这个时候的捕获的变量的指针的地址是只读的

__block 发生了什么?

过程:生成一个结构体,存放__block修饰的变量(这个结构体是不在 block 结构体内的,为了可以在不同的block体内使用)
结果:改变了自动变量的存储区域(个人理解是在了堆区)
作用:通过指针引用修饰的变量,可以修改变量的值

不调用block的情况下会不会循环引用?
也是会的。如果你的block已经被复制到了堆内存的话

解决循环引用的办法?

  • 事前避免,在使用引起循环的变量的时候加上__weak修饰符
  • 事后补救,在使用完block之后把block置为nil
  • 使用__block修饰的变量指向引起循环的变量,在调用之后置为nil
    使用第一种解循环的方式还需要注意什么
    使用下面这种写法,防止block调用的时候,self已经被释放,可能会有方法调用异常
__weak __typeof (self)weakSelf = self;
block = ^() {
__strong __typeof(weakSelf)strongSelf = weakSelf;
}

根据业务逻辑选择不同的办法


KVO 的原理

1.临时创建一个子类
2.改变监听对象的 isa 指针指向临时子类
3.改写临时子类被监听属性的 setter 方法。

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

通过上面两个方法来通知外界数据变化的
通过创建继承子类而不是直接在本类的原因是避免污染本类的其他属性的 set 方法

使用 Method Swizzing 也是可能会污染本类的
使用KVO之后class方法和object_getClass()方法获取到的类名不一样
class方法获取到的是真实类名,object_getClass()获取到的是有NSKVONotifying_前缀的类名

weak修饰的属性被置为nil的实现原理?
runtime 维护了一个 weak 表,用来存储指向某个对象(A)的所有weak指针,这个表是一个哈希表。key是某个对象(A)的地址, value是weak指针的地址数组。
深入学习

iOS runtime

基础知识
IMP
SEL
objc_object
isa
objc_class
ivar
Category
objc_msgSend都干了什么?
动态方法解析看这里
Method Swizzling
深入学习看这里

UITableView 的优化方法

iOS平台因为UIKit本身的特性,需要将所有的UI操作都放在主线程执行,所以有时候就习惯将一些线程安全性不确定的逻辑,以及它线程结束后的汇总工作等等放到了主线程,所以主线程包含大量计算、IO、绘制都有可能造成卡顿。
可以通过监控runLoop监控监控卡顿,调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿.
使用到CFRunLoopObserverRef,通过它可以实时获得这些状态值的变化
监控后另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手.
监控到了卡顿现场,当然下一步便是记录此时的函数调用信息,此处可以使用一个第三方Crash收集组件PLCrashReporter,它不仅可以收集Crash信息也可用于实时获取各线程的调用堆栈
当检测到卡顿时,抓取堆栈信息,然后在客户端做一些过滤处理,便可以上报到服务器,通过收集一定量的卡顿数据后经过分析便能准确定位需要优化的逻辑
设置正确的 reuseidentifer 以重用 cell

尽量将 View 设置为不透明,包括 cell 本身(backgroundcolor默认是透明的),图层混合靠GPU去渲染,如果透明度设置为100%,那么GPU就会忽略下面所有的layer,节约了很多不必要的运算。模拟器上点击“Debug”菜单,然后选择“color Blended Layers”,会把所有区域分成绿色和红色,绿色的好,红色的性能差(经过混合渲染的),当然也有一些图片虽然是不透明的,但是也会显示红色,如果检查代码没错的话,一般就是图片自身的性质问题了,直接联系美工或后台解决就好了。除非必须要用GPU加载的,其他最好要用CPU加载,因为CPU一般不会百分百加载,可以通过CoreGraphics画出圆角

有时候美工失误,图片大小给错了,引起不必要的图片缩放(可以找美工去改,当然也可以异步去裁剪图片然后缓存下来),还是使用Instrument的Color Misaligned Images,黄色表示图片需要缩放,紫色表示没有像素对齐。当然一般情况下图片格式不会给错,有些图片格式是GPU不支持的,就还要劳烦CPU去进行格式转换。还有可以通过Color Offscreen-Rendered Yellow来检测离屏渲染(就是把渲染结果临时保存,等到用的时候再取出,这样相对于普通渲染更消耗内存,使用maskToBounds、设置shadow,重写drawRect方法都会导致离屏渲染)
避免渐变,cornerRadius在默认情况下,这个属性只会影响视图的背景颜色和 border,但是不会离屏绘制,不影响性能。不用clipsToBounds(过多调用GPU去离屏渲染),而是让后台加载图片并处理圆角,并将处理过的图片赋值给UIImageView。UIImageView 的圆角通过直接截取图片实现,圆角路径直接用贝塞尔曲线UIBezierPath绘制(人为指定路径之后就不会触发离屏渲染),UIGraphicsBeginImageContextWithOptions。UIView的圆角可以使用CoreGraphics画出圆角矩形,核心是CGContextAddArcToPoint 函数。它中间的四个参数表示曲线的起点和终点坐标,最后一个参数表示半径。调用了四次函数后,就可以画出圆角矩形。最后再从当前的绘图上下文中获取图片并返回,最后把这个图片插入到视图层级的底部。
“Flash updated Regions”用于标记发生重绘的区域

如果 row 的高度不相同,那么将其缓存下来

如果 cell 显示的内容来自网络,那么确保这些内容是通过异步下载

使用 shadowPath 来设置阴影,图层最好不要使用阴影,阴影会导致离屏渲染(在进入屏幕渲染之前,还看不到的时候会再渲染一次,尽量不要产生离屏渲染)

减少 subview 的数量,不要去添加或移除view,要就显示,不要就隐藏

在 cellForRowAtIndexPath 中尽量做更少的操作,最好是在别的地方算好,这个方法里只做数据的显示,如果需要做一些处理,那么最好做一次之后将结果储存起来.

使用适当的数据结构来保存需要的信息,不同的结构会带来不同的操作代价

使用,rowHeight , sectionFooterHeight 和 sectionHeaderHeight 来设置一个恒定高度 , 而不是从代理(delegate)中获取

cell做数据绑定的时候,最好在willDisPlayCell里面进行,其他操作在cellForRowAtIndexPath,因为前者是第一页有多少条就执行多少次,后者是第一次加载有多少个cell就执行多少次,而且调用后者的时候cell还没显示

读取文件,写入文件,最好是放到子线程,或先读取好,在让tableView去显示

tableView滚动的时候,不要去做动画(微信的聊天界面做的就很好,在滚动的时候,动态图就不让他动,滚动停止的时候才动,不然可能会有点影响流畅度)。在滚动的时候加载图片,停止拖拽后在减速过程中不加载图片,减速停止后加载可见范围内图片
以上文字来源:
作者:si1ence
链接:https://www.jianshu.com/p/bc3f8424fad3
來源:

深入学习


离屏渲染

概念
GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
离屏渲染的代价

  • 创建新缓存区
  • 切换上下文

以下属性会触发离屏渲染:
masks
shadows
圆角
渐变
透明度
特殊的离屏渲染:cpu渲染
重写了drawRect 方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及了 CPU 渲染
深入学习


性能优化

  • 需要图片有圆角的时候,可以给UIImage一个分类,自己画一张带圆角的图片上去。

第三方框架学习

FDTemplateLayoutCell
SDWebImage
使用NSCache进行缓存
定期进行缓存清理

lazyScrollview
iosCharts
QMUI


实战

iPhone X 的适配

你可能感兴趣的:(2018年 iOS 面试知识梳理)