iOS面试题总结(二)

iOS 面试题总结(一)

101. 修改下面的代码:

typedef enum{

UserSex_Man,UserSex_Woman

}UserSex;

@interface UserModel : NSObject

@property (nonatomic , strong) NSString * name;

@property (nonatomic , assign) int age;

@property (nonatomic , assign) UserSex sex;

-(id)initUserModelWithUserName:(NSString *)name withAge:(int)age;

-(void)doLogin;

@end

这是修改后的代码:

//1.enum 建议使用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型

typedef NS_ENUM(NSInteger , USERSex){

USERSexMan,USERSexWoman

};

@interface MYUser : NSObject

//2.由于字符串值可能会改变,所以要把相关属性的“内存管理语义”声明为copy。

@property (nonatomic , copy , readonly) NSString * name;

//3.age 应该避免使用基本类型,建议使用 Foundation 数据类型

@property (nonatomic , assign , readonly) NSUInteger age;

//4.既然该类中已经有一个“初始化方法” (initializer),用于设置“姓名”(Name)、“年龄”(Age)和“性别”(Sex)的初始值: 那么在设计对应@property时就应该尽量使用不可变的对象:其三个属性都应该设为“只读”

@property (nonatomic , assign , readonly) USERSex sex;

//5.按照接口设计的惯例,如果设计了“初始化方法” (initializer),也应当搭配一个快捷构造方法。而快捷构造方法的返回值,建议为instancetype,为保持一致性,init方法和快捷构造方法的返回类型最好都用instancetype。

- (instancetype)initWithName:(NSString *)name age:(int)age sex:(USERSex)sex;

+ (instancetype)userWithName:(NSString *)name age:(int)age sex:(USERSex)sex;

@end

6.doLogIn方法不应写在该类中,doLogIn方法命名不规范:添加了多余的动词前缀

方法中不要用 with 来连接两个参数

7.enum中驼峰命名法和下划线命名法混用错误:枚举类型的命名规则和函数的命名规则相同:命名时使用驼峰命名法,勿使用下划线命名法。

102. 如何追踪 app 崩溃率,如何解决线上闪退

当 iOS 设备上的 app 闪退时,操作系统会生成一个 crash 日志保存在设备上。crash 日志上有很多有用的信息,比如每个正在执行线程的完整堆栈跟踪信息和内存映像,这样就能通过解析这些信息进而定位 crash 发生时的代码逻辑,从而找到 app 闪退的原因。通常来说,crash 产生的原因来源于两种问题:违反 iOS 系统规则导致的 crash 和 app 代码逻辑 bug 导致的 crash。

违反 iOS 系统规则产生 crash 的三种类型:

1. 内存报警闪退

当 iOS 检测到内存不足时,它的 VM 系统会发出警告通知,尝试回收一些内存,若情况没有得到足够的改善,iOS 会终止后台应用以回收更多内存,最后若内存还是不足,那么正在运行的应用可能会被终止掉。在 Debug 模式下,可以主动将客户端执行的动作逻辑写入一个 log 文件中,当内存报警时,可提醒当前内存吃紧。可以通过 Instruments 工具中的 Allocations 和 leaks 模块库来发现内存分配问题和内存泄漏问题。

2. 响应超时

当应用程序对一些特定的事件(比如启动、挂起、恢复、结束)响应不及时时,苹果的 watchdog 机制会把应用程序干掉,并生成一份响应的 crash 日志。

3. 用户强制退出

一看到用户强制退出,首先想到的可能是双击 home 键关闭应用。不过这种场景一般是不会产生 crash 日志的,因为双击 home 键后,所有的应用程序都处于后台状态,而 iOS 随时都有可能关闭后台进程,当应用阻塞界面并停止响应时这种场景才会产生 crash 日志。这里指的用户强制退出场景是稍微比较复杂点的操作:先按住电源键,直到出现滑动关机的界面时,再按住 home 键,这时候当前应用程序会被终止掉,并产生一份响应事件的 crash 日志。

应用逻辑的 Bug

大多数闪退崩溃日志的产生都是因为 bug,这种 bug 的错误种类有很多,比如:

SEGV:无效内存地址,比如空指针,未初始化指针,栈溢出等;

SIGABRT:收到 Abort 信号,可能自身调用 abort() 或者收到外部发送过来的信号;

SIGBUS:总线错误,与 SIGSEGV 不同的是,SIGSEGV 访问的是无效地址(比如虚存映射不到物理内存),而SIGBUS访问的是有效地址,但总线访问异常(比如地址对齐问题);

SIGILL:尝试执行非法的指令,可能不被识别或者没有权限;

SIGFPE:Floating Point Error,数学计算相关问题(可能不限于浮点计算),比如除零操作;

