文章目录

  • 前言
  • 开发环境
  • 问题描述
  • 问题分析
    • 1. 创建用于测试的Pod库
    • 2. 验证问题是否只存在于Pod库
    • 3. __OPTIMIZE__在什么时候会定义
    • 4. 影响__OPTIMIZE__定义的优化编译设置
    • 5. Pods工程的优化编译设置
    • 6. 自动修正Pods工程的优化编译设置
    • 7. Pods工程的多环境配置分析
      • 7.1. pods_project所属的类
      • 7.2. build_configurations的定义
      • 7.3. add_build_configuration的定义
      • 7.4. add_build_configuration的调试
      • 7.5. 构建设置的来源
      • 7.6. 配置类型的决定
      • 7.7. 配置类型的设置
      • 7.8. project配置的使用
      • 7.9. Podfile配置的作用范围
  • 解决方案
  • 总结
  • 最后

前言

开发Flutter插件封装一些原生代码时,遇到的一个奇怪的问题。这个问题虽小,但也很值得分析。本篇文章讲的比较细比较长,如果你能坚持阅读完,我相信你一定会有所收获。

开发环境

  • Xcode: 14.2
  • Cocoapods: 1.12.0

问题描述

Flutter插件中的原生代码运行时没有任何日志输出,但是通过断点调试可以确定是有执行的。

原生代码是用Objective-C写的,日志输出调用的的是NSLog方法,跳转到NSLog方法定义的地方,可以看到如下代码:

#ifndef __OPTIMIZE__
#define NSLog(...) NSLog(__VA_ARGS__)
#else
#define NSLog(...){}
#endif

问题分析

NSLog方法的宏定义可以看出,只有没定义 __OPTIMIZE__时,日志才能正常输出,那__OPTIMIZE__在什么时候会定义呢?如果你去搜索,很多文章会告诉你Release环境下会定义,Debug环境下不定义,也就是说这段代码的作用是屏蔽Release环境下的日志输出。这似乎是很常见的做法,那是当前项目运行环境有问题吗?多次确认当前项目运行的是Debug环境没错。

难道是因为Flutter插件项目的问题?感觉不太可能,不过Flutter插件项目是以Pod库的方式依赖的,这么一想,会不会这个问题只出现在被依赖的Pod库中?

1. 创建用于测试的Pod库

新建一个无关Flutter的Pod库试试,切换到根路径下执行命令:

pod lib create LogTest

这条命令的作用是创建一个名为LogTest的Pod库。执行后需要一步步完善信息,因为只是用于简单测试,不需要发布,所以除了platform要选iOSlanguage要选ObjC外,其他的随意填写选择。

创建完毕后,为了方便往库里添加代码,先修改Podfile文件依赖刚创建的LogTest库:

target 'xxx' dopod 'LogTest', :path => './LogTest'
end

执行命令让依赖生效:

pod install

现在一个空的Pod库已经准备好,打开Xcode项目,在[Pods工程] -> [Development Pods]可以看到LogTest,选中它后按下快捷键command + N然后选Cocoa Touch Class快速创建LogTest.hLogTest.m

LogTest.h

#import <Foundation/Foundation.h>#ifndef __OPTIMIZE__
#define NSLog(...) NSLog(__VA_ARGS__)
#else
#define NSLog(...){}
#endifNS_ASSUME_NONNULL_BEGIN@interface LogTest : NSObject+ (void)log:(NSString *)msg;@endNS_ASSUME_NONNULL_END

LogTest.m

#import "LogTest.h"@implementation LogTest+ (void)log:(NSString *)msg {NSLog(@"%@", msg);
}@end

到这一步,测试用的Pod库还未完全准备好,新建的LogTest.hLogTest.m文件需要从LogTest库根目录移到LogTest库根目录/LogTest/Classes,这是因为在LogTest.podspec已经指定了源码路径(s.source_files = 'LogTest/Classes/**/*'),不放在指定路径下无效。

做完以上操作,重新执行pod install,就可以在主工程内调用新建的Pod库啦!

注意:如果主工程使用的是Swift语言,Podfile中又没有配置use_modular_headers!,那么需要在桥接文件xxx-Bridging-Header.h中加上#import <LogTest/LogTest.h>

2. 验证问题是否只存在于Pod库

在主工程选个位置调用:

LogTest.log("这是测试")

发现Debug环境下还是没有日志输出,那么说明跟Flutter没有关系,这是iOS原生的问题。那如果在主工程使用这个宏定义正常吗?经测试,在主工程使用这个宏定义是正常的。

3. __OPTIMIZE__在什么时候会定义

前面提到__OPTIMIZE__在Release环境下会定义,不止一篇文章这么说,而且在主工程内测试也是没问题,真实性应该是没问题的。那么问题出在了哪里呢?

