Jul 1

实际项目中开发者工具pod依赖管理方案

Lrdcq , 2020/07/01 23:01 , 程序 , 閱讀(2084) , Via 本站原創
在iOS项目开发过程中,我们总是会加入一些开发者工具相关的代码,通过DEBUG/TEST宏或者其他形式防止相关代码引入到线上。但是随着我们做组件二进制化对相关类进行编译后pod沉淀,相关代码的处理就麻烦起来了。

- 首先明确,组件二进制化是iOS工程化的整体趋势,不可阻挡

那对应的,对于有开发者工具区分的二进制pod,我们如何进行管理呢?

别用宏了-变量控制

这个问题的根本原因是因为代码通过宏进行了开发者功能的控制。如果我们不通过宏,而是通过一个变量进行控制,把相关代码都打进去就好了。
#if defined(DEBUG)
    [do something];
#endif

//改为

if ([GlobelEnv isDebug]) {
    [do something];
}

好处:
- 省事儿
- 去除多余的宏,可以加快组件构建速度。同时组件也只需要构建一份,而不是2份甚至3份(debug版,test版本,release版)

坏处:
- 在线上代码引入了开发者逻辑,影响性能并且有一定风险
- 可能在线上引入多余的依赖库,大大增加包大小

方案使用现状:

其实目前各个大厂的大型项目大面积使用该方案。原因是这确实是一个真正省事儿的方案。不用考虑不同环境下的依赖治理(大厂的大型项目也很难去做干净的依赖治理,动不动就夹带上线下的依赖),并且项目到一定程度对开发者工具带来的包大小增加,性能影响也不太感冒了(除非是工具类/强性能需求的app)。甚至有可能还希望线上通过某些开关能打开开发者工具。

当然,如果对包大小,性能还在较为敏感的阶段,并且对线上代码有洁癖性的追求,这个方案是最恶劣的方案了。

别用宏了-线下组件

这个方案还是避免在一份代码中用宏。对应的,将所有开发者工具相关的代码抽到独立的pod库中,通过钩子等形式进行弱耦合。该独立pod库进行线下依赖。
//库:SomeA
#if defined(DEBUG)
    [do something];
#endif

//改为:

//库:SomeA
if ([SomeADebugHook].hookXHere) {//或者通过runtime判断约定类
    [SomeADebugHook].hookXHere();
}
//库:SomeADebugTool
[SomeADebugHook].hookXHere = ^{
    [do something];
}
//Podfile
pod 'SomeA', '1.0.0'
pod 'SomeADebugTool', '1.0.0', :configurations => ['Debug']

好处:
- 将代码中的debug宏管理转换为完全的组件拆分,并且拆出来的debug线下组件使用方可以根据场景进行编译甚至替换。
- debug代码不用带入线上,包大小上有一定优势。
- 这样拆分后也避免了重复编译和宏入侵,组件打包速度也快。

坏处:
- 随着debug逻辑复杂,需要拆分大量的hook在原始代码里,多多少少有一些包大小和性能上的损耗。(如果用runtime找类/找方法的方案会稍好一些,但是不是一个普遍性设计)
- 如果有逻辑相对复杂了,hook无法完全把开发者功能拆解干净,这个抽象方案无法100%覆盖宏解决的场景。更适合独立开发者组件的场景。

方案使用现状:

该方案比较适合一些开发者工具相对独立的大型组件。比如react-native容器组件,搭配一个ReactNativeJsDebuger的开发者工具。或者把相关开发者工具集中起来,弄一个LRDDebugKit包。业界常见的FBRetainCycleDetector,FLEX,MLeaksFinder这样的开发者组件也以这种形式集成里面。

但是,确实不太适合覆盖所有组件的开发者/线下常见,对一些组件单纯为了区分线上线下而做的宏,反而繁琐了。

用宏-多套组件

二进制化后用宏的一大问题就是需要发多个版本,假如我的组件代码通过DEBUG/TEST宏区分为3个版本,那么我就需要发3份,共计4个版本了。
//Podfile
pod 'AFNetworking' //源码版本
pod 'AFNetworking_binary' //二进制版-线上
pod 'AFNetworking_binary_DEBUG' //二进制版-DEBUG
pod 'AFNetworking_binary_TEST' //二进制版-TEST

然后podfile需要根据当前构建的场景不同去完成不同的pod install。
由于pod install时并不知道接下来的构建行为到底要执行什么环境的构建,考虑到pod install的非DEBUG构建一般在CI上进行,因此可以通过环境变量注入的方式,并且对pod函数进行二次封装。
def binary_pod(name, config)
    env = ENV['CI_BUILD_ENV']
    podname = "#{name}_binary"
    
    if env == 'DEBUG'
        podname = "#{podname}_DEBUG"
    elsif env == 'TEST'
        podname = "#{podname}_TEST"
    end
    
    pod(podname, config)
end

//Podfile使用
binary_pod 'AFNetworking'

就可以串接起来整个流程了。只是有一个环境变量需要注意。

好处:
- 二进制改造过程中现有宏使用该怎么样怎么样,不用做任何改变。
- podfile也几乎不用改动。

坏处:
- 一个组件要发布多套二进制组件,编译发布慢并且挺占空间的。
- 使用成本有一个小门槛。

使用情况:

这个方法适用于大部分普通小组件,应该是各个大厂小厂做二进制化后普遍采用的基础方案。只是podfile处理与使用的这一层自动化程度各有差异。

不过这个做法有一个大坑,是一个组件发布3份,如果线下线上依赖不同,我们组件一般只会写一个podspec,这咋整。

- 比较常见的做法是,因为二进制打包是去依赖的,所以spec依赖中不主动声明线下的依赖,而是把线下需要依赖xxx组件这个信息告诉用户,用户自己在podfile中去指定configurations把线下依赖给拉进来,或者自己判断是否已经包含了相关依赖即可。

- pod本身也提供了类似能力,可以在podspec中直接指定configurations(https://guides.cocoapods.org/syntax/podspec.html#dependency
spec.dependency 'AFNetworking', '~> 1.0', :configurations => ['Debug']

对于企业内部应用,configurations有统一规范并且相对固定,或者组件就为一个应用使用的情况,可以直接用过这个解决。当然,在组件里依赖项目的configurations名称本身是不好的行为。

——————

其他还有一些方案,均为这个3个方案的基础思路上展开与优化。目前看三个项目结合在一起使用是大厂的实际现状。

趋势上,宏+多套打包的方案应该会被逐步替换掉。可见的是无宏方案应该随着开发者工具的沉淀成为主流。
关键词:pod
logo