SIGPIPE:管道另一端没有进程接手数据;

crash 的收集:

打开 Xcode -> windows -> devices 选择 device logs 进行查看。一些三方库也可以

解决线上闪退

首先保证发布前充分测试,发布后若依然有闪退现象,查看崩溃日志,及时修复并发布

103. iOS 应用程序的生命周期

应用程序的状态:

Not running(未运行):程序没有启动;

Inactive(未激活):程序在前台运行,不过没有接收到事件。在没有事件处理情况下程序通常停留在这个状态;

Active(激活):程序在前台运行而且接收到了事件,这也是前台的一个正常的模式;

Background(后台):程序在后台而且能执行代码,大多数程序进入这个状态后会在这个状态上停留一会。时间到之后会进入挂起状态,有的程序经过特殊的请求后可长期处于 Background 状态;

Suspend(挂起):程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中,当系统内存过低时,系统就会把挂起的程序清除掉,为前台程序提供更多的内存。

iOS的入口在main.m文件:

int main(int argc, char *argv[])

{

@autoreleasepool {

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

}

}

main 函数的两个参数,iOS 中没有用到,包括这两个参数是为了与标准 ANSI C 保持一致。UIApplicationMain 函数,前两个参数和 main 函数一样,重点是后两个:

后两个参数分别表示程序的主要类和代理类。若主要类为 nil,将从 Info.plist 中获取,若 Info.plist 中不存在对应的 key,则默认为 UIApplication;代理类将在新建工程时创建。

根据 UIApplicationMain 函数,程序将进入 APPDelegate.m ,这个文件是 Xcode 新建工程时自动生成的。

104. __block  和 __weak 的区别

__block 不管是 ARC 还是 MRC 下都可以使用。可以修饰对象,还可以修饰基本数据类型;

__weak 只能在 ARC 下使用,只能修饰对象,不能修饰基本数据类型;

被 __block 修饰的对象在 Block 中被重新赋值,__weak 不可以。

105. 一个工人给老板打7天工,要求一块金条,这金条只能切2次,工人每天要1/7金条,该怎么分?

采用交换的方法,把金条切2次,分别得到整根金条的 1/7,2/7,4/7

第一天给 1/7,第二天给 2/7,收回 1/7,第三天给 1/7;

第四天给 4/7,收回 1/7 和 2/7 ,第五天给 1/7;

第六天给 2/7,收回 1/7,第七天给 1/7。

106. 初始化方法里为什么要写 self = [super init];以及中间发生了什么?

我们一直写类似 [[NSObject alloc] init]; 的表达式,而淡化了 alloc 和 init 的区别。一个 OC 的特性叫两步创建(two stage creation)。这意味着申请分配内存和初始化是两个分离的操作。

alloc 表示对象分配内存,这个过程涉及分配足够的可用内存来保存对象,写入 isa 指针,初始化 retain 计数,并且初始化所有实例变量。

init 表示初始化对象,这意味着把对象转换了一个可用的状态,通常指把可用的值赋给了对象的实例变量。

alloc 方法会返回一个合法的没有初始化的实例对象。每一个发送到实例的消息会被翻译为 objc_msgSend() 函数的调用,它的参数是指向 alloc 返回的对象的,名为 self 的指针。这样之后 self 已经可以执行所有方法了。

为了完成两步创建,第一个发送给新创建的实例的方法应该是约定俗成的 init 方法。注意在 NSObject 的 init 实现中,仅仅是返回了 self。

关于 init 有一个另外的重要的约定:这个方法可以(并且应该)在不能成功完成初始化的时候返回 nil;初始化可能因为各种原因失败,比如一个输入的格式错误,或者未能成功初始化一个需要的对象。

这样就能理解为什么需要总是调用 self = [super init];如果你的超类没有成功初始化它自己,你必须假设你在一个矛盾的状态,并且在你的实现中不要处理你自己的初始化逻辑,同时返回 nil。如果你不这样做,你可能会得到一个不能用的对象,并且它的行为是不可预测的,最终可能导致你的 APP 发生 crash。

107.下面代码会发生什么问题

@property (nonatomic, strong) NSString *target;

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i < 1000000 ; i++) {

dispatch_async(queue, ^{

self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];

});

}

崩溃。

因为这是个并行队列+异步的线程。假如队列A执行到步奏2,还没到步骤3时,队列B也执行到步骤2,那么这个对象就会被过度释放,导致向已释放内存对象发送消息而崩溃。

1.使用串行队列

将set方法改成在串行队列中执行就行,这样即使异步,但所有block操作追加在队列最后依次执行。

2. 使用atomic

atomic关键字相当于在setter方法加锁,这样每次执行setter都是线程安全的,但这只是单独针对setter方法而言的狭义的线程安全。