经过一番搜索,我找到了关于__OPTIMIZE__的文档说明,它是GCC的预定义宏:

__OPTIMIZE__
__OPTIMIZE_SIZE__
__NO_INLINE__

These macros describe the compilation mode. __OPTIMIZE__ is defined in all optimizing compilations. __OPTIMIZE_SIZE__ is defined if the compiler is optimizing for size, not speed. __NO_INLINE__ is defined if no functions will be inlined into their callers (when not optimizing, or when inlining has been specifically disabled by -fno-inline).

These macros cause certain GNU header files to provide optimized definitions, using macros or inline functions, of system library functions. You should not use these macros in any way unless you make sure that programs will execute with the same effect whether or not they are defined. If they are defined, their value is 1.

引自:https://gcc.gnu.org/onlinedocs/gcc-12.2.0/cpp/Common-Predefined-Macros.html

文档虽然内容不多,但是明确说明了__OPTIMIZE__会在所有优化编译中定义。这似乎和前面说的有点区别,Debug环境也不是不能设置优化编译,如果Debug环境设置了优化编译,那也会预定义宏__OPTIMIZE__。分析到这,感觉离真相越来越近,接下来就是检查优化编译设置。

4. 影响__OPTIMIZE__定义的优化编译设置

GCC文档中并没有优化编译设置相关的内容,不过按以往的经验,既然已经知道和GCC相关,直接在项目构建设置中搜索应该是能找到的。为了方便分析,我新建了一个Xcode项目,并按之前的项目通过自定义多个构建配置实现多环境(Dev/Pre/Prod,Dev属于Debug环境,Pre/Prod属于Release环境)。

在主工程的项目构建设置搜索关键词GCC

猜测Optimization Level应该就是我们要找的优化编译设置,再做进一步验证之前,先简单了解一下这个构建设置。

Optimization Level
Setting name: GCC_OPTIMIZATION_LEVEL
Specifies the degree to which the generated code is optimized for speed and binary size.

  • None: Do not optimize. [-O0] With this setting, the compiler’s goal is to reduce the cost of compilation and to make Debugging produce the expected results. Statements are independent—if you stop the program with a breakpoint between statements, you can then assign a new value to any variable or change the program counter to any other statement in the function and get exactly the results you would expect from the source code.
  • Fast: Optimizing compilation takes somewhat more time, and a lot more memory for a large function. [-O1] With this setting, the compiler tries to reduce code size and execution time, without performing any optimizations that take a great deal of compilation time. In Apple’s compiler, strict aliasing, block reordering, and inter-block scheduling are disabled by default when optimizing.
  • Faster: The compiler performs nearly all supported optimizations that do not involve a space-speed tradeoff. [-O2] With this setting, the compiler does not perform loop unrolling or function inlining, or register renaming. As compared to the Fast setting, this setting increases both compilation time and the performance of the generated code.
  • Fastest: Turns on all optimizations specified by the Faster setting and also turns on function inlining and register renaming options. This setting may result in a larger binary. [-O3]
  • Fastest, Smallest: Optimize for size. This setting enables all Faster optimizations that do not typically increase code size. It also performs further optimizations designed to reduce code size. [-Os]
  • Fastest, Aggressive Optimizations: This setting enables Fastest but also enables aggressive optimizations that may break strict standards compliance but should work well on well-behaved code. [-Ofast]
  • Smallest, Aggressive Size Optimizations: This setting enables additional size savings by isolating repetitive code patterns into a compiler generated function. [-Oz]

引自:https://developer.apple.com/documentation/xcode/build-settings-reference#Optimization-Level

根据Xcode官方给的文档,可以看出不同的优化级别其实就是对代码执行速度和二进制文件大小进行不同程度的优化。

  • Dev默认的优化级别是None[-O0],不做优化编译,这是为了更快的编译速度(通常来说,优化程度越高编译越耗时)和方便断点调试
  • Pre/Prod默认的优化级别是Fastest, Smallest[-Os],在启用Faster[-O2]中所有不会增加代码大小优化项的同时对代码大小进一步优化,用更慢的编译速度换来代码执行速度的提升和二进制文件大小的减小

接下来就是验证__OPTIMIZE__的定义是不是真的受这个设置影响。将Dev和Pre/Prod中的优化级别设置互换:

经测试,Dev环境下出现了预定义宏__OPTIMIZE__,Pre/Prod环境反而没有了。这结果和预期一致,可以说影响__OPTIMIZE__定义的优化编译设置就是Optimization Level

5. Pods工程的优化编译设置

分析到这,已经可以确定Pods工程的优化编译设置肯定是有问题的,接下来就是验证这一想法。在Pods工程的项目构建设置搜索关键词Optimization Level

