前言
参考资料
饿了么移动APP的架构演进
猿题库 iOS 客户端架构设计
客户端动态化系列之——URLRoute
iOS移动端架构的那些事
iOS关于架构的那些事
总结:一个iOS项目的代码组织架构
浅谈Hybrid技术的设计与实现之一
浅谈Hybrid技术的设计与实现之二
如何打造一个高性能Hybrid App
iOS应用架构谈(一):架构设计的方法论
接口编程那些事
谈谈依赖注入与面向接口编程思想
MVVM With ReactiveCocoa
**... 未完待续 **
其他信息
项目整体架构
下面项目设计中暂时没有设计到RN相关内容,参考了饿了么架构博客,暂时先这么画
模块化技术分析:
-
1.NetWork网络数据交互
- 1.1使用第三方网络请求处理库YTKNetwork
- 1.2网络层数据传输优化,Delegate与block选择Delegate
-
2.Data Storage本地数据存储
2.1可以选择查看iOS数据存储方式这篇 文章,对Core data/NSUserDefault/KeyChains封装描述
-
2.2 实体类数据
- 选择SQLite/FMDB/CoreData/NSCocing
- 自定义缓存类库,eg: YYCache、TMCache、LTMemory(基于ARC内存缓存框架)
-
2.3 网络响应数据
- 选择NSUserDefault、KeyChains、NSKeyedAchiver、NSData、NSFileManager...
- 3.MVVM业务架构模式
- 3.1ViewModel单独封装业务代码
- 提供统一的BaseViewModel提供一些基础性的功能代码
- 提供统一的业务抽象
- 通过开源依赖注入框架Objection来管理协议具体的实现类(接口编程)
- 使用ReactiveCocoa提供KVO功能开源框架,让ViewModel与RequestAPI(Data Controller/ Request Service) / ViewModelService / ViewModelServiceImpl / ViewUserInterFace /之间产生联动处理
- 4.Base公共类
- 4.1 BaseViewController
ViewControllerA -> ViewControllerB 是一个既定的关系。为了达到动态化,必然需要对整个app的导航进行统一处理。
- 4.1 BaseViewController
* 使用Aspects来hook系统的函数,实现一些动态加入的一些代码(日志、统计..)
* 结构化、步骤化的模板方法
* 统一push管理
* 统一的拦截处理
-
4.2 BaseCell、BaseTableViewCell
- 提供一个BaseTableViewController基类,提供一些公关的代码给其继承的子类
- 对于UITableViewCell类型处理
- 使用__kindof多个cell类型共存而不必要写显式强转,相当于把返回值定义成了 id 类型
- BaseCell根据对应实体类对象进行初始化subviews显示数据处理(使用initWithStyle方法)
- 对于UITableViewDelegate、UITableViewDataSource代理方法
- Cell高度的动态计算
- 对于其他工具方法
- 没有List数据时默认的背景View
- cell出现动画
-
5.Utility工具类
- AsyncLayer 异步绘制图层内容工具
- DispatchQueuePool 全局并发队列管理工具
- FileHash 哈希算法加密工具
- GestureRecognizer UIGestureRecognizer的子类用于接收触摸事件。
- Keychain Keychain保存的信息工具
- ThreadSafeArray 线程安全的NSMutableArray可变数组工具(为什么要这么做?因为基于UIKit框架的类大多都是线程不安全的,这里封装成线程安全)
- ThreadSafeDictionary 线程安全的NSMutableDictionary可变字典工具
- Timer 基于GCD线程安全的定时器(NSTimer)。
- Transaction 事务工具,让当前runloop 休眠前执行一次选择
- WeakProxy 代理用来装弱对象,它可以用来避免循环引用,例如用在NSTimer或CADisplayLink对象上
-
6.Hot fix热修复(iOS)
- 1.UIWebView + HTML5
- 2.cordova-ios+HTML5
- 3.Facebook开源ReactNative
- 4.动态下发可执行文件
- 4.1 下发lua脚本文件: WaxPatch
- 4.2 下发js脚本文件 : JSPatch
- 4.3 下发framework、.a、等其他可执行文件是苹果禁止的
- 7.PushNotification推送通知
- 极光:使用简单,用的人多,基于Binary Interface V2协议
- 融云:提供开源服务器端,灵活性较好
NetWork网络数据交互
步步为营----选择网络请求与响应框架
- 选择AFNetworking网络请求
- 选择YTKNetwork(基于 AFNetworking 封装的 iOS 网络库)
- 支持按时间缓存网络请求内容
- 支持按版本号缓存网络请求内容
- 支持统一设置服务器和 CDN 的地址
- 支持检查返回 JSON 内容的合法性
- 支持文件的断点续传
- 支持 block和 delegate两种模式的回调方式
- 支持批量的网络请求发送,并统一设置它们的回调
- ......
- 使用教程:基础使用教程 高级使用教程
- YTKNetwork使用过程
- 1. YTKNetworkConfig :(1)统一设置网络请求的服务器和 CDN 的地址(2)管理网络请求的 YTKUrlFilterProtocol 实例
- 2. YTKRequest:(1)把每一个网络请求封装成对象 =RequestApi (2)YTKNetworkAgent代理类对网络发起请求
(3)YTKNetworkAgent类直接耦合 AFNetworking
步步为营----网络层数据传输优化
** 1.网络请求为什么使用delegate而非block?**
答:(1)block很难追踪,难以维护 (2)block会延长相关对象的生命周期,会造成引用循环 (3)delegate效率比block高
核心功能类最好使用delegate回调,至于上层业务、UI事件等等可以使用block.
** 2.还有哪些可以进行网络传输优化? **
答:(1)直接使用ip地址,减少dns域名解析
(2)使用缓存手段减少请求的发起次数(规定几秒内连续网络请求,只使用本地缓存response data数据)
(3)每次App启动从服务器拿一份IP列表并缓存,这些IP是所有提供API的服务器的IP(NSURLProtocol)
(4)对传输的数据进行压缩,减小传输数据的体积
Data Storage本地数据存储
DataController文件夹
在iOS开发中数据存储的方式可以归纳为磁盘缓存和内存缓存:磁盘缓存分为两类:文件、数据库存储。
文件存储:NSKeyedArchiver归档、NSUserDefault偏好设置,归档或者plist文件存储可以选择保存到沙盒中,偏好设置系统规定只能保存到沙盒的Library/Preferences目录。
数据库存储:使用FMDB、SQLite,通过SQL直接访问数据库,也可以通过ORM进行对象关系映射访问数据库。这两种方式对应iOS中FMDB、SQLite和Core Data的内容,在此将重点进行分析:
内存缓存:苹果提供NSCache、NSMutableURL通过设置相应策略实现内存缓存
NSKeyedArchiver:存储实体类模型对象和Foundation对象数据
Keychain:通常要管理多个账户,需要登录用户id和密码
NSUserDefaults:NSUserDefaults存储的数据类型有NSNumber、NSString、NSDate、NSArray、NSDictionary、BOOL、
NSInteger、float、double
Core Data:Core Data是一个框架,用于在应用程序中管理对象模型层。
- 数据缓存方式
- 使用CoreData
- 使用YTKNetwork,直接缓存responseData
** CoreData的使用,如何处理多线程问题?**
答:
第一步:搭建 Core Data 多线程环境(NSManagedObjectContext
不是线程安全的)Core Data 对多线程的支持比较好,NSManagedObjectContext在初始化时可以指定并发模式,有三种选项(其中一种在iOS9.0后弃用):1.NSPrivateQueueConcurrencyType在一个私有队列中创建并管理 context 2.NSMainQueueConcurrencyType其实这种模式与第 2 种模式比较相似,只不过 context 与主队列绑定,也因此与应用的 event loop 很亲密。第二步:当 搭建多线程 Core Data 环境的方案一般如下,创建一个NSMainQueueConcurrencyType
的 context 用于响应 UI 事件,其他涉及大量数据操作可能会阻塞 UI 的就使用NSPrivateQueueConcurrencyType
的 context。环境搭建好了,如何实现多线程操作?官方文档《Using a Private Queue to Support Concurrency》为我们做了示范,在 private queue 的 context 中进行操作时,应该使紧接着通过该被管理对象上下文对象调用performBlock或performBlockAndWait方法进行操作,这样就可以确保在多线程操作安全
** 如何进行Core Data版本迁移?**
Core Data Model Versioning and Data Migration
讲的比较好的Core Data版本迁移博客,但是突然打不开了
**当我们仅仅是对数据模型增加实体或者可选属性时可以使用迁移准备(轻量级迁移) **
-
- 选中工程中的 xcdaramodeId 文件,Menu->Editor->Add Model Version
这一步添加完成之后,工程中的xcdaramodeId 文件将会被展开,并且出现了新增加的Model文件
- 选中工程中的 xcdaramodeId 文件,Menu->Editor->Add Model Version
-
- 在Xcode右侧的辅助工具栏中找到 Model Version, 选择刚刚添加的Model文件,这个时候你会发现Xcode目录中,Model文件上的绿色的勾选中了当前选择的Model文件
-
- 在新的Model文件中修改最新的Entities等信息,记得也同时修改NSManagedObject Subclass对应的实现
-
- 修改 NSPersistentStoreCoordinator部分实现:
官方文档中介绍如下的改变支持轻量级迁移:
- 为Entity简单的添加一个属性
- 为Entity移除一个属性
- 属性值由 Optional <-> Non-optional 之间转换
- 为属性设置 Default Value
- 重命名Entity或者Attribute
- 增加一个新的relationship 或者删除一个已经存在的 relationship
- 重命名relationship
- 改变relationship to-one <-> to-many 等
- 增加,删除Entities
- 增加新的 Parent 或者 Child Entity
- 从Hierarchy中移除Entities
** 迁移过程**
-
- 判断本地SQLite数据库文件是否存在,不存在直接退出整个迁移。
-
- 检测当前本地数据库和数据模型是否一致,如果一致就退出迁移。
** 如何自定义 Core Data 迁移? **
- 1.从旧模型中提取数据并使用这些数据来填充具有新的实体和关系的目标模型
- 2.如果对数据模型的修改只有增加一个实体或可选属性,轻量级的迁移是一个很好的选择
- 3.对于轻量级迁移,持久化存储会为你自动推断一个映射模型
- 4.NSMigrationManager能够推断这两个模型间的映射模型
- 5.如果我们想要迁移数据(或者为了以后备份),我们可以将一个字典传递给 -addPersistentStoreWithType:configuration:URL:options:error:
来完成改写。 - 6.NSEntityMigrationPolicy的实例为一个实体映射自定义的迁移策略。
这个类让我们不仅仅能修改实体的属性和关系,而且还能任意添加一些自定义的操作来完成每个实体的迁移。
** 如何进行大数据集迁移? **
- 如果你的存储包含了大量数据,以至到达一个临界点,迁移就会消耗过多内存,Core Data 提供了一个以数据块(chunks)的方式迁移的办法
解决办法是使用多映射模型分开你的迁移并为每个映射模型迁移一次
MVVM业务架构模式
ViewModel封装网络请求、步骤化逻辑代码、输入校验等UI逻辑等步骤
-
0.编写所有的ViewModel的父类BaseViewModel
- 提供统一添加Block
- 统一处理请求成功
- 1.获取response code
- 2.判断response code 是否合法
- 合法
- 不合法调用错误处理方法
- 统一处理请求错误
- 登录超时
- 发送通知
- ...
- 登录超时
- 统一处理请求失败(判断是哪一种系统网络请求错误)
- 将当前不合法的response code 保存为 error code
- 判断是哪一种系统网络请求错误
- 可以根据错误类型发出通知
- 执行fail block
-
1.抽象出每一个实体类对应的业务方法的协议(CustomerLogicProtocol)
- 1.1.一些简单的基本的Api请求,例如用户注册、用户登录、修改手势密码、返回一些处理某中情况的手势业务逻辑代码
- 1.2.一些较复杂流程的逻辑代码,并代替执行Api去请求服务器,例如登录之后,判断是否要执行,忘记手势密码、创建手势密码、未绑卡、未设置交易密码哪一个流程、提供Block让ViewController执行,让用户输入数据,然后ViewModel获取数据进行下一步 * 的处理.
-
2.编写协议实现类ViewModel
- 一些流程处理逻辑
-
3.通过开源依赖注入框架Objection将协议与实现类绑定.
- 3.1获取当前默认的对象容器_injector
- 3.2如果没有,创建一个新的对象容器_injector
- 3.3创建每一个module(不同的module管理一组协议与实现类的映射)
- 3.3.1还可以创建其他module..
- 3.4将module注册到对象容器
- 3.5_injector设置为默认的对象容器
为什么要用objection将一组相关的协议与实现类的module绑定?
答:使用的具体类的所有细节只有module类知道,外界调用者一概不知,尽可能减少耦合
- 4.使用ReactiveCocoa提供KVO功能开源框架,让ViewModel与UI层、其他Service层、DAO层之间产生联动处理
Base公共类
全局UIViewController公共的父类BaseViewController.
使用Aspects来hook系统的函数,实现一些动态加入的一些代码(日志、统计..)
1.在-[BaseViewController init]中完成hook,拦截到viewWillAppear:
、viewDidAppear:、viewWillDisappear:…等等方法.2.如果实现AspectToken该协议来定义属性,需要在-[UIViewController dealloc]
移除hook.(_undoSwizz 方法)-
提供一些子类化的ViewController一些基本的代码
- 0.调用dealloc 、initWithNibName方法
- 1.在viewDidLoad调用让子类重写的createSubviews方法,让子类控制器实例化自己的subviews.
- 2.每一个控制器接收到内存警告时,统一清除内存.(didReceiveMemoryWarning )
- 5.BaseViewController统一关注用户登录状态改变的通知、以及默认处理,或者子类重写对应方法做特殊的处理.
-
BaseViewController统一关注用户登录状态改变的通知、以及默认处理,或者子类重写对应方法做特殊的处理.
- 在-[BaseViewController init]添加通知关注.
- 关注网络状态改变
- 关注登录状态改变
子类可以重写isNeedObservenNetworkStatusNotify和isneedObservenLoginStatusNotify方法,来决定是否关注通知.
...
- 在-[BaseViewController init]添加通知关注.
提供统一的push viewController的入口,统一管理控制器的push.
统一给所有ViewController添加loading动画View
编写全局UITableViewController的公共父类BaseTableViewController
- 1.提供设置某一个section、某一个row的cell的回调Block
- 2.提供Cell被点击的回调Block
- 3.统一创建下拉刷新
- 4.提供个性化TableView相关显示
- 5.设置cell出现时候的动画类型
- 6.显示当dataSource.count==0时,显示默认背景View
- 7.继承自BaseViewController,会自动执行createSubviews这个方法
- 7.1createSubviews方法设置统一管理以上1~6列举的功能
编写全局Cell的公共父类BaseCell
1.首先定义一个协议,协议方法setupDataItem,所有Cell都必须实现,用于将传入的实体对象,设置到Cell的subViews上.
-
2.BaseCell对 setupDataItem: 做一个空实现,让子类去自己设置自己的实体对象。再就是提供根据Cell设置的所有的约束,计算得到cell的高度的工具函数.
- 2.1sizeForCellWithDefaultSize方法提供计算设置了约束的Cell的高度(根据FDTemplateLayoutCell自动布局调用systemLayoutSizeFittingSize API)
-
3.编写我们自己的子类Cell
- 3.1实现协议方法setupDataItem,将传入的实体类数据显示到subViews
* 3.2首页列表图加载默认(默认加载图片名字) - 3.3BaseTableViewController中的代理函数中,自动取出要显示的实体类对象之后,然后调用Cell协议中的方法setupDataItem:,让cell完成subveiws赋值.
- 3.1实现协议方法setupDataItem,将传入的实体类数据显示到subViews
App的crash,跟踪log
- APP crash,跟踪Log文件保存,使用开源库CocoaLumberjack
- crash跟踪使用的是bugly
- dSYM符号表文件来还原CrashLog日志中的堆栈
Hot fix热修复
- 热更新替换原生代码的思路:
- App启动时,首选从我们的服务器同步下载脚本代码
- 下载到手机本地后,然后解析并执行脚本代码
- 而脚本代码可以去操作原生代码
- 实际就是使用iOS runtime特性替换掉原来产生bug、crash的代码实现
- 一切替换成功之后,再进入App主界面UI
这样一来就通过下载脚本的方式临时对发出版本完成了热修复,注意这只是iOS运行时的特性在App启动的时刻完成的临时修复,但实际产生bug的代码仍然还是在App中,只有通过下次发版本才能彻底修复.
- JSPatch热修复框架:
- 对iOS系统版本必须是iOS7及以上版本(依赖javascriptcore.framework)
- 使用类JS的的伪代码编写脚本
- 使用苹果自带的JavaScriptCore.framework作为脚本解析、运行引擎
- 假设发出版本的App中的某一段代导致App程序crash
- 通过JSPatch可以下发这样一段的JS脚本代码到手机本地
- 如何在App启动的时刻从服务器同步下载到手机本地的脚本代码?
- didFinishLaunchingWithOptions方法初始化JSPatch
* 同步下载Patch补丁脚本文件
* 安装下载到的js脚本,可以缓存到手机本地,下次App启动直接读取加载 - 安装脚本完成完之后,再进入App主界面,此时bug代码已经被替换
- didFinishLaunchingWithOptions方法初始化JSPatch
**JSPatch是如何使用js脚本文件去替换objc的实现(JSPatch内部对js补丁文件的使用过程)? **
-
1.JSPatch初始化时:调用startEngine方法
该方法向JSContext注册了一些js代码中可以调用的c函数,再运行时用来完成,具体过程:- 创建一个类
- 给类类实现一个协议
- 给类添加对象方法、类方法
- 以及对象方法、类方法的实现替换
- oc对象 转 js对象
- 操作oc中的线程队列
- 加载JSPatch自带的一个 JSPatch.js文件
- …
2.将下载到的js补丁文件扔给JSPatch处理(evaluateScriptWithPath/evaluateScript)
-
3.最终使用正则表达式将我们的类js的脚本文件中的伪代码解析成符合规范的js代码的函数
- 3.1将解析完毕的js代码,交给JSContext运行,并借助前面加载的JSPtahc.js中的代码
-
4.当JSPatch.js处理完毕之后,回调在JSContext中注册好的c函数
4.1比如找到/创建一个类/修改类中的方法实现(对象方法or类方法)(执行defineClass方法)
-
4.2调用overrideMethod这个c函数完成函数实现的替换
- 4.2.1被替换实现方法的SEL标识
- 4.2.2被替换掉的方法实现的编码
- 4.2.3获取被替换掉的对象方法实现
- 4.2.4系统消息转发函数的实现
- 4.2.5替换系统消息转发函数实现,为 JPForwardInvocation这个c函数
- 4.2.6构造
ORIG被替换函数名
这个SEL回到被替换的函数实现 - 4.2.7构造
JP被替换函数名
作为key,使用全局字典将要替换的方法实现JSValue对象保存取来 - 4.2.8最后让被替换函数实现的SEL指向系统消息转发函数实现中(直接走到 forwardInvocation:这个SEL对应的函数实现中)
4.3如上完成了两个函数实现的替换(SEL指向JPForwardInvocation这个c函数实现)
-
4.4JPForwardInvocation这个c函数,这个函数是拦截了系统的消息转发
- 4.4.1判断是否是JSPatch处理过的方法(如果不是,直接放行让其回到被拦截的方法实现中去,停止往下执行)
- 4.4.2获取被替换函数执行的所有的参数列表,并转换成JS中使用的数据类型
- 4.4.3从上面_JSOverideMethods缓存字典中查询到被替换的方法实现JSValue对象(js脚本描述的新的方法实现)
- 4.4.4然后通过
-[JSValue callWithArguments:转换后的JS参数数组]
完成对js脚本中描述的新的方法实现的调用 - 4.4.5然后在对
-[JSValue callWithArguments:转换后的JS参数数组]
执行后的返回值,再按照typpe encodings进转换
对App中的产生bug、crash的函数进行运行时的的修复(替换),归根结底就是利用了iOS的runtime特性对objc_method_t函数对于的IMP指针的指向进行替换。
PushNotification推送通知
极光:基于Binary Interface V2协议
APNs 推送原理及问题
- (1)在 iOS 平台上,大部分应用是不允许在后台运行并连接网络的。在应用没有被运行的时候,只能通过 Apple Push Notification Service (APNs) 把数据发送到终端用户。
- (2)Apple 为应用开发者提供了一个 APNs 推送接口,称为 binary interface。
v1协议有几个问题
- 1.消息是否发送成功没有明确反馈
- 2.如果一个消息发送失败,比如deviceToken不合法,APNs会在大约500ms后断掉连接,在断链前发送的消息也会失效
- 3.feedBack service只能报告应用被卸载后,造成deviceToken失效这种错误,而不会报告deviceToken不合法类型的推送错误
- 4.当我们向一批用户发送消息时,只要有一个deviceToken不合法,将会可能造成若干用户收不到消息,并且没有办法知道哪些deviceToken不合法,哪些deviceToken需要重发
** APNs 丢失消息的原因? **
- feedBack service只能报告应用被卸载,造成deviceToken失效这种错误,而不会报告deviceToken不合法类型的推送错误
** v2协议解决v1的几个问题?**
在v1的基础上增加了identity的关键字,用于识别一条信息,如果出现错误,错误会被identity带回来
Expiry---离线消息超时时间,为0或者小于0,APNs不会保存这条信息如果发送出现错误,v2 会在断链之前返回一个错误应答(通过Identity带回来和错误码),根据错误应答,我们有机会找到是哪条消息发送出错,并确定哪些消息需要重发
** JPush iOS 推送相比直接向 APNs 推送有什么好处呢? **
1.减少开发及维护成本
2.减少运营成本:
融云:提供开源服务器端,灵活性较好,文档齐全
1.融云不维护好友关系,只负责转发消息(可以从web后台(也就是服务器端)可以直接调用融云提供的接口发送消息,消息从fromUserId转发到toUserId,那么用户的好友关系和群组是保存在公司的服务器的)
2.发送消息是走融云的服务器(token)fromUserId(自己的id)走到融云的服务器,然后融云服务器转发到好友toUserId那里,因为消息体中含有很多的属性,消息体本身可以附带信息,比如targetId(就是toUserId,这里两者一样),extra附加信息,功用于提供给开发者拓展能,可以使消息附带信息,但是大小有限制,一般附带字符串或json数据,字段解决了好友头像的及时更新功能,这个字段的用法放开发篇章中细细讲解。
3.每个APP的注册用户都会被分配一个userId,分配userId这个任务还是要我们后台自己来搞,当用户A注册的时候,后台分配给A一个userId并且返回给前端开发人员使用,前端开发人员拿到A的userId之后,就要用这个A用户的唯一的userId去登录融云的服务器,融云只负责转发消息,注意点不是任何人有个userId都可以随便连接融云的服务器,UserId经过Token验证的,有这个Token就让你连接,没有token的话,滚蛋
4.如何获取token?
token的获取是要自己公司的后台提供一个获取融云token的专门接口,自己的后台开发人员去看文档做接口,获取到token后,就可以拿一个用户A的userId去连接融云的服务器了,融云的方法叫connectWithToken
5.客户端appkey对应服务器端appSecret,例如注册用户使用A服务器,此时客户端B登陆想要登陆只能对应服务器端B,因为appkey与appSecret是一一对应的
6.实现单聊、群聊、聊天室conversationType设置对应类型
7.RCIMFriendsFetcherDelegate,RCIMUserInfoFetcherDelegagte:这个代理方法是提供好友信息的,enableMessageAttachUserInfo:默认NO,如果YES,发送消息会包含自己用户信息。
融云推送图(消息远程推送的流程):