iOS面试合集+答案(四)

2,346 阅读33分钟

image.png

这个栏目将持续更新--请iOS的小伙伴关注!

(答案不唯一,仅供参考,文章最后有福利)

六十一:什么是ARC?

ARC全称是 Automatic Reference Counting,是Objective-C的内存管理机制。简单地来说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

ARC的使用是为了解决对象retain和release匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。

以前需要手动的通过retain去为对象获取内存,并用release释放内存。所以以前的操作称为MRC (Manual Reference Counting)。

六十二:ARC的底层原理,怎么实现自动释放的,和MRC的区别是什么?

  • ARC管理原则:只要一个对象没有被强指针修饰就会被销毁,默认局部变量对象都是强指针,存放到堆里面,只是局部变量的强指针会在代码块结束后释放,对应所指向的内存空间也会被销毁。

  • MRC没有strong,weak,局部变量对象就是相当于基本数据类型。MRC给成员属性赋值,一定要使用set方法,不能直接访问下划线成员属性赋值,因为使用下划线是直接赋值(如_name = name),而set方法会多做影响引用计数方面的事情,比如retain。

六十三:苹果为什么推出ARC?

  • 在MRC时代,我们要想保持一个对象,只要“retain”。现在的ARC是不需要了,现在只需用一个指针指向这个对象,无非2种情况:第一:指针为空时,对象被释放咯。第二:指针不为空,对象会一直保存在堆里,如果当指针指向一个新的值时,原来的对象会被release一次,这个系统会在合适的时候自动帮我们搞掂,不需我们关心。

  • 而在ARC时,只要对象指针被置空,就会释放。否则,对象就会一直保持在堆上。当将指针指向新值时,原来的对象会被release 一次。

六十四:有了线程,你觉得为什么还要有runloop?,runloop和线程有什么关系?

解析:关于为什么要,我觉得runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。

关于这两者的更多关系:

  • runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
  • runloop在第一次获取时被创建,在线程结束时被销毁。
  • 对于主线程来说,runloop在程序一启动就默认创建好了。
  • 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。

六十五:objc中向一个nil对象发送消息将会发生什么?

首先,需要搞明白2个问题:

  • 什么是isa指针
  • 消息传递机制

isa指针是用于对象指向类对象,类对象指向元类对象的一个指针。而类对象和元类对象中又分别存放对象方法和类方法。 在消息传递机制中,就是通过isa指针来寻找到方法的实际调用地址的。

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。

六十六:常用的设计模式

1 代理模式
应用场景:当一个类的某些功能需要由别的类来实现,但是又不确定具体会是哪个类实现。
优势:解耦合
敏捷原则:开放-封闭原则

实例:

  • tableview的 数据源delegate,通过和protocol的配合,完成委托诉求。
  • 列表row个数delegate
  • 自定义的delegate

2 观察者模式
应用场景:一般为model层对,controller和view进行的通知方式,不关心谁去接收,只负责发布信息。
优势:解耦合
敏捷原则:接口隔离原则,开放-封闭原则

实例:

  • Notification通知中心,注册通知中心,任何位置可以发送消息,注册观察者的对象可以接收。
  • kvo,键值对改变通知的观察者,平时基本没用过。

3 MVC模式
应用场景:是一中非常古老的设计模式,通过数据模型,控制器逻辑,视图展示将应用程序进行逻辑划分。
优势:使系统,层次清晰,职责分明,易于维护
敏捷原则:对扩展开放-对修改封闭

实例:

  • model-即数据模型,view-视图展示,controller进行UI展现和数据交互的逻辑控制。

4 单例模式
应用场景:确保程序运行期某个类,只有一份实例,用于进行资源共享控制。
优势:使用简单,延时求值,易于跨模块
敏捷原则:单一职责原则

实例:

  • [UIApplication sharedApplication]。