从图中可以看到,和主工程的项目构建设置相比,多了Debug和Release构建配置,这两个项目创建时自带的构建配置明明已经删掉了,在Pods工程竟然还存在。最重要的是,Dev的优化级别竟然是Fastest, Smallest[-Os],怪不得Pod库在Dev环境也会有预定义宏__OPTIMIZE__,将优化级别手动修改为None[-O0],再次测试Pod库中的日志输出,一切正常!除了修改项目的构建设置,也可以通过修改目标中Pod库的构建设置实现日志正常输出。

补充两点关于构建设置的知识:

  1. 目标的构建设置继承于项目的构建设置
  2. 目标的构建设置优先级高于项目的构建设置

简单来说,当目标构建设置没有自定义时,会和项目构建设置保持一致;当目标构建设置存在自定义时,以目标构建设置为准。在Xcode中想要更加直观地查看构建设置层次,只需要选择Levels过滤器:

如果要关闭层次显示,选择Combined即可。上图中Resolved列的值是Xcode构建时最终生效的值。想详细了解更多关于这方面的内容,请看Xcode官方文档介绍。

用手动的方式修改构建设置,每次执行pod install都会重置,这肯定不是我们所想要的,那有没有更好的办法呢?

6. 自动修正Pods工程的优化编译设置

我们可以利用post_install这个Hook实现自动修正Pods工程的优化编译设置。

在这之前,首先要知道优化编译设置在配置文件中的名称,这个不难,在前面关于Optimization Level的官方文档中已经说了是GCC_OPTIMIZATION_LEVEL。然后是确定不同的优化级别设置在配置文件中对应具体什么值,找到Xcode项目根目录下的[xxx.xcodeproj]文件 -> 右键显示包内容 -> 打开[project.pbxproj]文件,搜索GCC_OPTIMIZATION_LEVEL,可以发现None[-O0]对应的值是0Fastest, Smallest[-Os]对应的是s(如果你没修改过优化级别设置,可能搜索不到,因为不设置时默认优化级别就是这个)。

准备就绪,接下来找到Pods项目(pods_project)中的构建配置数组(build_configurations),遍历找到Dev环境的构建配置,最后修改构建设置(build_settings)中的GCC_OPTIMIZATION_LEVEL的值为0

Podfile文件中加上:

post_install do |installer|installer.pods_project.build_configurations.each do |config|if config.name == 'Dev'config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0'breakendend
end

加上这个Hook后,每次执行pod install都会自动修正Pods工程的优化编译设置。

问题分析到此就结束了吗?当然没有,还有一个很关键的疑问没有解决,为什么Pods工程的多环境配置没有和主工程保持一致?

7. Pods工程的多环境配置分析

Pods工程的多环境配置相比主工程,一是多了Debug和Release构建配置,二是存在构建设置不一致的情况。

前面我们通过拿到Pods项目对象和构建配置数组实现了自动修正优化编译设置,既然有构建配置数组,那CocoaPods中肯定有地方往这个数组里面添加Dev等构建配置,在添加构建配置的位置打断点调试不就能知道Debug和Release的构建配置是怎么来的。现在思路有了,接下来就是找到合适的地方打断点调试。

7.1. pods_project所属的类

想知道pods_project对象所属的类不难,只需要在代码块中加上一行代码:

post_install do |installer|p installer.pods_project...
end

这行代码的作用就是打印pods_project对象,执行pod install输出:

#<Pod::Project> path:`xxx/app/Pods/Pods.xcodeproj` UUID:`xxx`

<Pod::Project>就是我们需要的,这表示pods_project对象所属的类是Pod模块下的Project类

7.2. build_configurations的定义

打开CocoaPods文档,找到Project类搜索build_configurations

没有找到关于build_configurations的定义,那可能是在Xcodeproj::Project父类中定义的。打开Xcodeproj文档,找到Project类搜索build_configurations

从定义源码可以看到,build_configurations原来不是数组,是一个方法,不过方法的返回值确实是数组。

7.3. add_build_configuration的定义

定义是找到了,但是往数组里面添加构建配置的位置没搜索到。不过,既然获取数组功能封装成了方法,我猜往数组里面添加对象应该也封装成了方法,方法命名很可能是add_build_configuration。果不其然,真有这个方法:

这部分代码对于本篇文章很重要,后面再具体分析。简单来说,这部分代码的作用就是创建构建配置对象存到数组并返回这个构建配置对象,如果在数组中已经存在同名的构建配置对象,那么直接返回不重新创建。

那直接在这里打断点调试?别急,回到CocoaPods文档,搜索这个方法,你会发现CocoaPods的Project类重写了方法:

# File 'lib/cocoapods/project.rb', line 374def add_build_configuration(name, type)build_configuration = supersettings = build_configuration.build_settingsdefinitions = settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)']defines = [defininition_for_build_configuration(name)]defines << 'DEBUG' if type == :debugdefines.each do |define|value = "#{define}=1"unless definitions.include?(value)definitions.unshift(value)endendsettings['GCC_PREPROCESSOR_DEFINITIONS'] = definitionsif type == :debugsettings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = 'DEBUG'endbuild_configuration
end

