# 架构心得

> 2019-07月底跳槽，从事的工作内容是基础平台内容，主要是基础工具和 SDK 的封装；工程化 cli 落地、研发管理、静态代码扫描等。虽然以前写代码也是站在封装、复用、聚合等出发点写代码，但是还是和真正写 SDK 注意点有很多不同，这也是为什么写这篇文章总结的原因。


## 一些注意点

- 当你开发某个功能的时候，轻易不要使用第三方的库。为什么？因为你难以确保业务方是否也在使用这个库，可能库在使用了，但是版本号不一致，就会造成 api 内部实现可能不一样，造成功能不符合预期或者一些神奇的 Bug。
- 假如你遇到上面的情况，你出于某种原因不得不使用某个第三方，但是你又必须考虑调用者的工程可能也加入了该库。解决方案大体有3种。1、推进业务方不要使用离散的功能三方库，比如 AFNetWorkging 不要自己引入，而是引入基础平台方封装好的网络功能库；2、自己将引入的第三方网络库选取主要用到的功能去自己实现掉。我们首先要自己这个第三方做了什么事情，提供了哪些功能，其中哪些功能是我们会使用到的，那么我们可以借鉴源代码，自己去做类似的事情，然后一个精简版的 AFNetWorking 就出来了；3、将第三方库的类名称、方法名称、Block、宏...都给更换名称（一开始想到找到一定的规则用自动化脚本去做，发现这样子不可能处理全部的 case，程序员自己脑子都想不全所有的 case，所以代码实现根本不可能；）一番操作下来发现还是人工手动操作效果最好
- 当你写某个功能的时候，你封装的 SDK 对于提供某个能力，项目以组件化的形式开展，所以你对外暴露的地方在于 Router 文件中， Router 负责解析 url，最后调用 [target performSelector withObject]，然后在 target 对象内部真正去实现某个功能，Router 一定只做最简单的事情，也就是 url parse，寻找 target，执行 performSelector。target 暴露某个接口，也许接口内部实现也很复杂，需要依赖其他几个 api 或者其他几个类的 api。所以 api 也就是函数需要做到单一原则。可能某个大的能力需要几个能力的聚合，这个大的函数内部依靠几个单独的函数逻辑才实现某个能力。可能由于版本迭代，你需要将之前不对外暴露的能力也要暴露出去，所以做好函数的单一功能非常重要，可拓展性强、易测试。
- 一定要写好 Unit Test。这样子不断版本迭代，对于 UT，输入恒定，输出恒定，这样内部实现如何变动不需要关心，只需要判断恒定输入，恒定输出就足够了。（针对每个函数单一原则的基础上也是满足 UT）
- 在做 SDK 的时候，对于一些方法或者函数的返回值，尽量要做到 iOS 和 Android 端的输出值的数据类型一致，除非某些特殊情况，无法保证一致的输出。
- 当你想写宏定义的时候应该先判断下是否存在，因为工程中很可能已经存在一个同名的宏。
  ```Objective-C
  #ifndef Hi
    #define Hi @"Hello, nice to meet you"
  #endif
  ```
- 避免重复宏定义
  因为宏定义可以多次，但是一个工程中有可能因为命名太规范了，大家不小心会为一个功能起一个同名的宏定义，所以我们在宏定义的时候需要做判断，不然多个同名宏定义，最后的功能会根据文件编译顺序决定，最后的宏定义才生效。
  ```Objective-c
  #ifndef CM_IS_CLASS
    #define CM_IS_CLASS(obj,cls) [obj isKindOfClass:[cls class]]
  #endif
  ```
- 对于你的某个 SDK，你在为某个方法、某个类、某个宏定义命名的时候需要注意选择合适的前缀
  比如。你的某个项目是在做监控，SDK 的名字叫做 Prism-Client。那么你的类名称、类方法名称、宏定义、分类名称、分类方法名称等都需要合适且统一的前缀，一般选取 `前3个字母组合`。当前的项目叫做 `PCT`。类前面加 PCT，类里面的方法不加前缀。分类名称加前缀 PCT，分类里面的方法前面加前缀，小写的 pct。
  普通类的方法不加前缀是因为普通类已经通过类名的唯一性确定了方法的唯一。
  分类里面方法加前缀是因为分类的方法在工程里面这个类都可以访问。所以要在方法前面区分
  ```Objective-C
  // 安全的数据获取方法
  #ifndef PCT_SAFE_STRING
      #define PCT_SAFE_STRING(x) (x) != nil ? (x) : @""
  #endif

  NSData+PCTAES.h
  - (NSData *)pct_AES128EncryptWithKey:(NSString *)key gIv:(NSString *)Iv;

  PCTRequestFactory.h
  + (void)fetchUploadConfigurationWithRequestURL:(NSString *)requestUrlString
                                                params:(NSDictionary *)params
                                               success:(void (^)(PRCConfigurationModel*model))success
                                               failure:(void (^)(NSError *error))failure;
  ```
  
