iOS 面试总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 面试总结相关的知识,希望对你有一定的参考价值。

APP崩溃

启动秒退

在新 ios 上正常的应用,到了老版本 iOS 上秒退最常见原因是系统动态链接库或Framework无法找到。这种情况通常是由于 App 引用了一个新版操作系统里的动态库(或者某动态库的新版本)或只有新 iOS 支持的 Framework,而又没有对老系统进行测试,于是当 App 运行在老系统上时便由于找不到而秒退。解决办法是等开发人员发现这个问题后升级程序,或由用户自行升级其操作系统。

还有一种常见的秒退是程序在升级时,修改了本地存储的数据结构,但是对用户既存的旧数据没有做好升级,结果导致初始化时因为无法正确读取用户数据而秒退。这类问题通常只需删除程序后重新安装一遍就能解决。但缺点是用户的既存数据会丢失——就算有备份可能也无济于事,因为备份下来的旧数据还是无法被正确升级。

还有一类秒退或是用到 App 里某个功能后必退的原因,是开发时用到了只有新版操作系统才支持的某个方法,而又没有对该方法是否存在于老系统中做出判断。例如程序启动时用到了 Game Center,而没有判断用户的机器是否支持 Game Center,于是就秒退了。

访问的数据为空或者类型不对

这类情况是比较常见的,后端传回了空数据,客户端没有做对应的判断继续执行下去了,这样就产生了crash。或者自己本地的某个数据为空数据而去使用了。还有就是访问的数据类型不是期望的数据类型而产生崩溃。比如,我期望服务端返回string类型,但是后台给我返回的是NSNumber类型,那么我操作时候用到的是string的方法。结果因为找不到对应的方法而崩溃。解决办法:1、服务端都加入默认值,不返回空内容或无key。或者是在客户端进行非空判断。2、对容易出错的地方提前进行类型判断。

点击事件方法处理不当

这类情况比较常见,比如我的点击事件需要对传入的参数做处理,但是点击时,我传入的对象类型或者传入为空,等到参数进行处理的时候,由于方法不当,产生crash。

数组越界

当客户端尝试对数组中的数据进行操作的时候,数组为空或者所做的操作index 超过数组本身范围,就会引起崩溃。

下拉刷新时崩溃

使用下拉刷新时,如果下拉的距离长了就会崩溃。原因是,在下拉刷新请求数据之前就将本地数组清空了。分析,下拉刷新的逻辑:1、下拉 2、下拉达到临界值时触发网络请求 3、等待数据加载到本地以后才更新datasource 4、tableview reloadData。 如果先清空数组再下拉请求,后果就是往下拉的距离超过一个 cell 的高度时,table view 的几个委托方法就会被调用,由于 data source 已经被清空,造成错误的内存访问(包括数组越界,访问已销毁的对象)导致 crash。

操作了不该操作的对象、野指针

iOS中有空指针和野指针两种概念。

空指针是没有存储任何内存地址的指针。如Student s1 = NULL;和Student s2 = nil;

而野指针是指指向一个已删除的对象("垃圾"内存既不可用内存)或未申请访问受限内存区域的指针。野指针是比较危险的。因为野指针指向的对象已经被释放了,不能用了,你再给被释放的对象发送消息就是违法的,所以会崩溃。

野指针访问已经释放的对象crash其实不是必现的,因为dealloc执行后只是告诉系统,这片内存我不用了,而系统并没有就让这片内存不能访问。

所以野指针的奔溃是比较随机的,你在测试的时候可能没发生crash,但是用户在使用的时候就可能发生crash了。

注意:arc环境比非arc环境更少出现野指针。

1、对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。

2、对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。

3、对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。

4、对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。

对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!

5、对象释放后再次release(几乎是必现Crash,但也有例外,很常见)

内存处理不当

用instruments排查内存泄露问题

主线程UI长时间卡死,被系统杀掉

主线程被卡住是非常常见的场景,具体表现就是程序不响应任何的UI交互。这时按下调试的暂停按钮,查看堆栈,就可以看到是到底是死锁、死循环等,导致UI线程被卡住。

多线程之间切换访问引起的crash

多线程引起的崩溃大部分是因为使用数据库的时候多线程同时读写数据库而造成了crash。

内存紧张

这个现在很少遇到了。

ARC内存泄漏

block系列

在 ARC 下,当 block 获取到外部变量时,由于编译器无法预测获取到的变量何时会被突然释放,为了保证程序能够正确运行,让 block 持有获取到的变量,向系统显明:我要用它,你们千万别把它回收了!然而,也正因 block 持有了变量,容易导致变量和 block 的循环引用,造成内存泄露!

对于 block 中的循环引用通常有两种解决方法:

1、将对象置为 nil ,消除引用,打破循环引用;

(这种做法有个很明显的缺点,即开发者必须保证 _networkFetecher = nil; 运行过。若不如此,就无法打破循环引用。

但这种做法的使用场景也很明显,由于 block 的内存必须等待持有它的对象被置为 nil 后才会释放。所以如果开发者希望自己控制 block 对象的生命周期时,就可以使用这种方法。)

2、将强引用转换成弱引用,打破循环引用;

(__weak __typeof(self) weakSelf = self;如果想防止 weakSelf 被释放,可以再次强引用 __typeof(&*weakSelf) strongSelf = weakSelf;代码 __typeof(&*weakSelf) strongSelf 括号内为什么要加 &* 呢?主要是为了兼容早期的 LLVM

block 的内存泄露问题包括自定义的 block,系统框架的 block 如 GCD 等,都需要注意循环引用的问题。

有个值得一提的细节是,在种类众多的 block 当中,方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API ,如

1
2
- enumerateObjectsUsingBlock:
- sortUsingComparator:

这一类 API 同样会有循环引用的隐患,但原因并非编译器做了保留,而是 API 本身会对传入的 block 做一个复制的操作。

delegate系列

1
@property (nonatomic, weak) id  delegate;

说白了就是循环使用的问题,假如我们是写的strong,那么 两个类之间调用代理就是这样的啦

1
2
3
4
5
6
7
8
9
10
BViewController *bViewController = [[BViewController alloc] init];
bViewController.delegate = self; //假设 self 是AViewController
[self.navigationController pushViewController:bViewController animated:YES];
 
/**
 假如是 strong 的情况
    bViewController.delegate ===> AViewController (也就是 A 的引用计数 + 1)
    AViewController 本身又是引用了  ===> delegate 引用计数 + 1
 导致: AViewController  Delegate ,也就循环引用啦
 */
  • Delegate创建并强引用了 AViewController;(strong ==> A 强引用、weak ==> 引用计数不变)

所以用 strong的情况下,相当于 Delegate 和 A 两个互相引用啦,A 永远会有一个引用计数 1 不会被释放,所以造成了永远不能被内存释放,因此weak是必须的。

performSelector 系列

performSelector 顾名思义即在运行时执行一个 selector,最简单的方法如下

1
- (id)performSelector:(SEL)selector;

这种调用 selector 的方法和直接调用 selector 基本等效,执行效果相同

1
2
[object methodName];
[object performSelector:@selector(methodName)];

但 performSelector 相比直接调用更加灵活

1
2
3
4
5
6
7
8
9
SEL selector;
if (/* some condition */) {
    selector = @selector(newObject);
else if (/* some other condition */) {
    selector = @selector(copy);
else {
    selector = @selector(someProperty);
}
id ret = [object performSelector:selector];

这段代码就相当于在动态之上再动态绑定。在 ARC 下编译这段代码,编译器会发出警告

1
warning: performSelector may cause a leak because its selector is unknow [-Warc-performSelector-leak]
  • 正是由于动态,编译器不知道即将调用的 selector 是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用 ARC 的内存管理规则来判断返回值是否应该释放。因此,ARC 采用了比较谨慎的做法,不添加释放操作,即在方法返回对象时就可能将其持有,从而可能导致内存泄露。

    以本段代码为例,前两种情况(newObject, copy)都需要再次释放,而第三种情况不需要。这种泄露隐藏得如此之深,以至于使用 static analyzer 都很难检测到。如果把代码的最后一行改成

    [object performSelector:selector];

    不创建一个返回值变量测试分析,简直难以想象这里居然会出现内存问题。所以如果你使用的 selector 有返回值,一定要处理掉。

  • 还有一种情况就是performSelector的延时调用[self performSelector:@selector(method1:) withObject:self.myView afterDelay:5];,performSelector关于内存管理的执行原理是这样的,当执行[self performSelector:@selector(method1:) withObject:self.myView afterDelay:5];的时候,系统将myView的引用计数加1,执行完这个方法之后将myView的引用计数减1,而在延迟调用的过程中很可能就会出现,这个方法被调用了,但是没有执行,此时myView的引用计数并没有减少到0,也就导致了切换场景的dealloc方法没有被调用,这也就引起了内存泄漏。

NSTimer

NSTimer会造成循环引用,timer会强引用target即self,在加入runloop的操作中,又引用了timer,所以在timer被invalidate之前,self也就不会被释放。

所以我们要注意,不仅仅是把timer当作实例变量的时候会造成循环引用,只要申请了timer,加入了runloop,并且target是self,虽然不是循环引用,但是self却没有释放的时机。如下方式申请的定时器,self已经无法释放了。

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(commentAnimation) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

解决这种问题有几个实现方式,大家可以根据具体场景去选择:

  • 增加startTimer和stopTimer方法,在合适的时机去调用,比如可以在viewDidDisappear时stopTimer,或者由这个类的调用者去设置。

  • 每次任务结束时使用dispatch_after方法做延时操作。注意使用weakself,否则也会强引用self。

1
2
3
4
5
6
7
- (void)startAnimation
{
    WS(weakSelf);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf commentAnimation];
    });
}
  • 使用GCD的定时器,同样注意使用weakself。