7.4. add_build_configuration的调试

开始调试前需要先搭建一个用于调试CocoaPods源码的环境,如果你没搭建过,可以参考这篇文章CocoaPods - 源码调试环境搭建。

打开CocoaPods项目源码下的lib/cocoapods/project.rb文件,找到add_build_configuration方法打上断点,运行调试:

从图中可以看到,首个添加的构建配置的名称是Debug,暂时不往下继续执行,先通过调用堆栈中的Xcodeproj::Project#initialize_from_scratch ...定位add_build_configuration方法的调用位置。调用位置是在父类Xcodeproj::Project的初始化方法中,源码如下:

def initialize_from_scratch@archive_version =  Constants::LAST_KNOWN_ARCHIVE_VERSION.to_s@classes         =  {}root_object.remove_referrer(self) if root_object@root_object = new(PBXProject)root_object.add_referrer(self)root_object.main_group = new(PBXGroup)root_object.product_ref_group = root_object.main_group.new_group('Products')config_list = new(XCConfigurationList)root_object.build_configuration_list = config_listconfig_list.default_configuration_name = 'Release'config_list.default_configuration_is_visible = '0'add_build_configuration('Debug', :debug)add_build_configuration('Release', :release)new_group('Frameworks')
end

这下可以解释为什么Pods工程会比主工程多Debug和Release构建配置,原来在pods_project对象初始化时会默认创建。

继续调试分析,代码的作用都一一做了说明:

def add_build_configuration(name, type)# 调用父类的add_build_configuration方法获取构建配置对象build_configuration = super# 获取构建配置中的构建设置。settings是一个Hash对象,Hash可以看作是Map或字典settings = build_configuration.build_settings# 获取key为GCC_PREPROCESSOR_DEFINITIONS的值,如果没获取到(为nil,即为false)则将['$(inherited)']赋值给definitionsdefinitions = settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)']# 创建defines数组# defininition_for_build_configuration的定义如下:# def defininition_for_build_configuration(name)#   "POD_CONFIGURATION_#{name.underscore}".gsub(/[^a-zA-Z0-9_]/, '_').upcase# end# 作用是根据构建配置名称生成类似这样的POD_CONFIGURATION_DEBUG字符串defines = [defininition_for_build_configuration(name)]# 如果配置类型为:debug,则将DEBUG字符串插入到defines数组末尾# :debug是一个符号(Symbol),符号可以看作是字符串常量defines << 'DEBUG' if type == :debug# 遍历defines数组defines.each do |define|# 拼接字符串,例如将DEBUG变为DEBUG=1value = "#{define}=1"# 如果definitions数组不包含value,则将value插入到defines数组头部unless definitions.include?(value)definitions.unshift(value)endend# 将修改保存到构建设置settings['GCC_PREPROCESSOR_DEFINITIONS'] = definitions# 如果配置类型是:debug,修改SWIFT_ACTIVE_COMPILATION_CONDITIONS的值为DEBUGif type == :debugsettings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = 'DEBUG'end# 返回构建配置对象build_configuration
end

调试过程中,发现一个奇怪的问题,Dev/Pre/Prod构建配置的配置类型都是:release,配置类型为:release时,settings对象中不存在GCC_OPTIMIZATION_LEVEL这个设置。如果构建设置直接复制于主工程,应该就没有这个问题了,但现在明显不是,CocoaPods很可能根据配置类型创建了新的构建设置,这引出一个新的疑问,构建配置的类型又是怎么决定的?

继续分析前,先确认一下GCC_PREPROCESSOR_DEFINITIONS是不是也有问题。开发中会涉及到这个构建设置的用法一般是在判断是否有定义DEBUG的时候,例如:

#ifdef DEBUG
#define NSLog(...) NSLog(__VA_ARGS__)
#else
#define NSLog(...)
#endif

实测Dev环境编译运行项目时DEBUG没定义,在Xcode中查看也确实是没定义DEBUG=1

所以在多环境配置下,除了预定义宏__OPTIMIZE__有问题,DEBUG定义也有问题,甚至一些本篇文章没提到的也有问题。

7.5. 构建设置的来源

要确定构建设置的来源,首先要找到创建构建配置的地方,也就是前面出现过的Xcodeproj::Project类中的add_build_configuration方法。创建构建配置对象的代码及说明:

