MVVM应该如何做?
苹果推崇的MVC的模式,在开发的过程中,优点是好上手,因为本身ViewController自带View,任何开发人员在接管代码的时候,都可以很方便的看到业务逻辑和事件响应,但是随着业务层的越来越繁重,逻辑层在Controller的迭代越来越多,使得ViewController越来越臃肿,很难管理,为了解决这个问题,就产生了MVVM,MVVM是在MVC的基础上做的优化,架构的目的在于高内聚,低耦合,我们希望做到的是,Model就做Model层的事情,VC就做VC的事情,不要有太多的牵扯,早起的苹果推荐MVC有一定的自己的原因,早起苹果的性能各方面不像现在这么强悍,一味的追求架构,不一定会减少代码量,甚至会增加很多类,对于系统来说是需要耗费更多的时间。
MVVM就是在VC和Model中间加了一个类似适配器的东西,叫做ViewModel,这个viewModel就是专门来分割业务逻辑以及View和Controller,把逻辑代码和网络请求放到VM中,但千万不可以把view的逻辑放到里面去,也就是说不能引入任何的VC,但是viewmodel是可以互相嵌套的,但是这样的处理是不够的,因为要完成数据的双向绑定,必然要加入RAC响应式,观察数据的变化响应UI的变化。
MVVM的缺点:随着业务逻辑的复杂,因为ViewModel是可以嵌套的,所以,如果是这种情况下,定位问题会变成更加困难,如果某个地方出现了问题,必须从里到外,一层一层去根据response寻找,每一层可能都会有对数据的处理,维护成本比较高。
MVP优点:完美的完成一对一的模型绑定,尤其是对于嵌套层次过深的情况,根据需要去创建对应的协议,然后完成代理三部曲即可。
MVP缺点:胶水代码过多。
ViewController的生命周期
1.alloc 创建对象,分配空间
2.initWithNibName:bundle 初始化对象,初始化数据(非storyBroad)
3.initWithCoder. (storyBroad创建VC)
4.loadView从nib 载入视图,如果没有nib,会创建一个空的。此方法系统调用
5.viewDidLoad 把view装到内存中。
6.viewWillAppear
7.viewDidLayoutSubviews
8.viewDisAppear
9.viewWillDisappear
10.viewDidDisappear
11.dealloc
12.didReceiveMemoryWarning
swift对于OC的优势
swift更加安全,引入了元组,支持多个不同的类型放到一起,引入了可选类型,并且对类型的判定,对于模棱两可的数据要给一个默认类型保证安全。更加容易阅读,代码风格偏向于前端,代码结构简单,抛弃了过多的h文件。增加了playground方式所写即所见。
多线程的生命周期
对于单核的cpu,其实本身不存在多线程,它只是多个线程在不同的时间片快速的切换,但是对于多核的cpu就不一样了,首先线程创建,进入runable状态,等待被调起,一旦被调起,要么是runing,要么是阻塞,阻塞结束以后会重新进入到runable状态,最后死亡。
KVO的底层原理
当一个对象第一次被观察的时候,会对被观察的对象产生一个派生类NSKVONotifying_ , 这个派生类运用了isa_swizzing把两个对象的指针调换,指针指向了这个派生子类,并且对每个属性生成了相应的set方法,在被调用之前调用willchangeVauleForkey记录旧值,改变时候调用didchangeVauleForKey改变新值,最后通过响应方法回调改变。在移除观察者的时候把isa指针指回原来的类。
app性能优化
这个问题比较宽泛,写一篇乃至几篇博客都不定能说得清楚。总的来说,从卡顿优化,内存优化,网络优化,代码结构优化。卡顿优化比如说关于tablview的性能,可以通过延迟异步的方式去处理一些网络请求或者是比较耗时的操作、也可以通过缓存计算数据来防止在拖动的时候反复调用cell方法计算造成的压力,另外可以减少图层设计,ios不像Flutter,他是层层叠加的,减少离屏渲染,因为在离屏渲染的时候会增加上下文切换的工作导致卡顿,主要表现为阴影和圆角的切图,可以直接让UI切圆角图。
内存优化大多数是针对于内存泄漏,可以通过苹果自带的leaks工具去检测;网络优化是针对于网络封装,如何更加轻量级以及针对性的去进行封装,但是这块可优化性不是很大,感知比较少,因为本身AFN已经做得足够好了,最多你在往上加一层我们自己的封装即可。代码结构这一块的话,主要是看有没有冗余代码,多余的类,因为类也参与到app的启动加载中,再说的深一点的话,还有启动优化,启动优化更多的是二进制重排,但这一块一般也不需要去做除非是很大的项目,启动加载牵扯的东西有很多,可以针对性的对方法进行排序,减少缺页异常带来的启动耗时。
上架问题
除去一些苹果严格要求的政策法规信息以外,就按照正常的app上线流程来看,基本上我碰到的上架问题,第一种,app的开发性问题,很多面向B端的产品,账号并不是对外开放的,所以这个时候就需要对app的功能性做一些说明,现在不知道要求是什么,反正如果被拒了会有问题反馈,按照问题回复即可。第二种,权限问题,这是后来app上线新增的,有一些必须要加的隐私权限必须要加。第三种,套壳问题,就是做一个app的时候,你可能要做另一个功能基本上一样,但名字不一样的app,这时候上架会因为查重被拒,这时候除了修改类名之外,更多的是要修改资源名,这些是比较影响的因素,代码倒是不会有问题。第四种,网络环境问题,上架需要支持ipv6,有遇到过在本地网络运行没问题,但是审核被拒就是给了一张白板的图,告诉我可能是不支持ipv6,这个时候就需要服务端去做一些处理,或者实在不行,如果是客户的服务端不好做修改,可以找一个支持ipv6的服务器地址,给定一个账号,把账号和服务器地址关联用这种方式通过审核。
修饰词
不管是什么时候,这个问题总是老生常谈。。。
readonly、只读,表示只生成get方法,不生成set方法,给他赋值会报错,但是如果通过kvc的形式,是可以给他赋值的,只要有个开关是打开状态,开启的方式是accessInstanceVariablesDirectly设置成yes,他会去找到他的成员变量赋值修改。
readwirte 可读可写。
atomic,原子锁。atomic是默认修饰的,他是属于那种可以保证你通过get和set的方法获取到完整的value值,内部只是一个简单的lock和unlock,但是并不能保证多个线程去访问的时候的读写锁,假如有多个线程访问同一个属性,可能这边在写入的时候,你读取的是另外一条线程写入的值,根本不能做到同步锁。
nonatomic,非原子锁,不安全,但是我们可以用多线程的方式人为的去干预线程的安全,且比起atomic,速度更快,消耗更少,所以我们经常会使用nonatomic去修饰一个属性。
assign 修饰基本数据类型,不会对引用计数加一,如果用它来修饰一个属性对象,被释放不会自动置为nil,会造成野指针。
weak 不能用来修饰基本数据类型,只能修饰对象,在修饰对象的时候,不会增加引用计数,但是会自动置nil,给他发消息也不会崩溃。oc支持。
strong 引用计数加1,一种强引用,避免对象被提前释放,延长了对象的生命周期
copy 也会使得引用计数+1,但是跟strong不同的是,只有在调用set方法的时候会有区别,在本身给成员变量赋值不会改变地址,在调用set方法时候,copy修饰的属性会调用一次copy方法,如果被修饰的对象是可变的,那么会进行一次深拷贝,如果是不可变,那么还是浅拷贝,具体来说,例如如果是NSString,那么无论是copy还是strong没有区别,如果是NSMutableString,那么copy修饰的对象地址跟原来的地址不同,如果想要NSString的对象在copy以后生成新的地址,那么需要用NSMutableCopy修饰。 注意如果用copy修饰可变数组,在增删改的时候会崩溃,因为本身copy修饰了以后数组就是不可变的。这也是runtime的一个体现,因为在编译的时候不会报错,运行的时候才会报错。
sdWebImage存储方式和使用
sdWebImage是一个UIimageView的分类,采用的是二级缓存机制,也就是内存和硬盘双缓存。在调用sd_setImageWithURl的时候,首先会在缓存中查找是否已经下载,如果找到了就会回调,如果没找到,会接着往硬盘中查找,如果找到了,会先往缓存中添加然后回调给视图,如果还是没找到,说明本来就不存在,那么它就是开启异步线程下载,下载完以后会同时保存在硬盘和缓存中。在硬盘中是通过键值的方式,他会把md5加密过后的url作为key保存在硬盘。
内存优化
在内存优化这一块,苹果底层已经做了很多优化,在wwdc2017的时候,ben老头就讲了在内存划分的时候,就存在了clear memory和dirty memory之分,clear memory就是只读的部分,dirty memory就是容易被修改的部分,但是后来发现,dirty memory在很多时候有大概3/4的区域是不常被修改的,所以就引入了rw_ext,这是一块扩展区,专门用来存像是分类,协议这种会动态改变的部分。所以我们在表层做的话,我觉得我可以尽可能少的去引入一些比较大的图片,减少透明图层的使用,不要产生循环引用,多一些懒加载的操作。
tableview重用机制
关键方法就是cellforRow的里面调用了dequeueReusableCellWithIdentifier,通过identityer去管理cell,每个cell都被identityer标识并且放在重用池当中,所有的cell划出屏幕,他会去找重用池里的cell,而不是每次都创建。
项目技术点
oc与swift的混编,加入桥接文件,把需要混编的oc或者swift文件放进去,如果是oc引入swift,需要手动导入头文件。
流媒体的接入,包括第三方和自研的视频sdk。
高德地图组件。
表单组件。
推送(友盟、信鸽、极光)
以前也做过支付
二维码车牌扫描(百度)
蓝牙打印 进来搜索蓝牙设备,注册监听蓝牙状态,匹配,保存设备号,打印。
原生和js交互
水印相机
断点上传和下载
断点下载这块在网上有很多案例,大致意思差不多,首先这个大文件是存在于服务器端,我们只需要通过url就可以去下载,同时url作为唯一的键值对会存在于本地,方便下次继续下载,url会通过加密的方式保存在沙盒文件中,同时存在沙盒文件的还有缓存文件,缓存model,在下载完成之前,这个缓存文件的大小永远显示的是0,缓存model是方便用于界面展示的一些数据,而断点下载的重点就在于点击暂停的时候,下次在开始下载会延续之前的下载点下载,那么苹果官方提供了一个方法 # cancelByProducingResumeData: 在暂停的时候调用会获取到rusumeData,在下次执行下载的时候,会优先判断这个rusumeData有没有数据,有数据会接着下载,但是对于这个方法能不能支持,需要参考苹果官方文档让后端去做配置,然后把下载完成的数据保存在沙盒文件中。
上传就稍微复杂一点了,实现方式也不一样,我这边采取的是表单上传的方式,但是网上的表单上传也有很多,不一定都可以用,我找了一个swift方式上传可以满足,但是用oc写完就有问题,由于时间比较紧,为了满足上线需要,于是做了桥接,表单上传有两点需要注意,第一,boundary用于分隔,是一定要加的,第二,在最外层的请求头加入总大小和每一片上传的范围以及对应的数据data,如果大小和数据对应不一致,那么会报错,后端是根据你的大小去把缓存数据存在服务器,会校验大小,每个body体里面还要设置头,具体原因不太明确,是格式要求。上传比下载简单的地方在于,不用缓存数据,model里面只需要记录上传文件的信息即可,也不用记录当前上传到哪儿了,这个上传的开始点,由后端通过接口请求,用于比如说中途断开下次继续上传的情况,后端会在每次上传一片完成以后,告诉你下次开始的点,只要是上传成功没有报错,那么就可以继续上传,直到最后一片,注意的是,每一片的大小和结尾的位置,比如说你每次传1000KB,那么范围应该是0-999KB。
runtime和runloop
runtime是一套oc使用的基于c的api,为的就是实现oc的语言的动态机制,所以为什么说oc相对于swift乃至c语言来说是一门动态语言。在整个项目进入到main之前,也就是dyld链接到镜像文件之后,在进入到objc_init初始化之后,就会对runtime进行初始化,第一次就用来回调镜像文件初始化成功,镜像文件初始化成功以后就可以加载初始化各种类的load方法。很多方法在声明完成以后,不实现也不会报错,因为他的实现是被推迟到了运行时,通过消息发送机制去调用,这样就可以大大减轻编译时的负担。另外,比如说使用respondsToSelector就是检测有没有响应某个方法,也是运行时机制的一个体现。还有就是关于在消息发送的过程中,可以通过动态方法决议去手动添加方法,在应用层面用的最多的就是关于分类的实现就是最好的体现了,分类本身是不能添加属性的,如果添加了,就必修手动为他实现相应的set和get方法。
runLoop底层就是一个do-while循环,但是这个do-while循环不会过多影响到cpu或者内存,他是在调起的时候进入while,在不需要的时候休眠跳出while,里面通过一种消息的回调机制返回。runtime和线程是一一对应的,runtime的创建也是依赖于线程,主线程是自动创建,随着程序运行一直存在。通常runloop的创建是把你所需要创建的事务(比如说nstimer、source、observer)和runloop的类型一起放进去初始化,然后调用。这些参数会在初始化的时候赋值给runloop的model,执行完毕以后调用callout函数给dowhile的result然后跳出循环,通常是对一些需要持续监听的事件会做相关的runloop操作,但是需要手动去停止并且回收runloop。
信号量和调度组
两种都是可以通过多线程的方式控制任务的执行顺序。
信号量在创建的时候给定一个值,只有这个值才能通过,他有两个关键的方法,一个是wait方法,他在底层会对信号量作-1操作,用于等待调度,而signal方法,会做+1操作,有一种情况是如果+1还是不能调起,还是<0就会报异常,说明wait调度次数太多了。
调度组就是线程组,底层实际调取的方法,一个是enter,一个是leave,我们可以把所有需要执行的任务放到这个group中,等所有任务完成以后,会回调它的notify函数,通过enter和leave有序的控制任务的执行。
关于block拷贝以及加入__strong的防止循环引用的意义
首先从栈区拷贝到堆区,然后内部会生成block自己的结构体成员变量,被block修饰的变量会被赋值进来,并且最后被结构体内部的成员变量捕获并拷贝一份作为自己的变量。
本身其实加__weak就可以防止循环引用,因为是弱引用不会使引用计数+1,加__strong是为了防止比如说存在用户操作过快或者线程跟不上,self被提前释放,那么里面的数据也得不到保存,__strong可以延长内部变量的生命周期,强引用使得引用计数+1,为什么此时不会造成循环引用?因为它是一个局部变量,在出了代码块以后,就会-1然后被释放
APNS补充
原理:通过注册推送通知,开启了推送以后,本机将会跟苹果服务器保持SSL长链接,并且返回devicetoken给设备,公司服务器在需要发送推送的时候,将消息和devicetoken一起发送给苹果服务器,苹果服务器在接受到消息以后统一推送给这些设备,设备打开对应的应用并且以声音或者图像的方式呈现出来。