(1)RunTime总结:
oc动态性, 运行时将代码转化为runtime的C代码
RunTime运行流程:
当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依次,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点IMP(类似于一个编号,是函数指针,指向函数实现,找到内存里对应函数),并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程。
1.动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个处理方法。不过使用该方法的前提是我们已经实现了该处理方法,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。
不过这种方案更多的是为了实现@dynamic属性。
2.备用接收者
如果在上一步无法处理消息,则Runtime会继续调用forwardingTargetForSelector:方法。
-(id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
3.完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
-(void)forwardInvocation:(NSInvocation*)anInvocation
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。
真正消息转发,也是Aspects的核心操作, 如果都找不到调用doesNotRecognizeSelector:方法抛出异常
类在OC中其实是一个指向objc_class的结构体指针,OC中对象的是objc_object。从这里可以知道,OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个 isa 指针,指向他所属的类。那么元类的类是什么?就是我们所说的元类 (MetaClass) ,所以,元类就是类所属的类。从消息机制的层面来说:
当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
当你给类发消息时,消息是在寻找这个类的元类的方法列表。
类的实例方法是存储在类的methodLists中,而类方法则是存储在元类的methodLists中,因此根据上图,NSObject的元类的superclass是指向Class,当调用[NSObject foo]的时候,因为这是一个类方法调用,所以从元类中查找签名为foo的方法,没有发现,然后再沿superclass继续查找,结果在Class中查找到该方法,于是调用该方法输出。但如果将NSObject的分类,换成其他类的分类,如NSString,会发现程序崩溃,这是因为签名为foo的函数在NSString中,而当我们进行类方法调用的时候,最后会查找到NSObject的Class中,但该Class中并没有对应的方法签名,于是再沿superclass向上查找,由于NSObject的superclass是nil,于是抛出unrecognized selector。
RunTime的实际应用
Category动态添加属性,重写set、get方法。
方法交换(黑魔法.hook,让SEL1->IMP2,SEL2->IMP1)
①用方法交换添加保护, 如数组赋值时添加越界判断(交换objectAtIndex方法)等等 ②统计页面点击数用(交换viewDidAppear) ③多继承 ④自动化归档(找到类的所有属性,执行NSCoding编解码) ⑤NSTime内存泄漏(vc被释放通过消息转发找回vc) ⑥系统类添加自定义方法, 写一些更便捷的代码,比如控件加手势,字典加加密方法,代码更简洁
(2)KVO总结:
KVO是OC的一种观察者设计模式,另一种是通知机制, 是基于runtime机制实现的, 也是一种响应式编程(kvo,block,代理,通知,定时器等)
KVO运行流程:
当观察对象A时,KVO动态创建了新的名为NSKVONotifying_A的新类,该类时为对象A的子类(根据父类—>子类, 创建类名,开辟内存空间. 利用RunTime拿到父类的函数实现,用黑魔法isa-swizzling交换父类方法调用) 并且KVO重写了新类的观察属性的setter方法,setter方法负责在调用原setter方法之前和之后,通知所有观察者该属性的变化情况(利用消息转发,子类—>父类)
子类重写setter方法:
KVO的键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在存取数值的前后分别调用2个方法;中间利用消息转发,之后,observeValueForKey:ofObject:change:context:也会被调用。
KVO注意事项:
①没有set方法无法观察,例如成员变量(类外部不可访问,不可赋值,类内部可以通过self->属性名或者属性名访问和赋值) ②观察可变数组方法不一样
KVO实际应用:
①监听属性(模型)变化 ②MVVM双向绑定 ③网络,断点续传
(3)MVC, MVP, MVVM总结:
MVC是运用最广泛的架构模式,MVP和MVVM是基于MVC衍生的新框架, 可以实现解耦, 真正实现高内聚低耦合的特性. 但架构没有最好的, 只有最合适的!!!
MVC, MVP, MVVM区别:
MVC缺点: 厚重的VC, 代码可读性差, 逻辑混乱, 基本无法测试.
在日常开发使用MVC中,经常为了减少代码量,冗余将Model写在View,这样View的移植性差, 增加了耦合性. MVP是面向协议编程,使用代理完成双向绑定, 但一些延迟性操作难以管理(如请求接口数据). MVVM是MV(X)系列最好的架构模式, 双向绑定,面向需求添加方法, 随掉随用, MVC的代码抽离也是一种MVVM思想
MVVM优点: 低耦合, 可重用性, 独立开发(业务逻辑通过VM和UI分离), 可测试(可以针对viewModel测试)
MVVM缺点: 界面异常BUG难调试, 代码不直观, 企业应用存在学习成本
(4)Block总结: ✨ 最常用没有之一, 敲黑板✨
将OC语言block改写成C语言的block, 动态生成.cpp文件(c++代码). block是可以%@打印的, 说明也是一个对象. 可以理解为特殊格式, 带函数回调的对象. block的灵活之处也在于可以将block作为一个属性封装, 保存一段代码块, 可以在任意时候调用
block分类:
NSGlobalBlock(全局)、NSStackBlock(栈)、NSMallocBlock(堆)
当block回调对外部变量操作时, 将外部变量copy到堆上,
block应用场景:
①传值 ②MVVM ③封装回调代码块
__block:
block对外部变量是只读的,要变成可读可写,就需要加上__block, 将栈中的block复制到堆上一份,从而避免了循环引用这个情况
__block原理: 能够将观察到int a=0的值copy到堆里, 对a的指针地址进行修改, block回调里a指针地址和外部变量a的指针地址相同(相当于浅拷贝, 拷贝指针地址) 栈/常量 -> copy -> 堆
block解决循环引用(面试总问:多种解决方法):
循环引用原因: A持有B,B又持有A, 就形成了互相持有, 形成了闭环. 从引用计数分析是B想释放, 但A还持有B, 同理A也无法释放(A,B就是self和block)
解决方案: ①弱引用__weak typeof(self) weakSelf = self;(原理强弱共舞) ②__block ViewController *bWeakSelf = self; 同时block回调里 bWeakSelf = nil; (原理把self至nil)
③self.block = ^(ViewController *obj){ };(原理以参数形式传入self)
block里Cope和Strong的区别:
因为在MRC下,block在创建的时候,它的内存是分配在栈(stack)上的,而不是在堆(heap)上,可能被随时回收。他本身的作用域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。在ARC下写不写都行,编译器会自动对block进行copy操作
在ARC下, 没区别, Strong在ARC也会自动将block拷贝到堆上, MRC需要使用Copy
(5)NSTime
两种初始化方法:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
注:不用scheduled方式初始化的,需要手动addTimer:forMode: 将timer添加到一个runloop中。而scheduled的初始化方法将以默认mode直接添加到当前的runloop中.
可能存在的问题:
(1)带有RunLoop的定时器发生内存泄漏
原因:RunLoop->timer->self 形成循环引用,无法在页面离开时释放
解决方案:①在生命周期代码里加上timer的停止代码(最简单方式) ②思路:不让RunLoop持有timer 实现:一种是使用runtime方法解决 另一种是使用NSProxy
错误方案:用weakSelf解决, 其实定时器内部是有strongSelf强引用的, 所以用weak无法解决定时器循环引用问题
(2)页面滑动操作定时器停止
原因:默认的NSDefaultRunLoopMode在滑动视图时会暂停定时器
解决方案:使用NSRunLoopCommonModes模式的Runloop即可解决
(3)NSTimer加到了RunLoop中但迟迟的不触发事件
原因:①每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动 ②timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会
解决方案:要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配
(3)NSTimer不准时触发事件(定时器不准!!!)
原因:程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloopmode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化
解决方案:①纳秒级精度的Timer,用mach_absolute_time()来实现更高精度的定时器 ②CADisplayLink是一个频率能达到屏幕刷新率的定时器类。iPhone屏幕刷新频率为60帧/秒,也就是说最小间隔可以达到1/60s ③GCD定时器代替.因为GCD定时器不受RunLoop影响.
(4)检测内存泄漏方案
①静态检测方法 ②动态检测方法instrument ③delloc ④腾讯三方库MLeaksFinder
(6)性能优化
优化cpu占有率, 提高用户体验(缩短加载时间, 确保帧数不会出现卡顿)
①重用池和懒加载 ②少用离屏渲染(如设置圆角不用Lab.layer.masksToBounds = true, 使用Core Graphics绘制, 还有设置阴影和光栅化也会触发离屏渲染) ③CADisplayLink来测量帧率(大于60fpz即没有卡顿感) ④Instuments, 静态分析等方法检测内存泄漏 ⑤减少UIWebView的使用 ⑥不阻塞主线程, 使用GCD等多线程技术 ⑦tableView预加载, 滑动流畅 ⑧MLeaksFinder检测内存泄漏
(7)GCD
同步 阻塞当前线程 不会开辟新线程 dispatch_sync(queue, ^{ //回调 });
异步 不会阻塞当前线程 开辟新线程 dispatch_async(queue, ^{ //回调 });
串行 一个个执行 dispatch_queue_t queue = dispatch_queue_create("标识符", DISPATCH_QUEUE_SERIAL);
并发 多个同时执行 dispatch_queue_t queue = dispatch_queue_create("标识符", DISPATCH_QUEUE_CONCURRENT);
主队列 异步队列不会开辟线程 需要等到主线程中的任务执行完 才会执行主队列中的任务
GCD 栅栏方法:dispatch_barrier_async (例任务123,栅栏,456 能控制任务执行顺序)
GCD 延时执行方法:dispatch_after (延迟调用,使用便捷)
GCD 一次性代码(只执行一次):dispatch_once (写单例等使用)
GCD 快速迭代方法:dispatch_apply (for循环添加异步并发)
GCD 的队列组:dispatch_group (监听 group 中任务的完成状态,可以当上面所有的任务都执行完成后,才执行任务dispatch_group_notify)
GCD 信号量:dispatch_semaphore (保证线程安全,为线程加锁)
(8)链式编程,函数式编程,响应式编程
链式:msonry使用链式编程,先执行A方法,再执行B方法… 核心是点语法调用方法, 点语法传参通过返回值block实现, 所以也使用了函数式编程
函数式:需要什么block返回什么
响应式:a=b+c 赋值之后 b 或者 c 的值变化后,a 的值不会跟着变化. 响应式编程,目标就是,如果 b 或者 c 的数值发生变化,a 的数值会同时发生变化; 标准应用:RAC框架(对KVO等效果的封装)
(9)KVO,KVC,代理,通知区别
KVC,即Key-Value-Coding,是一个非正式协议,使用字符串(key)来访问一个对象实例变量的机制
KVO,即Key-Value-Observing,它提供一种机制,当被观察者的属性值更改时,观察者就会接收到通知
通知监听不局限于属性的变化,还可以是状态的变化,监听范围广,例如键盘的出现、app进入后台等,使用也更灵活方便
KVO和通知都负责发送和接收通知,剩下的事情都由系统来完成,所以不用返回值,而delegate则需要协议和代理对象来关联
delegate适用于一对一,KVO和通知则适用于一对多情况, 代理效率更高
KVC和KVO实现的根本是OC语言的动态性和运行时runtime,以及访问器方法的实现
(10)weak 关键字, 相比 assign 有什么不同?
weak 在属性所指的对象遭到摧毁时,系统会将 weak 修饰的属性对象的指针指向 nil , 虽然assign不会增加引用计数但也不会自动至nil
assign内部还是添加了一层强引用
assign可以用于修饰非 OC 对象,而 weak 必须用于 OC 对象
(11)属性@property的实质
@property = ivar + getter + setter;
@synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
@dynamic告诉编译器,属性的setter与getter方法由用户自己实现。
(12)深浅拷贝的区别(Copy与MutableCopy) ✨这里面绕弯弯的地方很多,想了解最好自己写点逻辑测试,特别是浅拷贝的理解
简单来说:浅拷贝复制容器,深拷贝复制容器及其内部元素
有两种类型的对象拷贝,浅拷贝和深拷贝。正常的拷贝,生成一个新的容器,但却是和原来的容器共用内部的元素(即内存地址相同),这叫做浅拷贝。深拷贝不仅生成新的容器,还生成了新的内部元素(即内部元素虽然和原对象内部元素数值相同,但生成新的内存地址,新内部元素指向新地址,和原地址元素无任何关系),这叫深拷贝
误解:浅拷贝就是用copy,深拷贝就是用mutableCopy。如果有这样的误解,一定要更正过来。copy只是不可变拷贝,而mutableCopy是可变拷贝。比如,NSArray *arr = [modelsArray copy],那么arr是不可变的。而NSMutableArray *ma = [modelsArray mutableCopy],那么ma是可变的
这里需要注意浅拷贝虽然是指针拷贝,但只要copy就会生出新容器,不会随原内容改变而改变
注意:①对可变对象(mutable,model)无论使用copy还是mutableCopy(包括等号),都会深拷贝! 会生成新的内存地址 ②使用mutableCopy无论是可变还是不可变都是深拷贝! 会生成新的内存地址 ③对不可变对象使用copy,是浅拷贝,内部元素指向同一地址,容器类(数组,model)如果修改原数据的值,copy出来的值也会改变! ④copy和mutableCopy都是单层深拷贝,如数组套模型,只会copy数组元素,里面的模型因为没有生成新的容器,指向相同内存地址,所以改变modelA会改变modelB , 但是单层拷贝层如果发生改变不会改变另一个
让一个对象有copy功能:要想自定义对象可以复制,那么该类就必须遵守NSCopying 或 NSMutableCopying协议, 实现协议中copyWithZone或者mutableCopyWithZone方法
(13)nonatomic 和 atomic
对于线程的安全,有nonatomic,这样效率就更高了,但是不是线程安全的。如果要线程安全,可以使用atomic,这样在访问是就会有线程锁。但atomic只能保证set方法的线程安全(加了锁,效率会变低),并不是绝对的线程安全,所以在实际开发中很少使用,如果没有添加系统默认是使用atomic和strong类型
(14)SDWebImage底层原理
SDWebImage是一个图片加载和缓存的框架,通过三级缓存机制很好解决了图片缓存问题
SDWebImage加载图片的流程:
1、sd_setImageWithURL:placeholderImage:options: 会先把placeholderImage显示,然后SDWebImageManager根据URL开始处理图片
2、先从内存图片缓存查找是否有图片。若有显示图片
3、如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存。若有显示图片
4、如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,生成一个下载器SDWebImageDownloader开始下载图片(异步下载)
5、下载完成后显示图片,且内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程。
SDWebImage使用过程中可能存在的问题:
(1)Url链接的图片改变,但未显示新图片(Url地址本身未变)
问题原因:图片的缓存是根据url来设置的,在加载过程中找到了该Url对应的缓存,所以显示以前的图片
解决方法:①根据http,每个Url有一个ETag参数,是通过哈希编码得到的,当资源发生变更时,那么ETag也随之发生变化,客户端可以判断NSURLCache来判断该地址下图片是否发生改变(sd对应的方法options:SDWebImageRefreshCached,注意是比正常请求多消耗性能的) ②不使用缓存
(2)图片显示错乱的问题
问题原因:由于cell的重用导致,用户下拉或者上拉,当网络不好的情况,该cell的图片还没有被加载,但是对应的cell已经被显示,就会显示cell被重用之前的数据,造成数据混乱
解决方法:设置每个cell中image为nil或者设置默认图片
SDImageCache是怎么做数据管理的?
SDImageCache分两个部分,一个是内存层面的,一个是硬盘层面的。
内存层面的相当于是个缓存器,以Key-Value的形式存储图片,当内存不够的时候回清除所有缓存图片。
用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于一周的图片文件
当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有直接返回,没有的话就去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。
SDWebImage详解博客:
iOS-SDWebimage底层实现原理 - 木子沉雨 - 博客园
(15)组件化开发
原则
只能上层对下层依赖
项目公共代码资源 下沉
横向的依赖 最好下沉
组件建立接口(面向接口编程)
组件化分层
业务模块层、通用模块层(常用控件、三方SDK)、基础模块层(底层组件)
组件化解耦通讯
每个模块有bundle.plist文件,作为路由文件在application:didFinishLaunchingWithOptions: 回调函数中进行读取。同层数据传递通过通知或者runtime的反射函数IMP方法实现。
(16)启动优化
整理启动项, 总结出耗时长的模块
分为2部分 main函数之前耗时和mian之后耗时
Main前面主要做一些动态库dyld的加载和load方法的调用, 对于pre-main阶段,Apple提供了一种测量方法来计算耗时时长
减少依赖不必要的库,无论是动态还是静态库,如果可以的话把动态库改为静态库
用linkmap检测出所有的方法和类,在用AppCode找出无用代码和类,删除掉,减少load方法调用
少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize (可以减少main之前的运行时间)
对于main()阶段,主要是测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的耗时,就需要自己插入代码到工程中了。先在main()函数里用变量StartTime记录当前时间, 最后在didFinishLaunchingWithOptions里,再获取一下当前时间,与StartTime的差值即是main()阶段运行耗时。
懒加载(避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示)
图片压缩,图片小了,io操作量也会变小,启动就快了
非必要加载业务延迟去做(比如放到首页控制器的viewDidAppear方法)
主页不用storyboard或者xib加载, 全部改为纯代码
采用性能更好的API
总结起来,好像启动速度优化就一句话:让系统在启动期间少做一些事。当然我们得先清楚工程里做的哪些事是在启动期间做的、对启动速度的影响有多大,然后case by case地分析工程代码,通过放到子线程、延迟加载、懒加载等方式让系统在启动期间更轻松些。
其他文章
runloop
https://zhuanlan.zhihu.com/p/469956195
runtime
https://blog.csdn.net/weixin_61196797/article/details/130935222
https://zhuanlan.zhihu.com/p/397057341
https://blog.csdn.net/Nathan1987_/article/details/86536386
内存管理
https://zhuanlan.zhihu.com/p/399778887
方法交换实际应用
https://www.jianshu.com/p/6bcff1f9feee
iOS 启动优化+监控实践
https://www.jianshu.com/p/17f00a237284
蓝牙开发
https://blog.51cto.com/u_12902/7353078
组件化
https://juejin.cn/post/7012042697867264036
其他