模块化学习实践总结

3,292 阅读8分钟

项目想要oc和swift混编,因为是oc项目,如果直接新的业务都用swift写的话,可读性很差,

项目现状: 因为初期决定添加swift代码编写新业务,完全是不加考虑的跟风,现在项目中oc和swift代码相互引用,这样项目很乱,一个文件.m中可以包含oc和swift以及很多的import.所以根据一些文章想慢慢实践下项目组件化,主要分两步:

  • 模块化,尝试将已有的oc代码解耦,分成不同的模块,使用cocoapods本地库加载,这样也方便我们的会员端和商家端两个app使用相同的模块;
  • 路由,模块化后的文件在引用的时候还是有依赖,路由就是通过一些方法如url或者target-action的中间件来减少各组件之间的依赖,最大程度的解耦分离;

首先了解swift和oc相互访问的基质

module map 文件

module map文件就是对一个框架,一个库的所有文件的结构化描述。默认文件名是 module.modulemap 关于 LLVM module 系统更加详细的内容,可以参考 Clang 官方文档

Swift Module

苹果为 Swift 设计了 Swift ModuleSwift Module 可以将 Swift 解析后生成对应的 modulemap 和 umbrella.h 文件,SwiftModule 增加对编译器版本的依赖,编译产物与编译器 和 Swift 版本有关。如果想要实现 Swift 和 Objective-C 的互相访问,需要 Objective-C 库,以及对应的 umbrella.h 和 modulemap 支持。其中动态库 framework 是 Xcode 支持配置并生成 header,静态库 .a 需要自己编写对应的 umbrella.h 和 modulemap。即库之间无论何种语言实现,均需要封装为 LLVM Module 来相互访问。

Objective-c访问Swift的第三方库,在Objective-c类中导入’productName-Swift.h’, 同时Swift库中想要暴露给oc调用的类需要用public修饰,方法和属性需要用@objc修饰.


use_modular_headers!和use_framework!

use_framework这个指令是讲包含swift的三方库作为动态库来引入,在oc项目要使用swift的库,会生成一个<project-swift.h>的头文件,转义了swift中使用class和@objc的类信息并且暴露出来.我们使用swift库时需要引用这个头文件.其实也就是Swift Module的应用.

不使用use_framework指令,swift库就是一种静态的方式代入项目(相当于copy).编译不会生成头文件,所以也没有了引入的接口.

cocoapods1.5.0,xcode9以后,swift的库作为静态库来使用,因为动态库可能会影响app的启动时间.但是在swift项目中如果pod添加了oc的第三方库,需要为这个oc库开启use_modular_headers,这样cocoapods会为涉及到的oc库生成module map.


模块化-解耦

模块化,分离两种语言代码 使用cocoapods库的形式 先从底层部分开始抽离, 大致分成几种:

类扩展
工具类,基础组件
  • 宏定义,常亮等
  • 日期处理
  • 文件处理
  • 系统功能处理(短信,电话)
网络库
  • AFNetWorking
  • SDWebImage
功能组件(自定义UI控件)
  • 下拉菜单
业务组件(业务的具体实现)
  • 我的
  • 首页等等

规则:

  • 基础组件之间不应该产生依赖,如日期处理中不可以用到宏定义的一些常量,这里就涉及到组件之间的通讯问题
  • 功能组件和基础组件之间不应该产生依赖
  • 业务组件依赖基础组件,和功能组件,但是业务组件和业务组件之间不应该产生依赖

cocoapods 私有库的创建

新建一个LocalLib做本地库的文件夹, 新建库文件夹,如PHTool,在文件夹中创建podspec文件, pod spec create PHTool 修改PHTool.podspec,描述这些 license选用LICENSE source的git指向设置为空 subspec为子文件夹

runtime

当扩展文件中包含runtime的一些代码的时候 头部引用#import <objc/runtime.h>就可以

路由-组件化

组件之间的通讯问题

反射调用

通过NSClassFromString来解耦#import的引用.第一遍解耦通过这种方式,可以初步实现基本需求.

在oc项目反射swift的类时,直接NSClassFromString(类名),获取到的是nil,Swift创建的类为项目名.类名, 如NSClassFromString(项目名.类名) 程序启动时swift的class是没有加载的,swift也没有+load函数,所以NSClassFromString获取到的为空.

  • 通过反射可以获取到类然后实例化,进行跳转,但是一般我们的跳转都会传一些参数,如userId等,