1
2
3
4
5
6
7
WS(weakSelf);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
  [weakSelf commentAnimation];
});
dispatch_resume(timer);

内存泄漏的检测方式

1.静态分析

使用XCode分析功能,Product->Analyze

使用静态检测可以检查出一些明显的没有释放的内存,包括NSObject和CF开头的内存泄漏,最常见问题有2种,这些问题都不复杂,需要的是细心:

  • MRC的文件,经常遗漏release或者autorelease。

  • C方式申请的内存,忘记释放了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1
static inline NSString* iphone_device_info(){
  size_t size;
  sysctlbyname("hw.machine", NULL, &size, NULL, 0);
  char *machine = (char*)malloc(size);
  sysctlbyname("hw.machine", machine, &size, NULL, 0);
  NSString *platform = [NSString stringWithCString:machine encoding:NSASCIIStringEncoding];
  ...
}
//2
if (alpha != 1) {
  CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
  CGColorRef color = CGColorCreate(colorSpaceRef, (CGFloat[]){255, 255, 255, 0.3});
  [btn.layer setBorderColor:color];
}

不过在修改时应该注意:

  • 这些场景是否真的泄漏了,以免重复释放。

  • 注意该文件是MRC还是ARC,需要不同的内存处理方式。

  • 如果是C申请的内存,注意new delete, malloc free的配对处理。

比如我们的代码中会遇到这样的问题。

1
2
3
4
if ([self.itemOutput hasNewPixelBufferForItemTime:currentTime]) {
      [self displayPixelBuffer:[self.itemOutput copyPixelBufferForItemTime:currentTime itemTimeForDisplay:NULL]];
      [_program useGlProgram];
  }

进行静态检测时会报copyPixelBufferForItemTime内存泄漏,copy后的对象需要进行释放,可事实上,在“displayPixelBuffer”函数中已经对传入对内存进行了释放,我们姑且不论这样对写法是否合理,只是切记在修改时注意结合上下文处理需要释放的内存。

2.动态检测 使用instruments

在Allocation中我们主要关注的是Persistent和Persistent Bytes,分别表示当前时间段,申请了但是还没释放的内存数量和大小。

记住当前这两个值,然后进入某个新页面,退出该页面,观察这两个值是否增加。需要注意的是,由于有些图片调用本身是有缓存的,如果是用SDWebImage管理,则网络图片都会缓存在内存中。因此退出页面后内存有增加是正常的,而且还有些单例的内存也是不会释放的,我们可以再次进入同一个页面,在图片都加载过的情况下,反复进入退出查看内存状况,如果持续增加,则说明有泄漏。

详细使用:

Xcode之Instruments使用

第三方工具MLeaksFinder

1、使用简单,不侵入业务逻辑代码,不用打开 Instrument

2、不需要额外的操作,你只需开发你的业务逻辑,在你运行调试时就能帮你检测

3、内存泄露发现及时,更改完代码后一运行即能发现(这点很重要,你马上就能意识到哪里写错了)

4、精准,能准确地告诉你哪个对象没被释放

具体特点,原理和集成方式可以参考如下博客的内容:

MLeaksFinder:精准 iOS 内存泄露检测工具

MLeaksFinder 新特性

界面卡顿

一、离屏渲染

OpenGL中,GPU屏幕渲染有以下两种方式:

一.On-Screen Rendering

意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。当前屏幕渲染是不需要额外创建额外的缓存,也不需要开启新的上下文,相较于离屏渲染,性能更好。

但是受当前屏幕渲染的局限因素限制(只有自身上下文,屏幕缓存有限等),很多图形渲染,当前屏幕渲染是解决不了的,这时必须使用到离屏渲染。

二.Off-Screen Rendering

离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

特殊的离屏渲染:CPU渲染

如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染,整个渲染过程有CPU在APP内同步的完成,渲染得到的bitmap(位图)最后再交由GPU用于显示。

  • 注意:CoreGraphics通常是线程安全的,所以可以进行一步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下

1
2
3
4
5
6
7
8
9
10
11
-(void)display {
 dispatch_async(backgroundQueue, ^{
     CGContextRef ctx = CGBitmapContextCreate(...);
     // draw in context...
     CGImageRef img = CGBitmapContextCreateImage(ctx);
     CFRelease(ctx);
     dispatch_async(mainQueue, ^{
         layer.contents = img;
     });
 });
}