注意事项:确保使用者只能通过 getInstance方法才能获得,单例类的唯一实例。
java,C++中使其没有公有构造函数,私有化并覆盖其构造函数。
object c中,重写allocWithZone方法,保证即使用户用 alloc方法直接创建单例类的实例,

返回的也只是此单例类的唯一静态变量。

5 策略模式
应用场景:定义算法族,封装起来,使他们之间可以相互替换。
优势:使算法的变化独立于使用算法的用户
敏捷原则:接口隔离原则;多用组合,少用继承;针对接口编程,而非实现。

实例:

  • 排序算法,NSArray的sortedArrayUsingSelector;经典的鸭子会叫,会飞案例。

注意事项:
1.剥离类中易于变化的行为,通过组合的方式嵌入抽象基类
2.变化的行为抽象基类为,所有可变变化的父类
3.用户类的最终实例,通过注入行为实例的方式,设定易变行为防止了继承行为方式,导致无关行为污染子类。完成了策略封装和可替换性。

6 工厂模式
应用场景:工厂方式创建类的实例,多与proxy模式配合,创建可替换代理类。
优势:易于替换,面向抽象编程,application只与抽象工厂和易变类的共性抽象类发生调用关系。
敏捷原则:DIP依赖倒置原则

实例:

  • 项目部署环境中依赖多个不同类型的数据库时,需要使用工厂配合proxy完成易用性替换

注意事项:项目初期,软件结构和需求都没有稳定下来时,不建议使用此模式,因为其劣势也很明显,增加了代码的复杂度,增加了调用层次,增加了内存负担。所以要注意防止模式的滥用。

六十七:单例会有什么弊端?

主要优点:

  • 1、提供了对唯一实例的受控访问。
  • 2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  • 3、允许可变数目的实例。

主要缺点:

  • 1、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

六十八:你会如何存储用户的一些敏感信息,如登录的token

使用keychain来存储,也就是钥匙串,使用keychain需要导入Security框架

iOS的keychain服务提供了一种安全的保存私密信息(密码,序列号,证书等)的方式,每个iOS程序都有一个独立的keychain存储。相对于 NSUserDefaults、文件保存等一般方式,keychain保存更为安全,而且keychain里保存的信息不会因App被删除而丢失,所以在 重装App后,keychain里的数据还能使用。从iOS 3.0开始,跨程序分享keychain变得可行。

如何需要在应用里使 用使用keyChain,我们需要导入Security.framework ,keychain的操作接口声明在头文件SecItem.h里。直接使用SecItem.h里方法操作keychain,需要写的代码较为复杂,为减轻 咱们程序员的开发,我们可以使用一些已经封装好了的工具类,下面我会简单介绍下我用过的两个工具类:KeychainItemWrapper和 SFHFKeychainUtils。

自定义一个keychain的类

  • CSKeyChain.h
1.  @interface CSKeyChain : NSObject
2. 
3.  + (NSMutableDictionary *)getKeychainQuery:(NSString *)service;
4. 
5.  + (void)save:(NSString *)service data:(id)data;
6. 
7.  + (id)load:(NSString *)service;
8. 
9.  + (void)delete:(NSString *)service;
10. 
11. @end
  • CSKeyChain.m