def add_build_configuration(name, type)build_configuration_list = root_object.build_configuration_list# 判断是否已经创建过if build_configuration = build_configuration_list[name]# 创建过直接返回构建配置对象build_configurationelse# 创建构建配置对象build_configuration = new(XCBuildConfiguration)# 设置构建配置名称build_configuration.name = name# 获取默认构建设置# 默认构建设置按构建配置类型分三种(:all/:release/:debug),其中:all类型的构建设置是通用的common_settings = Constants::PROJECT_DEFAULT_BUILD_SETTINGS# 深拷贝一份通用构建设置给settingssettings = ProjectHelper.deep_dup(common_settings[:all])# 深拷贝一份对应配置类型的构建设置合并到settingssettings.merge!(ProjectHelper.deep_dup(common_settings[type]))# 设置构建设置build_configuration.build_settings = settings# 保存构建配置对象并返回build_configuration_list.build_configurations << build_configurationbuild_configurationend
end

PROJECT_DEFAULT_BUILD_SETTINGS的定义(折叠了:all类型,完整定义请看Xcodeproj::Constants):

所以现在可以确定CocoaPods会根据配置类型创建新的构建设置,如果配置类型是对的,按这个方式创建的构建设置应该是没问题的。目前就剩最后一个问题,构建配置的类型是怎么决定的?

7.6. 配置类型的决定

重新运行前面的调试,跳过初始化时的两次调用add_build_configuration方法(跳过创建Debug/Release构建配置),在调用堆栈找到新的调用位置:

关键在于build_configurations这个Hash对象,里面存放着构建配置名称和类型的映射关系。通过不断打断点和利用调用堆栈,一直追寻build_configurations,终于找到了build_configurations初始化的地方。TargetInspector类(位于lib/cocoapods/installer/analyzer/target_inspector.rb)中的compute_results方法:

def compute_results(user_project)raise ArgumentError, 'Cannot compute results without a user project set' unless user_projecttargets = compute_targets(user_project)project_target_uuids = targets.map(&:uuid)build_configurations = compute_build_configurations(targets)platform = compute_platform(targets)archs = compute_archs(targets)swift_version = compute_swift_version_from_targets(targets)result = TargetInspectionResult.new(target_definition, user_project, project_target_uuids,build_configurations, platform, archs)result.target_definition.swift_version = swift_versionresult
end

位于同文件内的compute_build_configurations方法:

def compute_build_configurations(user_targets)if user_targetsuser_targets.flat_map { |t| t.build_configurations.map(&:name) }.each_with_object({}) do |name, hash|hash[name] = name == 'Debug' ? :debug : :releaseend.merge(target_definition.build_configurations || {})elsetarget_definition.build_configurations || {}end
end

观察compute_build_configurations方法可以发现,里面最关键的代码应该是:

hash[name] = name == 'Debug' ? :debug : :release

原来只有当构建配置的名称为Debug时,才会被设置为:debug配置类型,其余均默认为:release配置类型。

问题的根源总算找到了,那能从根源上解决该问题吗?直接修改CocoaPods源码肯定是不靠谱的,应该还有其他办法。

7.7. 配置类型的设置

compute_build_configurations方法继续分析,这段代码:

user_targets.flat_map { |t| t.build_configurations.map(&:name) }.each_with_object({}) do |name, hash|hash[name] = name == 'Debug' ? :debug : :release
end.merge(target_definition.build_configurations || {})

等价于:

build_configurations = user_targets.flat_map { |t| t.build_configurations.map(&:name) }.each_with_object({}) do |name, hash|hash[name] = name == 'Debug' ? :debug : :release
end
build_configurations.merge(target_definition.build_configurations || {})

user_targets...end返回的Hash对象调用了合并方法,如果target_definition.build_configurationsnil,则合并一个空的Hash对象{}。如果key重复了,合并方法会用新的value覆盖原来的value。如果有方法能设置target_definition.build_configurations的值,那问题是不是就从根源上解决了?

通过单步调试,来到了TargetDefinition类(位于lib/cocoapods-core/podfile/target_definition.rb文件)中的build_configurations方法:

def build_configurationsif root?get_hash_value('build_configurations')elseget_hash_value('build_configurations') || parent.build_configurationsend
end

get_hash_value的定义:

def get_hash_value(key, base_value = nil)# 检查key,不支持的key会抛异常# HASH_KEYS是一个字符串数组,里面包含build_configurationsunless HASH_KEYS.include?(key)raise StandardError, "Unsupported hash key `#{key}`"end# 如果key对应的value为nil,则将base_value赋值给internal_hash[key]# .nil?方法用于判断对象是否存在internal_hash[key] = base_value if internal_hash[key].nil?# 返回key对应的valueinternal_hash[key]
end

看来internal_hash是关键,从调试结果来看,get_hash_value返回值一直是nil。搜索target_definition.rb文件找到internal_hash初始化位置:

