一个 app 从设计到发布应用商店会经历需求设计、UI设计、架构设计、开发、优化、测试等流程,每个环节做的好与坏都会影响到整个 app 的质量,作为开发人员,不仅要对需求以及 UI 的合理性进行评审,还要保证提供给测试的代码是经过自测并且覆盖了相关测试用例,更重要的是要做好架构设计、开发以及优化这三个环节。

架构设计

架构设计可以以业务为驱动,按照最适合自己的方式去设计,设计思路和方法尽量统一,同类型问题用同样方式去解决,下面几点是我认为比较重要的:

  • 关注分离
    将系统分解为多个模块并各司其职,纵向分层,横向拆分业务。

  • 高内聚
    一个模块只完成一个功能,并合理有度的进行封装。

  • 松耦合
    依赖关系越少越好,尽量不横向依赖,不跨层访问。继承是紧耦合的一种操作,它通常与多态一起存在,如果仅仅是为了代码重用而做的继承还不如用组合。

  • 适度设计
    对业务方该限制的地方要进行限制,该灵活的地方也要给业务方创造灵活实现的条件,架构的设计要保持一定量的超前性而做到易扩展,但不要设计过度,避免多做无用功而增加框架复杂度。好的架构是随着时间而更新的,而不是一开始就设计好了一切。

  • 技术选型
    不要仅是为了体验新技术而改变现有架构,等别人把坑踩平后再根据团队及业务情况考虑是否使用,技术本身没有什么好不好,只有适合不适合。

框架是服务开发人员的,需要满足通用性和易用性两个要求,其中易用性更重要

开发

MVVM

MVVM 相对 MVC 来说最大的优点是,ViewModel 是独立的,可复用到别的模块,并且更易于做单元测试,而 View 和 ViewController 仅用来做 UI 展示,它与 ViewModel 之间是相互影响的,ViewModel 处理过的数据要能够及时响应到 View,而用户在 View 中做了修改数据的操作后,ViewModel 中对应的数据也应该及时更新,响应式编程方式能够更容易的处理这个过程

响应式编程

响应式编程是一种面向数据及其变化响应的编程方式,它能够将数据、UI事件和异步操作更方便的进行序列化处理,相比传统编程方式来说,它可以使逻辑相关的代码能够紧凑的聚在一起,它通过信号机制来实现,用信号来记录值的变化,并通过信号绑定的方式来响应变化,下面这个例子很好的描述了响应式编程的作用

a = 2 // signal
b = 2 // signal
c = a + b // signal binding c = 4 
b = 3 // c = 5

目前我们在 Swift 项目中选择使用 RxSwift + MVVM 的方式进行开发,使用 RxSwift 需要掌握以下几个概念:

  • Observable(被观察者):相当于一个事件序列,它会主动向订阅者发送新产生的事件内容,包括 onNext、onError、onCompleted()

  • Subscribe(订阅者):订阅后便可以根据事件产生的数据进行操作

  • Scheduler(调度器):可分别通过 subscribeOn 和 observeOn 指定 Observable 或 Subscribe 在哪个线程上执行,默认是当前线程。

  • Operator(操作符):RxSwift 提供了很多操作符,合理运用便会极大提高开发效率

  • Subject:既可以作为 Observable 发送事件也可以作为 Subscribe 监听事件,它包括以下几种:

    • PublishSubject:订阅者只会接收到订阅操作之后的事件,也就是所说的热信号。

    • ReplaySubject:订阅者会接受到订阅之前的事件(可根据 bufferSize 指定数量)以及订阅之后的事件,类似于冷信号。

    • BehaviorSubject:订阅之后首先会接收到最近一次发送的事件,如果最近没有发送,那么发送一个初始事件。

    • AsyncSubject:订阅者只有在 Observable 发送 onCompleted() 时才能够收到它之前发送的最后一个事件,即使是在这之后的新订阅者也能够收到。

跨平台开发

跨平台开发的方式很多,做技术选型时要根据业务及团队情况去选择

Hybrid(H5 + Native)