1.   #import "CSKeyChain.h"
2.   #import<Security/Security.h>
3.
4.   @implementation CSKeyChain
5.
6.   + (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
7.       return [NSMutableDictionary dictionaryWithObjectsAndKeys:
8.               (__bridge_transfer id)kSecClassGenericPassword,(__bridge_transfer id)kSecClass,
9.               service, (__bridge_transfer id)kSecAttrService,
10.              service, (__bridge_transfer id)kSecAttrAccount,
11.              (__bridge_transfer id)kSecAttrAccessibleAfterFirstUnlock,(__bridge_transfer id)kSecAttrAccessible,
12.              nil];
13.  }
14.
15.  + (void)save:(NSString *)service data:(id)data {
16.      // 获得搜索字典
17.      NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
18.      // 添加新的删除旧的
19.      SecItemDelete((__bridge_retained CFDictionaryRef)keychainQuery);
20.      // 添加新的对象到字符串
21.      [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge_transfer id)kSecValueData];
22.      // 查询钥匙串
23.      SecItemAdd((__bridge_retained CFDictionaryRef)keychainQuery, NULL);
24.  }
25. 
26.  + (id)load:(NSString *)service {
27.      id ret = nil;
28.      NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
29.      // 配置搜索设置
30.      [keychainQuery setObject:(id)kCFBooleanTrue forKey:(__bridge_transfer id)kSecReturnData];
31.      [keychainQuery setObject:(__bridge_transfer id)kSecMatchLimitOne forKey:(__bridge_transfer id)kSecMatchLimit];
32.      
33.      CFDataRef keyData = NULL;
34.      
35.      if (SecItemCopyMatching((__bridge_retained CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
36.          @try {
37.              ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge_transfer NSData *)keyData];
38.          } @catch (NSException *e) {
39.              NSLog(@"Unarchive of %@ failed: %@", service, e);
40.          } @finally {
41.          }
42.      }
43.    
44.      return ret;
45.  }
46.  
47.  + (void)delete:(NSString *)service {
48.      NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
49.      SecItemDelete((__bridge_retained CFDictionaryRef)keychainQuery);
50.  }
51.
52.  @end
  • 在别的类实现存储,加载,删除敏感信息方法
1.   // 用来标识这个钥匙串
2.   static NSString * const KEY_IN_KEYCHAIN = @"com.cs.app.allinfo";
3.   // 用来标识密码
4.   static NSString * const KEY_PASSWORD = @"com.cs.app.password";
5.
6.   + (void)savePassWord:(NSString *)password {
7.       NSMutableDictionary *passwordDict = [NSMutableDictionary dictionary];
8.       [passwordDict setObject:password forKey:KEY_PASSWORD];
9.       [CSKeyChain save:KEY_IN_KEYCHAIN data:passwordDict];
10.  }
11.
12.  + (id)readPassWord {
13.      NSMutableDictionary *passwordDict = (NSMutableDictionary *)[CSKeyChain load:KEY_IN_KEYCHAIN];
14.      return [passwordDict objectForKey:KEY_PASSWORD];
15.  }
16.
17.  + (void)deletePassWord {
18.      [CSKeyChain delete:KEY_IN_KEYCHAIN];
19.  }

六十九:UIScrollView大概是如何实现的,它是如何捕捉、响应手势的?

UIScrollView在滚动过程当中,其实是在修改原点坐标。当手指触摸后, scroll view会暂时拦截触摸事件,使用一个计时器。假如在计时器到点后没有发生手指移动事件,那么 scroll view 发送 tracking events 到被点击的 subview。假如在计时器到点前发生了移动事件,那么 scroll view 取消 tracking 自己发生滚动。

首先了解下UIScrollView对于touch事件的接收处理原理:

  • UIScrollView应该是重载了hitTest 方法,并总会返回itself 。所以所有的touch 事件都会进入到它自己里面去了。内部的touch事件检测到这个事件是不是和自己相关的,或者处理或者除递给内部的view。

  • 为了检测touch是处理还是传递,UIScrollView当touch发生时会生成一个timer。

    • 如果150ms内touch未产生移动,它就把这个事件传递给内部view
    • 如果150ms内touch产生移动,开始scrolling,不会传递给内部的view。(例如, 当你touch一个table时候,直接scrolling,你touch的那行永远不会highlight。)
    • 如果150ms内touch未产生移动并且UIScrollView开始传递内部的view事件,但是移动足够远的话,且canCancelContentTouches = YES,UIScrollView会调用touchesCancelled方法,cancel掉内部view的事件响应,并开始scrolling。(例如, 当你touch一个table, 停止了一会,然后开始scrolling,那一行就首先被highlight,但是随后就不在高亮了)

