IOS基础流程:测试、打包与上线

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、单元测试
    • 1、认识测试用例
    • 2、控制器提供的接口方法
    • 3、准备工作
    • 4、进行逻辑测试
    • 5、进行异步测试
    • 6、性能测试
  • 二、UI测试
    • 1、UI测试的系统方法
    • 2、系统自动打印UI代码
  • 三、OCMock依赖注入
    • 1、测试Person类中的方法
    • 2、测试删除表格中的某一行
    • 3、测试MVVM模式
  • 四、shell脚本打包
    • 1、在终端运行shell脚本进行打包
    • 2、解析用于打包的shell脚本
  • 五、shell打包上线
    • 1、在终端运行shell脚本进行打包上线
    • 2、解析用于打包上线的shell脚本
  • 六、fastlane打包上线
    • 1、fastlane的介绍
    • 2、在终端运行fastlane进行打包上线
  • 七、利用 Jenkins 持续集成 iOS 项目
    • 1、为什么我们需要持续集成
    • 2、安装 Jenkins
    • 3、构建项目
    • 4、自动化打包命令
    • 5、打包完成自动化上传 fir / 蒲公英
  • 八、app 配置多个环境变量

一、单元测试

@interface UnitTestingTests : XCTestCase

@end

1、认识测试用例

a、初始化方法

每一个测试用例都会执行该方法,所以可以在这里进行初始化的操作。

- (void)setUp
{
    [super setUp];
    NSLog(@"初始化");
}
b、销毁清除方法

每一个测试用例都会执行该方法,所以可以在这里进行清除的操作。

- (void)tearDown
{
    [super tearDown];
    
    NSLog(@"销毁清除");
}
c、测试方法
- (void)testExample
{
    NSLog(@"冬天在毛衣上会摸到静电");
}
d、性能测试方法
- (void)testPerformanceExample
{
    NSLog(@"性能测试");

    [self measureBlock:^{
        NSLog(@"耗时操作");
    }];
}
e、运行测试用例
❶ 点击 Product -> Test 运行测试用例
❷ 红色表示失败,绿色则表示测试用例通过
❸ 控制台的输出结果
Test Suite 'UnitTestingTests' started at 2021-02-24 21:51:48.860
Test Case '-[UnitTestingTests testExample]' started.
2021-02-24 21:51:48.872320+0800 UnitTesting[58726:1830446] 初始化
2021-02-24 21:51:56.459236+0800 UnitTesting[58726:1830446] 冬天在毛衣上会摸到静电
2021-02-24 21:51:56.459540+0800 UnitTesting[58726:1830446] 销毁清除
Test Case '-[UnitTestingTests testExample]' passed (7.593 seconds).
Test Case '-[UnitTestingTests testPerformanceExample]' started.
2021-02-24 21:51:56.460521+0800 UnitTesting[58726:1830446] 初始化
2021-02-24 21:51:56.460686+0800 UnitTesting[58726:1830446] 性能测试
2021-02-24 21:51:56.766234+0800 UnitTesting[58726:1830446] 耗时操作
2021-02-24 21:51:56.766504+0800 UnitTesting[58726:1830446] 耗时操作
......
/Users/xiejiapei/Desktop/UnitTesting/UnitTestingTests/UnitTestingTests.m:38: Test Case '-[UnitTestingTests testPerformanceExample]' measured [Time, seconds] average: 0.000, relative standard deviation: 52.001%, values: [0.000262, 0.000140, 0.000113, 0.000134, 0.000109, 0.000105, 0.000106, 0.000176, 0.000387, 0.000316], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
2021-02-24 21:51:56.989829+0800 UnitTesting[58726:1830446] 销毁清除
Test Case '-[UnitTestingTests testPerformanceExample]' passed (0.530 seconds).
Test Suite 'UnitTestingTests' passed at 2021-02-24 21:51:56.991.
     Executed 2 tests, with 0 failures (0 unexpected) in 8.123 (8.131) seconds

2、控制器提供的接口方法

a、数字相加
- (int)getPlus:(int)num1 num2:(int)num2
{
    return num1 + num2 + 10;
}
b、加载网络数据
- (void)loadData:(void (^)(id data))dataBlock
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2];
        
        NSString *dataString = @"海子的诗";
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"更新UI");
            dataBlock(dataString);
        });
    });
}
c、打开相机
- (void)openCamera
{
   for (int i = 0; i<100; I++)
    {
       NSLog(@"摄影写真");
   }
}

3、准备工作

❶ 新建测试用例文件
❷ 声明代表ViewController的属性变量
#import <XCTest/XCTest.h>
#import "ViewController.h"

@interface ViewControllerUnitTest : XCTestCase

@property (nonatomic, strong) ViewController *vc;

@end
❸ 设置初始化与销毁控制器
- (void)setUp
{
    self.vc = [[ViewController alloc] init];
}

- (void)tearDown
{
    self.vc = nil;
}

4、进行逻辑测试

❶ 给出测试条件
int num1 = 10;
int num2 = 20;
❷ 在接口方法中使用测试条件返回计算结果
int num3 = [self.vc getPlus:num1 num2:num2];
❸ 计算结果未符合预期结果则测试失败提出警告
XCTAssertEqual(num3, 30,@"加法接口测试失败");
❹ 由于预期结果为40,所以测试失败,控制台输出结果
Test Case '-[ViewControllerUnitTest testExample]' started.
/Users/xiejiapei/Desktop/UnitTestingDemo/UnitTestingDemoTests/ViewControllerUnitTest.m:39: error: -[ViewControllerUnitTest testExample] : ((num3) equal to (30)) failed: ("40") is not equal to ("30") - 加法接口测试失败

5、进行异步测试

- (void)testAsync
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"不符合期望时长"];

    [self.vc loadData:^(id data) {
        XCTAssertNotNil(data);
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
        NSLog(@"超时错误信息 = %@",error);
    }];
}
❶ 由于未能达到异步函数在2秒之内执行完毕的期望故报错
❷ 倘若符合期望执行完毕的时长控制台则会输出如下信息
Test Case '-[ViewControllerUnitTest testAsync]' started.
2021-02-25 20:48:23.432118+0800 UnitTestingDemo[60531:1932975] 更新UI
2021-02-25 20:48:23.433121+0800 UnitTestingDemo[60531:1932975] 超时错误信息 = (null)
Test Case '-[ViewControllerUnitTest testAsync]' passed (5.543 seconds).

6、进行性能测试

- (void)testPerformanceExample
{
    [self measureBlock:^{
        [self.vc openCamera];
    }];
}
❶ 性能测试结果
❷ 倘若实际运行效果与期望性能要求相差太大则会报错
❸ 局部性能测试
- (void)testPerformanceLocal
{
    [self measureMetrics:@[XCTPerformanceMetric_WallClockTime] automaticallyStartMeasuring:NO forBlock:^{
        [self.vc openCamera];
        
        [self startMeasuring];
        [self.vc openCamera];
        [self stopMeasuring];
    }];
}

二、UI测试

无论是单元测试还是UI测试都不会放入到打包文件之中。