3.使用weak关键字

weak的setter没有保留新值或者保留旧值的操作,所以不会引发重复释放。当然这个时候要看具体情况能否使用weak,可能值并不是所需要的值。

4.使用Tagged Pointer

Tagged Pointer是苹果在64位系统引入的内存技术。简单来说就是对于NSString(内存小于60位的字符串)或NSNumber(小于2^31),64位的指针有8个字节,完全可以直接用这个空间来直接表示值,这样的话其实会将NSString和NSNumber对象由一个指针转换成一个值类型,而值类型的setter和getter又是原子的,从而线程安全。

比如上述代码的字符串改短一些,就不会崩溃了。


108.单例的利弊

优点:

1.一个类只能被实例化一次,提供了对唯一实例的受控访问;

2.节省系统资源;

3.允许可变数目的实例

缺点:

1.一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”

2.由于单例模式中没有抽象层,因此单例类的扩展有很大困难;

3.滥用单例将带来一些负面问题,比如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出,如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

109.dsym 你是如何分析的?

Xcode 编译项目后,我们会看到一个 dsym 文件,dsym 是保存 16 进制函数地址映射信息的中转文件,我们调试的 symbols 都会包含在这个文件中,并且每次编译项目的时候都会生成一个新的 dsym 文件。

dsym 文件有什么用?

当我们软件 release 模式打包或上线后,不会像我们在 Xcode 中那样直观的看到用户崩溃的错误,这个时候我们就需要分析 crash report 文件了,iOS 设备中会有日志文件保存我们每个应用出错的函数内存地址,通过 Xcode 的 organizer 可以将 iOS 设备中的 DeviceLog 导出成 crash 文件,这个时候我们就可以通过出错的函数地址去查询 dsym 文件中程序对应的函数名和文件名。大前提是我们需要有软件版本对应的 dsym 文件,这也是为什么我们很有必要保存每个发布版本的 Archives 文件了。

如何将文件一一对应?

每个 xx.app 和 xx.app.dsym 文件都有对应的 UUID, crash 文件也有自己的 UUID,只要这三个文件的 UUID 一致,我们就可以通过他们解析出正确的错误函数信息了。1. 查看 xx.app 文件的 UUID,terminal 中输入:dwarfdump --uuid xx.app/xx (xx 是项目名)。2. 查看 xx.app.dsym 文件的 UUID ,terminal 中输入:dwarfdump --uuid xx.app.dsym。3. crash 文件内第一行 Incident Identifier 就是 UUID。

110.App 启动的完整过程

1.解析 info.plist 2.加载相关信息,例如闪屏 3. 沙盒建立、权限检查 4. Mach-O 加载 5. 如果是二进制文件,寻找合适当前 CPU 类别的部分 6. 定位内部、外部指针引用,例如字符串、函数等 7. 执行声明为 attribute(constructor)的 C 函数 8. 加载类的扩展中的方法 9. C++ 静态对象加载,调用 Objc 的 +load 函数。

程序执行:

1.main 函数 2.执行 UIApplicationMain 函数 3. 创建 UIApplication 对象 4.创建 UIApplicationDelegate 对象并复制 5.读取配置文件 info.plist, 设置程序启动的一些属性 6. 创建应用程序的 Main Runloop 循环 7. UIApplicationDelegate 对象开始处理监听事件 8. 程序启动后,首先调用 application.didFinishLaunchingWithOptions:方法 9. 如果 info.plist 中配置了启动的 storyBoard 文件名,则加载 storyboard 文件 10.如果没配置,则根据代码创建 UIWindow -> rootViewController ->显示。

111.造成 app 启动过慢,你想到的原因有哪些?

App 启动过程中每一个步骤都会影响启动性能,有些部分所消耗的时间少之又少,而有些部分无法避免,下面只列出可以优化的部分:

main 函数之前耗时的影响因素:

1.动态库加载越多,启动越慢

2.objc 类越多,启动越慢

3.C 的 constructor 函数越多,启动越慢

4.C++ 静态对象越多,启动越慢

5.objc 的 +load 越多,启动越慢。在 objc 类的数目一样多的情况下,需要加载的动态库越多,app启动就越慢。同样的,在动态库一样多的情况下,objc 的类越多,app 的启动也越慢。需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难察觉得出,但1000个类和10000个类的分别就开始明显了。同样的,尽量不要写 attribute((constructor)) 的 C 函数,也尽量不要用到 C++ 的静态对象。至于 objc 的 +load 方法,似乎大家已经习惯不用它了。任何情况下,能用 dispatch_once() 来完成的,就尽量不要用到以上的方法。

main 函数之后耗时的影响因素:

1.这行main()函数的耗时;