七十:如何实现夜间模式?

  • 1.准备两套资源,分别对应日间模式和夜间模式。

  • 2.在系统全局保存一个变量(BOOL isNight),根据用户的操作改变这个变量的值;

  • 3.把每个需要被改变的view, viewcontroller加入通知中心中监听(NeedTransferToNight和NeedTransferToDay)事件;

  • 4.默认为日间模式,isNight = YES.

  • 5.当用户点击夜间按钮时,如果isNight == YES, 讲此变量的值置为NO,通知中心发布NeedTransferToNight通知,所有需要被改变的view和viewcontroller在监听到此事 件时使用夜间资源重新绘制自身。其他view在初始化时如果发现isNight为YES.则使用夜间资源初始化自身。(反之亦然)

  • 6.运行程序,可以看到夜间模式。

七十一:如何捕获异常?

  • 在app启动时(didFinishLaunchingWithOptions),添加一个异常捕获的监听。
1.- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
2.    // Override point for customization after application launch.
3.    
4.    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
5.
6.    return YES;
7.}
  • 实现捕获异常日志并保存到本地的方法。
1.   void UncaughtExceptionHandler(NSException *exception){
2.       
3.       // 异常日志获取
4.       NSArray  *excpArr = [exception callStackSymbols];
5.       NSString *reason = [exception reason];
6.       NSString *name = [exception name];
7.    
8.       NSString *excpCnt = [NSString stringWithFormat:@"exceptionType: %@ \n reason: %@ \n stackSymbols: %@",name,reason,excpArr];
9.       
10.      // 日常日志保存(可以将此功能单独提炼到一个方法中)
11.      NSArray  *dirArr  = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
12.      NSString *dirPath = dirArr[0];
13.      NSString *logDir = [dirPath stringByAppendingString:@"/CrashLog"];
14.    
15.      BOOL isExistLogDir = YES;
16.      NSFileManager *fileManager = [NSFileManager defaultManager];
17.      if (![fileManager fileExistsAtPath:logDir]) {
18.          isExistLogDir = [fileManager createDirectoryAtPath:logDir withIntermediateDirectories:YES attributes:nil error:nil];
19.      }
20.    
21.      if (isExistLogDir) {
22.          // 此处可扩展
23.          NSString *logPath = [logDir stringByAppendingString:@"/crashLog.txt"];
24.          [excpCnt writeToFile:logPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
25.      }
26.  }

七十二:frame与center bounds的关系

  • frame属性是相对于父容器的定位坐标。

  • bounds属性针对于自己,指明大小边框,默认点为(0,0),而宽和高与frame宽和高相等。

  • center属性是针对与frame属性的中心点坐标。

  • 当frame变化时,bounds和center相应变化。

  • 当bounds变化时,frame会根据新bounds的宽和高,在不改变center的情况下,进行重新设定。

  • center永远与frame相关,指定frame的中心坐标!

七十三:断点续传如何实现的?

断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当 某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。

使用多线程断点续传下载的时候,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,多个线程并发可以占用服务器端更多资源,从而加快下载速度。

在下载(或上传)过程中,如果网络故障、电量不足等原因导致下载中断,这就需要使用到断点续传功能。下次启动时,可以从记录位置(已经下载的部分)开始,继续下载以后未下载的部分,避免重复部分的下载。断点续传实质就是能记录上一次已下载完成的位置。

断点续传的过程

  • 1.断点续传需要在下载过程中记录每条线程的下载进度;
  • 2.每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库;
  • 3.在每次向文件中写入数据之后,在数据库中更新下载进度;
  • 4.下载完成之后删除数据库中下载记录。

七十四:通知,代理,KVO的区别,以及通知的多线程问题

1. delegate

当我们第一次编写ios应用时,我们注意到不断的在使用“delegate”,并且贯穿于整个SDK。delegation模式不是IOS特有的模式,而是依赖与你过去拥有的编程背景。针对它的优势以及为什么经常使用到,这种模式可能不是很明显的。

delegation的基本特征是:一个controller定义了一个协议(即一系列的方法定义)。该协议描述了一个delegate对象为了能够响应一个controller的事件而必须做的事情。协议就是delegator说,“如果你想作为我的delegate,那么你就必须实现这些方法”。实现这些方法就是允许controller在它的delegate能够调用这些方法,而它的delegate知道什么时候调用哪种方法。delegate可以是任何一种对象类型,因此controller不会与某种对象进行耦合,但是当该对象尝试告诉委托事情时,该对象能确定delegate将响应。

delegate的优势:

  • 1.非常严格的语法。所有将听到的事件必须是在delegate协议中有清晰的定义。
  • 2.如果delegate中的一个方法没有实现那么就会出现编译警告/错误
  • 3.协议必须在controller的作用域范围内定义
  • 4.在一个应用中的控制流程是可跟踪的并且是可识别的;
  • 5.在一个控制器中可以定义定义多个不同的协议,每个协议有不同的delegates
  • 6.没有第三方对象要求保持/监视通信过程。
  • 7.能够接收调用的协议方法的返回值。这意味着delegate能够提供反馈信息给controller

缺点:

  • 1.需要定义很多代码:1.协议定义;2.controller的delegate属性;3.在delegate本身中实现delegate方法定义
  • 2.在释放代理对象时,需要小心的将delegate改为nil。一旦设定失败,那么调用释放对象的方法将会出现内存crash
  • 3.在一个controller中有多个delegate对象,并且delegate是遵守同一个协议,但还是很难告诉多个对象同一个事件,不过有可能。

2. notification

在iOS应用开发中有一个”Notification Center“的概念。它是一个单例对象,允许当事件发生时通知一些对象。它允许我们在低程度耦合的情况下,满足控制器与一个任意的对象进行通信的目的。这种模式的基本特征是为了让其他的对象能够接收到在该controller中发生某种事件而产生的消息,controller用一个key(通知名称)。这样对于controller来说是匿名的,其他的使用同样的key来注册了该通知的对象(即观察者)能够对通知的事件作出反应。

通知优势:

  • 1.不需要编写多少代码,实现比较简单;
  • 2.对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单
  • 3.controller能够传递context对象(dictionary),context对象携带了关于发送通知的自定义的信息

缺点:

  • 1.在编译期不会检查通知是否能够被观察者正确的处理;
  • 2.在释放注册的对象时,需要在通知中心取消注册;
  • 3.在调试的时候应用的工作以及控制过程难跟踪;
  • 4.需要第三方对喜爱那个来管理controller与观察者对象之间的联系;
  • 5.controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况;
  • 6.通知发出后,controller不能从观察者获得任何的反馈信息

3. KVO

KVO是一个对象能够观察另外一个对象的属性的值,并且能够发现值的变化。前面两种模式更加适合一个controller与任何其他的对象进行通信,而KVO更加适合任何类型的对象侦听另外一个任意对象的改变(这里也可以是controller,但一般不是controller)。这是一个对象与另外一个对象保持同步的一种方法,即当另外一种对象的状态发生改变时,观察对象马上作出反应。它只能用来对属性作出反应,而不会用来对方法或者动作作出反应。

优点:

  • 1.能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
  • 2.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
  • 3.能够提供观察的属性的最新值以及先前值;
  • 4.用key paths来观察属性,因此也可以观察嵌套对象;
  • 5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察

缺点:

  • 1.我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
  • 2.对属性重构将导致我们的观察代码不再可用;
  • 3.复杂的“IF”语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向;
  • 4.当释放观察者时不需要移除观察者。

总结:

  1. 从上面的分析中可以看出3中设计模式都有各自的优点和缺点。其实任何一种事物都是这样,问题是如何在正确的时间正确的环境下选择正确的事物。下面就讲讲如何发挥他们各自的优势,在哪种情况下使用哪种模式。注意使用任何一种模式都没有对和错,只有更适合或者不适合。每一种模式都给对象提供一种方法来通知一个事件给其他对象,而且前者不需要知道侦听者。在这三种模式中,我认为KVO有最清晰的使用案例,而且针对某个需求有清晰的实用性。而另外两种模式有比较相似的用处,并且经常用来给controller间进行通信。那么我们在什么情况使用其中之一呢?

  2. 根据我开发iOS应用的经历,我发现有些过分的使用通知模式。我个人不是很喜欢使用通知中心。我发现用通知中心很难把握应用的执行流程。UserInfo dictionaries的keys到处传递导致失去了同步,而且在公共空间需要定义太多的常量。对于一个工作于现有的项目的开发者来说,如果过分的使用通知中心,那么很难理解应用的流程。

  3. 我觉得使用命名规则好的协议和协议方法定义对于清晰的理解controllers间的通信是很容易的。努力的定义这些协议方法将增强代码的可读性,以及更好的跟踪你的app。代理协议发生改变以及实现都可通过编译器检查出来,如果没有将会在开发的过程中至少会出现crash,而不仅仅是让一些事情异常工作。甚至在同一事件通知多控制器的场景中,只要你的应用在controller层次有着良好的结构,消息将在该层次上传递。该层次能够向后传递直至让所有需要知道事件的controllers都知道。

  4. 当然会有delegation模式不适合的例外情况出现,而且notification可能更加有效。例如:应用中所有的controller需要知道一个事件。然而这些类型的场景很少出现。另外一个例子是当你建立了一个架构而且需要通知该事件给正在运行中应用。

  5. 根据经验,如果是属性层的时间,不管是在不需要编程的对象还是在紧紧绑定一个view对象的model对象,我只使用观察。对于其他的事件,我都会使用delegate模式。如果因为某种原因我不能使用delegate,首先我将估计我的app架构是否出现了严重的错误。如果没有错误,然后才使用notification。

七十五:你一般是如何优化你的APP的?

一、首页启动速度

  • 启动过程中做的事情越少越好(尽可能将多个接口合并)

  • 不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新)

  • 在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)

  • 尽量减小包的大小

  • 优化方法:

    • 量化启动时间
    • 启动速度模块化
    • 辅助工具(友盟,听云,Flurry)