def initialize(name, parent, internal_hash = nil)@internal_hash = internal_hash || {}@parent = parent@children = []@label = nilself.name ||= name# 如果parent是一个TargetDefinition对象,则将当前TargetDefinition对象存入到parent的children数组if parent.is_a?(TargetDefinition)parent.children << selfend
end

internal_hash变量名前面加个@表示这是一个实例变量。在这个构造方法中打上断点,重新运行调试。首次进入断点,internal_hash是nil,通过调用堆栈找到调用的地方,来到了Podfile类的构造方法:

构造方法中默认创建了名为PodsTargetDefinition对象(后面用Pods-TargetDefinition指代该对象),并将该对象设为current_target_definition。接着执行instance_eval(&block),猜猜这是用来干什么的?继续单步调试下去,最终会发现来到了Xcode项目里面的Podfile文件:

继续调试,来到了DSL模块(位于lib/cocoapods-core/podfile/dsl.rb)中的target方法:

def target(name, options = nil)if optionsraise Informative, "Unsupported options `#{options}` for " \"target `#{name}`."endparent = current_target_definitiondefinition = TargetDefinition.new(name, parent)self.current_target_definition = definitionyield if block_given?
ensureself.current_target_definition = parent
end

原来Podfile文件中的target配置是方法调用(其实不止这个,在Podfile文件中的配置都能在DSL模块中找到对应的方法),每调用一次都会创建一个TargetDefinition对象。如果target方法后面有代码块(do...end),那么block_given?将为true,同时yield关键字的作用是调用代码块,所以yield if block_given?这行代码的作用就是如果存在代码块就调用。

单步调试进入到target方法的代码块中,use_frameworks!也是一个方法,同样位于DSL模块:

def use_frameworks!(option = true)current_target_definition.use_frameworks!(option)
end

current_target_definition调用的use_frameworks!TargetDefinition类中的方法:

def use_frameworks!(option = true)value = case optionwhen true, falseoption ? BuildType.dynamic_framework : BuildType.static_librarywhen HashBuildType.new(:linkage => option.fetch(:linkage), :packaging => :framework)elseraise ArgumentError, "Got `#{option.inspect}`, should be a boolean or hash."endset_hash_value('uses_frameworks', value.to_hash)
end

最终通过set_hash_value方法完成了设置:

def set_hash_value(key, value)unless HASH_KEYS.include?(key)raise StandardError, "Unsupported hash key `#{key}`"endinternal_hash[key] = value
end

分析到这,我突然想起来前面好像没有确认TargetDefinition类中有没有设置build_configurations的方法,搜索一番,找到了这个:

def build_configurations=(hash)set_hash_value('build_configurations', hash) unless hash.empty?
end

Ruby方法名后面加=是很常见的做法,这样做的好处是,可以像变量赋值那样(build_configurations = xxx)调用方法。

经过前面的调试,大概可以猜到,build_configurations=方法调用的地方应该和use_frameworks!方法一样,都在DSL模块中。在DSL模块中搜索build_configurations,找到这个:

def project(path, build_configurations = {})current_target_definition.user_project_path = pathcurrent_target_definition.build_configurations = build_configurations
end

竟然是project配置!众里寻他千百度。蓦然回首,那人却在,灯火阑珊处。

7.8. project配置的使用

如果你看过Flutter项目中的iOS部分,那应该在Podfile文件中看到过这个:

project 'Runner', {'Debug' => :debug,'Profile' => :release,'Release' => :release,
}

Flutter创建项目时已经默认帮你设置好了构建配置的类型,其中Runner是Xcode项目的名称。参照这个,在用于测试的Xcode项目中加上project配置:

project 'app', {'Dev' => :debug,'Pre' => :release,'Prod' => :release,
}

重新执行pod install命令,不管是GCC_OPTIMIZATION_LEVEL,还是GCC_PREPROCESSOR_DEFINITIONS,一切正常!如果想了解更多关于project配置的使用,请看CocoaPods-Core文档。

如果你看了CocoaPods-Core文档中的示例,不知道有没有这样一个疑问,为什么示例中的project配置放在了target方法的代码块里面,这和放在外面有什么区别吗?

# This Target can be found in a Xcode project called `FastGPS`
target 'MyGPSApp' doproject 'FastGPS'...
end# Same Podfile, multiple Xcodeprojects
target 'MyNotesApp' doproject 'FastNotes'...
end

7.9. Podfile配置的作用范围

根据前面的分析,可以知道Podfile的配置实际是调用DSL模块中的方法,进而配置current_target_definition指向的TargetDefinition对象。结合断点调试,current_target_definition变化如下:

  1. 首先是Podfile对象初始化时默认创建的Pods-TargetDefinition