2.执行 applicationWillFinishLaunching 的耗时;

3.rootViewController 及其 子视图的加载、view及其子view的加载。

应该在400ms内完成main函数之前的加载

整体过程耗时不能超过20秒,否则系统会 kill 掉进程,app启动失败

112.0x8badf00d表示什么?

该编码表示应用是因为发生 watchdog 超时而被 iOS 终止。通常是应用花费太多时间而无法启动、终止或响应应用系统事件。

0xbad22222:该编码表示 VoIP 应用因为过于频繁重启而被终止

0xdead10cc:读作”dead lock“,表示应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止

0xdeadfa11:读作”dead fail“,表示应用是被用户强制退出的。根据苹果文档,强制退出发生在用户长按开关按钮直到出现”滑动关机“然后长按home键。

113.怎么防止反编译?

1.本地数据加密:对 NSUserDefaults、sqlite 存储文件数据加密,保护账号和关键信息;

2.URL编码加密:对程序中出现的 URL 进行编码加密,防止 URL 被静态分析;

3. 对客户端传输数据提供加密方案,有效防止通过网络接口拦截获取数据;

4.对应用程序的方法名和方法体进行混淆,保证源码被逆向后无法解析代码;

5.对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低;

6.借助第三方加固,例如网易云易盾。

114.block 和函数指针的理解

相似点:

函数指针和 block 都可以实现回调的操作,声明上也很相似,实现上都可以看成是一个代码片段。

函数指针类型和 Block 类型都可以作为变量和函数参数的类型(typedef定义别名之后,这个别名就是一个类型)。

不同点:

函数指针只能指向预先定义好的函数代码块(可以是其他文件里面定义,通过函数参数动态传入),函数地址是在编译时就已经确定好的。

Block 本质是 OC 对象,是 NSObject 的子类,可以接受消息。

函数里面只能访问全局变量,而 Block 代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过 __block 访问指示符修饰的局部变量还可以在 block 代码块里面进行修改)。

从内存角度看,函数指针只不过是指向代码区的一段可执行代码,而 Block 实际上是程序运行过程中在栈内存动态创建的对象,可以向其发送 copy 消息将 block 对象拷贝到堆内存,以延长其生命周期。

115.objc 在向一个对象发送消息时,发生了什么?

根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有找到,再沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),再发送消息。

116.什么时候会报 unrecognized selector 错误?iOS 有哪些机制来避免走到这一步?

当发送消息时,我们会根据类里面的methodLists 列表去查询我们要动用的 SEL,当查询不到时,会一直沿着父类查询,当最终查询不到的时候,会报 unrecognized selector 错误。

当系统查询不到方法的时候,会调用  +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我们一次机会来添加调用不到的方法。或者我们可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法来保证不会崩溃。

117.给类添加了一个属性后,在类结构体里哪些元素会发生变化?

instance_size:实例的内存大小,objc_ivar_list *ivars:属性列表

118. runloop 是来做什么的?runloop 和线程有什么关系?主线程默认开启了runloop 么?子线程呢?

runloop: 从字面意思看:运行循环、跑圈,其实它内部就是 do-while 循环,在这个循环内部不断地处理各种任务(比如 Source、Timer、Observer)事件。runloop 和线程的关系:一个线程对应一个 RunLoop,主线程的 RunLoop 默认创建并启动,子线程的 RunLoop 需手动创建且手动启动(调用 run 方法)。RunLoop 只能选择一个 Mode 启动,如果当前 Mode 中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出 RunLoop。

119.runloop 的 mode 是用来做什么的?有几种 mode?

mode 是 runloop 里面的运行模式,不同模式下的 runloop 处理的事件和消息有一定的差别。

系统默认注册了5个 mode:

1.kCFRunLoopDefaultMode:app 默认 mode,通常主线程是在这个 mode 下运行的。

2.UITrackingRunLoopMode:界面跟踪 mode,用于 Scrollview 追踪触摸滑动,保证界面滑动时不受其他 mode 影响。

3.UIInitializationRunLoopMode:在刚启动 App 时进入的第一个 mode,启动完成后就不再使用。

4.GSEventReceiveRunLoopMode:接受系统事件的内部 mode,通常用不到。

5.kCFRunLoopCommonModes:这是一个占位的 mode,没有实际作用。

注意,iOS 对以上5个 mode 进行了封装:NSDefaultRunLoopMode、NSRunLoopCommonModes。

120.+load 和 +initialize 的理解

load 是在被添加到 Runtime 时开始执行,父类最先执行,然后是子类,最后是 Category。又因为是直接获取函数指针来执行,不会像 objc_msgSend 一样会有方法查找的过程

initialize 最终是通过 objc_msgSend 来执行的。objc_msgSend 会执行一系列方法查找,并且 Category 的方法会覆盖。