二、页面浏览速度

  • json的处理(iOS 自带的NSJSONSerialization,Jsonkit,SBJson)
  • 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)
  • 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)
  • 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)
  • 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载)
  • 算法的优化(核心算法的优化,例如有些app 有个 联系人姓名用汉语拼音的首字母排序)

三、操作流畅度优化:

  • Tableview 优化(tableview cell的加载优化)
  • ViewController加载优化(不同view之间的跳转,可以提前准备好数据)

四、数据库的优化:

  • 数据库设计上面的重构
  • 查询语句的优化
  • 分库分表(数据太多的时候,可以分不同的表或者库)

五、服务器端和客户端的交互优化:

  • 客户端尽量减少请求
  • 服务端尽量做多的逻辑处理
  • 服务器端和客户端采取推拉结合的方式(可以利用一些同步机制)
  • 通信协议的优化。(减少报文的大小)
  • 电量使用优化(尽量不要使用后台运行)

六、非技术性能优化

  • 产品设计的逻辑性(产品的设计一定要符合逻辑,或者逻辑尽量简单,否则会让程序员抓狂,有时候用了好大力气,才可以完成一个小小的逻辑设计问题)
  • 界面交互的规范(每个模块的界面的交互尽量统一,符合操作习惯)
  • 代码规范(这个可以隐形带来app 性能的提高,比如 用if else 还是switch ,或者是用!还是 ==)
  • code review(坚持code Review 持续重构代码。减少代码的逻辑复杂度)
  • 日常交流(经常分享一些代码,或者逻辑处理中的坑)