def initialize(defined_in_file = nil, internal_hash = {}, &block)self.defined_in_file = defined_in_file@internal_hash = internal_hashif block# self是Podfile对象,Pods-TargetDefinition的parent指向了Podfile对象default_target_def = TargetDefinition.new('Pods', self)# 设置为抽象目标,这意味着Pods工程不会引入该target(在TARGETS列表中不会有这个target)# 抽象目标的好处体现在作用范围上,继续看下去就明白了default_target_def.abstract = true@root_target_definitions = [default_target_def]# current_target_definition实例变量初始化@current_target_definition = default_target_definstance_eval(&block)else@root_target_definitions = []end
end
  1. 然后是执行到target方法时,会临时指向新创建的TargetDefinition对象,方法执行结束后恢复原来的指向
def target(name, options = nil)if optionsraise Informative, "Unsupported options `#{options}` for " \"target `#{name}`."end# 如果是嵌套的target方法调用,parent指向的是上一层的TargetDefinition对象,反之指向的是名为Pods的TargetDefinition对象parent = current_target_definitiondefinition = TargetDefinition.new(name, parent)# 执行代码块前设置为刚创建的TargetDefinition对象,如果代码块内有嵌套的target方法,parent将为刚创建的TargetDefinition对象self.current_target_definition = definitionyield if block_given?
# ensure关键字的作用是确保后面的代码一定会执行,不会因为前面抛异常而终止(执行代码块可能会抛异常)
ensure# 恢复到target方法执行前指向的对象self.current_target_definition = parent
end

根据以上两点变化,我们可以知道,在Podfile文件中,如果project配置放在target代码块外面(不管在target配置的前面还是后面),构建配置的类型将会设置到Pods-TargetDefinition,作用范围将会是全部的target配置;如果放在target代码块里面,构建配置的类型将会设置到当前的TargetDefinition对象,作用范围只限于当前的target配置及嵌套的子target配置。

再继续简单补充一些内容方便理解。project配置的build_configurations对应的获取方法是这样的:

def build_configurationsif root?get_hash_value('build_configurations')elseget_hash_value('build_configurations') || parent.build_configurationsend
end

代码里出现的root?是一个方法:

def root?parent.is_a?(Podfile) || parent.nil?
end

这方法用来判断当前是否为根TargetDefinition对象,判断条件很简单,如果parent指向了Podfile对象或者parentnil,那当前就是根TargetDefinition对象。那么显而易见,初始化时parent参数传入Podfile对象的Pods TargetDefinition就是一个TargetDefinition对象。

所以如果当前get_hash_value('build_configurations')返回nil,会继续调用parentbuild_configurations方法获取,就这样一级一级找上去,最终来到根TargetDefinition对象Pods-TargetDefinitionbuild_configurations方法,这时还获取不到那就是真获取不到。

关于抽象目标的作用,从前面的分析不难看出,能限定Podfile配置的作用范围。CocoaPods也提供了专门的配置(abstract_target)用于创建抽象目标,配置对应的具体方法还是在DSL模块中:

def abstract_target(name)target(name) doabstract!yield if block_given?end
enddef abstract!(abstract = true)current_target_definition.abstract = abstract
end

如果你想在Podfile文件中让两个target共用一些配置,可以这样做:

# 名称随意取
abstract_target 'Shows' do# 两个target都依赖这个库pod 'ShowsKit'target 'ShowsiOS' dopod 'ShowWebAuth'endtarget 'ShowsTV' dopod 'ShowTVAuth'end
end

以上示例来自CocoaPods-Core文档,有所删减改动。

pod配置的依赖对应的获取方法:

def dependenciesif exclusive?non_inherited_dependencieselse# non_inherited_dependencies:当前target中的依赖数组# parent.dependencies:调用parent的dependencies方法获取继承的依赖数组non_inherited_dependencies + parent.dependenciesend
end

解决方案

在Xcode项目的Podfile文件中加入以下配置:

project '【Xcode项目名称】', {'【Degbug环境的配置名称】' => :debug,'【Release环境的配置名称】' => :release,
}

按项目的实际情况填写并替换【...名称】,映射不限个数,实际有多少个环境配置就写多少个。

例如有一个叫app的Xcode项目有三个环境配置,配置名称分别是Dev、Pre和Prod,其中Dev属于Debug环境,Pre和Prod属于Release环境,那么需要加入的配置是这样的:

project 'app', {'Dev' => :debug,'Pre' => :release,'Prod' => :release,
}

当然,如果你有看前面的问题分析,就会知道这配置还可以继续精简。因为自定义的环境配置全部都会被默认映射为:release类型,所以可以精简为:

project 'app', {'Dev' => :debug,
}

总结

本篇文章中的多环境配置是通过自定义构建配置实现的,多环境配置不止这一种方式,还可以通过多Target实现,不过如果你是通过多Target实现的多环境应该不会碰到这个问题,前提是你没自定义构建配置(保持默认的Debug/Release配置)。文中出现了比较多的Ruby代码,重要的部分我都尽量做了说明,如果你不熟悉Ruby语法,建议亲自上手调试CocoaPods源码,关于调试环境的搭建请看这篇文章CocoaPods - 源码调试环境搭建。

