项目想要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 Module
。Swift 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的组件化方案
- 在app启动时实例化各组件模块,注册URL.
- 当组件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
总结
从头开始了解组件化的过程中,学习了很多优秀方案的思想,了解了组件化的原理及操作.
组件化的过程是解耦的过程,想各个业务组件进行分离,业务组件可以依赖基础组件和功能组件(例如网络请求).
我们经常做的给第三方封装一层自己的类,如封装一层AFNetWorking,其实就是解耦的操作.
组件化可以通过runtime的特性,使用NSClassFormString
和perfromSelector
这些方法来实现解耦,但是这些都是硬解的操作,因为字符串和方法的属性都是靠我们自己定义的规范来的,
而使用协议可以优化这一点,当传参有问题的时候在编译阶段就暴露出来了.
写的比较乱和杂,有不对的地方还请留言指出,谢谢