0%

iOS面试复习

1 OC基础

1.1 分类(category)

使用场景:

  1. 给现有类添加方法

  2. 分解大类,如按功能划分

  3. 声明私有方法

底层实现(Runtime):

1
2
3
4
5
6
7
8
typedef struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
}

tips:分类不能添加对象,但可使用关联对象间接实现

分类和拓展的区别:

  1. 分类为运行时决议,扩展为编译时决议

  2. 分类有声明和实现,而拓展只以声明形式存在,多数情况下寄生于宿主类.m中

  3. 可以为系统类添加分类,但不能为系统类添加拓展

1.2 扩展(Extension)

使用场景:

  1. 声明私有属性
  2. 声明私有方法
  3. 声明私有成员变量

特点:

  1. 编译时决议
  2. 只以声明形式存在,多数情况下寄生于宿主类.m中
  3. 不能为系统类添加扩展

1.3 代理

特点:

  1. 是一种软件设计模式
  2. iOS中以@protocol形式体现
  3. 传递方式一对一

1.4 通知

特点:

  1. 使用观察者模式来实现的用意跨层传递消息的机制
  2. 传递方式为一对多

1.5 KVC(Key-value coding)

使用字符串关联进行赋值

就俩方法:

1
2
-(id)valueForKey:(NSString *)key
-(void)setValue:(id)value forKey:(NSString *)key

1.6 KVO(Key-value Observing)

是观察者模式的又一个实现

使用isa混写(isa-swizzling)实现

触发方法:

  1. 使用setter方法改变值

  2. 使用KVC赋值

  3. 手动触发KVO则需添加如下方法:

    1
    2
    3
    [self willChangeValueForKey:...];
    一般在这里修改值
    [self didChangeValueForKey:...];

1.7 属性关键字

1.7.1 读写权限相关
  1. readonly(只读)
  2. readwrite(读写)
1.7.2 原子性相关
  1. atomic(原子性)
  2. nonatomic(非原子性)
1.7.3 引用计数相关
  1. retain/strong
  2. assign/unsafe_unretained
  3. weak
  4. copy

1.8 Runtime

1.8.1 objc_object

数据结构包含:

1
2
3
4
5
6
7
isa_t isa;

关于isa操作相关

关联对象相关

内存管理相关
1.8.2 objc_class(继承自objc_object)

数据结构包含:

1
2
3
4
5
class superClass;

cache_t cache;

class_data_bits_t bits;
1.8.3 isa相关
1.8.3.1 isa指针类型(共用体isa_t)

指针型isa(isa的值代表class的地址)

非指针型isa(isa的值的部分代表class的地址)

1.8.3.2 指向
  1. 对象的isa指向它的类对象
  2. 类对象的isa指向它的元类对象
1.8.4 cache_t

定义:

  1. 用于快速查找方法执行函数

  2. 是可增量扩展的哈希表结构

  3. 是局部性原理的最佳应用

1.8.5 class_data_bits_t

定义:

  1. 是对class_rw_t的封装
  2. class_rw_t代表了类相关的读写信息,是对class_ro_t的封装
  3. class_ro_t代表了类相关的只读信息
1.8.6 class_rw_t

数据结构包含:

1
2
3
4
5
6
7
class_ro_t

protocols

properties

methods
1.8.7 class_ro_t

数据结构包含:

1
2
3
4
5
6
7
8
9
name

ivars

properties

protocols

methodlist
1.8.8 method_t

数据结构包含:

1
2
3
SEL name; // 名称
const char *types; // 返回值及参数
IMP imp; // 函数体
1.8.9 消息传递
1.8.9.1 objc_msgSend方法
1
2
3
4
5
void obic_msgSend(void /*id self, SEL op, ...*/)
例:
[self class];

objc_msgSend(self,@Selector(class))
1.8.9.2 objc_msgSendSuper方法
1
2
3
4
5
6
7
8
9
10
void objc_msgSendSuper(void /*struct objc_super *super,SEL op, ...*/)
其中super结构如下:
struct objc_super {
_unsafe_unretained id receiver;
...
}
例:
[super class];
即:
objc_msgSendSuper(super,@Selector(class))

面试可能问题:

1、在同一个类中调用[self class] 和 [super class]得到的结果分别是什么?

答案必须都是当前类喽,因为消息的接受者都是当前对象

1.8.10 消息转发

消息转发共有三个阶段:方法解析处理阶段、快速转发阶段、常规转发阶段

越早处理,所付出的代价越小

1.8.10.1 resolveClassMethod/resolveInstanceMethod

这个阶段,用于通过Runtime方式动态添加方法实现IMP

涉及方法:

1
2
+ (BOOL)resolveClassMethod:(SEL)sel // 类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel // 实例方法

例:

1
2
3
4
5
6
7
8
9
10
11
+(BOOL)resolveInstanceMethod:(SEL)sel{
//判断是否为外部调用的方法
if ([NSStringFromSelector(sel) isEqualToString:@"testFunction"]) {
/**
对类进行对象方法 需要把方法添加进入类内
*/
[LMRuntimeTool addMethodWithClass:[self class] withMethodSel:sel withImpMethodSel:@selector(addDynamicMethod)];
return YES;
}
return [super resolveInstanceMethod:sel];
}
1.8.10.2 forwardingTargetForSelector

这个阶段用于将消息转发给另一个对象接受,该对象应该具备该消息的具体实现

涉及方法:

1
-(id)forwardingTargetForSelector:(SEL)aSelector

例:

1
2
3
4
5
6
-(id)forwardingTargetForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) isEqualToString:@"testFunction"]) {
return [BackupTestMessage new];
}
return [super forwardingTargetForSelector:aSelector];
}
1.8.10.3 methodSignatureForSelector和forwardInvocation

该阶段可以将消息转发给多个对象

涉及方法:

1
2
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
//如果返回为nil则进行手动创建签名
if ([super methodSignatureForSelector:aSelector]==nil) {
NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sign;
}
return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
//创建备用对象
BackupTestMessage * backUp = [BackupTestMessage new];
SEL sel = anInvocation.selector;
//判断备用对象是否可以响应传递进来等待响应的SEL
if ([backUp respondsToSelector:sel]) {
[anInvocation invokeWithTarget:backUp];
}else{
// 如果备用对象不能响应 则抛出异常
[self doesNotRecognizeSelector:sel];
}
}

1.9 Runloop

1.9.1 数据结构

包含数据:

1
2
3
4
5
pthread
currentMode
modes
commonModes
commonModeItems
1.9.2 CFRunloopMode

数据结构包含:

1
2
3
4
5
name	如:NSDefaultRunloopMode
source0 集合
source1 集合
observers 数组
timers 数组
1.9.3 CFRunloopSource
1.9.3.1 source0

需要手动唤醒线程

1.9.3.2 source1

具备唤醒线程的能力

1.9.4 CFRunloopTimer
1.9.5 CFRunloopObserver

观测时间点:

  1. kCFRunloopEntry
  2. kCFRunloopBeforeTimers
  3. kCFRunloopBeforeSources
  4. kCFRunloopBeforeWaiting
  5. kCFRunloopAfterWaiting
  6. KcfRunloopExit

2 设计原则

2.1 单一职责原则

一个类只负责一件事

2.2 依赖倒置原则

抽象不应该依赖于具体实现,具体实现可以依赖于抽象

2.3 开闭原则

对修改关闭,对扩展开放

2.4 里氏替换原则

父类可以被子类无缝替换,且原有功能不受任何影响

2.5 接口隔离原则

使用多个专门的协议,而不是一个庞大臃肿的协议

2.6 迪米特法则

一个对象应当对其他对象有尽可能少的了解,高内聚、低耦合

3 设计模式

3.1 代理模式

3.2 工厂模式

3.3 观察者模式

3.4 单例模式

3.5 MVC

3.6 MVVM

3.7 MVP

4 算法

4.1 不用中间变量,交换A和B的值

1
2
3
4
5
6
7
8
9
10
11
12
13
// 加法
void reserve(int a, int b) {
a = a + b;
b = a - b;
a = a - b;
}

// 异或法(不进位2进制加法)
void reserve(int a, int b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
}

4.2 求最大公约数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 遍历法
int method1(int a, int b) {
int max = 0;
for (int i = 0; i <= b; i++) {
if (a % i == 0 && b % i == 0) {
max = i;
}
}
return max;
}

// 辗转相除法(这里要求a>=b)
int method2(int a, int b) {
int r;
while (a % b > 0) {
r = a % b;
a = b;
b = r;
}
return b;
}

4.3 判断质数

质数指只能被1或本身整除的数

1
2
3
4
5
6
7
8
9
10
int check(int a) {
// sqrt用于求非负数的平方根,减少循环次数
for (int i = 2; i <= sqrt(a); i ++) {
if (a % i == 0) {
return 0;
}
}

return 1;
}

4.4 字符串反转

1
2
3
4
5
6
7
8
9
void reserve(char *cha,int length) {
char *begin = cha;
char *end = cha + strlen(cha) - 1;
while (begin < end) {
char temp = *begin;
*(begin ++) = *end;
*(end --) = temp;
}
}

4.5 选择排序