普通的离屏渲染

相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

  • 1 创建新的缓冲区

想要进行离屏渲染,首先要创建一个新的缓冲区。

  • 2 上下文切换

离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文回家从离屏切换到当前屏幕。而上下文环境的切换时要付出很大代价的。

设置了以下属性时,都会触发离屏绘制:

1.shouldRasterize(光栅化)

2 masks(遮罩)

3 shadows (阴影)

4 edge antialiasing (抗锯齿)

5 group opacity (不透明)

其中shouldRasterize(光栅化)是比较特殊的一种:

光栅化概念:将图转化为一个个栅格组成的图像。

光栅化特点:每个元素对应帧缓冲区中的一像素。

shouldRasterize = YES 在其他属性触发离屏渲染的同时, 会将光栅化后的内容缓存起来,如果对应的layer 及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES, 这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。

相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。

当你使用光栅化时,你可以开启"Color Hits Green And Misses Red"来检查该场景下光栅化操作是否是一个号的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。

如果光栅化的层变红的太频繁那么光栅化对优化可能没有多少用处,位图缓存从内存中删除又重新创建的太过频繁,红色表明缓存重建的太迟。可以针对性的选择某个较小而深的层结构进行光栅化,来尝试减少渲染时间。

注意:

对于经常变动的内容, 这个时候不要开启,否则会造成性能的浪费。

例如我们日常经常打交道的TableViewCell, 因为TableViewCell 的重绘是很频繁的(因为cell的复用),如果cell的内容不断变化,则cell需要不断重绘,如果此时设置了cell.layer可光栅化,则会造成大量的离屏渲染,降低图形性能。

为什么使用离屏渲染

当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。

屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示在一个屏幕外上下文中被渲染(不论死CPU还是GPU)。

所以当使用离屏渲染的时候会很容易造成性能消耗,因为在OpenGL里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换也是很耗性能的。

如何选择

摆在我们面前的有三个选择:当前屏幕渲染、离屏渲染、CPU渲染。该使用哪个呢?

  • 尽量使用当前屏幕渲染

    鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。

  • 离屏渲染、CPU渲染

    由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效果,直接使用CPU渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。普通的离屏绘制是发生在绘制服务(是独立的处理过程)并且同时通过GPU执行。当OpenGL的绘制程序在绘制每个layer的时候,有可能因为包含多子层级关系而必须停下来把他们合成到一个单独的缓存里。你可能认为GPU应该总是比CPU牛逼一点儿,但是在这里我们还是需要慎重的考虑一下。因为对GPU来说,当当前屏幕到离屏上下文环境的来回切换,代价是非常大的。因为对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做的更好,所以如果你正在尝试处理一些复杂的层级,并且在犹豫到底用-[CALayer setShouldRasterize:]还是通过CoreGraphics来绘制层级上的所有内容,唯一的方法就是测试并且进行权衡。

Instuments监测离屏渲染

Instruments的Core Animation工具中又几个和离屏渲染相关的检查选项:

  • Color offscreen-Rendered Yellow

    开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。

  • Color Hits Green and Misses Red

    如果shouldRasterize 被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该出存在性能问题了。

    iOS 版本上的优化

    iOS 9.0之前 UIImageView跟UIButton设置圆角都会触发离屏渲染

    iOS 9.0 之后 UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会再触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染。

    离屏渲染总结

  • 1.尽量使用当前屏幕渲染,能不适用离屏渲染则尽量不用,你应当尽量避免使用layer的border、corner、shadow、mask等技术。

  • 2.必须离屏渲染时,相对简单的视图应该使用CPU渲染,相对复杂的视图则使用一般的离屏渲染。

二、tableview滑动卡顿

  • 1."让出"主线程,让主线程减负。所谓"让出"主线程,指的是不要什么操作都放在主线程里。放在主线程中的一般都是视图相关的操作,比如添加子视图、更新子视图、删除子视图等。像其他的图片下载、数据请求这样的操作尽量不要放在主线程中进行。

1
2
3
4
5
6
7
8
9
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
  //处理一些跟当前视图没关系的事情  
  //...  
 
  //只用来操作与当前视图有关系的事情,比如:刷新tableView  
  dispatch_async(dispatch_get_main_queue(), ^{  
      [tableView reload];  
  });  
});
  • 2.正确重用cell。正确重用cell不仅仅要重用cell视图,还需要好好重用cell的子视图。

1
2
3
4
5
6
static NSString *Identifier = @"WeatherCell";  
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:Identifier];  
if (!cell) {  
  cell = [[UITableViewCell alloc] initWithStyle:   
                                reuseIdentifier:]   
}

