介绍下内存的几大区域?
栈,由系统分配和释放,存放函数的参数值和局部变量等。特点是效率高,不灵活。
堆,由程序员分配和释放,alloc的都是放在堆里。特点是灵活方便, 但是效率相比而言较低。
BSS,全局变量和静态变量
常量区,存放常量
代码段,存放程序的代码
组件化
组件化也是模块化,用来拆分代码,解决代码耦合。各组件可以自由独立开发,独立测试,多人员开发时提高效率。
关键点:解耦。
两种方案。
- 蘑菇街 url router
思路:各组件向router注册接口,但是不能直接注册,否则会产生依赖。因此要通过字符串->方法
的映射去调用。runtime 接口的 className + selectorName -> IMP 是一种,注册表的 key -> block 是一种,而前一种是 OC 自带的特性,后一种需要内存维持一份注册表。
router中维护一个字典,两种注册方式。
一种使用[[Routable sharedRouter] map:@"user" toController:[UserController class]];
注册,实际是以user
为key
,以一个包含viewcontroller class
以及一些打开方式等参数的option
作为value。使用时直接调用open
,在open
函数内部实现跳转。
另一种是使用[self.router map:@"users" toCallback:^(NSDictionary *params) { }];
注册,实际是注册user
为key
,block
为value
。在使用open
调用后,才执行上面block
内部代码。
实际应用第二种。使用方式可以在didFinishLaunchingWithOptions
中
[MGJRouter registerURLPattern:@"mgj://detail?name=:name&summary=:summary" toHandler:^(NSDictionary *routerParameters) {
NSString *name = routerParameters[@"name"];
NSString *summary = routerParameters[@"summary"];
// create view controller with id
// push view controller
}];
上面组件注册,在某个需要跳转页面的地方执行
[MGJRouter openURL:@"mgj://detail?name=:name&summary=:summary" withParam:@{@"name": @"zql", @"summary":@"wuyanzu" }];
所以可以本地维护一个url表。同时在router内部也可以人工维护一个传入name,获取对应classname的表。
使用协议优化。
定义某一组件对应协议P,协议包括一个必须实现的方法C,返回这一组件的控制器。
创建protocolManager
类,提供注册方法A,传入TestEntry
和TestProtocol
,以及传入TestProtocol
获取TestEntry
方法B。内部实现是存入以string
类型的protocol
为key
,entry
为value
的字典中。
在TestEntry
类中,遵循协议P,使用load
方法调用上面的注册方法A。再实现协议P的方法C,返回一个具体的控制器。
最后在调用处,使用方法B传入协议P获取entry
,再调用其C方法获取控制器,直接跳转即可。
- 安居客 target-action
组件a 有target类,提供action方法,方法里有各种调用a的方式
总的CTMediator类,提供方法 这个方法可以执行传入的target中的action方法
然后在定义一个CTMediator的分类a 在分类a里调用CTMediator的方法,传入a的target名和action名
这样其他组件 想要进入组件A 只需要调用分类A即可
组件b 调用分类a
最终采用这种。
runloop
runloop
对于一个标准的iOS开发来说都不陌生,应该说熟悉runloop
是标配,下面就随便列几个典型问题吧
1. app如何接收到触摸事件的
首先runloop有source0和source1,其中注册source1是接受外部系统事件回调。当触摸摇晃等发生时,会由IOKitFramework(所属最底层Drawin),生成IOHIDEvent,再由mach port转发给进程。此时source1注册的回调就会执行,UIApplicationHandleEventQueue会把IOHIDEvent包装成UIEvent进行处理和分发。
手势识别和更新界面,手势发生变化和界面发生变化都会将这个手势或者view标记成待处理。然后苹果注册了一个Observer监测BeforeWaiting(进入休眠前),在进入休眠前便利待处理的任务,处理手势和更新页面。
2. 为什么只有主线程的runloop
是开启的
默认开启,接受处理事件,一直循环,不退出。而一般的子线程结束后都要退出,所以需要的话子线程的runloop要手动开启。
3. 为什么只在主线程刷新UI
这是由于UIKit框架的设计和线程安全性要求所决定的。由于UIKit中大多数控件都是nonatomic,线程不安全的。在多个线程上同时操作UI,可能会导致未定义的行为或崩溃。
对view的所有变化会在当前runloop结束的时候统一绘制,被称为绘图循环。
渲染流程。
UIKit: 包含各种控件,负责对用户操作事件的响应,本身并不提供渲染的能力
Core Animation: 负责所有视图的绘制、显示与动画效果
OpenGL ES: 提供2D与3D渲染服务
Core Graphics: 提供2D渲染服务
Graphics Hardware: 指GPU
4. PerformSelector
和runloop
的关系
将要执行的方法,和延迟的timer加入当前runloop中。如果子线程没有开runloop,就会失效。
线程刚创建时并没有RunLoop对象, RunLoop会在第一次获取它时创建,RunLoop会在线程结束时销毁。
performSelector通过将选择器添加到runloop中,实现了在适当的时机执行指定方法的功能。这使得我们能够在合适的线程上执行代码,并保持与UI事件处理的协调。
5. 如何使线程保活
关键点是在子线程中创建一个长期执行的循环(例如,使用NSRunLoop),这样子线程就能够保持活跃状态。通过添加一个占位的端口(port)到循环中,并运行循环,可以防止子线程在任务完成后自动退出。
需要注意的是,保活子线程可能会导致资源占用,因此请确保在适当的时机停止或退出子线程,以避免不必要的开销和内存泄漏。
加入runloop,代码如下。
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
//开启一个子线程, 并运行RunLoop
self.thread = [[QLThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.thread) {
[self performSelector:@selector(printThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
}
// 子线程需要执行的任务
- (void)printThread {
NSLog(@"%s thread:%@",__func__, [NSThread currentThread]);
}
// 在子线程停止当前线程的RunLoop
- (void)stop {
if (!self.thread) return;
[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 停止RunLoop
- (void)stopRunLoop {
self.stoped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
// 保活线程
- (void)hold {
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
- (void)dealloc {
[self stop];
}
KVO
1. 实现原理
KVO 就是通过 Runtime 替换被观察类的 Setter 实现,从而在发生改变时发起通知。
具体:当观察对象A时,KVO会在运行时动态创建A的子类NSKVONotifying_A,并使原来指向A的指针指向它。然后重写其setter方法。新的setter方法会在原setter方法执行前和执行后通知观察者。
在动态生成的子类的setter方法中会调用willChangeValueForKey:和didChangeValueForKey:。之后也会调用observeValueForKey:ofObject:change:context:,继而实现KVO中的回调。
2. 如何手动关闭kvo
通过设置 automaticallyNotifiesObserversForKey 为 False 实现取消自动触发
手动调用,同时执行willchange和didchange
3. 通过KVC修改属性会触发KVO么
会,kvc内部setvalueforkey也会触发
4. 哪些情况下使用kvo会崩溃,怎么防护崩溃
要remove
5. kvo的优缺点
优点:不需要污染被监控者
缺点: 不够灵活,不能传Block
Block
1. block
的内部实现,结构体是什么样的
实质是对象,内部是结构体。包括,指针,标志位,函数指针,捕获变量。
2. block是类吗,有哪些类型
匿名函数,不是类。全局Block,堆Block,栈Block。
3. 一个int
变量被 __block
修饰与否的区别?block的变量截获
会。捕获值,修饰后会变结构体,在堆。
4. block
在修改NSMutableArray
,需不需要添加__block
不需要。NSMutableArray本来就是对象类型
5. 怎么进行内存管理的
block在栈上时,系统管理。在堆上时,用到哪个对象,对哪个对象进行内存管理。
6. block
可以用strong
修饰吗
对于block类型的变量或属性,在赋值时会自动进行copy操作,并对block进行强引用,因此不需要显式使用strong修饰关键字。
7. 解决循环引用时为什么要用__strong、__weak
修饰
self持有block,又在block中使用self,造成循环引用。
8. block
发生copy
时机
block作为函数返回值时
将block赋值给__strong指针时
block作为Cocoa API中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时
GCD
//www.greatytc.com/p/42ddf70d8666
1.iOS开发中有多少类型的线程?分别对比
主线程和子线程。主线程响应处理UI,子线程耗时操作。
2.GCD有哪些队列,默认提供哪些队列
默认提供的The main queue
和Global queue
。以及用户使用dispatch_queue_create
创建的串行队列(传:dispatch_queue_current),和并行队列(传:dispatch_queue_concurrent)。
3.GCD有哪些方法api
dispatch_get_main_queue
、dispatch_get_global_queue
、dispatch_queue_create
、dispatch_sync
、dispatch_async
、dispatch_group_create
、dispatch_barrier_async
、dispatch_after
、dispatch_once
、dispatch_apply
4.GCD线程 & 队列的关系
主队列是一群任务的集合,按顺序取出,再放在主线程执行。是否开新线程主要看是不是异步,主队列除外,主队列都在主线程。
不同的线程好比不同的马路主路,不同的队列好比等待进入主路的辅路,任务好比汽车。串行队列是一条辅路多车排队,并行队列是多条辅路,多车分开排队。
1)主队列 dispatch_get_main_queue()
- 主队列异步任务:不会开启新线程,主队列任务都只能在主线程内执行,会异步延后执行。
- 主队列同步任务:不会开启新线程,队列内任务同步执行,会和原主队列内任务互相等待,造成你死锁。
2)串行队列 dispatch_queue_create("cx",DISPATCH_QUEUE_SERIAL)
- 串行队列异步任务:开启新线程,在新线程内串行执行。
- 串行队列同步任务:并不会开启新线程。在主线程内串行依次执行队列中任务。(不同于上文,串行队列中内容和主队列中内容分开,可以依次执行,不会存在互相等待死锁的情况)
3)并行队列 dispatch_queue_create("concurrent",DISPATCH_QUEUE_CONCURRENT);
- 并行队列异步任务:开启多个新线程,同时并行执行。
- 并行队列同步任务:不会开启新线程,主线程内同步执行。
5.实现方案
pthread : 通用,跨平台,难度大。
NSThread: 面向对象,简单易懂。
GCD: 旨在替代NSThread,充分利用设备多核,使用普遍。
NSOperation: 基于GCD,更面向对象,提供更简单实用功能。
6.如何实现同步,有多少方式就说多少
直接使用dispatch_sync
使用dispatch_group_create
+dispatch_group_wait
。
dispatch_apply
7.dispatch_once实现原理
dispatch_once_t 其实是 long 类型,取其地址作为唯一标识符,保证 block 内部任务执行且仅被执行一次。
8.什么情况下会死锁
在主线程直接执行dispatch_sync
。比如在viewdidload
内执行dispatch_sync
。
原因:viewdidload
方法在主队列第一位,dispatch_sync
内部event
被放入主队列第二位。所以event
在viewdidload
内部,前者不执行完后者无法执行完。内部又是dispatch_sync
,队列中的viewdidload
不执行完event
就无法执行。造成了死锁。
解决:创建一个新队列,将event
放入新队列中。这样主队列和我们创建的队列中的任务会按顺序执行,因为不在一个队列,所以不存在一个队列中等待第一个任务执行完才能执行第二个任务的情况。所以可以完美同步解决这个死锁。
9.有哪些类型的线程锁,分别介绍下作用和使用场景
dispatch_semaphore。比如卖票。包括creat创建,wait加锁,signal解锁。当多线程同时对一个数组进行操作时,在操作前加锁,操作后解锁即可。
10.NSOperationQueue中的maxConcurrentOperationCount默认值
最大并发数,默认-1表示不限制。1时串行,大于1时并发。
11.NSTimer、CADisplayLink、dispatch_source_t 的优劣
NSTimer 常用
CADisplayLink适合做界面的不停重绘
dispatch_source_t可以使用子线程,用于长时间定期(比如隔十分钟监测有没有新邮件)。
12.GCD的六种组合
- 同步+主队列:死锁,如上。
- 同步+串行队列:从上到下顺序执行。
- 同步+并发队列:同步无法开新线程,如上从上到下顺序执行。
- 异步+主队列:单一线程,主线程。不按重上到下,按照异步内任务顺序执行。
- 异步+串行队列:新创建线程。主线程正常运行,子线程按照异步内任务顺序执行。
- 异步+并发队列:创建多个线程,同时运行。
视图&图像相关
1.AutoLayout的原理,性能如何
原理:
首先,将View放到Window上,并添加相应的约束;
布局引擎会在updateConstraints()过程中生成与约束相关的方程式;
在layoutSubviews()过程中,View会从布局引擎中取出方程式计算的结果并用于View的布局。
性能:
iOS12有大幅提升。之前视图嵌套对性能指数级增长,现在线性,基本和frame(绝对布局)性能差不多。
尽量与父视图和兄弟视图交互约束,不要与不同父视图的控件交互约束。
2.UIView & CALayer的区别
平行的层级关系,每个UIView都有一个CALayer实例属性。
UIView属于UIKit,父类UIResponder,用于响应事件,构建用户界面。
CALayer属于Quartz2D,父类NSObject,更底层更灵活,用于管理图形元素,形变位置等操作,以及制作动画。
3.事件响应链
//www.greatytc.com/p/2d4e423be403
事件的传递:
iOS使用hit-testing寻找触摸的view。如果触摸点在A区域中,继续检查A的子视图B和C,如果在B中,继续检查B的子视图D和E,直到找到最上层视图成为hit-testing的view。其中三种情况hit-testing不执行,也就是不响应点击事件,透明度<0.01
、hidden=YES
、userInteractionEnabled = NO
从上至下一次查询,从父视图向子视图查询。这也就是当子视图超过父视图而没有被剪切正常显示的时候,点击外部区域无法响应的原因。
事件的响应:
子视图能处理则处理,不能处理则传递给父视图。
子view ——> 父view ——> 控制器view ——> controller ——> window ——> application ——> 无效放弃
一个事件多个视图处理:
重写touchesBegan
方法,在内部处理事件之后,再执行[super touchesBegan:touches withEvent:event];
,使其父视图处理。
3.drawrect & layoutsubviews调用时机
drawrect:
- 如果在UIView初始化时没有设置frame,会导致drawRect不被自动调用
- sizeToFit后会调用。这时候可以先用sizeToFit中计算出size,然后系统自动调用drawRect方法
- 通过设置contentMode为.redraw时,那么在每次设置或更改frame的时候自动调用drawRect
- 直接调用setNeedsDisplay,或者setNeedsDisplayInRect会触发drawRect
layoutSubViews调用时机: - init初始化不会调用layoutSubviews方法
- addSubview时会调用
- 改变一个UIView的frame时会调用
- 滚动一个UIScrollView导致UIView重新布局时会调用
- 旋转Screen会触发父UIView上的事件
- 手动调用setNeedsLayout或者layoutIfNeeded
4.UI的刷新原理
5.隐式动画 & 显示动画区别
隐式动画:不指定任何动画类型,仅改变非根层layer(手动创建的layer)动画属性,coreAnimation会决定如何何时去做动画,你不用做额外操作。
使用CATransaction
相关方法修改动画时间等。
https://www.bbsmax.com/A/D8547yv3JE/
显式动画:需要创建一个动画对象,并设置开始和结束值,直到把动画应用到某图层上,动画才开始执行
6.动画
Core Animation:
CABasicAnimation: 只能实现系统定义的一些简单动画
CAKeyframeAnimation
CAEmitterLayer:
7.什么是离屏渲染
//www.greatytc.com/p/52c72f18e142
使用Core Graphics API 会触发离屏渲染。
CPU计算显示内容交给GPU,GPU渲染后放入缓存区,视频控制器取出显示。但是有些效果被认为不能直接呈现于屏幕前,而需要在别的地方做额外的处理,进行预合成,就是离屏渲染。离屏渲染要创建新的缓存区,多次切换上下文(当前屏,离屏),所以开销很大。
解决:
给图片设置圆角时,把背景设置成透明,可以避免离屏渲染。
设置阴影时,指定一个与边界相同的简单路径
裁剪视图:对于需要圆角效果的视图,可以使用cornerRadius属性并将masksToBounds设置为YES,这样可以在不引起离屏渲染的情况下实现圆角效果。
使用合适的图层属性:在涉及复杂的动画或图形效果时,可以使用shouldRasterize属性将图层内容缓存为位图,以减少离屏渲染的次数。
iconBtn.layer.shadowColor = [UIColor colorWithHex:0xd6d6d6].CGColor;
iconBtn.layer.shadowOffset = CGSizeMake(-2, 2);
iconBtn.layer.shadowOpacity = 0.15;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:iconBtn.bounds];
iconBtn.layer.shadowPath = path.CGPath;
[iconBtn addTarget:self action:@selector
8.imageName & imageWithContentsOfFile区别
imageName自带缓存机制。频繁使用的小图片。
imageWithContentsOfFile不带缓存机制,会导致内存暴涨。一次性加载的大图片。
9.图片是什么时候解码的,如何优化
//www.greatytc.com/p/4da6981a746c
解码:
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。图片解码是相对很耗时的操作。
创建图片不会解码,图片被设置到UIImageView被提交到GPU前解码。
优化:
- SDWebImage的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。
- PNG图片更大,解码快,小图选PNG。JPEG图片更小,解码慢,大图选JPG。
- 图片适量压缩,使用Assets自带压缩。
- 使用CGContext比ImageIO绘制更快,但CPU占用更高。
图片渲染怎么优化
- 对象创建:用轻量的对象代替重量的对象。比如用CALayer代替UIView,用CATextLayer代替UILabel。另外通过 Storyboard 创建视图对象时比代码消耗大很多。
- 对象销毁:可以放在后台销毁的放在后台销毁
- 对象调整:尽量避免添加移除视图。
- 布局计算:尽量在后台计算视图布局和缓存视图布局。
- Autolayout:复杂视图Autolayout会产生性能问题。
- 文本计算:如果一个界面中包含大量文字,文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。另外这两个操作尽可能得在后台操作。
- 文本渲染:自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。
- 图片的解码:后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。例如SDWEBImage.
- 图像的绘制: CoreGraphic 方法通常都是线程安全的,所以 [UIView drawRect:] 里面的绘制可以放到后台线程执行。
如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象,怎么解决
会浪费GPU