断断续续写了几个晚上终于写完啦

Flutter iOS问题记录 - 多环境配置下Pod库的宏定义失效相关推荐

  1. xampp环境配置下出现的问题解决 — mysqli_real_connect(): (HY000/1045): Access denied for user ‘root‘@‘localhost‘

    XAMPP 环境配置下出现的问题 版本 :xampp 7.3.1      今天,柳妹在虚拟机上面又一次搭建xampp的环境的时候,在mysql的管理界面对于root@localhost 管理用户进行 ...

  2. Linux下conda环境配置及第三方库安装

    conda的好处在于可以针对不同的python项目,为其设定专有的环境.每次运行不同的项目时,conda可以灵活的实现环境切换,避免了一些依赖项的杂糅或是不匹配的问题. 首先安装Anaconda,Li ...

  3. Flutter iOS问题记录 - Fastlane打包的ipa包上传fir后不显示应用版本名称

    文章目录 前言 开发环境 问题描述 问题分析 解决方案 总结 最后 前言 看到又是应用版本名称的问题,我心里已经大概知道是什么原因了. 开发环境 Flutter: 3.3.5 Dart: 2.18.2 ...

  4. iOS开发 React-native开发环境配置

    英文原文地址:https://facebook.github.io/react-native/docs/getting-started.html 1.安装 homebrew (若已经安装可跳过此步) ...

  5. linux下查看系统自身宏定义

    跨平台程序,经常要用到区分系统的宏定义,比如windows中的WIN32, WIN64, Linux中的 unix, linux等等系统自定义宏 那么在linux下面,怎么查看系统有哪些自定义宏咧,用 ...

  6. [转]OpenGL超级宝典 5e 环境配置

    OpenGL超级宝典(第五版)环境配置 1.各种库的配置 (1)glew 下载:https://sourceforge.net/projects/glew/files/glew/1.7.0/glew- ...

  7. python html5游戏_10天制作html5游戏-卡坦岛-第一天,环境配置

    卡坦岛是一款类似<文明>系列游戏的桌游,玩家要在由六边形组成的地图上发展自己的定居地与城市,以此累积胜利点,最先达到10点胜利点的玩家将获得游戏胜利.本系列专栏就将从零开始,在浏览器上实现 ...

  8. Fluent UDF编译环境配置 VS2019

    Fluent UDF编译环境配置 VS2019 环境配置 问题记录 继续记录调试过程 仅用一个host 仅用一个node 两个都放进去 换个电脑继续报错 记录错误 环境配置 生成PATH文件的,有的没 ...

  9. OpenGL超级宝典 5e 环境配置

    OpenGL超级宝典(第五版)环境配置 1.各种库的配置 (1)glew 下载:https://sourceforge.net/projects/glew/files/glew/1.7.0/glew- ...

最新文章

  1. MVC3 基本业务开发框架(强转)
  2. C/C++堆、栈及静态数据区详解
  3. H3 BPM报销流程开发示例
  4. T-SQL 之 多表联合更新
  5. 一个有趣的问题,讨论讨论
  6. 我的代码第一次运行时的样子
  7. 其他的AdapterView——Spinner
  8. centos6.5 redis3 开机自动启动命令设置
  9. HTML5网页语音识别功能演示
  10. 关于ie浏览器的问题
  11. xgboost安装_机器学习笔记(七)Boost算法(GDBT,AdaBoost,XGBoost)原理及实践
  12. findwindow\sendmessage向第三方软件发送消息演示
  13. 美区苹果id被禁用原因和解除限制方法
  14. 基于共现网络原理将剧本《人民的名义》人物关系社交网络可视化
  15. 五笔难拆字拆分方法汇总及详解
  16. 生活大爆炸第四季 那些精妙的台词翻译
  17. C库函数——fabs()
  18. Nodejs版本更新
  19. C#SpinWait和volatile一点温习
  20. Hadoop2.7.3 mapreduce(三)SequenceFile和MapFile 简介与应用

热门文章

  1. Python 数据处理数据挖掘(四):用户分层模型RFM
  2. 运用超链接访问自建网站
  3. 计算机特岗面试的书籍,特岗信息技术考试.docx
  4. Ag (the_silver_searcher) 安装使用
  5. 李小龙的5大敏捷教练技巧
  6. 在项目中使用8脚继电器正反转,实现控制推杆电机的伸缩。
  7. spring cloud微服务分布式云架构 - common-service 项目构建过程
  8. 5.27下周黄金行情走势预测及开盘操作策略
  9. Unity3D 实践学习1 GUI井字棋的实现
  10. 谷歌相中的怪咖:Magic Leap创始人有何不同