上面的代码在有cell可重用的时候,不会再创建新的cell,但是下面的一句话基本上会让重用粉身碎骨。

1
2
3
4
//清空子视图  
[cell.contentView.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOLBOOL *stop) {  
   [obj removeFromSuperview];  
}];

上面这段代码之所以会出现,原因众所周知:cell重用时会出现重叠。"解决"了重叠问题,那么新问题来了,重新创建视图既消耗内存还占用时间,严重会出现滑动出现卡顿现象,而且都删除了重建还能叫重用么?

三、其他解决卡顿办法

  • imageView 尽量设置为不透明

    opaque尽量设为YES,当imageView的opaque设置为YES的时候其alpha的属性就会无效,imageView的半透明取决于其图片半透明或者imageView本身的背景色的合成的图层view是半透明的。如果图片全部不是半透明就不会触发图层的blend操作,整个图层就回不透明,如果叠加的图片有出现半透明的,就会立马触发图层的blend操作,整个图层不透明。

    opaque设为NO的话,当opaque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果。

  • 背景色尽可能设为alpha值为1

    当某一块图层的alpha和其superView的背景色alpha不一样的时候会触发alpha合成操作,这是一项看似简单但是却非常消耗CPU性能的操作。

  • UIView的背景色设置

    UIview的背景色尽量不要设置为clearColor,这样也会触发alpha叠加,在tableview滑动时候是非常消耗性能的。子视图的背景色尽可能设置成其superView的背景色,这样图层合成的时候就不会触发blend操作了。

  • 最好不适用带有alpha通道的图片,如果有alpha尽量让美工取消alpha通道。

    alpha通道 是透明的意思。

  • cell上layer尽量避免使用圆角

    在工作中关于滑动界面我们会时常遇到cell行设置头像为圆角等需求,这时候我们尽量避免使用layer.masksToBounds,因为这会触发离屏渲染(上面有讲离屏渲染)。

    优化UIImageView圆角方式 用贝塞尔曲线,不会触发离屏渲染:

1
2
3
4
5
6
7
8
9
10
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
                         cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
  • 优化图片的加载方式

