2018年 iOS 面试知识梳理

2018-03-28  本文已影响95人  柯浩然

#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

同步和异步的主要区别在于会不会阻塞当前线程。
同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
需要传入俩个参数,第一个是唯一标识符(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.

串行队列 异步执行 会开启一条新线程,但是任务还是串行,所以任务还是一个一个执行。

GCD的其他用法

GCD 的应用

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 进行继承,在这之上添加成员变量和成员方法,提高整个代码的复用性。

frame 和 bounds

问题

带着问题看下面的代码

代码如下

    _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}}
        }];
    }];

运行效果

frame 和 bounds.gif
代码解析

代码结论

坐标转换 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 的关系


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 __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 渲染
深入学习


性能优化


第三方框架学习

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

lazyScrollview
iosCharts
QMUI


实战

iPhone X 的适配

上一篇下一篇

猜你喜欢

热点阅读