七十六:push Notification原理

本地推送:不需要联网也可以推送,是开发人员在APP内设定特定的时间来提醒用户干什么

远程推送:需要联网,用户的设备会于苹果APNS服务器形成一个长连接,用户设备会发送uuid和Bundle idenidentifier给苹果服务器,苹果服务器会加密生成一个deviceToken给用户设备,然后设备会将deviceToken发送给APP的服务器,服务器会将deviceToken存进他们的数据库,这时候如果有人发送消息给我,服务器端就会去查询我的deviceToken,然后将deviceToken和要发送的信息发送给苹果服务器,苹果服务器通过deviceToken找到我的设备并将消息推送到我的设备上,这里还有个情况是如果APP在线,那么APP服务器会于APP产生一个长连接,这时候APPF服务器会直接通过deviceToken将消息推送到设备上

七十七:iOS 中内省的几个方法?

对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。

OC运行时内省的4个方法:

判断对象类型:

1. -(BOOL) isKindOfClass:            判断是否是这个类或者这个类的子类的实例
2. -(BOOL) isMemberOfClass:      判断是否是这个类的实例

判断对象/类是否有这个方法

1. -(BOOL) respondsToSelector:                      判读实例是否有这样方法
2. +(BOOL) instancesRespondToSelector:      判断类是否有这个方法