load 只在被添加到 Runtime 时执行1次,initialize 收到第一条消息前,可能永远不会调用。如果存在继承关系时,有可能调用多次。

runtime 会自动 load 所有引用到项目里的类,而 initialize 是懒加载,用到的时候才会执行。

121.简述 Objective-C 的方法调用流程

SEL:OC 在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个 ID,本质上就是一个字符串。只要方法名称相同,那么它们的 ID 就相同。

IMP:实际上就是一个函数指针,指向方法实现的首地址。

Method:它等于 SEL + IMP + method_types,相当于在 SEL 和 IMP 之间建立了一个映射。

方法调用流程:

1.检查 selector 是否需要忽略;

2.检查 target 是否为 nil,如果是 nil 就直接 cleanup,然后 return;

3.在 target 的 Class 中根据 selector 去找 IMP;

4.在当前的 class 的方法缓存(cache methodLists)里寻找,找到了跳到对应的方法实现,没找到继续往下执行;

5.从当前 class 的方法列表(methodLists)里查找,找到了添加到方法缓存列表里,然后跳到对应的方法实现(需要注意的是,在 superClass 中寻找 IMP 时,不论是在 cache methodLists 还是 methodLists 中找到 IMP,都会先存入当前 class 的 cache methodLists 再跳转到对应的方法实现。),没找到继续往下执行;

6.从 superClass 的缓存列表和方法列表里查找,直到查找到根类;

7.若还是找不到 IMP ,则进入消息动态处理和消息转发流程;

消息动态处理:

1.+ (BOOL)resolveInstanceMethod:(SEL)sel;

2.- (id)forwardingTargetForSelector:(SEL)aSelector;

如果上述两个时机都无法处理消息,则会进入消息转发流程:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

- (void)forwardInvocation:(NSInvocation *)anInvocation

如果 methodSignatureForSelector 返回的 NSMethodSignature 是 nil 的话,不会继续执行 forwardInvocation ,转发流程终止,抛出无法处理的异常。

如果 methodSignatureForSelector 返回了方法签名,我们还有最后一次机会处理这个消息,那就是在 forwardInvocation 回调里进行处理。

122.比较 OC 的三种定时器 CADisplayLink、NSTimer、GCD

CADisplayLink:依靠设备屏幕刷新频率触发事件,是时间间隔最准的定时器。

优点:比其他两种更准确,最适合做 UI 不断刷新的事件,过度相对流畅、无卡顿感。

缺点:由于依托于屏幕刷新频率,若 CPU 不堪重负而影响了屏幕刷新,那么我们的触发事件也会受到相应的影响;

selector 触发的时间间隔只能是 duration 的整倍数;

selector 事件如果大于其触发间隔就会造成掉帧现象;

CADisplayLink 不能被继承;

NSTimer

优点:使用相对灵活、应用广泛。

缺点:受 runloopmode 影响严重,使用不当会造成内存泄露。

GCD(dispatch_source_t)

优点:不受 runloopmode 影响,使用相对简单。

缺点:虽说不受 runloopmode 的影响,但其计时效应仍不失百分之百准确的。另外,它的触发事件也可能被阻塞,当 GCD 内部管理的所有线程都被占用时,其触发事件将被延迟。使用不当也会造成内存泄漏。

123.简述 iOS 签名机制

首先,先将 App 内容通过摘要算法得到摘要,再用私钥对摘要进行加密得到密文,将源文本、密文和私钥对应的公钥一并发布即可。验证方首先查看公钥是否是私钥方的,然后用公钥对密文进行解密得到摘要,将 app 用同样的摘要算法得到摘要,将两个摘要进行比对,如果相等那么一切正常。这个过程只要一步出问题就视为无效。

124.谈谈 iOS 开发中 Debug 和 Release 的区别和使用

Debug 是调试版本,主要是让程序员使用,在调试的过程中 Debug 会启动更多服务来监控错误,运行速度相对较慢,而且比较耗能。只有 debug 版的程序才能设置断点、单步执行、使用 TRACE/ASSERT等调试输出语句。

Release 是发布版本,主要是让用户使用,在使用过程中会去掉那些繁琐的监控服务,运行速度相对较快、而且比较节省内存。

125.为什么当 Core Animation 完成时,layer 又会恢复到原先的状态

因为这些产生的动画只是假象,并没有对 layer 进行改变。图层树里的呈现树实际上是模型图层的复制,但是它的属性值表示了当前外观效果,动画的过程实际上只是修改了呈现树,并没有对图层的属性进行改变,所以在动画结束后图层会恢复到原先状态。

126. category 和 extension 的区别

类别中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已);