选出最小/最大的数,放在第一位,然后再在剩下的列表数据里再选出最小/最大数放在第二位,以此类推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (NSArray *)sort1:(NSArray *)array {
NSMutableArray<NSNumber *> *res = [array mutableCopy];

for (int i = 0; i < res.count - 1; i ++) {
int index = i;
for (int j = i + 1; j < res.count; j ++) {
if (res[j].intValue < res[index].intValue) {
index = j;
}
}

if (index > i) {
NSNumber *temp = res[index];
res[index] = res[i];
res[i] = temp;
}
}

return [res copy];
}

4.6 冒泡排序

从首位起,与右侧数比较,大的话交换,直到最后一位。然后重新从首位开始同样进行比较及交换操作,直到最后第二位,以此类推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSArray *)sort2:(NSArray *)array {
NSMutableArray<NSNumber *> *res = [array mutableCopy];

for (int i = 0; i < res.count - 1; i ++) {
for (int j = 0; j < res.count - i - 1; j ++) {
if (res[j].intValue > res[j + 1].intValue) {
NSNumber *temp = res[j];
res[j] = res[j + 1];
res[j + 1] = temp;
}
}
}

return [res copy];
}

4.7 折半查找

数组必须是有序的

搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int findKey(int *arr, int length, int key) {
int min = 0, max = length - 1, mid;
while (min <= max) {
mid = (min + max) / 2; //计算中间值
if (key > arr[mid]) {
min = mid + 1;
} else if (key < arr[mid]) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}

4.8 快速排序

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];

while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}

5 性能优化

参考文章:https://www.jianshu.com/p/4e9c6a048f6f

5.1 卡顿优化

  1. 尽可能减少CPU、GPU资源消耗

  2. 按照60FPS的刷帧率,每隔16ms就会有一次VSync信号

  3. 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView

  4. 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改

  5. 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性

  6. Autolayout会比直接设置frame消耗更多的CPU资源

  7. 图片的size最好刚好跟UIImageView的size保持一致

  8. 控制一下线程的最大并发数量

  9. 尽量把耗时的操作放到子线程

  10. 文本处理(尺寸计算、绘制)

  11. 图片处理(解码、绘制)

  12. 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示

  13. GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸

  14. 尽量减少视图数量和层次

  15. 减少透明的视图(alpha<1),不透明的就设置opaque为YES

  16. 尽量避免出现离屏渲染

离屏渲染消耗性能的原因:

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

离屏渲染的触发场景:

  1. 光栅化,layer.shouldRasterize = YES
  2. 遮罩,layer.mask
  3. 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
  4. 阴影,layer.shadowXXX

卡顿检测:

可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的

5.2 耗电优化

5.2.1 尽可能降低CPU、GPU功耗
  1. 少用定时器
5.2.2 优化I/O操作
  1. 尽量不要频繁写入小数据,最好批量一次性写入
  2. 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
  3. 数据量比较大的,建议使用数据库(比如SQLite、CoreData)

5.3 网络优化

  1. 减少、压缩网络数据
  2. 如果多次请求的结果是相同的,尽量使用缓存
  3. 使用断点续传,否则网络不稳定时可能多次传输相同的内容
  4. 网络不可用时,不要尝试执行网络请求
  5. 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
  6. 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

5.4 定位优化

  1. 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
  2. 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
  3. 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
  4. 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
  5. 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:

5.5 硬件检测优化

用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

5.6 APP的启动优化

通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1

APP的冷启动可以概括为3大阶段

  1. dyld
  2. runtime
  3. main

APP的启动 - dyld

dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

  • 启动APP时,dyld所做的事情有
  • 装载APP的可执行文件,同时会递归加载所有依赖的动态库
  • 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理

APP的启动 - runtime

  • 启动APP时,runtime所做的事情有
  • 调用map_images进行可执行文件内容的解析和处理
  • 在load_images中调用call_load_methods,调用所有Class和Category的+load方法
  • 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
  • 调用C++静态初始化器和attribute((constructor))修饰的函数
  • 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理

总结一下

  • APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
  • 并由runtime负责加载成objc定义的结构
  • 所有初始化工作结束后,dyld就会调用main函数
  • 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

APP的启动优化

dyld阶段

  • 减少动态库、合并一些动态库(定期清理不必要的动态库)
  • 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
  • 减少C++虚函数数量
  • Swift尽量使用struct

runtime阶段

  • 用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、ObjC的+load

main阶段

  • 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
  • 按需加载

5.7 安装包瘦身

  1. 采取无损压缩
  2. 去除没有用到的资源
  3. 编译器优化
    • Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES
    • 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions
    • 利用AppCode检测未使用的代码:菜单栏 -> Code -> Inspect Code
    • 编写LLVM插件检测出重复代码、未被调用的代码