1、UI测试的系统方法

a、初始化方法
- (void)setUp
{
    // 当发生错误的时候立即停止UI测试
    self.continueAfterFailure = NO;
    
    NSLog(@"UI测试-初始化");
}
b、销毁方法
- (void)tearDown
{
    NSLog(@"UI测试-销毁");
}
c、逻辑测试
- (void)testExample
{
    // UI测试需要启动APP
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
}
d、性能测试
- (void)testLaunchPerformance
{
    // 检测APP的运行时长
    [self measureWithMetrics:@[[[XCTApplicationLaunchMetric alloc] init]] block:^{
        [[[XCUIApplication alloc] init] launch];
    }];
}

2、系统自动打印UI代码

当我们在界面上点击UI控件进行操作的时候,系统会在测试方法中自动打印相应代码记录下用户进行的每一个操作步骤。

// 性能测试
- (void)testLaunchPerformance
{
    XCUIApplication *app = [[XCUIApplication alloc] init];
    
    XCUIElement *element = [[[[[[[[[[XCUIApplication alloc] init] childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
    XCUIElement *textField = [[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0];
    [textField tap];
}

然后我们再将性能测试方法中生成的这串代码放到逻辑测试中去,并且添加 [app launch];让APP自动运行,再添加[textField typeText:@"xiejiapei"];让键盘自动输入文本,这样就让人工智能自动进行了UI测试。

// 逻辑测试
- (void)testExample
{
    // UI测试需要启动APP
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
    
    XCUIElement *element = [[[[[[[[[[XCUIApplication alloc] init] childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
    XCUIElement *textField = [[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0];
    [textField tap];
    [textField typeText:@"xiejiapei"];
}

三、OCMock依赖注入

1、测试Person类中的方法

a、导入OCMock框架

需要注意的是该框架是嵌套导入到OCMockDemoTests中,而不是直接导入到OCMockDemo中,如果直接导入则无法在OCMockDemoTests找到该框架。

target 'OCMockDemo' do
  target 'OCMockDemoTests' do
    inherit! :search_paths
    pod 'OCMock'
  end
end
b、创建一个用于检测的Person类
@implementation Person

- (NSString *)getPersonName
{
    return @"OCMock";
}

@end
c、实现对Person类中的方法进行检测的测试用例
- (void)testPerson
{    
    Person *person = [[Person alloc] init];
    
    // 创建一个mock对象,相当于复制了Person类
    Person *mock_p = OCMClassMock([Person class]);
    
    // 可以给这个mock对象的方法设置预设的参数和返回值
    OCMStub([mock_p getPersonName]).andReturn(@"OCMock");
    
    // 用这个预设的值和实际的值比较看二者是否相等
    XCTAssertEqualObjects([mock_p getPersonName], [person getPersonName],@"===");
}

倘若不相等则会报错


2、测试删除表格中的某一行

- (void)testTableViewDelete
{
    ViewController *vc = [[ViewController alloc] init];
    id tableMock = OCMClassMock([UITableView class]);
    
    // 创建数据
    vc.dataArray = [NSMutableArray arrayWithCapacity:1];
    for (int i = 0; i<10; I++)
    {
        [vc.dataArray insertObject:[NSString stringWithFormat:@"第%d个数据",i] atIndex:0];
    }
    
    // 创建删除位置
    NSIndexPath *path = [NSIndexPath indexPathForRow:29 inSection:0];
    
    // 进行关联
    [vc tableView:tableMock commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:path];
    
    OCMVerify([tableMock deleteRowsAtIndexPaths:@[path] withRowAnimation:UITableViewRowAnimationFade]);
}

由于我们创建的测试数据只有10条,但是这里我们想要删除的位置是29自然超出了能力范围,所以理所当然地报错了。


3、测试MVVM模式

a、MVVM各模块
Model
@interface Dog : NSObject

@property(nonatomic, copy) NSString *userName;

@end
ViewModel
@implementation Manager

- (NSArray *)fetchDogs
{
    return @[];
}

@end
View
@implementation IDCardView

- (void)dogIDCardView:(Dog *)dog
{
    NSLog(@"为🐶创建身份证");
}

@end
ViewController
@implementation OCMockViewController

// 更新每条狗的身份证信息
- (void)updateIDCardView
{
    NSArray *dogs = [self.manager fetchDogs];
    if (dogs != nil)
    {
        for (Dog *dog in dogs)
        {
            [self.idCardView dogIDCardView:dog];
        }
    }
}

@end
b、书写MVVM的测试用例
- (void)testMVVMDemo
{
    OCMockViewController *vc = [[OCMockViewController alloc] init];
    
    // 创建用于获取数据的工具类
    id manager = OCMClassMock([Manager class]);
    
    // 创建测试数据 (model)
    Dog *dog1 = [[Dog alloc] init];
    dog1.userName = @"旺财";
    Dog *dog2 = [[Dog alloc] init];
    dog2.userName = @"来福";
    NSArray *array = @[dog1,dog2];
    OCMStub([manager fetchDogs]).andReturn(array);
    
    // 更新UI (view)
    id cardView = OCMClassMock([IDCardView class]);
    vc.idCardView = cardView;
    
    // 验证vc中的方法是否有效
    OCMVerify([vc updateIDCardView]);
}

四、shell脚本打包

1、在终端运行shell脚本进行打包

❶ 由于我没有开发者账户,所以只能使用苹果自带的自动生成证书功能
❷ 在终端输入运行shell脚本的命令
xiejiapei@MacBook-Pro shell脚本打包 % ./xcodebuild.sh
❸ shell脚本开始运行进行编译和打包
*** 正在 清理工程 ***
*** 清理完成 ***
*** 正在 编译工程 For Release
*** 编译完成 ***
*** 正在 打包 ***
...
❹ 由于我没有开发者账户所以无法生成能够打包的证书便会报错
2021-02-27 17:26:26.986 xcodebuild[70982:2342320] [MT] IDEDistribution: -[IDEDistributionLogging _createLoggingBundleAtPath:]: Created bundle at path '/var/folders/4j/8sf803t53794f1rjm67xzr1r0000gn/T/shell脚本打包_2021-02-27_17-26-26.983.xcdistributionlogs'.
error: exportArchive: No profiles for 'com.xiejiapei' were found
❺ 倘若有开发者账户则会在目录下生成ipa包

2、解析用于打包的shell脚本

两个文件成对出现
a、配置工程名称等
❶ 工程名
project_name=shell脚本打包
❷ 打包模式 Debug/Release
development_mode=Release
❸ scheme名
scheme_name=shell脚本打包

b、配置文件路径
❶ plist文件所在路径
exportOptionsPlistPath=./DevelopmentExportOptionsPlist.plist
❷ 导出.ipa文件所在路径
exportFilePath=~/Desktop/$project_name-ipa
❸ plist文件中需要配置APPID、描述文件和打包方法

c、清理工程
echo '*** 正在 清理工程 ***'
#xcodebuild \
#clean -configuration ${development_mode} -quiet  || exit 
echo '*** 清理完成 ***'

d、编译工程
echo '*** 正在 编译工程 For '${development_mode}
xcodebuild \
archive -project ${project_name}.xcodeproj \
-scheme ${scheme_name} \
-configuration ${development_mode} \
-archivePath build/${project_name}.xcarchive -quiet  || exit
echo '*** 编译完成 ***'

e、进行打包
xcodebuild -exportArchive -archivePath build/${project_name}.xcarchive \
-configuration ${development_mode} \
-exportPath ${exportFilePath} \
-exportOptionsPlist ${exportOptionsPlistPath} \
-quiet || exit

f、删除build包
if [[ -d build ]]; then
   rm -rf build -r
fi

倘若将这段代码注释掉不再删除build包则会在目录下生成shell脚本打包.xcarchive文件。

显示其内容会发现有以下文件夹。

打开其中的Product文件夹后会发现有一个Applications,其就是我们编译后的APP,ipa包就是由其产生而来。


g、上传.ipa文件
if [ -e $exportFilePath/$scheme_name.ipa ]; then
    echo "*** .ipa文件已导出 ***"
    cd ${exportFilePath}
    echo "*** 开始上传.ipa文件 ***"
    #此处上传分发应用
    echo "*** .ipa文件上传成功 ***"
else
    echo "*** 创建.ipa文件失败 ***"
fi
echo '*** 打包完成 ***'

五、shell打包上线

1、在终端运行shell脚本进行打包上线

a、生成ipa包
❶ 既然是要发布,那么便不能再使用用于真机测试的自动生成证书描述文件功能,而应该去使用开发者账号生成的发布证书,因为我实际没有该证书(😂),所以这里报错了。
❷ 在终端输入运行shell脚本的命令
xiejiapei@MacBook-Pro shell打包上线 % ./shell.sh
❸ 接下来终端就会询问我们想发布的方式,输入1表示APP要上线到APPStore,输入2表示只是企业内部使用
请输入你想发布的方式 ? [ 1:app-store 2:ad-hoc] 
1
❹ 然后就开始清理并编译工程
///-----------
/// 正在清理工程
///-----------
///--------
/// 清理完成
///--------

///-----------
/// 正在编译工程:Release
///-----------
❺ 当然这里我又由于没有开发者证书出错了
xcodebuild: error: Scheme shell打包上线 is not currently configured for the archive action.
❻ 理想情况大概如下所示
///--------
/// 编译完成
///--------

///----------
/// 开始ipa打包
///----------

ipa包导出路径

///----------
/// ipa包已导出
///----------

///----------
/// 打包ipa完成
///----------

///----------
/// 开始发布ipa包
///----------
❼ 假如一切顺利的话,生成的ipa包应该如下所示

b、发布ipa包
❶ 询问我们开发者账号的密码。在shell文件中配置的开发者账号是2170928274@qq.com,密码是iOS123456,通过双重验证生成的APP专用密码是tcha-fzfe-jcmd-tken,这里需要输入的是双重验证的密码
xxx's password: tcha-fzfe-jcmd-tken
❷ 接下来可能会发生版本冲突的问题。我们可以通过提高Verson(发布版本)或者 Build(内部测试版本)进行解决
❸ 如果没有问题的话就表示我们打包上线成功了,那么就可以进入到App Store Connect网站查看一下APP是否已经上线,如果发现已经上线,则直接点击提交审核即大功告成

c、发布平台:fir或者蒲公英
❶ 发布的方式选择ad-hoc
请输入你想发布的方式 ? [ 1:app-store 2:ad-hoc] 
2
❷ 询问我们想要发布的平台(两个平台都比较常用)
请输入你要发布的平台 ? [ 1:fir 2:蒲公英]
1
❸ 如果输出Published succeed则表示发布成功了,接下来就可以到发布的平台查看发布结果

2、解析用于打包上线的shell脚本

a、生成ipa包
❶ 配置工程名称与导出路径
#工程绝对路径
project_path=$(cd `dirname $0`; pwd)

#工程名 将XXX替换成自己的工程名
project_name=shell打包上线

#scheme名 将XXX替换成自己的sheme名
scheme_name=shell打包上线

#打包模式 Debug/Release
development_mode=Release

#build文件夹路径
build_path=${project_path}/build

#plist文件所在路径
exportOptionsPlistPath=${project_path}/exportAppstore.plist

#导出.ipa文件所在路径
exportIpaPath=${project_path}/IPADir/${development_mode}
❷ 输入想发布的方式
echo "请输入你想发布的方式 ? [ 1:app-store 2:ad-hoc] "

##
read number
    while([[ $number != 1 ]] && [[ $number != 2 ]])
    do
        echo "兄台,只能输入 1 or 2"
        echo "请输入你想发布的方式 ? [ 1:app-store 2:ad-hoc] "
        read number
    done

if [ $number == 1 ];
    then
    development_mode=Release
    exportOptionsPlistPath=${project_path}/exportAppstore.plist
## 证书名字

    else
    development_mode=Release
    exportOptionsPlistPath=${project_path}/exportTest.plist

fi
❸ 清理与编译工程
echo '///--------'
echo '/// 清理完成'
echo '///--------'
echo ''

echo '///-----------'
echo '/// 正在编译工程:'${development_mode}
echo '///-----------'
xcodebuild \
archive -workspace ${project_path}/${project_name}.xcworkspace \
-scheme ${scheme_name} \
-configuration ${development_mode} \
-archivePath ${build_path}/${project_name}.xcarchive -quiet  || exit

echo '///--------'
echo '/// 编译完成'
echo '///--------'
echo ''
❹ 开始ipa打包
echo '///----------'
echo '/// 开始ipa打包'
echo '///----------'
xcodebuild -exportArchive -archivePath ${build_path}/${project_name}.xcarchive \
-configuration ${development_mode} \
-exportPath ${exportIpaPath} \
-exportOptionsPlist ${exportOptionsPlistPath} \
-quiet || exit

# 删除build包
if [[ -d build ]]; then
rm -rf build -r
fi
❺ 导出ipa包
if [ -e $exportIpaPath/$scheme_name.ipa ];
    then
    echo '///----------'
    echo '/// ipa包已导出'
    echo '///----------'
    open $exportIpaPath
    else
    echo '///-------------'
    echo '/// ipa包导出失败 '
    echo '///-------------'
fi
echo '///------------'
echo '/// 打包ipa完成  '
echo '///-----------='
echo ''

b、开始发布ipa包:验证并上传到App Store
❶ 输入路径
if [ $number == 1 ];
    then
altoolPath="/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/altool"
❷ 将-u 后面的XXX替换成自己的AppleID的账号,-p后面的XXX替换成自己的密码
#tcha-fzfe-jcmd-tken
#iOS123456
"$altoolPath" --validate-app -f ${exportIpaPath}/${scheme_name}.ipa -u 2170928274@qq.com [-p iOS123456]
"$altoolPath" --upload-app -f ${exportIpaPath}/${scheme_name}.ipa -u 2170928274@qq.com -p tcha-fzfe-jcmd-tken

c、发布的方式选择ad-hoc
❶ 输入要发布的平台
else

    echo "请输入你要发布的平台 ? [ 1:fir 2:蒲公英] "
    
    read platform
        while([[ $platform != 1 ]] && [[ $platform != 2 ]])
        do
            echo "兄台,只能输入 1 or 2"
            echo "请输入你要发布的平台 ? [ 1:fir 2:蒲公英] "
            read platform
        done
❷ 上传到Fir
if [ $platform == 1 ];
    then
    #上传到Fir
    #将XXX替换成自己的Fir平台的token
    fir login -T d58c5754d2aff40b4d883ddfefa0d079
    fir publish $exportIpaPath/$scheme_name.ipa
❸ 上传到蒲公英
else
    echo "开始上传到蒲公英"
    #上传到蒲公英
    #蒲公英aipKey
    MY_PGY_API_K=6eea452856b7cc838fc92758d60b3fac
    #蒲公英uKey
    MY_PGY_UK=13446cb63555819b058a2aa28150cc01

    curl -F "file=@${exportIpaPath}/${scheme_name}.ipa" -F "uKey=${MY_PGY_UK}" -F "_api_key=${MY_PGY_API_K}" https://qiniu-storage.pgyer.com/apiv1/app/upload
fi

六、fastlane打包上线

1、fastlane的介绍

a、fastlane的作用
  • 一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。
  • 避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。
  • 通过配置自动上传到蒲公英、fir.im内测平台进行测试分发,也可以直接上传到TestFlightiTunes Connect
  • 保证证书的同步更新,在新电脑能够迅速具备项目打包环境。

b、fastlane的常用命令
  • gym:fastlane提供的打包工具
  • snapshot:生成多个设备的截图文件
  • frameit :对截图加一层物理边框
  • increment_build_number:自增build number,然后与之对应的get_build_numberVersion number同理
  • cert:创建一个新的代码签名证书
  • sigh:生成一个provisioning profile并保存打当前文件
  • pem:确保当前的推送证书是活跃的,如果没有会帮你生成一个新的
  • match:在团队中同步证书和描述文件。(这是一种全新的管理证书的方式)
  • testflight:上传ipatestflight
  • deliver:上传ipaAppStore

c、fastlane的配置文件
  • Appfile:存储有关开发者账号相关信息
  • Fastfile:核心文件,主要用于 命令行调用和处理具体的流程,lane相对于一个方法或者函数
  • Deliverfile:deliver工具的配置文件
  • metadata:元数据文件夹
  • Matchfile:Match操作对应的配置文件
  • screenshots:截图文件夹

2、在终端运行fastlane进行打包上线

a、安装fastlane
xiejiapei@MacBook-Pro ~ % sudo gem install fastlane -n /usr/local/bin
Successfully installed google-api-client-0.38.0
...
Installing ri documentation for fastlane-2.176.0
34 gems installed

b、在项目根目录下,初始化Fastlane
xiejiapei@MacBook-Pro fastlane打包 % fastlane init
[✔] 🚀 
...
❶ 问你想要用Fastlane做什么
[15:22:48]: What would you like to use fastlane for?
1. 📸  Automate screenshots
2. 👩✈️  Automate beta distribution to TestFlight (自动testfilght型配置)
3. 🚀  Automate App Store distribution (自动发布型配置)
4. 🛠  Manual setup - manually setup your project to automate your tasks (需要手动配置内容)
?  
❷ 选择3后提示输入开发者账号和密码
...
[15:29:04]: Please enter your Apple ID developer credentials
[15:29:04]: Apple ID Username:
8615659281705
❸ 登录成功后会提示你是否需要下载你的App的metadata,点y等待即可
[15:30:15]: Would you like fastlane to manage your app's metadata? (y/n)
❹ 初始化成功后会在当前工程目录生成一个fastlane文件夹
  • metadata和screenshots:分别对应App元数据和商店应用截图
  • Appfile:存放App的apple_idteam_id app_identifier等信息
  • Deliverfile:发布的配置信息,一般情况用不到
  • Fastfile:是我们最应该关注的文件,也是我们的工作文件
❺ 修改Fastfile文件
default_platform(:iOS)

platform :iOS do
  desc "Push a new release build to the App Store"
  lane :release do
    build_app(scheme: "fastlane打包")
    upload_to_app_store(force: true)
  end
end
❻ 在终端输入fastlane直接打包上线
xiejiapei@MacBook-Pro fastlane打包 % fastlane

c、打包到蒲公英
❶ 蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件
desc "打包到pgy"
#option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台
lane :test do |options|
gym(
  clean:true, #打包前clean项目
  export_method: "ad-hoc", #导出方式
  scheme:"fastlane打包", #scheme
  configuration: "Debug",#环境
  output_directory:"./app",#ipa的存放目录
  output_name:get_build_number()#输出ipa的文件名为当前的build号
  )
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end
❷ 在工作目录的终端执行打包命令,打包成功后如果蒲公英绑定了微信或是邮箱手机号会给你发通知的
fastlane test desc:测试打包

七、利用 Jenkins 持续集成 iOS 项目

众所周知,现在App的竞争已经到了用户体验为王,质量为上的白热化阶段。用户们都是很挑剔的。如果一个公司的推广团队好不容易砸了重金推广了一个APP,好不容易有了一些用户,由于一次线上的bug导致一批的用户在使用中纷纷出现闪退bug,轻则,很可能前期推广砸的钱都白费了,重则,口碑不好,未来也提升不起用户量来了。静下心来分析一下问题的原因,无外乎就是质量没有过关就上线了。除去主观的一些因素,很大部分的客观因素我觉得可以被我们防范的。根据大神们提出的一套开发规范建议,CI + FDD,就可以帮助我们极大程度的解决客观因素。本文接下来主要讨论 Continuous Integration 持续集成(简称CI)。

1、为什么我们需要持续集成

谈到为什么需要的问题,我们就需要从什么是来说起。那什么是持续集成呢。CI是一种开发实践。实践应该包含3个基本模块,一个可以自动构建的过程,自动编译代码,可以自动分发,部署和测试。一个代码仓库,SVN或者Git。最后一个是一个持续集成的服务器。通过持续集成,可以让我们通过自动化等手段高频率地去获取产品反馈并响应反馈的过程。我来说说用了CI以后带来的一些深有体会的优点。

缩减开发周期,快速迭代版本

每个版本开始都会估算好开发周期,但是总会因为各种事情而延期。这其中包括了一些客观因素。由于产品线增多,迭代速度越来越快,给测试带来的压力也越来越大。如果测试都在开发完全开发完成之后再来测试,那就会影响很长一段时间。这时候由于集成晚就会严重拖慢项目节奏。如果能尽早的持续集成,尽快进入迭代环中,就可以尽早的暴露出问题,提早解决,尽量在规定时间内完成任务。

自动化流水线操作带来的高效

其实打包对于开发人员来说是一件很耗时,而且没有很大技术含量的工作。如果开发人员一多,相互改的代码冲突的几率就越大,加上没有产线管理机制,代码仓库的代码质量很难保证。团队里面会花一些时间来解决冲突,解决完了冲突还需要自己手动打包。这个时候如果证书又不对,又要耽误好长时间。这些时间其实可以用持续集成来节约起来的。一天两天看着不多,但是按照年的单位来计算,可以节约很多时间!

随时可部署

有了持续集成以后,我们可以以天为单位来打包,这种高频率的集成带来的最大的优点就是可以随时部署上线。这样就不会导致快要上线,到处是漏洞,到处是bug,手忙脚乱弄完以后还不能部署,严重影响上线时间。

极大程度避免低级错误

我们可以犯错误,但是犯低级错误就很不应该。这里指的低级错误包括以下几点:编译错误,安装问题,接口问题,性能问题。 以天为单位的持续集成,可以很快发现编译问题,自动打包直接无法通过。打完包以后,测试扫码无法安装,这种问题也会立即被暴露出来。接口问题和性能问题就有自动化测试脚本来发现。这些低级问题由持续集成来暴露展现出来,提醒我们避免低级错误。

持续化集成工具——Jenkins

Jenkins 是一个开源项目,提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上。同时 Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。根据官方定义,Jenkins有以下的用途:

  • 构建项目
  • 跑测试用例检测bug
  • 静态代码检测
  • 部署

关于这4点,实际使用中还是比较方便的:

1.构建项目自动化打包可以省去开发人员好多时间,重要的是,Jenkins为我们维护了一套高质量可用的代码,而且保证了一个纯净的环境。我们经常会出现由于本地配置出错而导致打包失败的情况。现在Jenkins就是一个公平的评判者,它无法正确的编译出ipa,那就是有编译错误或者配置问题。开发人员没必要去争论本地是可以运行的,拉取了谁谁谁的代码以后就不能运行了。共同维护Jenkins的正常编译,因为Jenkins的编译环境比我们本地简单的多,它是最纯净无污染的编译环境。开发者就只用专注于编码。这是给开发者带来的便利。

2.这个可以用来自动化测试。在本地生成大批的测试用例。每天利用服务器不断的跑这些用例。每天每个接口都跑一遍。看上去没必要,但是实际上今天运行正常的系统,很可能由于今天的代码改动,明天就出现问题了。有了Jenkins可以以天为单位的进行回归测试,代码只要有改动,Jenkins就把所有的回归测试的用例全部都跑一遍。在项目工期紧张的情况下,很多情况测试都不是很重视回归测试,毕竟很可能测一遍之后是徒劳的“无用功”。然而由于回归测试不及时,就导致到最后发版的时候系统不可用了,这时候回头查找原因是比较耗时的,查看提交记录,看到上百条提交记录,排查起来也是头疼的事情。以天为单位的回归测试能立即发现问题。测试人员每天可以专注按单元测试,一周手动一次回归测试。这是给测试者带来的便利。

3.这个是静态代码分析,可以检测出很多代码的问题,比如潜在的内存泄露的问题。由于Jenkins所在环境的纯净,还是可以发现一些我们本地复杂环境无法发现的问题,进一步的提高代码质量。这是给质检带来的便利。

4.随时部署。Jenkins在打包完成之后可以设定之后的操作,这个时候往往就是提交app到跑测试用例的系统,或者部署到内测平台生成二维码。部署中不能安装等一些低级问题随之立即暴露。测试人员也只需要扫一下二维码即可安装,很方便。这也算是给测试带来的便利。


2、安装 Jenkins

我们来开始安装Jenkins。从官网https://jenkins.io/上下载最新的pkg安装包。安装完成之后,Safari可能会自动打开,如果没有自动打开,打开浏览器,输入http://localhost:8080/。这个时候如果你重启电脑会发现Jenkins给你新增了一个用户,名字就叫Jenkins,不过这个时候你不知道密码。你可能会去试密码,肯定是是不对的,因为初始密码很复杂。这个时候正确做法是打开http://localhost:8080/会出现下图的重设初始密码的界面。

按照提示,找到/Users/Shared/Jenkins/Home/这个目录下,这个目录虽然是共享目录,但是有权限的,非Jenkins用户/secrets/目录是没有读写权限的。

打开initialAdminPassword文件,复制出密码,就可以填到网页上去重置密码了。一路安装过来,输入用户名,密码,邮件这些,就算安装完成了。还是继续登录localhost:8080 ,选择“系统管理”——“管理插件”,我们要先安装一些辅助插件。

安装GitLab插件。因为我们用的是GitLab来管理源代码,Jenkins本身并没有自带GitLab插件,所以我们需要依次选择 系统管理->管理插件,在“可选插件”中选中“GitLab Plugin”和“Gitlab Hook Plugin”这两项,然后安装。

安装Xcode插件。同安装GitLab插件的步骤一样,我们依次选择系统管理->管理插件,在“可选插件”中选中“Xcode integration”安装。安装完了这个,我们就可以配置一个构建项目了。


3、构建项目

点击新建好的项目,进来配置一下General参数。这里可以设置包的保留天数还有天数。

接着设置源码管理。由于现在我用到的是GitLab,先配置SSH Key,在Jenkins的证书管理中添加SSH。在Jenkins管理页面,选择Credentials,然后选择Global credentials (unrestricted),点击Add Credentials,如下图所示,我们填写自己的SSH信息,然后点击Save,这样就把SSH添加到Jenkins的全局域中去了。

如果正常的配置正确的话,是不会出现下图中的那段红色的警告。如果有下图的提示,就说明Jenkins还没有连通GitLab或者SVN,那就请再检查SSH Key是否配置正确。

构建触发器设置这里是设置自动化测试的地方。这里涉及的内容很多,暂时我也没有深入研究,这里暂时先不设置。有自动化测试需求的可以好好研究研究这里的设置。不过这里有两个配置还是需要是配置的。

轮询源码管理。需要设置源码的路径才能起到轮询的效果。一般设置为类似结果: 0/5 * * * * 每5分钟轮询一次。

Poll SCM (poll source code management) 

定时build。一般设置为类似: 00 20 * * * 每天 20点执行定时build。当然两者的设置都是一样可以通用的。格式是这样的:分钟(0-59) 小时(0-23) 日期(1-31) 月(1-12) 周几(0-7,0和7都是周日)

Build periodically

构建环境设置。iOS打包需要签名文件和证书,所以这部分我们勾选Keychains and Code Signing IdentitiesMobile Provisioning Profiles。 这里我们又需要用到Jenkins的插件,在系统管理页面,选择Keychains and Provisioning Profiles Management

进入Keychains and Provisioning Profiles Management页面,点击“浏览”按钮,分别上传自己的keychain和证书。上传成功后,我们再为keychain指明签名文件的名称。点击Add Code Signing Identity,最后添加成功后如下图所示:

我第一次导入证书和Provisioning Profiles文件,就遇到了一点小“坑”,我当时以为是需要证书,但是这里需要的Keychain,并不是cer证书文件。这个Keychain其实在/Users/管理员用户名/Library/keychains/login.keychain,当把这个Keychain设置好了之后,Jenkins会把这个Keychain拷贝到/Users/Shared/Jenkins/Library/keychains这里,(Library是隐藏文件)。Provisioning Profiles文件也直接拷贝到/Users/Shared/Jenkins/Library/MobileDevice文件目录下。

这样Adhoc证书和签名文件就在Jenkins中配置好了,接下来我们只需要在item设置中指定相关文件即可。 回到我们新建的item,找到构建环境,按下图选好自己的相关证书和签名文件。

接下来在进行构建的设置。我们这里选择执行一段打包脚本。

构建后操作。这里我们选择Execute a set of scripts,这里也是一个脚本,这个脚本用来上传自动打包好的ipa文件。至此,我们的Jenkins设置就全部完成了。点击构建,就会开始构建项目了。

构建一次,各个颜色代表的意义如下:

天气的晴雨表代表了项目的质量,这也是Jenkins的一个特色。

如果构建失败了,可以去查看Console Output可以查看log日志。


4、自动化打包命令

在日常开发中,打包是最后上线不可缺少的环节,如果需要把工程打包成 ipa 文件,通常的做法就是在 Xcode 里点击 「Product -> Archive」,当整个工程 archive 后,然后在自动弹出的 「Organizer」 中进行选择,根据需要导出ad hocenterprise 类型的 ipa 包。虽然Xcode已经可以很完美的做到打包的事情,但是还是需要我们手动点击5,6下。加上我们现在需要持续集成,用打包命令自动化执行就顺其自然的需要了。

xcodebuild + xcrun命令

Xcode为我们开发者提供了一套构建打包的命令,就是xcodebuildxcrun命令。xcodebuild把我们指定的项目打包成.app文件,xcrun将指定的.app文件转换为对应的.ipa文件。下面10个命令最主要的还是前3个。其中-target-configuration 参数可以使用xcodebuild -list获得,-sdk 参数可由 xcodebuild -showsdks 获得,[buildsetting=value ...]用来覆盖工程中已有的配置。

  • -project -workspace:这两个对应的就是项目的名字。如果有多个工程,这里又没有指定,则默认为第一个工程。
  • -target:打包对应的targets,如果没有指定这默认第一个。
  • -configuration:如果没有修改这个配置,默认就是Debug和Release这两个版本,没有指定默认为Release版本。
  • -buildsetting=value ...:使用此命令去修改工程的配置。
  • -scheme:指定打包的scheme。
NAME
xcodebuild – build Xcode projects and workspaces

SYNOPSIS
1. xcodebuild [-project name.xcodeproj] [[-target targetname] … | -alltargets] [-configuration configurationname] [-sdk [sdkfullpath | sdkname]] [action …] [buildsetting=value …] [-userdefault=value …]

2. xcodebuild [-project name.xcodeproj] -scheme schemename [[-destination destinationspecifier] …] [-destination-timeout value] [-configuration configurationname] [-sdk [sdkfullpath | sdkname]] [action …] [buildsetting=value …] [-userdefault=value …]

3. xcodebuild -workspace name.xcworkspace -scheme schemename [[-destination destinationspecifier] …] [-destination-timeout value] [-configuration configurationname] [-sdk [sdkfullpath | sdkname]] [action …] [buildsetting=value …] [-userdefault=value …]

4. xcodebuild -version [-sdk [sdkfullpath | sdkname]] [infoitem]

5. xcodebuild -showsdks

6. xcodebuild -showBuildSettings [-project name.xcodeproj | [-workspace name.xcworkspace -scheme schemename]]

7. xcodebuild -list [-project name.xcodeproj | -workspace name.xcworkspace]

8. xcodebuild -exportArchive -archivePath xcarchivepath -exportPath destinationpath -exportOptionsPlist path

9. xcodebuild -exportLocalizations -project name.xcodeproj -localizationPath path [[-exportLanguage language] …]

10. xcodebuild -importLocalizations -project name.xcodeproj -localizationPath path

下面第3个命令就是专门用来打带有Cocopods的项目,因为这个时候项目工程文件不再是xcodeproj了,而是变成了xcworkspace了。

build
Build the target in the build root (SYMROOT). This is the default action, and is used if no action is given.

analyze
Build and analyze a target or scheme from the build root (SYMROOT). This requires specifying a scheme.

archive
Archive a scheme from the build root (SYMROOT). This requires specifying a scheme.

test
Test a scheme from the build root (SYMROOT). This requires specifying a scheme and optionally a destination.

installsrc
Copy the source of the project to the source root (SRCROOT).

install
Build the target and install it into the target’s installation directory in the distribution root (DSTROOT).

clean
Remove build products and intermediate files from the build root (SYMROOT).

再来说说xcrun命令。参数不多,使用方法也很简单,xcrun -sdk iphoneos -v PackageApplication +下述一些参数。

Usage:
PackageApplication [-s signature] application [-o output_directory] [-verbose] [-plugin plugin] || -man || -help

Options:

[-s signature]: certificate name to resign application before packaging
[-o output_directory]: specify output filename
[-plugin plugin]: specify an optional plugin
-help: brief help message
-man: full documentation
-v[erbose]: provide details during operation

参数都了解之后,我们就来看看该如何用了。下面这个是使用了xcodebuild + xcrun命令写的自动化打包脚本。

# 工程名
APP_NAME="YourProjectName"
# 证书
CODE_SIGN_DISTRIBUTION="iPhone Distribution: Shanghai ******* Co., Ltd."
# info.plist路径
project_infoplist_path="./${APP_NAME}/Info.plist"

#取版本号
bundleShortVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${project_infoplist_path}")

#取build值
bundleVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${project_infoplist_path}")

DATE="$(date +%Y%m%d)"
IPANAME="${APP_NAME}_V${bundleShortVersion}_${DATE}.ipa"

#要上传的ipa文件路径
IPA_PATH="$HOME/${IPANAME}"
echo ${IPA_PATH}
echo "${IPA_PATH}">> text.txt

//下面2行是没有Cocopods的用法
echo "=================clean================="
xcodebuild -target "${APP_NAME}"  -configuration 'Release' clean

echo "+++++++++++++++++build+++++++++++++++++"
xcodebuild -target "${APP_NAME}" -sdk iphoneos -configuration 'Release' CODE_SIGN_IDENTITY="${CODE_SIGN_DISTRIBUTION}" SYMROOT='$(PWD)'

//下面2行是集成有Cocopods的用法
echo "=================clean================="
xcodebuild -workspace "${APP_NAME}.xcworkspace" -scheme "${APP_NAME}"  -configuration 'Release' clean

echo "+++++++++++++++++build+++++++++++++++++"
xcodebuild -workspace "${APP_NAME}.xcworkspace" -scheme "${APP_NAME}" -sdk iphoneos -configuration 'Release' CODE_SIGN_IDENTITY="${CODE_SIGN_DISTRIBUTION}" SYMROOT='$(PWD)'

xcrun -sdk iphoneos PackageApplication "./Release-iphoneos/${APP_NAME}.app" -o ~/"${IPANAME}"
gym 命令

说到gym,就要先说一下fastlanefastlane是一套自动化打包的工具集,用 Ruby 写的,用于 iOS 和 Android 的自动化打包和发布等工作。gym是其中的打包命令。要想使用gym,先要安装fastlane

sudo gem install fastlane --verbose

fastlane包含了我们日常编码之后要上线时候进行操作的所有命令。

deliver:上传屏幕截图、二进制程序数据和应用程序到AppStore
snapshot:自动截取你的程序在每个设备上的图片
frameit:应用截屏外添加设备框架
pem:可以自动化地生成和更新应用推送通知描述文件
sigh:生成下载开发商店的配置文件
produce:利用命令行在iTunes Connect创建一个新的iOS app
cert:自动创建iOS证书
pilot:最好的在终端管理测试和建立的文件
boarding:很容易的方式邀请beta测试
gym:建立新的发布的版本,打包
match:使用git同步你成员间的开发者证书和文件配置
scan:在iOS和Mac app上执行测试用例

整个发布过程可以用fastlane描述成下面这样:

lane :appstore do
  increment_build_number
  cocoapods
  xctool
  snapshot
  sigh
  deliver
  frameit
  sh "./customScript.sh"

  slack
end

这里可能大家还会听过一个命令叫 xctoolxctool是官方xcodebuild命令的一个增强实现,输出的内容比xcodebuild直观可读得多。通过brew即可安装。

brew install xctool

使用gym自动化打包,脚本如下:

#计时
SECONDS=0

#假设脚本放置在与项目相同的路径下
project_path=$(pwd)

#取当前时间字符串添加到文件结尾
now=$(date +"%Y_%m_%d_%H_%M_%S")

#指定项目的scheme名称
scheme="DemoScheme"

#指定要打包的配置名
configuration="Adhoc"

#指定打包所使用的输出方式,目前支持app-store, package, ad-hoc, enterprise, development, 和developer-id,即xcodebuild的method参数
export_method='ad-hoc'

#指定项目地址
workspace_path="$project_path/Demo.xcworkspace"

#指定输出路径
output_path="/Users/your_username/Documents/"

#指定输出归档文件地址
archive_path="$output_path/Demo_${now}.xcarchive"

#指定输出ipa地址
ipa_path="$output_path/Demo_${now}.ipa"

#指定输出ipa名称
ipa_name="Demo_${now}.ipa"

#获取执行命令时的commit message
commit_msg="$1"

#输出设定的变量值
echo "===workspace path: ${workspace_path}==="
echo "===archive path: ${archive_path}==="
echo "===ipa path: ${ipa_path}==="
echo "===export method: ${export_method}==="
echo "===commit msg: $1==="

#先清空前一次build
gym --workspace ${workspace_path} --scheme ${scheme} --clean --configuration ${configuration} --archive_path ${archive_path} --export_method ${export_method} --output_directory ${output_path} --output_name ${ipa_name}

#输出总用时
echo "===Finished. Total time: ${SECONDS}s==="

5、打包完成自动化上传 fir / 蒲公英

要上传到 fir / 蒲公英,都需要注册一个账号,获得token,之后才能进行脚本化操作。

自动化上传fir
  • 安装fir-clifir的命令行工具
  • 需要先装好ruby再执行
gem install fir-cli
#上传到fir
fir publish ${ipa_path} -T fir_token -c "${commit_msg}"
自动化上传蒲公英
#蒲公英上的User Key
uKey="7381f97070*****c01fae439fb8b24e"
#蒲公英上的API Key
apiKey="0b27b5c145*****718508f2ad0409ef4"
#要上传的ipa文件路径
IPA_PATH=$(cat text.txt)

rm -rf text.txt

#执行上传至蒲公英的命令
echo "++++++++++++++upload+++++++++++++"
curl -F "file=@${IPA_PATH}" -F "uKey=${uKey}" -F "_api_key=${apiKey}" http://www.pgyer.com/apiv1/app/upload
完整的持续集成流程

经过上面的持续化集成,现在我们就拥有了如下完整持续集成的流程:


八、app 配置多个环境变量

谈到多环境,相信现在大多公司都至少有2-3个app环境了,比如Test环境,UAT(User Acceptance Test)用户验收测试环境,Release环境等等。当需要开发打多个包的时候,一般常见做法就是直接代码里面修改环境变量,改完之后Archive一下就打包了。当然这种做法很正确,只不过不是很优雅很高效。如果搭建好了Jenkins(搭建教程),我们利用它来优雅的打包。如果利用Jenkins来打包,我们就需要来给app来配置一下多个环境变量了。之后Jenkins分别在不同环境下自动集成即可。接下来,我们来谈谈常见的2种做法。

1、利用Build Configuration来配置多环境

前言里面我们先谈到了需求,由于需要配置多个环境,并且多个环境都需要安装到手机上,那么可以配置Build Configuration来完成这个任务。

新建Build Configuration

先点击Project里面找到Configuration,然后选择添加,这里新加一个Configuration。系统默认是2个,一个Debug,一个Release。这里我们需要选择是复制一个Debug还是ReleaseReleaseDebug的区别是,Release是不能调试程序,因为默认是屏蔽了可调试的一些参数,具体可以看BuildSetting里面的区别,而且Release编译时有做编译优化,会比用Debug打包出来的体积更小一点。

这里我们选择一个Duplicate “Debug” Configuration,因为我们新的环境需要debug,添加完了之后就会多了一套Configuration了,这一套其实是包含了一些编译参数的配置集合。如果此时项目里面有cocopods的话,打开Configuration Set就会发现是如下的样子。注意刚刚新建完Build Configuration之后,这时如果有pod,请立即执行一下pod install

pod安装完成之后会自动生成xcconfig文件,如果你手动新建这个xcconfig,然后把原来的debugrelease对应的pod xcconfig文件内容复制进来,这样做是无效的,需要pod自己去生成xcconfig文件才能被识别到。

新建完Build Configuration,这个时候需要新建pod里面对应的Build Configuration,要不然一会编译会报错。如果没用pod,可以忽略一下这一段。如下图新建一个对应之前Porject里面新建的Build Configuration

新建Scheme

接下来我们要为新的Configuration新建一个编译Scheme

建完成之后,我们就可以编辑刚刚新建的Scheme,这里可以把Run模式和Archive都改成新建Scheme。如下图:

注意如果是使用了Git这些协同工具的同学这里还需要把刚刚新建的Scheme共享出去,否则其他人看不到这个Scheme。选择Manage Schemes

新建User-defined Build Settings

再次回到ProjectBuild Settings里面来,Add User-Defined Setting

我们这里新加入2个参数,CustomAppBundleld是为了之后打包可以分开打成多个包,这里需要2个不同的Id,建议是直接在原来的Bundleld加上Scheme的名字即可。CustomProductName是为了app安装到手机上之后,手机上显示的名字,这里可以按照对应的环境给予描述,比如测试服,UAT,等等。如下图。这里值得提到的一点是, PodsBuild_DIR这些目录其实是Pods自己生成好的,之前执行过Pod install之后,这里默认都是配置好的,不需要再改动了。

修改info.plist文件 和 Images.xcassets

先来修改一下info.plist文件。由于我们新添加了2个CustomAppBundleldCustomProductName,这里我们需要把info.plist里面的Bundle display name修改成我们自定义的这个字典。编译过程中,编译器会根据我们设置好的Scheme去自己选择DebugRelease分别对应的ProductName

我们还需要在Images.xcassets里面新添加2个New iOS App Icon,名字最好和scheme的名字相同,这样好区分。
新建完AppIcon之后,再在Build Setting里面找到Asset Catalog Compiler里面,然后把这几种模式下的App Icon set Name分别设置上对应的图标。

既然我们已经新建了这几个scheme,那接下来怎么把他们都打包成app呢?这里分享一下我设置这些环境的心得。一切切记,每个环境都要设置好DebugRelease!千万别认为线上的版本只设置Release就好,哪天需要调试线上版本,没有设置Debug就无从下手了。也千万别认为测试环境的版本只要设置Debug就好,万一哪天一个测试环境需要发Release包,那又无从下手了。我的建议就是每个环境都配置DebugRelease,即使以后不用,也提前设置好,以防万一。合理的设置应该如下图这样。

| -------------------------- |------------------|
|           Scheme           |   Configurations |  
| -------------------------- |------------------| 
|      XXXXProjectTest       |      Debug       | 
|                            |------------------|
|                            |      Release     | 
| -------------------------- |------------------|
|      XXXXProjectAppStore   |      Debug       | 
|                            |------------------|
|                            |      Release     | 
| -------------------------- |------------------|
|      XXXXProjectUAT        |      Debug       | 
|                            |------------------|
|                            |      Release     | 
| -------------------------- |------------------|

注意这里一定要把Scheme的名字和编译方式区分开,选择了一个Scheme,只是相当于选择了一个环境,并不是代表这Debug还是Release。我建议Scheme只配置环境,而进来的RunArchive来配置DebugRelease,我建议每个Scheme都按照下图来,Run对应的DebugArchive对应的Release

配置好上述之后,就可以选择不同环境运行app了。可以在手机上生成不同的环境的app,可以同时安装。如下图。

配置和获取环境变量

当然使用一个单例也可以做到环境切换。新建一个单例,然后可以在设置菜单里面加入一个列表,里面列出所有的环境,然后用户选择以后,单例就初始化用户所选的环境。这种方式就是在一个app里面切换多种环境。


2、利用文件来配置多环境

创建一个xcconfig文件

创建好了这个文件,我们在project里面设置一下。在这些地方把配置文件换成我们刚刚新建的文件。

接下来就要编写我们的xcconfig文件了。这个文件里面可以写的东西挺多的。细心的同学就会发现,其实我们一直使用的cocopods就是用这个文件来配置编译参数的。我们随便看一个简单的cocopodsxcconfig文件,就是下面这样子:

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Forms"
OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/Forms"
OTHER_LDFLAGS = $(inherited) -ObjC -l"Forms"
PODS_ROOT = ${SRCROOT}/Pods

我们由于需要配置网络环境,可以写成cocopods那样。GCC_PREPROCESSOR_DEFINITIONSGCC预编译头参数,通常我们可以在Project 文件下的 Build Settings 对预编译宏定义进行默认赋值。

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) WEBSERVICE_URL='$(REQUESTBASE_URL)' MESSAGE_SYSTEM_URL='$(MESSAGE_SYSTEM_URL)'

Preprocessor Macros 其实是按照 Configuration 选项进行默认配置的, 它是可以根据不同的环境预先制定不同定义的宏,或者为不同环境下的相同变量定义不同的值。xcconfig 我们可以写入不同的 Configuration 选项配置不同的文件。每一个 xcconfig 可以配置 Build Settings 里的属性值, 其实实质就是通过 xcconfig 去修改 GCC_PREPROCESSOR_DEFINITIONS 的值,这样我们就可以做到动态配置环境的需求了。

最后还需要提的一点是,这个配置文件的level的问题。现在本地有这么多配置,到底哪一个最终生效呢?打开Build 里面的level,我们来看一个例子。我们目前可以看到有5个配置,他们是有优先级的。优先级是从左往右,依次降低的。Resolved = target-level > project-level > 自定义配置文件 > iOS 默认配置。左边第一列永远显示的是当前生效的最终配置结果。知道了这个优先级之后,我们可以更加灵活的配置我们的app了。最后这里有一个Demo,配置了Cocopods,配置了xcconfig文件,还有Build Configuration的。


3、利用Targets来配置多环境

配置一个多环境其实一个Schemexcconfig已经完全够用了,为什么还要有这个第三点呢?虽说仅仅为了配置一个多环境这点“小事”,但是利用多个Targets也能实现需求,只不过有点“兴师动众”了。利用Targets可以瞬间大批量产生大量的app。仅仅只用一套代码,就可以生产出7个app。7个app的证书都是不同的,配置也都不同,但是代码只需要维护一套代码,就可以完成维护7个app的目标。

下面我们来看看怎么新建Targets,有2种方法。一种方法是完全新建一个Targets,之后看你需求是怎么样的。如果是想利用Targets可以瞬间大批量产生大量的app,可以把新建出来的project删掉,本地还是维护一套代码,然后在新建的TargetsBuild Phases里面去把本地现有代码加上,参数自己可以随意配置。这样也是一套代码维护多个app。

第二种方法就是复制一个原有的Targets,这种做法只用自己去改参数就可以了。

由于我们新建了Targets,相当于新建了一个app了。所以里面的所有的文件全部都可以更改。包括info.plist,源码引用,Build Settings……所有参数都可以改,这样就不仅仅局限于修改Schemexcconfig,所以之前说仅仅配置一个多环境用Targets有点兴师动众,但是它确实能完成目的。

最后关于Targets还有一点想说的,如果大家有多个app,并且这几个app之间有超过80%的代码都是完全一样的,或者说仅仅只是个别界面显示不同,逻辑都完全相同,建议大家用Targets来做,这样只需要维护一套代码就可以了。维护多套相同的代码,实在太没有效率了。一个bug需要在多套代码上面来回改动,费时费力。

这时候可能有人会问了,如果维护一套代码,以后这些app如果需求有不同怎么办?比如要进入不同界面,跳转不同界面,页面也显示不同怎么办?这个问题其实很简单。在Targets里面的Compile Sources里面是可以给每个不同的Targets添加不同的编译代码的。只需要在每个不同的Targets里面加入不同界面的代码进行编译就可以了,在跳转的那个界面加上宏,来控制不同的app跳转到相应界面。这样本地还是维护的一套代码,只不过每个Targets编译的代码就是这套代码的子集了。这样维护起来还是很方便。也实现了不同app不同界面,不同需求了。

其实这部分的需求源自于上面Jenkins自动化持续集成,有一个需求是能打不同环境的包。之前没有Jenkins的时候就改改URL运行一遍就好,虽说做法不够优雅,但是也不麻烦。现在想持续集成,只好把环境都分好,参数配置正确,这样Jenkins可以一次性多个环境的包一起打。真正做到多环境的持续集成。


Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容