类扩展不仅仅可以增加方法,还可以增加实例变量或属性,只是该实例变量默认是@private类型的(使用范围只能在自身类,而不是子类或其他地方);

类扩展中声明的方法没有被实现,编译器会报警告,但是类别中的方法没被实现编译器是不会有警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中的。

类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。

定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。


127.同样是从网上下载图片而不是从缓存取图片,为什么 AFN 显示图片不如 SDWebImage 流畅?

因为 SDWebImage 有一个 decoder。UIImage 的 imageWithData 函数是每次画图的时候才将 Data 解压成 ARGB 的图像。所以每次画图时,会有一个解压操作,这样效率很低,但是只是瞬时的内存需求。为了提高效率,通过 SDWebImageDecoder 将包装在 data 的资源解压,然后画在另外一张图片上,这样新的图片就不需要再重复解压了。这是典型的拿空间换时间的做法。

128.简述 KVO 的原理

KVO 是基于 Runtime 机制实现的,当某个类的属性第一次被观察时,系统就会在运行期间动态的创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。派生类在被重写的 setter 方法内实现真正的通知机制。比如,原类为 Person,那么生成的派生类名为 NSKVONotifying_Person。每个类对象中都有一个 isa 指针指向当前类,当一个类对象第一次被观察,那么系统会将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的 setter 方法。键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey: 和 didChangeValueForKey: 。在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这时就会记录旧值,而当改变发生后,didChangeValueForKey:就会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

129.简述 weak 实现的原理

Runtime 维护了一个 weak 表,用于存储指向某个对象的所有 weak 指针。这个 weak 表其实是一个 hash 表,key 是所指对象的地址,value 是 weak 指针的地址数组。初始化时,runtime 会调用 objc_initWeak 函数来初始化一个新的 weak 指针指向对象的地址。添加引用时,objc_initWeak 函数会调用 objc_storeWeak() 函数来更新指针指向,创建对应的弱引用表。释放时,调用 clearDeallocating 函数,它首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,然后从 weak 表中删除,最后清理对象的记录。

130.显式动画和隐式动画的区别

CALayer 由于本身并不包含在 UIKit 中,所以它不能响应事件,考虑到它的动画操作功能,因此它的很多属性在修改时都能形成动画效果,这种效果就是隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。隐式动画代码编写起来更简单。

显示动画比隐式动画要复杂得多,它要求创建动画对象,设置它们的属性,然后将这些动画对象应用到你希望动画的对象上。当代码运行后,视图会暂时出现然后再次消失,这其实是显式动画的副产品,是隐式动画和显式动画的一个非常明显的区别。当你调用一个显式动画时,你动画的属性将默认重置为它的原始值,而隐式动画将保留在最终状态。

显示动画通过明确的调用begin,commit来提交动画。苹果用block在内部自动调用CATransaction的+begin和+commit方法,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对+begin和+commit匹配的失误造成的风险。

131.const 和 static 的联系

被 const 修饰的变量是只读的,它修饰的变量值是不可变的。

static 用来规定作用域和存储方式。对于局部变量,static 改变了它的存储方式,使它变成了静态的局部变量,即编译时就为变量分配内存,调用结束后存储空间不释放,使得该局部变量有记忆功能,可以记忆上次的数据,不过由于仍是局部变量,因而只能在代码块内部使用(作用域不变)。

对于全局变量,被 static 修饰后仅限于当前文件引用,其他文件不能通过extern来引用

使用static const 联合修饰的变量,可以定义一个只能在当前文件访问的全局常量,在同一个文件内可以取代 #define。

static const修饰变量和宏定义的比较:

相同点:都不能再被修改,一处修改,其他都改了

不同点:static const修饰的变量会编译检测,会报编译错误,系统会为它分配内存,而宏不会,更加高效。而宏定义只是简单的替换,系统不会为它分配内存,但使用大量的宏容易增加编译时间。宏能定义一些函数而const不能。

132.#define 的实质是什么?

#define 属于预编译指令,预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。所以预处理过程是先于编译器对源代码进行处理的。#define 的本质其实就是文本替换。

133.HTTP post 的 body 体使用 form-urlencoded 和 multipart/form-data 的区别

form-urlencoded 是默认的 mime 内容编码类型,是通用的,但是它在传输比较大的二进制或文本数据时效率很低。

multipart/form-data 是上传文件、图片或二进制数据和非 ASCII 数据时使用。

134.区分黑盒测试与白盒测试

黑盒测试:已知产品所应具有的功能,通过测试来检测每个功能是否都能正常使用,黑盒测试着眼于程序外部结构、不考虑内部逻辑结构、针对软件界面和软件功能进行测试。它是穷举输入测试,只有把所有可能的输入都作为测试情况使用,才能以这种方法查出程序中所有的错误。