1
2
UIImage Image = [UIImage imageNamed:@"helloworld"
UIImage Image = [UIImage imageWithContentOfFile:@"helloworld"];

图片的两种加载方式:

第一种:当我们经常需要这张图片并且仅仅是小图的时候,我们可以使用此方式加载图片。这种方式是把图片缓存在图片缓冲区,当我们使用的时候会通过图片的名字也就是通过key的方式去查找图片在缓存区的内存地址。

当我们使用很多图片的时候系统就会开辟很多内存来存储图片。

第二种:当我们使用工程里面的一张大图并且使用次数很少甚至为1次的时候,我们优先会采用这种方式来加载图片,这种方式当使用完图片的时候会立即丢弃释放资源,所以对性能不会带来负担。

  • 尽量延迟图片的加载

    当我们在滑动页面的时候,尤其是对于那种布局特别复杂的cell,滑动的时候不要加载图片,当滑动停止的时候再进行图片的加载。

    我们都知道不管是tableview还是scrollview在滚动的时候需要显示东西都是通过runloop去拿。当滚动的时候runloop会处于NSRunLoopTrackingMode的模式,我们可以通过一个主线程队列dispatch_after或者selfPerformSeletor设置runloop的模式为NSDefaultRunLoopMode模式,就可以做到停止滚动再加载图片。注:其实严格意义上selfPerformSelector的事件就是在主线程队列中等待。

  • 使用懒加载,即 需要时再加载。

  • 最终要的是避免阻塞主线程。

    让图片的绘制、图片的下载、对象的创建、文本的渲染等这些耗时的操作尽可能采用子线程的方式去处理,对于layer以及UI的操作不得不在主线程里面,只能想办法优化(Facebook -> AsyncDisplayKit)

  • xib、storyBoard、纯代码

    storyBoard可以为开发者节省大量的时间,提高开发效率,但是对于那种复杂的滑动界面,用storyBoard时非常耗资源的,对于那种重用性不强固定不怎么变化的界面还是storyBoard省事儿。

  • 不要重复创建不必要的tableviewCell

    UItableView只需要一屏幕的cell对象就可以了,因为tableview提供了cell的缓存机制,在不可见的时候,可以将其缓存起来,而在需要时继续使用即可。值得一提的是,cell被重用时,它内部绘制的内容并不会被自动清除,因此你可能需要调用setNeedsDisplayInRect:或setNeedsDisplay方法。

  • 减少视图的数目

    UITableViewCell包含了textLabel、detailTextLabel和imageView等view,而你还可以自定义一些视图放在它的contentView里,然而view是很大的对象,创建它会消耗较多的资源,并且也影响渲染性能。如果你的cell包含图片且数量较多,使用默认的cell会非常影响性能。最佳的解决办法是继承UITableViewCell,并在其drawRect:中自行绘制:

1
(void)drawRect:(CGRect)rect { if (image) { [image drawAtPoint:imagePoint]; self.image = nil; } else { [placeHolder drawAtPoint:imagePoint]; } [text drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation]; }

不过这样一来,你会发现选中一行后,这个cell就变蓝了,其中的内容就被挡住了。最简单的方法就是将cell的selectionStyle属性设为UITableViewCellSelectionStyleNone,这样就不会被高亮了。

此 外还可以创建CALayer,将内容绘制到layer上,然后对cell的contentView.layer调用addSublayer:方法。这个例 子中,layer并不会显著影响性能,但如果layer透明,或者有圆角、变形等效果,就会影响到绘制速度了。解决办法可参见后面的预渲染图像。

不要做多余的绘制工作。

在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。

例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。

预渲染图像。

你会发现即使做到了上述几点,当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕

  • 少用addView给cell动态添加view,可以初始化的时候就添加,然后通过hide控制是否显示。

  • 提前计算并缓存好高度,因为heightForRow最频繁的调用。

  • 善用hidden隐藏(显示)Subview

iOS 高效添加圆角效果实战讲解

内存恶鬼drawRect

UIKit性能调优实战讲解

图片压缩

  • 一 首先要知道 什么是压缩:

    “压” 指文件体积变小,但是像素不变,长宽尺寸不变,那么质量可能下降

    “缩” 指文件的尺寸变小,也就是像素数减少,而长宽尺寸变小,文件体积同样会减小。

  • 二 图片的压处理

    图片的压处理,我们可以使用UIImageJPEGRepresentation或UIImagePNGRepresentation方法实现

    如代码:

1
2
3
4
5
6
- (void)_imageCompression{
    UIImage *image = [UIImage imageNamed:@"HD"];
   //第一个参数是图片对象,第二个参数是压的系数,其值范围为0~1。
    NSData * imageData = UIImageJPEGRepresentation(image, 0.7);
    UIImage * newImage = [UIImage imageWithData:imageData];
}

关于PNG和JPEG格式压缩

UIImageJPEGRepresentation函数需要两个参数:图片的引用和压缩系数UIImagePNGRepresentation只需要图片引用作为参数。UIImagePNGRepresentation(UIImage image)要比UIImageJPEGRepresentation(UIImage image,1.0)返回的图片数据量大很多。同样的一张照片,使用UIImagePNGRepresentation(image)返回的数据量大小为200k,而UIImageJPEGRepresentation(image,1.0)返回的数据量大小只为150k,如果对图片的清晰度要求不是极高,建议使用UIImageJPEGRepresentation,可以大幅度降低图片数据量。注意:压缩系数不宜太低,通常是0.3~0.7,过小则可能会出现黑边等。

  • 三 图片“缩”处理

    通过[image drawInRect:CGRectMake(0,0,targetWidth,targetHeight)可以进行图片“缩”处理。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
  *  图片压缩到指定大小
  *  @param targetSize  目标图片的大小
  *  @param sourceImage 源图片
  *  @return 目标图片
  */
 - (UIImage*)imageByScalingAndCroppingForSize:(CGSize)targetSize withSourceImage:(UIImage *)sourceImage
{
UIImage *newImage = nil;
CGSize imageSize = sourceImage.size;
CGFloat width = imageSize.width;
CGFloat height = imageSize.height;
CGFloat targetWidth = targetSize.width;
CGFloat targetHeight = targetSize.height;
CGFloat scaleFactor = 0.0;
CGFloat scaledWidth = targetWidth;
CGFloat scaledHeight = targetHeight;
CGPoint thumbnailPoint = CGPointMake(0.0,0.0);
if (CGSizeEqualToSize(imageSize, targetSize) == NO)
{
    CGFloat widthFactor = targetWidth / width;
    CGFloat heightFactor = targetHeight / height;
    if (widthFactor > heightFactor)
        scaleFactor = widthFactor; // scale to fit height
    else
        scaleFactor = heightFactor; // scale to fit width
    scaledWidth= width * scaleFactor;
    scaledHeight = height * scaleFactor;
    // center the image
    if (widthFactor > heightFactor)
    {
        thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5;
    }
    else if (widthFactor < heightFactor)
    {
        thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5;
    }
}
UIGraphicsBeginImageContext(targetSize); // this will crop
CGRect thumbnailRect = CGRectZero;
thumbnailRect.origin = thumbnailPoint;
thumbnailRect.size.width= scaledWidth;
thumbnailRect.size.height = scaledHeight;
 
[sourceImage drawInRect:thumbnailRect];
newImage = UIGraphicsGetImageFromCurrentImageContext();
if(newImage == nil)
    NSLog(@"could not scale image");
 
//pop the context to get back to the default
UIGraphicsEndImageContext();
 
  return newImage;
}

数据持久化

数据持久化是一种非易失性存储技术,在重启计算机或设备后也不会丢失数据,试讲内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。数据模型可以是任何数据结构或对象模型,存储模型可以是关系模型、XML、二进制流等。持久化技术主要用于MVC模型中的model层。目前iOS平台上主要使用如下四种技术:

NSUserDefaults()

属性列表概念:属性列表是一种基于xml序列化的数据欧诺个就存储文件,又称plist文件,原理是将一些基本数据类型读写进plist文件(plist文件是XML格式文件,因为常用于存储配置信息,使用又称作plist格式文件)并以明文方式存储在设备中。许多OC 的基本数据类型(如NSArray、NSString 等)本身提供了向plist文件读写的方法,但是实际项目中我们用到的更多是NSUserDefaults,NSUserDefaults是苹果基于属性列表所封装的一个单例类,该类提供了基本数据类型的plist文件存储方法,因为其使用方便,代码易懂, NSUserDefaults成为了最常用的数据持久化方式之一。

NSUserDefaults常用方法

1
2
3
4
5
6
7
8
9
10
11
12
//从 NSUserDefaults 中取出 key 值所对应的 Value
id = [[NSUserDefaults standardUserDefaults] objectForKey:(NSString *)];
 
//将数据对象存储到 NSUserDefaults 中
[[NSUserDefaults standardUserDefaults] setObject:(id)
                                          forKey:(NSString *)];
 
//将数据对象从 NSUserDefaults 中移除
[[NSUserDefaults standardUserDefaults] removeObjectForKey(NSString *)];
 
//同步更新到Plist文件,当修改了 NSUserDefaults 的数据后,必须进行此步操作
[[NSUserDefaults standardUserDefaults] synchronize];

NSUserDefaults特点

  • NSUserDefaults常用于存储OC基本数据类型,不适合存储自定义对象,NSUserDefaults支持的数据类型有:NSNumer(NSInteger,float,double)NSSstring,NSDate,NSArray,NSDictionary,BOOL.

  • 自定义对象可以转化成基本类型NSData后再使用NSUserDefaults机型存储,但并不常用。

  • 当plist文件存储的数据发生改变(写操作)时,需要调用syschronize方法同步,否则数据无法同步保存。

  • Key值应具有唯一性,重名时将覆盖先前key值。

  • 实际开发中,NSUserDefaults常用语于存储配置信息,优点是简便,缺点是所有数据都以明文存储在plist文件中,容易被解读导致安全性不高。

NSUserDefautls将数据存储在什么地方了?

它存储在应用的一个plist文件中,在程序沙盒位置的/Library/Prefereces里面有个plist文件,存储的就是你的userDefaults,想要删掉的话,用removeObjectForKey或者删掉沙盒,也就是你的应用程序然后重新安装。

  • 此外还可以自定义plist文件的位置进行存储数据

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
NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
1.获得文件路径
 
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
2.存储
 
NSArray *array = @[@"123", @"456", @"789"];
[array writeToFile:fileName atomically:YES];
3.读取
 
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
4.注意
 
只有以上列出的类型才能使用plist文件存储。
存储时使用writeToFile: atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。
读取时使用arrayWithContentsOfFile:方法。

对象归档(序列化)

和属性列表一样,对象归档也是将对象写入文件并保存在硬盘内,所以本质上是另一种形式的序列化(存储模型不同)。虽说任何对象都可以被序列化,但只有某些特定的对象才能放置到某个集合类(例如:NSArray,NSMutableArray,NSDictionary,NSData等)中,并使用该集合类的方法在属性列表中读写,一旦将包含了自定义对象的数组写入属性列表,程序就会报错。归档与属性列表方式不同,属性列表只有指定的一些对象才能进行持久化且明文存储,而归档时任何实现了NSCoding协议的对象都可以被持久化,且归档后的文件是加密的。对象归档涉及两个类: NSKeyedArchiver和NSKeyedUnarchiver,这两个类是NSCoder的子类,分别用于归档和解档。

对象归档

现在,我们有一个自定义的Person类,该类有name,age,height三个属性,其.h文件如下

1
2
3
4
5
6
//Person.h
#import
@interface Person:NSObject
@property(nonatomic,copy)NSString *name;
@property(nonatomic,assign)int age;
@property(nonatomic,assign)double height;

在.m文件中,我们要实现NSCoding中的两个协议方法,这两个方法分别在归档和解档时会被调用,Person类的.m文件如下

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
//Person.m
#import"Person.h"
@implementation Person
/*
* 归档时调用该方法,该方法说明哪些数据需要储存,怎样储存
*/
- (void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:_name forKey:@"name"];
    [encoder encodeInt:_age forKey:@"age"];
    [encoder encodeDouble:_name forKey:@"height"];
}
 
/*
* 归档时调用该方法,该方法说明哪些数据需要解析,怎样解析
*/
-(id)initWithCoder:(NSCoder *)decode
{
    if (self = [super init]) {
        _name = [decode decodeObjectForKey:@"name"];
        _age = [decode decodeIntForKey:@"age"];
        _height = [decode decodeDoubleForKey:@"height"];
    }
    return self;
}
@end

这个Person类就具有了归档与解档能力,当你需要对一个Person类的实力对象进行储存或者解析时,在你自己的方法中只要键入如下代码即可,下面两个方法对应两个按钮的回调,点击按钮时分别执行person对象的归档和解档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//写操作
- (IBAction)Write {
    Person *p = [[Person alloc]init];
    p.name = @"jin";
    p.age = 10;
    p.height = 176.0;
 
    //设置归档后文件路径
    NSString *path = @"/Users/macbookair/Desktop/person.data";
    //归档
    [NSKeyedArchiver archiveRootObject:p toFile:path];
}
 
//读操作
- (IBAction)read {
 
    //设置路径
    NSString *path = @"/Users/macbookair/Desktop/person.data";
 
    //解档
    Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    NSLog(@"%@--%d---%f",p.name ,p.age ,p.height);
 
}

对象归档特点

可以将自定义对象写入文件或从文件中读出。

由于归档时进行了加密处理,因此安全性高于属性列表。

CoreData(集成化)

当你的应用程序需要在本地存储大量的关系型数据模型时,显然上述方法并不适合,因为不论对象归档还是数据列表,一旦数据模型之间存在依赖关系,问题就将变得复杂。而此时iPhone自带的轻量级数据库Sqlite便成为我们的首选,如果你熟悉数据库,那么恭喜,CoreData也将不再神秘,你可以理解为它是苹果对Sqlite封装的一个框架,你可以在Xcode中进行Sqlite数据库的可视化操作

为什么要使用CoreData?

CoreData脱离了Sql语句,集成化更高。实际上,一个成熟的工程中一定是对数据持久化进行了封装的,应该避免在业务逻辑中直接编写Sql语句。

CoreData对版本迁移支持的较好,App升级之后数据库字段或者表有更改会导致crash,CoreData的版本管理和数据迁移变得非常有用,手动写sql语句操作相对麻烦一些。

CoreData不光能操纵SQLite,CoreData和iCloud的结合也很好,如果有这方面需求的话优先考虑CoreData。

CoreData是支持多线程的,但需要thread confinement的方式实现,使用了多线程之后可以最大化的防止阻塞主线程。

Sqlite(灵活)

Sqlite是iPhone自带的的数据库管理系统。如果你对数据库和Sql语句不陌生,那么在介绍完CoreData后,你一定不满足CoreData,作为一个程序员,也许你更希望能够直接操作数据库。既然苹果选择Sqlite作为数据库,自然也提供了一系列可以直接操作它的函数(C语言函数),利用这些函数你完全能够自己封装一个sqlite数据库框架,但同时你必须熟悉sql语句以及C语言语法。SQLite数据库的几个特点:

基于C语言开发的轻型数据库

在iOS中需要使用C语言语法进行数据库操作、访问(无法使用ObjC直接访问,因为libqlite3框架基于C语言编写)

SQLite中采用的是动态数据类型,即使创建时定义了一种类型,在实际操作时也可以存储其他类型,但是推荐建库时使用合适的类型(特别是应用需要考虑跨平台的情况时)

建立连接后通常不需要关闭连接(尽管可以手动关闭)

#FMDB

FMDB框架中重要的框架类

FMDatabase

FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句

FMResultSet

使用FMDatabase执行查询后的结果集

FMDatabaseQueue

用于在多线程中执行多个查询或更新,它是线程安全的

数据库第三方框架FMDB详细讲解

网络传输协议

深入浅出-iOS的TCP/IP协议族剖析&&Socket

iOS-网络编程(一)HTTP协议

RunTime

以上是关于iOS 面试总结的主要内容,如果未能解决你的问题,请参考以下文章

面试常用的代码片段

iOS开发-面试总结(十七)

iOS开发-面试总结(十五)

面向面试编程代码片段之GC

iOS面试--虎牙最新iOS开发面试题

2021最新iOS面试题总结