0%

2020年iOS面试题总结(一)

Objective-C 部分

OC对象的本质

一个OC对象占用多少内存?

系统分配了16个字节给NSObject对象(通过malloc_size函数获得)。

但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)。

  • OC对象 最少占用 16 个字节内存 .
  • 当对象中包含属性, 会按属性占用内存开辟空间. 在结构体内存分配原则下自动偏移和补齐 .
  • 对象最终满足 16 字节对齐标准 .
  • 属性最终满足 8 字节对齐标准 .
  • 可以通过 #pragma pack() 自定义对齐方式 .

对象的isa指针指向哪里?

instance对象的isa指向class对象

class对象的isa指向meta-class对象

meta-class对象的isa指向基类的meta-class对象

OC的类信息存放在哪里?

对象方法、属性、成员变量、协议信息,存放在class对象中

类方法,存放在meta-class对象中

成员变量的具体值,存放在instance对象

KVO本质

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

表述方案一:

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类

  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数

    willChangeValueForKey:

    父类原来的setter

    didChangeValueForKey:
  • 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)

表述方案二:
当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

如何手动触发KVO?

手动调用willChangeValueForKey:和didChangeValueForKey:

1
2
3
4
5
6
7
8
9
10
11
12
// 实现
- (void)viewDidLoad {
[super viewDidLoad];

Person *person = [[Person alloc]init];;
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[p willChangeValueForKey:@"name"];
[p didChangeValueForKey:@"name"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@\n, 携带信息:%@", object, keyPath, change, context);
}

被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

直接 修改成员变量 会触发KVO么?

不会触发KVO

  • KVC 对属性赋值时候 是会在这个类里边 去查找 _age isAge setAge setIsAge 等方法的 ,最终会调用属性的 setter 方法 ,那么如果添加了 KVO 还是会被触发的 .
  • 相反 设置成员变量 _age 由于不会触发 setter 方法 ,因此不会去触发 KVO 相关的代码 .

通过KVC修改属性会触发KVO么?

会触发KVO

KVC在赋值时候,内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:) 发送通知.

setValue:forKey:的原理

setValue:forKey:的原理

valueForKey:的原理

valueForKey:的原理

KVC 原理

KVC的赋值和取值过程是怎样的?原理是什么?

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
调用 setValue:forKey:

  • setKey,_setKey ->找到了则进行赋值,未找到调用 accessInstanceVarlableDirctly 是否运行 修改值,返回YES

  • 调用_key, _isKey, key, isKey 进行赋值

Category原理探究

Category的使用场合是什么?

  • 在不修改原有类代码的情况下,为类添对象方法或者类方法
  • 或者为类关联新的属性
  • 分解庞大的类文件

使用场合:

  • 添加实例方法
  • 添加类方法
  • 添加协议
  • 添加属性
  • 关联成员变量

Category的实现原理

Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息

在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

Category和Class Extension的区别是什么?

Class Extension在编译的时候,它的数据就已经包含在类信息中

Category是在运行时,才会将数据合并到类信息中

Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

  • 有load方法

  • load方法在runtime加载类、分类的时候调用

  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

initialize方法如何调用,以及调用时机

  • 当类第一次收到消息的时候会调用类的initialize方法

  • 是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的

  • 优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的

load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

  • load 是类加载到内存时候调用, 优先父类->子类->分类

  • initialize 是类第一次收到消息时候调用,优先分类->子类->父类

  • 同级别和编译顺序有关系

  • load 方法是在 main 函数之前调用的

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果

  • Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构) 可以通过 runtime 动态的关联属性

Block 原理

block的原理是怎样的?本质是什么?

  • block 本质其实是OC对象

  • block 内部封装了函数调用以及调用环境