- 一般来说如果你的某个文件代码中高频率的使用宏，且宏里面是做一些运算，建议使用内联函数代替，因为内联函数效率高，且在编译阶段可以检查错误。函数的调用顺序底层是出入栈的过程，Frame Pointer、Stack Pointer。一个栈保存当前函数的局部变量、参数、返回地址。所以不同函数的调用会效率有影响，如果高频使用的函数建议用内联函数。
  内联函数和宏的区别
  优点相比于函数
  
  - inline 函数避免了普通函数的,在汇编时必须调用 call 的缺点:取消了函数的参数压栈，减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快
  - 集成了宏的优点,使用时直接用代码替换(像宏一样)
  优点相比于宏
  - 避免了宏的缺点:需要预编译.因为 inline 内联函数也是函数,不需要预编译
  - 编译器在调用一个内联函数时，会首先检查它的参数的类型，保证调用正确。然后进行一系列的相关检查，就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性
  - 可以使用所在类的保护成员及私有成员。
  注意事项
  - 内联函数只是我们向编译器提供的申请,编译器不一定采取inline形式调用函数
  - 内联函数不能承载大量的代码.如果内联函数的函数体过大,编译器会自动放弃内联
  - 内联函数内不允许使用循环语句或开关语句
  - 内联函数的定义须在调用之前
  - Objective-C 中内联函数用 NS_INLINE ，等价于 static inline。且内联函数的命名需要注意，在该模块内的内联函数需要加前缀。
  ```Objective-C
  NS_INLINE NSString * PCTGetTableNameFromType(PCTLogTableType type){
      if (type == PCTLogTableTypeMeta) {
          return PRC_LOG_TABLE_META;
      }
      if (type == PCTLogTableTypePayload) {
          return PRC_LOG_TABLE_PAYLOAD;
      }
      return @"";
  }
  ```
- 什么情况下用统跳（路由能力）？
  技术 SDK 的话，因为可能依赖非常多的其他技术 SDK 所以会比较难梳理出一个需要暴露的能力，非常难抽象
  业务 SDK 很清楚需要暴露哪些能力。所以我们一般将业务 SDK 提供统跳能力，技术 SDK 不提供

- 基础平台组做什么？怎么做？
  业务线的同学一般做的事情就是在操作 UI，手机屏幕很小，要做的事情也会比较单一，可能就是单击某个按钮然后多线程异步去处理某个逻辑（网络、数据库、File等），然后异步回调里面回调主线程去更新 UI。所以做的事情的广度不一样。基础平台组做的事情一般来说脱离独立的 UI，换句话说就是焦点不在于 UI，而在于整个的架构逻辑，比如一个数据上报 SDK。它考虑的事情不是 UI 怎么用，而是数据来源是什么，我设计的接口需要暴露什么信息，数据如何高效存储、数据如何校验、数据如何高效及时上报。

  假如我做的数据上报 SDK 可以上报 APM 监控数据、同时也开放能力给业务线使用，业务线自己将感兴趣的数据并写入保存，保证不丢失的情况下如何高效上报。因为数据实时上报，所以需要考虑上传的网络环境、Wi-Fi 环境和 4G 环境下的逻辑不一样的、数据聚合组装成自定义报文并上报、一个自然天内数据上传需要做流量限制等等、App 版本升级一些数据可能会失去意义、当然存储的数据也存在时效性。种种这些东西就是在开发前需要考虑清楚的。所以基础平台做事情基本是 **设计思考时间：编码时间 = 7:3**

  为什么？假设你一个需求，预期10天时间；前期架构设计、类的设计、Uint Test 设计估计7天，到时候编码开发2天完成。

  这么做的好处很多，比如：
    1. 除非是非常优秀，不然脑子想的再前面到真正开发的时候发现有出入，coding 完发现和前期方案设计不一样。所以建议用流程图、UML图、技术架构图、UT 也一样，设计个表格，这样等到时候编码也就是 coding 的工作了，将图翻译成代码
    2. 后期和别人讨论或者沟通或者 CTO 进行 code review 的时候不需要一行行看代码。你将相关的架构图、流程图、UML 图给他看看。他再看看一些关键逻辑的 UT，保证输入输出正确，一般来说这样就够了
    3. 软件项目管理也一样，制定进度表、确定干系人、kick-of meeting 等、定期碰头

- 一般来说不要在  load 方法里面做非本类的事情。
  一般来说，不应该在当前类的 `load` 方法里面写和其他类有关系的代码，除非非做不可。
  ```Objective-C
  + (void)load
  {
      NSLog(@"%zd", [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus);
  }
  ```
  之前在做一个类 `PCTRequestFactory` 用来管理网络相关的逻辑。需要判断网络状态，我们都知道 AFNetWorking 第一次判断网络状态得到的是 AFNetworkReachabilityStatusUnknown。而我的逻辑需要 SDK 启动的时候判断网络状态，然后去上报数据。所以刚开始 AFNetworkReachabilityStatusUnknown 显然不能上报 Crash 数据，所以想着是将第一次的网络状态获取放到 **load** 方法里。这样是没问题的，可以拿到网络状态，但是我们知道 load 是类加载的时候调用的，打开 Xcode 看到 Build Phases 里面 `Link BiBinary With Libraries` 这个里面的库的顺序决定了里面的类加载顺序。我们知道 Pod 的原理是在 Podfile 里面描述的 pod 库依赖，然后会按照字典序（首字母排序去）引入，所以 AFNetWorking 这个肯定早，所以会成功的。但是万一是人工手动去引入或者修改库的位置，则在 PCTRequestFactory 里面的 load 方法执行的时候不一定可以保证 AFNetworkReachabilityManager 已经加载好。所以将 load 逻辑移动到 init 里面。

  另外，load 方法一般只做和本类有关系的逻辑，比如 hook 方法。