可以通过performSelector传参

遇到的问题

NSClassFromString在类名或方法名出现改动时在编译阶段不会抛出异常error.

中间件

使用中间件来调用其他模块,这样在编译阶段可以避免NSClassFromString的问题. 但是中间件不应该是一个包含所有模块的类,这不是一个成熟的解决方案. 比较成熟的中间件方案有很多,我大概了解的有两种,一种是早期的蘑菇街的URL路由方案,大概早到2015年, 另一种是采用target-action的CTMediator方案

url方案(对外公开API接口)

在网上搜到的最多的是以前蘑菇街app的组件化方案

  1. 在app启动时实例化各组件模块,注册URL.
  2. 当组件a需要调用组件b的时候,想manager传递url,manager调度组件b,完成任务.

target-action方案和mediator(中间件)模式

casa的框架CTMediator,就是target-action方案.casa大神的博客放到了下面.

target是类似一个模块的接口,当组件a需要调用组件b的时候,组件a通过中间件向组件b的target_b传递消息,target_b调用组件b的内容.

CTMediator的方案是:target_A相当于组件的头文件,主项目通过runtime的NSClassFormString获得target_A,performSelector获得target中实现的组件的初始化方法.将返回的初始化组件(大部分情况下是控制器),然后返回给主项目,进行组件间的通讯或跳转. 在CTMediator中,有缓存和类获取失败的处理.

Lotusoot

组件测试

在测试的时候不需要的其他同事正在开发中的组件,可以先移除,项目不会受到影响

在分离项目的过程中遇到的一些问题

不使用use_framework

因为动态库对app启动有影响,而且swift和cocoapod都支持了静态库,所以最好不使用use_framework,但是在oc类中引用swift三方库时,静态库是没有对应的<xxx-Swift.h>文件,所以没办法引用在pod里的第三方库.

这里我的解决方案是为第三方库添加一个对应的swift的类来充当入口文件的作用,swift引用swift的库是没有问题的,这样编译时会有对应的入口文件被转化为<xxx-Swift.h>,moreover,这样的入口文件也符合我们的解耦思路.

继承

面向对面的一大特性是继承,在我们的项目中,几乎所有的ViewController都是继承自一个baseController基础控制器,在这里设置了一些控制器的共同特性如统一的背景颜色,加载框等.前期我们通过这种特性,在迭代时可以少写很多基础UI方法,这是继承的特性.但在是分离模块的时候就很痛苦了.而且这种方法有个弊端是我们习惯了继承自UIKit,要改变习惯也是很费力的.

这里有一篇文章跳出面向对象思想(一) 继承,里面谈到继承是紧耦合的一种模式,主要的提现就在于牵一发动全身,就是现在这种情况.文章的解决方案是用组合替代继承.

类似我项目现在的情况,我选择使用extension来代替继承,这样可以在扩展中实现通用的方法,既可以习惯的使用UIKit,也可以解耦这些继承的controller.

工具类的使用

一个工具类可以同时在两个模块中使用,这时两个模块都会依赖这个工具类,好像解耦并没有效果. 但其实不是,我们解耦的目的是为了解除大模块之间的相互耦合,小的通用工具类我觉得是可以使用cocoapads的本地库做依赖的.

Demo

Demo地址

总结

从头开始了解组件化的过程中,学习了很多优秀方案的思想,了解了组件化的原理及操作.

组件化的过程是解耦的过程,想各个业务组件进行分离,业务组件可以依赖基础组件和功能组件(例如网络请求).

我们经常做的给第三方封装一层自己的类,如封装一层AFNetWorking,其实就是解耦的操作.

组件化可以通过runtime的特性,使用NSClassFormStringperfromSelector这些方法来实现解耦,但是这些都是硬解的操作,因为字符串和方法的属性都是靠我们自己定义的规范来的, 而使用协议可以优化这一点,当传参有问题的时候在编译阶段就暴露出来了.

写的比较乱和杂,有不对的地方还请留言指出,谢谢


参考链接:

iOS应用架构谈 开篇

iOS 混编 模块化/组件化 经验指北

Cocoapods系列教程(三)——私有库管理和模块化管理

iOS 组件化实践思考

Swift 和 Objective-C 混编在有赞移动的实践

iOS 组件化 使用cocoapods集成实战演练

Cocoapods官方文档

iOS使用核心的50行代码实现一个路由组件

组件化的一个新想法