白盒测试:全面了解程序内部逻辑结构、对所有逻辑路径进行测试。是穷举路径测试,在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。

135.iOS 7的多任务添加了哪两个新的 API? 各自的使用场景是什么

后台获取(Background Fetch):后台获取使用场景是用户打开应用之前就使 app 有机会执行代码来获取数据、刷新UI。这样在用户打开应用的时候,最新的内容将已经显现在用户眼前,而省去了所有的加载过程。

推送唤醒(Remote Notifications):使用场景是使设备在接收到远端推送后让系统唤醒设备和我们的后台应用,并先执行一段代码来准备数据和 UI,然后再提示用户有推送。这时用户如果解锁设备进入应用后将不会再有任何加载过程,新的内容将直接得到呈现。

136.CocoaPods 工作原理

CocoaPods 的工作主要是通过ProjectName.xcworkspace 来组织的,在打开 ProjectName.xcworkspace 文件后,发现 Xcode 会多出一个 Pods 工程。

1.库文件引入及配置:库文件的引入主要由 Pods 工程中的 Pods-ProjectName-frameworks.sh 脚本负责,在每次编译时,该脚本会帮你把预引入的所有第三方库文件打包成 ProjectName.a 静态库文件,放在我们原 Xcode 工程中 Framework 文件夹下,供工程使用。

如果 Podfile 使用了 use_frameworks!,这是生成的是 .framework 的动态库文件。

2.Resource 文件:Resource 资源文件主要由 Pods 工程中的 Pods-ProjectName-resources.sh 脚本负责,在每次编译的时候,该脚本会帮你将所有三方库的 Resource 文件 copy 到目标目录中。

3.依赖参数设置:在 Pods 工程中的每个库文件都有一个相应的 SDKName.xcconfig,在编译时,CocoaPods 就是通过这些文件来设置所有的依赖参数的,编译后,在主工程的 Pods 文件夹下会生成两个配置文件,Pods-ProjectName.debug.xcconfig、Pods-ProjectName.release.xcconfig。

137.了解 Handoff、HomeKit、HealthKit、VoiceOver 、iBeacons、Metal 是什么吗

Handoff 就是将手机上不方便做的事情通过 iCloud 转移到电脑上去做。

HomeKit 是苹果2014年发布的智能家居平台,这些产品可以通过 iPhone、iPad等控制灯光、室温、风扇以及其他家用电器。

HealthKit 框架提供了一个结构,应用可以使用它来分享健康和健身数据,简单地说,HealthKit 框架用来与苹果的健康应用做数据交互,比如可以从 HealthKit 中读取用户的记步数据、向苹果的健康应用中写入用户的血糖、血压、心跳等数据。

VoiceOver 是苹果的“读屏”技术,可以读出屏幕上的信息,以帮助盲人进行人机交互。

iBeacons 是通过低功耗蓝牙技术进行一个十分精确的微定位和室内导航,设备可以接收一定范围由其他 iBeacons 发出来的信号。

Metal 是一套用于 iPhone、iPad 上 GPU 编程的高效框架,一般做游戏的可能了解的比较多。OpenGL 可跨多个平台使用,而且资料丰富,而 Metal 仅仅局限于 iPhone 或 iPad ,而且资料少的可怜。但 Metal 在运行性能上要比 OpenGL ES 高效,它大大减少了资源的开销。 

138.Cocoa Touch 的类名为什么是以两个大写字母开头的

以 NS 为例,NS 代表的是 NeXTSTEP,是乔布斯在1985年离开苹果之后创建的电脑公司,该公司的产品包括了一款 OC 开发的操作系统,该操作系统里面有很多 NS 的缩写,后来在1996年 Apple 收购了 NeXTSTEP,里面的一些东西就成了 OS X 和 iOS 的一部分,NS 前缀的习惯也就保存了下来。

如今,iOS 命名规范倡导一个类或方法的开头两个或三个大写字母指代公司或个人,或框架名称等容易和其他区分开来的。还有很重要的一点,OC 没有命名空间的概念,使用这样大写字母前缀的方式可以有效的避免命名冲突的问题。

139.instancetype 和 id 的异同

相同点:都可以作为方法的返回类型

不同点:instancetype 可以返回和方法所在类相同类型的对象,id 只能返回未知类型的对象;

instancetype 只能作为返回值,不能像 id 那样作为参数。instancetype只适用于初始化方法和便利构造器的返回值类型。

对于 init 方法,id 和 instancetype 是没有区别的。因为编译器会把 id 优化为 instancetype。当明确返回的类型就是当前类时,使用 instancetype 能避免 id 带来的编译不出的错误情况。

140.数组列表(ArrayList)和数组(Array)的区别