__block的作用是什么?有什么使用注意点?

  • 如果需要在 block 内部修改外部的 局部变量的值,就需要使用block 修饰(全局变量和静态变量不需要加block 可以修改)

  • __block 修饰以后,局部变量的数据结构就会发生改变,底层会变成一个结构体的对象,结构内部会声明 一个 __block修饰变量的成员, 并且将 __block修饰变量的地址保存到堆内存中. 后面如果修改 这个变量的值,可以通过 isa 指针找到这个结构体,进来修改 这个变量的值;

  • 可以在 block 内部修改 变量的值

block的属性修饰词为什么是copy?使用block有哪些使用注意?

  • block 一旦没有进行copy操作,就不会在堆上

  • 使用注意:循环引用问题 (外部使用__weak 解决)

block在修改NSMutableArray,需不需要添加__block?

  • 如果是操作 NSMutableArray 对象不需要,因为 block 内部拷贝了 NSMutableArray对象的内存地址,实际是通过内存地址操作的

  • 如果 NSMutableArray 对象要重新赋值,就需要加__block

Block 内部为什么不能修改局部变量,需要加__block

  • 通过查看Block 源码,可以发现, block 内部如果单纯使用 外部变量, 会在 block 内部创建同样的一个变量,并且将 外部变量的值引用过来..(只是将外部变量值拷贝到 block 内部), 内部这个变量和外部 实际已经没关系了

  • 从另一方面分析,block 本质也是一个 函数指针, 外部的变量也是一个局部变量,很有可能 block 在使用这个变量时候,外部变量已经释放了,会造成错误

  • 加了__block 以后, 会将外部变量的内存拷贝到堆中, 内存由 block 去管理.

OC 的消息机制

讲一下 OC 的消息机制

  • OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)

  • objc_msgSend底层有3大阶段: 消息发送(当前类、父类中查找)、动态方法解析、消息转发

消息发送流程

  • 当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析

  • 类方法调用同上,只不过 isa 指针找到元类对象;

动态方法解析机制

当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
//  动态方法绑定- 实例法法调用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
Method method = class_getInstanceMethod(self, @selector(test));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}

// 类方法调用
+(BOOL) resolveClassMethod:(SEL)sel....

消息转发机制流程

未找到动态方法绑定,就会进行消息转发阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 快速消息转发- 指定消息处理对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [Student new];
}
return [super forwardingTargetForSelector:aSelector];
}

// 标准消息转发-消息签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(run))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//内部逻辑自己处理
}

Runtime

  • Objective-C runtime是一个运行时库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理

什么是Runtime?平时项目中有用过么?

  • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行

  • OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数

  • 平时编写的OC代码,底层都是转换成了Runtime API进行调用

runtime具体应用

  • 利用关联对象(AssociatedObject)给分类添加属性

  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)

  • 交换方法实现(交换系统的方法)

  • 利用消息转发机制解决方法找不到的异常问题

[self class] 和 [super class] 的区别

的区别

[self class] 和 [super class] 都是给当前类返送消息,spuer 表示在父类中查找
[self superClass] 和 [super superclass] 也是也当前类发消息,返回父类

第一个打印:
MJStudent / MJStudent/ MJerson / MJPerson

表示对象是否为当前类或者子类的 类型
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
```isMemberOfClass``` 表示是否为当前类的的类型
```isMemberOfClass``` 分为- 对象方法 和+ 类方法2中
```- (bool)isMemberOfClass;``` 比较的是类对象
```+ (bool)isMemberOfClass;``` 比较的是元类

第二个打印:
1 ,0, 0, 0

## [RunLoop](https://juejin.im/post/5add46606fb9a07abf721d1d "")

### 讲讲 RunLoop,项目中有用到吗?

- runloop运行循环,保证程序一直运行,主线程默认开启
- 用于处理线程上的各种事件,定时器等
- 可以提高程序性能,节约CPU资源,有事情做就做,没事情做就让线程休眠

应用范畴:
定时器,事件响应,手势识别,界面刷新,以及autoreleasePool 等等

### runloop内部实现逻辑?

