iOS开发常用技术底层实现(精简概述)

·  阅读 323
原文链接: www.jianshu.com

(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找不到就进入消息转发


消息转发机制, 消息重定向在第三步

OC对象的本质<二> 实例对象,类对象,元类对象

runtime - iOS类对象、实例对象、元类对象

(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原理 - 简书

iOS-SDWebimage底层实现原理 - 木子沉雨 - 博客园