使用 WebView 承载 H5 页面并通过 JSBridge 实现交互,性能略差,可通过预下载资源等方式进行优化。

Reatct Native

RN 中虚拟 DOM 会通过 JavaScriptCore 映射为原生控件,性能比 H5 高一些,但由于渲染时需要 JS 和Native 通信,在有些场景(拖动)可能会因为通信频繁导致卡顿,可以通过延迟渲染方式解决,但更重要的问题是维护成本大。

Flutter

Flutter 使用 Skia 2D 渲染引擎来绘制 UI,采用 Dart 语言开发,Dart 是类型安全的语言,比 JS 运行速度快也更安全一些,它支持 AOT(Ahead of time 提前编译) 和 JIT(Just-in-time 即时编译) 两种运行方式,在开发阶段采用 JIT 来实现热重载,在发布时采用 AOT 来生成高效的 ARM 代码以保证应用性能,所以 Flutter 应用发布后不能像 Hybrid 和 RN 那些使用 JS 作为开发语言的框架去动态下发代码。

SwiftUI

SwiftUI 使用 DSL 声明式 UI、跨自家平台、Xcode 通过单独编译我们当前操作的视图文件,利用 Swift dynamic repacement 特性将更新内容注入到正在运行的程序中来实现热重载。

上面几种跨平台方案中,Hybrid 比较适合我所在的团队,Flutter 我们一直在关注,SwiftUI 还不成熟,而且仅适配 iOS13+ 系统,所以我们在简单学习后就放下了,过几年再关注。

组件化开发

随着 app 功能和体积的增长,也带来了如编译速度变慢、提交代码经常出现冲突、以及业务代码混在一起,开发功能类似的新项目时需要从头搭建等问题,于是很多团队都选择了利用组件化开发来解决这些问题,在组件化过程中需要注意下面列出的几个问题

  • 怎样合理拆分组件
  • 组件之间的依赖关系
  • 组件中的资源访问
  • 组件之间的路由跳转
  • 组件的多环境打包

详细介绍可参照上一篇文章 iOS 组件化开发

优化

线上 Bug

线上 Bug 可按以下优先级(由高到低)来处理:

  • 次数较多的 Bug 不管是否好处理都需要优先处理,并根据情况决定是否需要发版,还需要将原因及如何避免等问题分享给团队成员,避免其他人再次出现类似问题

  • 次数较少但是容易处理的 Bug

  • 无法复现但是有一定影响的 Bug,可根据情况添加保护性代码避免 app 崩溃,但要求能够预知是否会造成其他问题(数据异常,页面异常等)

  • 次数少处理难度大的 Bug 放最后,有时间时再去调查原因

修改 Bug 后要告知测试人员影响范围,避免因没覆盖测试而上线出现问题,新版本上线后需要关注修改过的问题是否还会出现。

数据安全

我们从 AppStore 下载的 app 其实都已经由苹果加壳,要砸壳后才能有办法看到汇编形式的源代码,如果我们要自己再加一层壳,可以对关键代码的方法名做一些混淆,以增加别人分析代码的复杂度,不过这对于大多数 app 来说不太值得去做,我们可以把精力放在其他更易做并且更有效率的地方,例如:

  • 使用 Https 保证数据传输安全

  • 不将敏感数据直接存储在沙盒里

  • 调用接口时 app 要做验签,并且对传输的敏感数据进行加密

  • 不要将对称加密用到的 Key 存储在客户端

启动时间

可以通过 Instruments 的 Time Profiler 工具来查看耗费时间,也可以通过在 Xcode 中修改项目的 scheme,在 Run -> Auguments 增加环境变量 DYLD_PRINT_STATISTICS 并设置为 1,然后观察控制台打出的 log 来查看

  • pre-main 阶段系统会由 dyld(the dynamic link editor)加载相关动态链接库,然后再初始化 runtime 环境,并由 runtime 触发 + load 方法,所以不要在 load 方法里执行耗时任务,否则会影响启动时间,并且用不到的代码文件要及时删除。

  • 检查 didFinishLaunchingWithOptions 及启动相关的方法,将部分操作改为异步执行或延迟处理

  • 首页相关 ViewController 尽量使用代码实现

