[toc]
1. 常见的多线程有几种,区别在哪,优缺点
常见的有NSThread,NSOperation & NSOperationQueue,GCD,Pthreads,其中Pthreads一般不会用到,NSThread多用于调试。
GCD是基于c的底层api,NSOperation底层通过GCD实现。
NSThread
–优点:NSThread 比其他两个轻量级,使用简单
–缺点:需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有一定的系统开销NSOperation
–封装完善,提供各种方便的Api。可以控制依赖,优先级,继承,键值对观察、最大并发量这些;
支持KVO。可以监测operation是否正在执行、是否结束,是否取消
提供方法可以取消还未执行的线程。但是没办法做到取消一个正在执行的线程;
–较GCD会多一点开销,运行效率较GCD慢些;
*GCD
-简单高效,GCD执行任务可以自由组装,自由度比NSOperation高;直接使用GCD效率确实会更高效,NSOperation会多一点开销
-未提供取消线程的方法等,功能没有NSOperation全面
相关详细内容可参考:https://www.jianshu.com/p/0b0d9b1f1f19
2.进程与线程
进程:
- 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
- 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
- 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源
线程
- 程序执行流的最小单元,线程是进程中的一个实体.
- 一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
进程和线程的关系
- 线程是进程的执行单元,进程的所有任务都在线程中执行
- 线程是 CPU 分配资源和调度的最小单位
- 一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
- 同一个进程内的线程共享进程资源
3. 在多线程情况下,如何保证数据安全
使用线程同步技术解决,常见的线程同步技术就是加锁,加锁常见有以下几种,@synchronized、NSLock、dispatch_semaphore(信号量)、NSRecursiveLock (递归锁)、NSConditionLock(条件锁)、pthread_mutex(互斥锁)、OSSpinLock(自旋锁)、NSCondition
其中@synchronized是一个对象层面的锁,用法简便,性能较差;
NSRecursiveLock递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中;
dispatch_semaphore信号量,会先将信号量减一,并判断是否大于等于0,如果是,则返回0,并继续执行后续代码,否则,使线程进入睡眠状态,让出cpu时间。直到信号量大于0或者超时,则线程会被重新唤醒执行后续操作;
NSConditionLock条件锁,只有满足一定条件的情况下才能打开这把锁。lockWhenCondition::获取锁,如果condition与属性相等,则可以获得锁,否则阻塞线程,等待被唤醒;unlockWithCondition:释放锁,并修改condition属性。
pthread_mutex互斥锁,同一时刻只能有一个线程获得互斥锁,其余线程处于挂起状态
OSSpinLock自旋锁,循环获取锁,在没有获得锁的期间,b线程会一直处于忙等的状态,等待时会消耗大量 CPU 资源,但是可以节省唤醒时间和性能,所以用在对安全性要求不高,临界区执行时间比较短的环境
NSCondition,一种最基本的条件锁。手动控制线程wait和signal。
针对性能,总的来说:
OSSpinLock和dispatch_semaphore的效率远远高于其他。
@synchronized和NSConditionLock效率较差。
鉴于OSSpinLock的不安全,所以我们在开发中如果考虑性能的话,建议使用dispatch_semaphore。
如果不考虑性能,只是图个方便的话,那就使用@synchronized
参考:https://www.jianshu.com/p/938d68ed832c
拓展:atomic并不能完全保证多线程安全问题,因为atomic即getter方法和setter内部增加了线程同步锁,只能保证方法内部的线程同步,如果通过别的方式获取到属性进行修改,比如下例,就会出现问题
NSMutableArray *arr = self.dataArr;//getter方法是安全的
for (int i = 0; i<5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[arr addObject:@"1"];//这里会有多线程操作_dataArr,atomic无法保证这里的线程同步
});
}
4.死锁是怎么出现的 必要条件
使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列
四个条件:
(1) 互斥条件:互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
(2)请求与保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放。
(3)不剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放。
(4)循环等待条件:若干进程之间形成的一种头尾相接的循环等待资源关系。
解决方法:
(1)破坏请求与保持条件:一次性申请所有需要用到的资源。
(2)破坏不剥夺条件:占用部分资源的线程进一步申请不到其他的资源时,可以主动释放它占有的资源。
(3)破坏循环等待条件:按某种序申请资源,释放资源则反序释放,破坏循环等待条件。
5.自动释放池的原理 生命周期 创建和销毁
自动释放池转成C++后发现,构造函数调用objc_autoreleasePoolPush(),~__AtAutoreleasePool() 析构函数调用 objc_autoreleasePoolPop(),实际上就是调用AutoreleasePoolPage的push和pop两个类方法
一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。
pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址即 pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。
一个线程的自动释放池是一个指针堆栈
每一个指针或者指向被释放的对象,或者是自动释放池的 POOL_BOUNDARY,POOL_BOUNDARY 是自动释放池的边界。
一个池子的 token 是指向池子 POOL_BOUNDARY 的指针。当池子被出栈的时候,每一个高于标准的对象都会被释放掉。
堆栈被分成一个页面的双向链表。页面按照需要添加或者删除。
本地线程存放着指向当前页的指针,在这里存放着新创建的自动释放的对象。
生命周期:
App一启动,系统就会添加两个Observer,监听主线程对应的RunLoop,主要负责autoreleasepool的创建和销毁。第一个Observer监听的是“即将进入RunLoop”状态,它的回调里会创建一个autoreleasepool,这个Observer的优先级最高,以此保证创建autoreleasepool发生在其它所有回调之前。第二个Observer监听的是“线程即将进入休眠”和“刚刚退出RunLoop”两个状态,“线程即将进入休眠”时它的回调里会销毁旧autoreleasepool并创建新autoreleasepool,“刚刚退出RunLoop”时它的回调里会销毁autoreleasepool,这个Observer的优先级最低,以此保证销毁autoreleasepool发生在所有回调之后。
参考:
https://www.jianshu.com/p/9dad9c3247ed
https://www.jianshu.com/p/554c9fe0f041
6.App优化有哪些方面 详细介绍方案
内存优化、耗电优化、启动优化、卡顿优化、安装包瘦身,网络优化等
- 内存优化
- 处理内存泄漏等问题
- 在临时创建大量对象时,使用NSAutoreleasepool
- 正确运用多线程技术,控制多线程同时执行并发量
- 正确使用cell复用和单例延迟加载(lazy load) Views
- 避免过于庞大的xib
- 根据实际情况选择是否缓存图片,是否需要缓存到内存
- 减少离屏渲染,如UIButton设置圆角,UIImageView设置阴影效果等;避免对UIView使用透明
- 使图片符合UIImageView的尺寸。不要在运行的时候再让UIImageView自行压缩
- 处理低内存警告
- 耗电优化
App耗电主要关注以下几个地方,CPU处理、网络、定位、图像
- CPU处理:
1). 尽可能少用定时器;
2). 优化I/O操作(所谓的I/O操作也就是文件操作,我们简称为I/O操作。怎么优化呢?尽量不要频繁写入小数据,最好批量一次性写入。读写大量主要的数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问);
- 数据量比较大的,建议使用数据库(SQlite、CoreData);
2.网络:
1). 减少、压缩网络数据。(不同格式数据提交关系:XML提交比较大;JSON 提交比较小,protobuf提交最小)
2).如果多次请求的结果相同,尽量使用缓存。NSMutableRequest 里面可以设置使用NSCache进行缓存;
3). 尽量使用断点续传,否则网络不稳定的时候可能多次传输相同的内容。(传输1M文件,如果一次性下载,一旦网络问题下载失败,下次重新请求,会从头开始。之前下载过的部分会进行重新下载,断点续传可以保证之前下载的数据缓存起来);
4). 网络不可用,不要尝试执行网络请求;
5). 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
6). 批量传输,比如下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
- 定位:
1). 如果只是需要快速确定用户的位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电;
2). 如果不是导航的应用,尽量不要实时更新位置,定位完毕就关掉定位服务;
3). 尽量降低定位精度,比如尽量不要使用精度最高的KCLLocationAccuracyBest;精度越高,硬件模块功耗越大;
4). 需要后台定位时,尽量设置pauseLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新。
5). 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:
- 图像:
1). 图片与imageView相同大小,避免多余运算
2). 可以使用整副的图片,增加应用体积,但是节省CPU
- 启动优化
- 先说下启动流程,APP的启动可以分为2种
冷启动(Cold Launch):从零开始启动APP
热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
APP启动时间的优化,主要是针对冷启动进行优化
通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1
App的冷启动大概分为三个阶段,dyld,runtime,main
1). dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)
启动APP时,dyld所做的事情有
装载APP的可执行文件(可执行文件包含代码和动态库依赖信息),同时会递归加载所有依赖的动态库
当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理
2). 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:方法
2.接下来介绍一下启动优化的点:
按照不同的阶段
dyld
减少动态库、合并一些动态库(定期清理不必要的动态库)
定期清理不必要的类、分类, 装载可执行文件时候有加载类分类的操作.
runtime
少在+load方法里写逻辑代码,可以用+initialize方法和dispatch_once取代
main
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
1)把可以延迟执行的逻辑,做延迟执行处理;可以延迟加载的库,做延迟加载处理
2)避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。
3)首页控制器用纯代码方式来构建。
- 卡顿优化
卡顿监测:
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
除了用xcode的Time profiler (程序耗时检测), Core Animation(检测刷新帧率)工具外,代码层面也可以检测.
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
检测runloop间隔,打印主线程堆栈卡顿原因:
界面展示流程: cpu计算->GPU渲染->帧缓存->视频控制器读取->显示到屏幕
在iOS中是双缓冲机制,有前帧缓存、后帧缓存
每次垂直同步信号出来时候,把处理好的帧显示在屏幕上.按照60FPS的刷帧率,每隔16ms就会有一次VSync信号
垂直同步信号来的时候,GPU如果没有处理完成,只能将上一次处理好的帧显示出来,这就是掉帧,等下一次垂直同步信号出来后,这个数据处理好后再显示.
卡顿解决的主要思路
尽可能减少CPU、GPU资源消耗
- 卡顿解决方案
cpu优化
1) 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView(下划线)
2) 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
3) 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
4) Autolayout会比直接设置frame消耗更多的CPU资源
5) 图片的size最好刚好跟UIImageView的size保持一致
6) 控制一下线程的最大并发数量
7) 尽量避免日期格式转换 [NSDate dateWithString:@"1990-11-11" format:@"yyyy-MM-dd"]
8) 尽量把耗时的操作放到子线程
文本处理(尺寸计算、绘制)
图片处理(解码、绘制)
9) tableView不要动态创建子控件,尽可能使用懒加载,尽量少设置透明度.
GPU优化:
1)尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
2)尽量减少视图数量和层次
3)减少透明的视图(alpha<1),不透明的就设置opaque为YES
4)尽量避免出现离屏渲染
在OpenGL中,GPU有2种渲染方式
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
(离屏渲染消耗性能的原因
需要创建新的缓冲区
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕)
5)根据实际情况控制图片加载方式
- 安装包瘦身优化
安装包(IPA)主要由可执行文件(源代码文件 编译链接生产的)、资源(图片 音视频 stroyboard xib)组成
项目编译完生产app文件,app文件压缩后成IPA文件
1) 对资源文件进行无损压缩后再拖进项目里使用
2) 清理项目中不再用到的资源文件
3) 编译器优化,去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions可以适当地减小安装包的体积
4) 手动移除代码
5) 视频/音频 大图片资源不要放到包里,可以从服务端下载.
6) 使用 xib/storyboard 来开发视图界面会一定程序增加安装包的大小。尽可能用代码布局.
参考:
https://www.jianshu.com/p/ea6c31fe837c
https://www.jianshu.com/p/00e042ca2e72
https://www.jianshu.com/p/870864e2a384
7.MVC与MVVM
MVVM是用ViewModel替代了苹果版MVC Controller的角色。
不过MVVM和MVP不同的地方就在于ViewModel和View存在一个双向绑定,具体地说就是:
ViewModel最基本的职责当然是起到Presenter的调度作用,因此它会持有(绑定)View;
然后ViewModel之所以叫ViewModel,是因为它还是一个用来供View显示的实体,这个实体是拥有属性的,它的属性跟Model的属性基本保持一致,我们在加载到Model之后,会把Model的每个属性依次赋值给ViewModel相应的属性,而不是赋值给View去显示。而是View会持有(绑定)这个ViewModel,并监听ViewModel属性的变化来显示和更新UI,这就使得View更有封装性,同时显示数据和更新数据更加灵活。(View监听ViewModel属性的变化可以用RAC或者FaceBook的KVOController)
- Controller-ViewModel-Model 模式
1)建立协议,协议内容为,当VM数据发生变化,对应的Controller需要进行的操作。
@protocol SearchViewProtocol <NSObject>
// 刷新显示
- (void)reloadTable;
@end
由Controller遵循,并实现协议
#pragma mark - View protocol
- (void)reloadTable {
[self.tableView reloadData];
}
- Controller里面,创建ViewModel属性
@property (nonatomic, strong, readonly) SearchViewModel *vm;
完成初始化,并将Controller自己当做参数传入,实现双向绑定
_vm = [[SearchViewModel alloc] initWithView:self];
3)在ViewModel里面,创建拥有Controller属性,weak修饰
并建立初始化方法,在方法中完成赋值
@property (nonatomic, weak, readonly) id< SearchViewProtocol > view;
//ViewModel初始化,拥有Controller属性
- (instancetype)initWithView:(id< SearchViewProtocol >)view;
4)当Controller触发点击事件等时,调用ViewModel提供的方法完成数据处理,再通过SearchViewProtocol协议,使用代理完成Controller事件回传
- View-ViewModel-Model 模式
参考:https://www.jianshu.com/p/0d7fb890d3b6
8.深拷贝和浅拷贝 集合类深拷贝怎么实现
copy:copy拷贝出来的对象类型总是不可变类型(例如, NSString, NSDictionary, NSArray等等)
mutableCopy拷贝出来的对象类型总是可变类型(例如, NSMutableString, NSMutableDictionary, NSMutableArray等等)
对象要想具有copy和mutablecopy功能要是NSCopying和NSMutableCopy协议,实现copywithzone和mutablecopywithzone
- 非集合类对象:
对immutable对象进行copy操作,是指针复制mutableCopy操作时内容复制
对mutable对象进行copy和mutableCopy都是内容复制
可变对象通过copy之后就变成不可变了对象。
2.集合类对象:
在集合类对象中,对immutable对象进行copy,是指针复制,mutableCopy是内容复制。
在集合类对象中,对mutable对象进行copy和mutableCopy都是内容复制。
在集合类对象中,对对象进行copy的对象就是不可变的,进行mutable就是可变的
但是,对于集合对象的内容复制仅仅是对对象本身,但是对象的里面的元素还是指针复制。要想复制整个集合对象,就要用集合深复制的方法,有两种:
(1)使用initWithArray:copyItems:方法,将第二个参数设置为YES即可
NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];
(2)将集合对象进行归档(archive)然后解归档(unarchive):
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
参考:
https://www.jianshu.com/p/1a553d58707b?utm_campaign=hugo
https://blog.csdn.net/u012094456/article/details/102951796
9.KVO原理
1、KVO是关于runtime机制实现的
2、当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制
3、如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
4、每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统就会偷偷讲isa指针指向动态生成的派生类,从而在给被监控属性复制是执行的是派生类的setter方法
5、键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForkey:和didChangeValueForKey:;在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而observeValueForKey:ofObject:change:context:也会被调用
参考:https://blog.csdn.net/github_38318102/article/details/80651506
10.KVC底层实现
KVC赋值的底层实现
1.按照setKey和_setKey的顺序找set方法赋值。
2.如果找不到set方法,就调用accessinstancevariablesdirectly获取返回值。
3.返回YES,按照_key,_isKey,key,isKey的顺序找成员变量赋值。
4.返回NO,抛出异常Nsunknownkevexception。
5.如果四个成员变量也找不到,就抛出异常Nsunknownkevexception。
KVC取值的底层实现
1.按照getKey,key,isKey,_key的顺序找get方法取值。
2.如果找不到上述四个get方法,就调用accessinstancevariablesdirectly获取返回值。
3.返回YES,按照_key,_isKey,key,isKey的顺序找成员变量取值。
4.返回NO,抛出异常Nsunknownkevexception。
5.如果四个成员变量也找不到,就抛出异常Nsunknownkevexception。
补充
1.accessinstancevariablesdirectly方法返回是否允许访问成员变量,默认返回YES。
2.@property修饰的成员变量会自动生成对应的getter和setter方法。
3.通过KVC修改属性会触发KVO,因为KVC内部调用了willChangeValueForKey和didChangeValueForKey方法。
参考:https://blog.csdn.net/weixin_31064353/article/details/113897899
11.NSTimer解决循环引用问题
NSTimer创建的定时器,使用时会造成循环引用(target对self做了强引用,self又对timer进行了强引用),从而导致内存泄漏
有以下几种方法来解决循环引用问题:
- 合适的时机关闭定时器
#pragma mark - 第一种方法:合适的时机关闭定时器
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (!parent) {
[_timer invalidate];
_timer = nil;
}
}
- 更改target,并给target通过Runtime添加方法
// 添加属性
@property (nonatomic, strong) id target;
// 第二种方法
_target = [NSObject new];
class_addMethod([_target class], @selector(run), class_getMethodImplementation([self class], @selector(run)), "v@:");
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_target selector:@selector(fire) userInfo:nil repeats:YES];
- (void)dealloc {
if (_timer) {
[_timer invalidate];
_timer = nil;
}
}
- (void)run
{
NSLog(@"Hello");
}
- 通过使用NSProxy解决循环引用
新建一个FKProxy继承自NSProxy,添加属性target,实现方法
@property (nonatomic, weak) id target;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
把timer的target改为FKProxy类型的对象
@property (nonatomic, strong) FKProxy *fkProxy;
_fkProxy = [FKProxy alloc]; //FKProxy只有alloc方法,没有init方法
_fkProxy.target = self;
_timer3 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_fkProxy selector:@selector(run) userInfo:nil repeats:YES];
- 添加一个NSTimer的分类,使用block解决循环应用
分类实现方法
+ (NSTimer *)fk_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block {
void (^inBlock)(void) = [block copy];
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(fk_blockHandle:) userInfo:inBlock repeats:repeats];
}
+ (void)fk_blockHandle:(NSTimer *)timer
{
if (timer.userInfo) {
void(^block)(void) = (void (^)(void))timer.userInfo;
block();
}
}
使用分类方法创建定时器
__weak typeof (self) weakSelf = self;
_timer4 = [NSTimer fk_scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf run];
}];
参考:https://www.jianshu.com/p/722464260b9e
12.iOS APP生命周期 和 UIViewController的生命周期
https://www.jianshu.com/p/1f6820a7d3fd