应用场景:在开发app过程中,你肯定需要有测试环境,而测试环境和线上环境的切换如果你是根据修改代码来实现那你就太low了。这时候你会实现一个debug功能模块,不但你能进行环境切换,产品经理,测试员同样能用这功能。而随着产品逐渐成熟,需要的debug功能也会越来越多,如:ABTest切换,用户登录登出,指定页面跳转,日志查看等功能。从另一个方面说你的debug模块代码会越来越多。那么,怎样让debug代码不污染线上app呢?
一般,你可能会想到的是我只需要在每个类或方法上添加上宏的判断就行了,在debug下才会定义某个类或方法,如下:
#if DEBUG
@interface DebugA : UIViewController
// 方法...
@end
#endif
#if DEBUG
@implementation DebugA
// 方法实现...
@end
#endif
#if DEBUG
DebugA *a = [[DebugA alloc] init] ;
// 方法调用...
#endif
上面的方法,看起来是可行的。它保证了只有在 Debug 环境下,才会有 DebugA类的定义和实现,也保证了只在 Debug 环境下才对 DebugA 进行实例化。
问题: 虽然在 Release 下类和方法是没定义了,但是文件在打包的时候还是会被打包进app, 另外,类的定义和实现都用宏包起来,从一定程度上对代码造成了污染; 另一个问题,如果在实现过程中涉及到图片或者xib的使用,这些文件在 Release 下即使不显示也不使用,但是已经被打包进 app 了。怎么杜绝这两个问题呢?
EXCLUDED_SOURCE_FILE_NAMES能很好的帮你解决这个问题。从key的名称可以大概知道,它是用来通过文件名排除资源文件的。
你只需要在相应的 Target 下的 Build Settings 中添加 User-Defined Setting,然后添加 EXCLUDED_SOURCE_FILE_NAMES,并在不同编译配置下进行文件名的忽略(支持正则匹配)即可。
添加User-Defined Setting
添加EXCLUDED_SOURCE_FILE_NAMES key
上图,在 Release 下通过匹配文件名存在 Debug 字样的文件,这样,在 Release 下就会将文件名包含 Debug 的文件忽略了。所以,在定义 debug 功能模块的类和方法上都不需要通过 #if Debug #endif来污染代码了,同样的,debug 的资源文件也不会被打包进 Release 下的 app 内了。
等等,问题解决了吗?如果你的需求是只要 release 下就排除 debug 文件,那上面的方案已经足够了。但如果你遇到像我这样的需求,那就得另寻蹊径了。
由于公司项目使用 cocopods 来管理各个业务线模块,每个业务在更新了业务代码后需要打一个提供给其他业务使用的 rc 包,而这个 rc 包的打包环境是 Release,但我们又需要 debug 功能。基于这种需求,我们通过 ruby 脚本动态的定义了一个宏:IS_BETA,下面是代码片段:
if xcconfig.include? 'GCC_PREPROCESSOR_DEFINITIONS = $(inherited)'
if debugMode
c1 = xcconfig.sub('GCC_PREPROCESSOR_DEFINITIONS = $(inherited)', 'GCC_PREPROCESSOR_DEFINITIONS = $(inherited) IS_BETA=1')
else
c1 = xcconfig.sub('GCC_PREPROCESSOR_DEFINITIONS = $(inherited)', 'GCC_PREPROCESSOR_DEFINITIONS = $(inherited) IS_BETA=0')
end
File.open(xcconfig_path, "w") { |file| file << c1 }
else
if debugMode
s1 = 'GCC_PREPROCESSOR_DEFINITIONS = IS_BETA=1'
else
s1 = 'GCC_PREPROCESSOR_DEFINITIONS = IS_BETA=0'
end
File.open(xcconfig_path, "a+") { |file| file.write("\n#{s1}") }
end
内部代码则是通过这个宏来判断是否引入 debug 代码,如下:
#if IS_BETA
DebugA *a = [[DebugA alloc] init] ;
// 方法调用
#endif
这时候,上面的方法就没法用了。既然这个宏是通过脚本动态写入的,我们是否同样可以动态写入 EXCLUDED_SOURCE_FILE_NAMES的值呢?当然是可以的。同样通过 ruby 脚本,引入当前项目模块,遍历所有 target 的 build configuration,然后根据当前要编译的类型(这里定义为 defaultType)给 ** EXCLUDED_SOURCE_FILE_NAMES** 对应的设置上值。如下:
require 'xcodeproj'
def dealBuildSettings
project = Xcodeproj::Project.open('./Train.xcodeproj')
project.targets.each do |target|
target.build_configurations.each do |config|
if $defaultType == 'rc'
config.build_settings['EXCLUDED_SOURCE_FILE_NAMES'] = ''
else
if config.name== 'Release'
config.build_settings['EXCLUDED_SOURCE_FILE_NAMES'] = '*Debug*.*'
else
config.build_settings['EXCLUDED_SOURCE_FILE_NAMES'] = ''
end
end
end
end
project.save()
end
最后在 podfile 引入这个脚本,并执行下 dealBuildSettings 即可。这样,在通过 Jenkins打包时,执行 podfile 后就会动态根据环境去设置 ** EXCLUDED_SOURCE_FILE_NAMES** 的值了。
附:
通过 xcodebuild -showBuildSettings可以查看当前项目下的所有配置信息; xcodebuild -configuration Release -showBuildSettings命令,可以查看当前项目 Release下的所有配置信息,通过这些配置信息你可以在写脚本时知道对应的 key 名称。