App 瘦身

  • 将部分资源文件从工程中移除,改为从接口获取,并且可以通过给数据加版本号,避免每次重复下载。

  • 清理无用的代码文件和资源,不要 @1x 图片,如果用到了 ProtoBuf,可以将 .proto 文件从 Compile Sources 中移除。

  • 压缩图片 tinypng.com

  • 小图标可以使用 iconfont 替代,大图可以用 pdf 替代

  • 启动图界面用 LaunchScreen.storyboard,不要用多张 Default 图。

  • 用 .xcassets 来管理图片,Xcode 能够把里边的所有 png 图片压缩成 .car 文件。并且 iOS9 之后,如果是用 .xcassets 管理图片,AppStore 会根据不同设备准备不同的安装包,每个包内只有对应尺寸的图片,例如 iPhone6 下载 app 时,包里就只有 @2x 图。

  • 放弃 iPhone5s 之前发布的 32 位手机,仅支持 arm64

流量

  • 合理使用本地缓存数据并及时更新缓存

  • 考虑用 ProtoBuf 替代 JSON,也可以仅是个别数据量大的接口改为 ProtoBuf 格式数据

  • 轮询接口方式可以用 WebSocket 替代

  • 请求的图片格式考虑由 jpg 改为 webp,减少图片下载时间,再想办法提高图片转码时间

  • 按 UIImageView 大小取合适大小的图片,不要直接取原图,同一图片用在不同界面时,尽量要求 UI 统一视图比例,然后 app 统一获取同样大小图片,这样做的话不管对 app 还是 CDN 都能够提高缓存利用率

  • 非关键的业务数据,可以通过合并接口的方式,减少和服务器的交互次数。

  • 请求接口前客户端通过对 request params 做检查来减少不必要的网络请求

  • 可以用 ETag 这种服务端加本地验证的方式处理返回数据,当数据没有更新时不返回数据,而是返回 304

  • 数据量大时要提示用户是否在非 wifi 环境下下载

  • 下载功能要有取消下载的逻辑

UI 交互

  • 延迟处理不需要马上展现的视图或操作

  • 不要阻塞主线程,能异步去做的就不要同步。

  • 由于 UIKit 中大部分对象都不是线程安全的,所以 UI 操作都需要放在主线程做串行处理,否则可能会导致未知行为(动画异常、页面错乱、Crash)。

  • 优化表格滑动

    • 提前算 Cell 高度并缓存,如果要加载的数据也是要经过处理才展示的也提前处理好

    • 异步绘制,滑动过程中不加载图片

    • 当 Cell 中有很多子视图并且他们要在不同条件下展示时,考虑拆成多个 Cell

    • 不要在 Cell 中频繁生成 NSDateFormatter 对象

  • 通过设置模拟器的 Debug 菜单下的几项设置来查看图层视图是否需要优化

    • Color Blended layers
      大多情况是因为视图的 backgroundColor 与父视图颜色不一致或者是透明的,它会用红色表示有问题的视图,而绿色的表示没问题

    • Color copied images
      当图片的色彩格式不能被 GPU 处理时,会交由 CPU 处理,应该尽可能避免这种问题

    • Color misaligned images
      图片大小与控件不一致,会用黄色表示图片被缩放了,用紫色表示像素没对齐

    • Color offscreen-rendered
      用黄色标示哪些 layer 做了离屏渲染(当 GPU 无法将渲染结果直接写入 frame buffer,而是将结果暂存在其他内存后再写入 frame buffer),多数时候离屏渲染会影响性能,应该尽量避免,无法避免时可通过 shouldRasterize 来缓存渲染结果,降低影响,在开发过程中需要重点关注列表中对 View 做圆角设置和添加阴影操作。

      • cornerRadius + clipsToBounds 设置圆角会触发离屏渲染,仅使用 cornerRadius 不会触发,但效果不好,可以用带圆角效果的 layer 盖住视图实现圆角效果,以避免离屏渲染

      • 使用 shadowPath 添加阴影可以避免离屏渲染