Array 可以保存基本类型和对象类型,ArrayList 只能保存对象类型;

Array 容量(即空间大小)是静态固定的,ArrayList 是动态变化的;

ArrayList 有更加丰富的方法,比如:addAll()、removeAll()、iterator()。

141.Swift 中的 nil 与 OC 中的 nil 的区别

在 OC 中,只有对象才能设置为 nil,而 Swift 中除了对象,int、struct、enum 等任何可选类型都可设置为 nil,而且 nil 只能用在可选类型的变量和常量;

在 OC 中,nil 是一个指向不存在对象的指针,Swift 中 nil 不是指针,而是一个确定的值,用来表示值缺失。

142.NSURLSession 和 NSURLConnection 的区别

NSURLSession 是 NSURLConnection 的替代者,是对 NSURLConnection 进行了重构优化后的新的网络访问接口;

NSURLConnection 在下载文件时,先将整个文件下载到内存,然后再写入沙盒,如果文件比较大,就会出现内存暴涨的情况。而使用 NSURLSession 下载文件时,会默认下载到沙盒中的 temp 文件夹中,不会出现内存暴涨的情况,但是下载完成后会将 temp 中的临时文件删除,需要在初始化任务方法的时候,在 completionHandler 回调中增加保存文件的代码。

NSURLConnection 实例化对象,开始默认请求就发送,不需要调用 start 方法,而 cancel 可以停止请求的发送,停止后不能继续访问,需要创建新的请求,而 NSURLSession 有三个控制方法:cancel、suspend、resume,暂停后可以通过 resume 继续恢复当前的请求任务;

NSURLSession 的构造方法 sessionWithConfiguration:delegate:delegateQueue 中有一个 NSURLSessionConfiguration 类的参数可以设置配置信息,其决定了 cookie,安全和高速缓存策略,最大主机连接数、资源管理、网络超时等配置,而 NSURLConnection 不能进行这个配置,相比于 NSURLConnection 依赖于一个全局的配置对象,缺乏灵活性而言,NSURLSession 有了很大的改进;

使用NSURLSession进行断点下载更加便捷。

143.类(Class)和结构体(Struct)区别

内存管理方式不一样,类是引用类型,分配在堆上,结构体是值类型,分配在栈上;

类可以继承,而结构体不能。

对于引用类型,对一个变量执行的操作会影响另一个变量所引用的对象。对于值类型,每个变量都有其自己的数据副本,对一个变量执行的操作不会影响另一个变量。

144.SDWebImage 发生内存警告是怎么处理

//内存警告时候用

-(void)applicationDidReceiveMemoryWarning:(UIApplication*)application{

[[SDWebImageManager sharedManager].imageCache cleanDisk];

[[SDWebImageManager sharedManager] cancelAll];    

}

145.SDWebImage 清空缓存 clearDisk 和 cleanDisk 区别

cleanDisk:清除过期缓存,计算当前缓存大小和设置的最大缓存数量比较,如果超出那么会继续删除(按照文件创建的先后顺序);

clearDisk:粗暴的直接删除,然后重新创建。

146.NSCache优于NSDictionary的几点

NSCache 中的 key 不必实现 copy,NSDictionary 中的 key 必须实现 copy,不需要实现NSCopying 协议;

实现缓存时应选用 NSCache 而非 NSDictionary,因为 NSCache 可以提供优雅的自动删减功能,而且是线程安全的。

147.NSUserDefaults 的东西放到 Library/Preferences 还是 Library/Caches 下

手动保存的文件在 documents 文件里,NSUserDefaults 保存的文件在 Library/Preferences 目录文件夹里。

148.for 循环和 for in 的区别

for in 遵循了 NSFastEnumeration 协议,它只有一个方法:

- (NSUInteger)countByEnumeratingWithState: (NSFastEnumerationState *)state objects:(id *)stackbuffer count:(NSUInteger)len;

它直接从 C 数组中取对象。对于可变数组来说,它最多只需要两次就可以获取全部元素。如果数组还没有构成循环,那么第一次就获得了全部元素,跟不可变数组一样。但如果数组构成了循环,那么就需要两次,第一次获取对象数组的起始偏移到循环数组末端的元素,第二次获取存放在循环数组起始处的剩余元素。而 for 循序比 for in 速度慢一点,是因为 for 循环每次都要调用 objectAtIndex:方法。

若我们遍历时不需要获取当前遍历操作所针对的下标,我们就可以选择 for in。

149.[obj class] 方法和 objc_getClass(obj) 方法有什么区别

当 obj 为实例变量时,[obj class] 方法和 objc_getClass(obj) 方法一样,都是获得 isa 指针,即指向类对象的指针。

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

你可能感兴趣的:(iOS面试题总结(二))