![runloop内部实现逻辑](http://cdn.xuebaonline.com/2020-ms-stp4.png "")

实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

### runloop和线程的关系?

- 每条线程都有唯一的一个与之对应的RunLoop对象

- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建

- RunLoop会在线程结束时销毁

- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop


### timer 与 runloop 的关系?

timer 定时器,是基于 runloop 来实现的, runloop 在运行循环当中,监听到了定制器 就会执行;所以 timer 需要添加到 runloop 中去, 注意子线程的 runloop 默认是不开启的,如果在子线程执行 timer 需要手动开启 runloop

### 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?

将 timer 对象添加到 runloop 中,并修改 runloop 的运行 mode

``` Objective-C
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:nil];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

runloop 是怎么响应用户操作的, 具体流程是什么样的?

  • source1 捕捉用户触摸事件
  • source0 去处理触摸时间

说说runLoop的几种状态

添加Observer监听RunLoop的所有状态

说说runLoop的几种状态

  • kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
  • kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
  • kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
  • kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
  • kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
  • kCFRunLoopExit = (1UL << 7), //即将退出Loop
  • kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变

runloop的mode作用是什么?

runloop 只能在一种 mode 下运行, 做不同的事情,runloop 会切换到对应的 model 下来执行,默认是 kCFRunLoopDefaultMode 如果视图滑动再回切换到 UITrackingRunLoopMode,如果需要在多种 mode 下运行则需要手动设置 kCFRunLoopCommonModes;

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode

  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

  • kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

如何实现一个常驻线程

  • 创建RunLoop
  • 向RunLoop中添加port、source等来保证RunLoop不退出
  • 启动RunLoop

多线程

同一时间,CPU 只能处理理一条线程, 只有一条线程在⼯工作 多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换) 如果 CPU 调度线程的时间⾜够快, 就造成了多线程并发执⾏的假象

  • 优势

    充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“⾏行 计算”状态
  • 弊端

    新线程会消耗内存控件和cpu时间,线程太多会降低系统行性能。

iOS的多线程方案有哪几种?你更倾向于哪一种?

iOS的多线程方案

倾向于GCD ,简单灵活,使用方便

你在项目中用过 GCD 吗?

使用过

GCD中有2个用来执行任务的函数

  • 用同步的方式执行任务

    1
    2
    3
    dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
    queue:队列
    block:任务
  • 用异步的方式执行任务

1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

GCD 的队列类型

GCD的队列可以分为2大类型

  • 并发队列(Concurrent Dispatch Queue)

    可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)

    并发功能只有在异步(dispatch_async)函数下才有效

  • 串行队列(Serial Dispatch Queue)

    让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

说一下 OperationQueue 和 GCD 的区别,以及各自的优势

  • GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装

  • GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺序、设置最⼤大并发数量量

  • NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现

  • NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)

  • GCD的执⾏行行速度⽐NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖\或者要监听任务的执⾏行行情况:NSOperationQueue

线程安全的处理手段有哪些?

  • 加锁
  • 同步执行

OC你了解的锁有哪些?在你回答基础上进行二次提问?

os_unfair_lock ios10 开始
OSSpinLock ios10 废弃
dispatch_semaphore 建议使用,性能也比较好
dispatch_mutex
dispatch_queue 串行
NSLock 对 mutex 封装
@synchronized 性能最差

追问一:自旋和互斥对比?

  • 什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
  • 什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

追问二:使用以上锁需要注意哪些?

  • 注意死锁
  • 在串行队列使用同步,容易造成死锁

追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!

两种锁的加锁原理:

  • 互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

  • 自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位

请问下面代码的打印结果是什么?

请问下面代码的打印结果是什么?

打印 1,3

performSelector after 是基于 timer 定制器,定时器又是基于 runloop 实现的

任务2在子线程中,子线程默认 runloop 是不开启的,所以不执行2

请问下面代码的打印结果是什么?

请问下面代码的打印结果是什么?

打印1

start 执行完,线程就销毁了.任务 test 没法执行了

使用CADisplayLink、NSTimer有什么注意点?

  • CADisplayLink 保证调用频率和刷帧频率一直,60FPS, 不用设置时间间隔,每秒钟60次

    • 可以使用 proxy 代理解决循环引用
  • CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

内存管理

介绍下内存的几大区域

  • 低地址-> 高地址

  • 保留->代码段->数据段(字符串常量,已初始化全局数据,未初始化数据)>堆->栈内存-> 内核区域

  • 代码段: 编译之后的代码

  • 数据段: 字符串常量,已经初始化的全局变量,或者静态变量,未初始化的全局变量,静态变量

  • 堆 (低>高) 通过 alloc malloc calloc 动态分配的内存

  • 栈 (高地址 从 低地址) 函数调用开销()

讲一下你对 iOS 内存管理的理解

  • 在iOS中,使用引用计数来管理OC对象的内存

  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

  • 内存管理的经验总结

    • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
  • 可以通过以下私有函数来查看自动释放池的情况

    `extern void _objc_autoreleasePoolPrint(void);`

ARC 都帮我们做了什么?

LLVM + Runtime 会为我们代码自动插入 retain 和 release 以及 autorelease等代码,不需要我们手动管理

weak指针的实现原理

  • Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

  • runtime对注册的类, 会进行布局,对于weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键, 在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。

autorelease对象在什么时机会被调用release

iOS在主线程的Runloop中注册了2个Observer

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()

  • 第2个Observer

    • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()

    • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

    • objc_autoreleasePoolPop()调用时候回给 pool 中的对象发送一次 release 消息

方法里有局部对象, 出了方法后会立即释放吗

  • 如果是普通的 局部对象 会立即释放
  • 如果是放在了 autoreleasePool 自动释放池,则会等runloop 循环,进入休眠前释放

思考以下2段代码能发生什么事?有什么区别?

思考以下2段代码能发生什么事?有什么区别?

  • 第一个内存会暴涨,self.name 会不停的创建
  • 第二个内存固定,会使用 Tagged Pointer 将值存在地址中

你在项目中是怎么优化内存的?

内存优化可以从 内存泄漏内存开销 2方面入口

  • 减少内存泄露

    • 可以使用静态分析以及instruments的leaks 分析
    • 注意 NStimer 以及 block ,delegate 等的使用,避免循环引用
  • 降低内存使用峰值

    • 关于图片加载占用内存问题:imageNamed: 方法会在内存中缓存图片,用于常用的图片。
      imageWithContentsOfFile: 方法在视图销毁的时候会释放图片占用的内存,适合不常用的大图等。

    • tableView cell 尽量使用重用机制,减少额外的开销

    • tableView 列表图片展示尽量使用缩略图

    • 延迟加载对象,节约内存开销

    • 避免短时间大量创建对象,配合 autoreleasePool 减少内存峰值

    • 重用大开销对象,比如: NSDateFormatter和NSCalendar

    • 加载 html 尽量使用 wkwebView

    • 单例使用不易过多

    • 线程最大并发数

APP优化

优化你是从哪几方面着手?

  • 卡顿优化
  • 启动优化
  • 耗电量优化
  • app 瘦身

CPU 占用率、 内存使用情况、网络状况监控、启动时闪退、卡顿、FPS、使用时崩溃、耗电量监控、流量监控….

列表卡顿的原因可能有哪些?你平时是怎么优化的?

  • 最常用的就是cell的重用, 注册重用标识符

    • 如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell;
      — 如果有很多数据的时候,就会堆积很多cell。
      — 如果重用cell,为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,如果没有再重新创建cell
  • 避免cell的重新布局
    — cell的布局填充等操作 比较耗时,一般创建时就布局好
    — 如可以将cell单独放到一个自定义类,初始化时就布局好

  • 提前计算并缓存cell的属性及内容
    — 当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度
    — 而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell

  • 减少cell中控件的数量
    — 尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件,
    — 不适用的可以先隐藏

  • 不要使用ClearColor,无背景色,透明度也不要设置为0
    — 渲染耗时比较长

  • 使用局部更新
    — 如果只是更新某组的话,使用reloadSection进行局部更新

  • 加载网络数据,下载图片,使用异步加载,并缓存

  • 少使用addView 给cell动态添加view

  • 按需加载cell,cell滚动很快时,只加载范围内的cell

  • 不要实现无用的代理方法,tableView只遵守两个协议

  • 缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可

  • 不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。

  • 预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;

  • 使用正确的数据结构来存储数据。

app 启动优化

  • pre-main 之前

    • 排查无用的动态库(定期清理)
    • 减少ObjC类(项目中不适用的的库,废弃的代码等)、方法(selector)、分类(category)的数量、无用的库
    • 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize1.
  • main 函数之后的 didFinishLaunchingWithOptions 加载完之前

    • 不影响用户体验的操作,做延迟加载,不要全部放在 didFinishLaunchingWithOptions中去做
    • 版本更新,一些三方初始化,不需要在 didFinishLaunchingWithOptions 初始化的放到,界面展示完以后再初始化
    • 一些网络请求延迟 请求..
    • 一些业务逻辑延迟 加载
    • 初始化第三方 SDK
    • 配置 APP 运行需要的环境
    • 自己的一些工具类的初始化

app 耗电量优化

  • 不要频繁的刷新页面,能刷新1行cell最好只刷新一行,尽量不要使用reloadData.

  • 选择正确的集合

    • NSArray,使用index来查找很快(插入和删除很慢)
    • 字典,使用键来查找很快
    • NSSets,是无序的,用键查找很快,插入/删除很快
  • 少用运算获得圆角,必须要用圆角的话,不如把图片本身就做成圆角

  • 懒加载,不要一次性创建所有的subview,而是需要时才创建.

  • 重用机制

  • 图片处理

    • 图片与imageView相同大小,避免多余运算
    • 可以使用整副的图片,增加应用体积,但是节省CPU
    • 可调大小的图片,可以省去一些不必要的空间
    • CALayer,CoreGraphics,甚至OpenGL来绘制,消耗CPU
  • cache,cache,cache(缓存所有需要的)

    • 服务器相应结果的缓存(图片)
    • 复杂计算结果的缓存(UITableView的行高)
  • 尽量少用透明或半透明,会产生额外的运算.

  • 使用ARC减少内存失误,dealloc需要重写并对属性置为nil

  • 避免庞大的xib,storyBoard,尽量使用纯代码开发

  • CPU层面

    • Timer的时间间隔不宜太短,满足需求即可
    • 线程适量,不宜过多,不要阻塞主线程
    • 优化算法,减少循环次数
    • 定位和蓝牙按需取用,定位之后要关闭或降低定位频率
    • 一些硬件的使用,不使用就关掉

app 的包瘦身

app 的包瘦身

APP 架构

讲讲 MVC、MVVM、MVP,以及你在项目里具体是怎么写的?

  • MVC Model-view-controller 数据-视图-控制器

    • 一般控制器用于管理数据和视图, 数据和视图交互都是通过控制器来进行的.视图和数据进行了解耦, 但是我们日常使用经常会将模型绑定给视图.模型封装在视图内部,外部不用管理视图内部业务逻辑,这数据 mvc 的变种, 控制器只给视图模型数据就好了. 缺点是视图和 模型有耦合;
  • MVVM Model-view-viewModel 模型-视图-视图模型

    • view 和 model 的交互通过viewmodel 来进行交互,实现数据的双向绑定
  • MVP Model-view - Presenter 模型-视图-主持人

    • view 和 model 的交互通过Presenter,controller通过Presenter来管理 model 和 View

你自己用过哪些设计模式?

结合自己项目来讲吧

一般开始做一个项目,你的架构是如何思考的?

  • 根据模块,使用 mvc 功能划分..结合自己项目讲比较容易
  • 涉及到东西也比较多,比较杂,大到整个项目架构,小到一个 view 的架构;没具体的答案
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道