在 Objective-C 中,id类型类似于(void*) ,可以指向任何类的对象,但在运行时对象的类型不再是id,而是该对象真正所属的类。

1. Person *person = [[Person alloc] init];  
2. NSArray *arr = @[person];
3. id  obj = arr[0];    //OC集合中取出的对象都是id类型
4. 此时可通过
5. BOOL  isPersonClass = [obj  isKindOfClass: [Person class] ];
6. 来判断obj是否Person类型或其子类的对象。

在 Objective-C 中,用父类类型定义的指针,可以指向其子类的对象,但在运行时对象真实类型会是子类。

1. //例如 Boy是Person的子类,现定义:
2. Person  *p = [[Boy alloc] init];
3. 可通过 BOOL  isBoy = [p  isMemberOfClass: [Boy class] ]; 
4. 判断Person *类型的p是否是Boy类型。

七十八:class方法和objc_getClass方法有什么区别?

1.当参数obj为Object实例对象
object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。

2.当参数obj为Class类对象
object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针;[obj class]返回的则是其本身。

3.当参数obj为Metaclass类对象
object_getClass(obj)返回元类对象中的isa指针,因为元类对象的isa指针指向根类,所有返回的是根类对象的地址指针;[obj class]返回的则是其本身。

4.obj为Rootclass类对象
object_getClass(obj)返回根类对象中的isa指针,因为跟类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针;[obj class]返回的则是其本身。

总结:
经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

七十九:一个int变量被__block修饰与否的区别?

1. 没有修饰,被block捕获,是值拷贝。
2. 使用__block修饰,会生成一个结构体,复制int的引用地址。达到修改数据。

1、block截获自动变量(局部变量)值

对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

image.png

2、 __block 修饰的外部变量

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值。

image.png

3、Block的存储域及copy操作

先来思考一下:Block是存储在栈上还是堆上呢?
其实,block有三种类型:

  • 全局块(_NSConcreteGlobalBlock)
  • 栈块(_NSConcreteStackBlock)
  • 堆块(_NSConcreteMallocBlock)

全局块存在于全局内存中, 相当于单例.
栈块存在于栈内存中, 超出其作用域则马上被销毁
堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存
简而言之,存储在栈中的Block就是栈块、存储在堆中的就是堆块、既不在栈中也不在堆中的块就是全局块。

遇到一个Block,我们怎么这个Block的存储位置呢?

(1)Block不访问外界变量(包括栈中和堆中的变量)

Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。

(2)Block访问外界变量

MRC 环境下:访问外界变量的 Block 默认存储栈中。
ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。

4、防止 Block 循环引用
Block 循环引用的情况:
某个类将 block 作为自己的属性变量,然后该类在 block 的方法体里面又使用了该类本身,如下:

1. self.someBlock = ^(Type var){
2.     [self dosomething];
3. };

解决办法:

(1)ARC 下:使用 __weak

1. __weak typeof(self) weakSelf = self;
2. self.someBlock = ^(Type var){
3.    [weakSelf dosomething];
4. };

(2)MRC 下:使用 __block

1. __block typeof(self) blockSelf = self;
2. self.someBlock = ^(Type var){
3.    [blockSelf dosomething];
4. };
5.

值得注意的是,在ARC下,使用 __block 也有可能带来的循环引用,如下:

1.  // 循环引用 self -> _attributBlock -> tmp -> self
2.  typedef void (^Block)();
3.  @interface TestObj : NSObject
4.  {
5.      Block _attributBlock;
6.  }
7.  @end
8.
9.  @implementation TestObj
10 - (id)init {
11.     self = [super init];
12.     __block id tmp = self;
13.     self.attributBlock = ^{
14.         NSLog(@"Self = %@",tmp);
15.         tmp = nil;
16.    };
17. }
18.
19. - (void)execBlock {
20.     self.attributBlock();
21. }
22. @end
23.
24. // 使用类
25. id obj = [[TestObj alloc] init];
26. [obj execBlock]; // 如果不调用此方法,tmp 永远不会置 nil,内存泄露会一直在
27.

5、有时候我们经常也会被问到block为什么 常使用copy关键字?

block 使用 copy 是从 MRC遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。
如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”

八十:什么是离屏渲染?什么情况下会触发?该如何应对?

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

离屏渲染出发的场景有以下:

  • 圆角 (maskToBounds并用才会触发)
  • 图层蒙版
  • 阴影
  • 光栅化
为什么要有离屏渲染?

大家高中物理应该学过显示器是如何显示图像的:需要显示的图像经过CRT电子枪以极快的速度一行一行的扫描,扫描出来就呈现了一帧画面,随后电子枪又会回到初始位置循环扫描,形成了我们看到的图片或视频。

为了让显示器的显示跟视频控制器同步,当电子枪新扫描一行的时候,准备扫描的时发送一个水平同步信号(HSync信号),显示器的刷新频率就是HSync信号产生的频率。然后CPU计算好frame等属性,将计算好的内容交给GPU去渲染,GPU渲染好之后就会放入帧缓冲区。然后视频控制器会按照HSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,就显示出来了。具体的大家自行查找资料或询问相关专业人士,这里只参考网上资料做一个简单的描述。

离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了。

由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

为什么要避免离屏渲染?

CPU GPU 在绘制渲染视图时做了大量的工作。离屏渲染发生在 GPU 层面上,会创建新的渲染缓冲区,会触发 OpenGL 的多通道渲染管线,图形上下文的切换会造成额外的开销,增加 GPU 工作量。如果 CPU GPU 累计耗时 16.67 毫秒还没有完成,就会造成卡顿掉帧。

圆角属性蒙层遮罩 都会触发离屏渲染。指定了以上属性,标记了它在新的图形上下文中,在未愈合之前,不可以用于显示的时候就出发了离屏渲染。

  • 在OpenGL中,GPU有2种渲染方式

    • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
    • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
  • 离屏渲染消耗性能的原因

    • 需要创建新的缓冲区
    • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
  • 哪些操作会触发离屏渲染?

    • 光栅化,layer.shouldRasterize = YES
    • 遮罩,layer.mask
    • 圆角,同时设置 layer.masksToBounds = YES、layer.cornerRadius大于0
    • 考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片
    • 阴影,layer.shadowXXX,如果设置了 layer.shadowPath 就不会产生离屏渲染

传送门:

iOS面试合集+答案(一) (juejin.cn)

iOS面试合集+答案(二) (juejin.cn)

iOS面试合集+答案(三) (juejin.cn)

iOS面试合集+答案(五) (juejin.cn)

常见的iOS开发面试题(题集) (juejin.cn)

iOS面试资料大全 (qq.com)