(1)RunTime总结:
oc动态性, 运行时将代码转化为runtime的C代码
RunTime运行流程:
生成对应objc_msgSend方法 isa指针查看当前类有没有这个方法, 之后寻找父类, 每个方法SEL(方法选择器)对应IMP(类似于一个编号,是函数指针,指向函数实现,找到内存里对应函数), 直到NSObeject, 如果找不到IMP, 会进入消息转发机制, resolveClassMethod, resolveInstanceMethod, forwardingTargetForSelector, forwardInvocation 第一个方法所属类方法动态方法解析, 第二个和第一个类似,是对应实例方法的, 第三个是备援接受者, 第四个方法是消息重定向, 真正消息转发,也是Aspects的核心操作, 如果都找不到调用doesNotRecognizeSelector:方法抛出异常
RunTime的实际应用
交换方法(黑魔法.hook,让SEL1->IMP2,SEL2->IMP1),为系统类添加自定义方法,三方Aspects
①用方法交换添加保护, 如数组赋值时添加越界判断等等 ②统计页面点击数用 ③多继承 ④自动化归档(kvo) ⑤NSTime内存泄漏(vc被释放通过消息转发找回vc) ⑥系统类添加自定义方法, 写一些更便捷的代码,比如控件加手势,字典加加密方法,代码更简洁
isa指针当前类的方法,找不到寻找元类是否有该方法,到NSObject找不到就进入消息转发
消息转发机制, 消息重定向在第三步
(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出来)
让一个对象有copy功能:要想自定义对象可以复制,那么该类就必须遵守NSCopying 或 NSMutableCopying协议, 实现协议中copyWithZone或者mutableCopyWithZone方法
copyWithZone方法
(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底层实现原理 - 木子沉雨 - 博客园
作者:铁头娃_e245
链接:https://www.jianshu.com/p/9f9b1bf1b79c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。