loyaltoorigin

  • Home

  • Categories

  • Archives

Objective-C 快速消息转发机制在项目中的使用

Posted on 2020-01-24 | Edited on 2021-01-24 | In iOS | Comments:

OC 消息转发机制, 想必大家并不陌生, 作为 iOS 开发, 作为面试必问, 即使不是深入了解, 也肯定有所耳闻。网上介绍消息转发机制的文章一大堆, 但说到具体应用场景的寥寥无几, 有的说到消息转发的应用, - (void)forwardInvocation:(NSInvocation )anInvocation* 方法中调用另一个对象执行对应方法, 快速转发能实现的场景却用标准消息转发来实现, 未免有点大材小用。

简述 OC 消息转发机制

当 OC 对象调用未实现的方法时, 通过消息转发机制可以使对象执行用户预先定义的处理过程, 会按下图流程依次调用方法。

forward_process.png

从上图可以看出, 当对象无法响应方法时, 会依次调用这4个方法。消息转发有快速转发和标准(完整)转发两类, 快速转发即实现 - (id)forwardingTargetForSelector:(SEL)aSelector 方法, 快速消息转发仅需要返回一个可执行的对应方法的对象, 走完下面两个方法即处理对应方法, 本文主要介绍的就是快速转发在项目的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//1.当一个对象调用未实现的方法时,会调用该方法,通过该方法决定是否动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// NSLog(@"%s", __func__);
// if (sel == @selector(run)) {

// class_addMethod(self, sel, (IMP)funcRun, "v@:@@");
// return YES;
// //YES表示执行该方法 , 不进行下一步
// //NO表示不执行该方法

// ❤返回值没什么意义, 只能说是一种规范, 源码中并没有拿返回值做相关操作, 如果动态添加了方法,
// 无论 YES or NO, 都不会到下一步, 因为动态方法解析后会再去找这个方法, 找到直接跳转执行函数, msg_send方法结束,
// 只有当 不动态添加方法, 才会进下一步
// }

//返回NO进入到下一步
return NO;
return [super resolveInstanceMethod:sel];
}


//2.这个方法用于指定备用对象相应这个selector,不能指定为self.
// 如果返回某个对象,会调用对象的对应方法[dog run],如果该对象没实现该方法,会报错;返回nil则进入下一步

- (id)forwardingTargetForSelector:(SEL)aSelector {
return [Dog new];
}

快速消息转发在项目中的使用

自定义容器 view 转发给自己的 subview

在日常开发中, 我们往往会遇到一个场景, 一个同样的自定义 view 用于 cell 中和不用于 cell 中, 这时候我们会自定义一个 view 暴露如下图所示接口, 供外界调用。

view_interface.png

当我们使用这个 view 时候, 直接使用就可以了。但当需要该 view 嵌到 cell 中使用时, 还需要创建一个 cell 容器类, 将这个 view 添加进去, 而这个 cell 类其实仅仅作为添加 view 的容器, 本身并不具备什么逻辑, 当外界调用 cell 时, 它应该和 view 保持一样的接口如下图所示:

cell_interface.png

传统的写法, 在 getter 、setter 方法实现中, 调用添加在自己中的 view 的对应方法即可, 如图所示:

cell_common_implementation.png

但这里如果使用快速消息转发, 会是代码更加简洁, 我们仅需要在 - (id)forwardingTargetForSelector:(SEL)aSelector 方法中 return 加在自己中的自定义 view 对象即可, 当然这里要配合 @dynamic 关键字使用。因为 @property 关键字会生成对应的 getter 、setter 方法, 这样的话 cell 相当于实现了 getter 、setter 方法, 自然不会走到 - (id)forwardingTargetForSelector:(SEL)aSelector 中。

而 @dynamic 关键字意味着编译器不会帮助我们自动生成 getter 、setter 方法。这样一来, 相当于我们只申明了这些方法而并没有实现这些方法, 当调用时便会遵循消息转发机制的步骤, 从而调用 - (id)forwardingTargetForSelector:(SEL)aSelector。

cell_forward_implementation.png

可以对比一下, 代码简洁了很多, 如果自定义 view 对外暴露的属性及方法更多, 这么写省去的代码就越多。后续如果需要增加对外暴露接口, 只需要在 .h 文件中添加对应接口, 如果是属性, .m 文件 @dynamic 关键字也要添加对应属性。

数据模型数据结构不一致

曾经在项目中做直播间道具相关业务, 第一次使用了快速消息转发。由于第二期添加了背包道具, 就是下图中的 SPTBagPropInfo 类, 相比于原来的道具, 背包道具是通过做任务等赠送的, 所以多了个数量字段, 作为我们前端来说, 理想的数据模型即和原道具数据结构一致, 只是增加一个数量字段即可, 但是后端同学给到的数据结构却是在外层加了数量这个字段, 对于我们来说, 相当于一个新对象, 这样对我们来说用起来是不方便的。

model_interface.png

所以在 .m 文件中, 直接转发给 self.detail, 即转发给原来的道具对象, 仅需自己实现原来道具没有的方法即可。

model_implementation.png

可以看出, 这里实现接口对外统一的作用只要还是靠协议, 快速转发和上一个案例其实本质是一致的, 直接转发给了自己内部的一个对象, 使 .m 文件中省去了一些冗余代码。

总结

通过项目中的一些实际场景, 使用到消息转发机制, 能够更加深刻了解相关知识点。本文主要是自己的一点思考和尝试, 如果有说的不对的地方, 望不吝赐教。

通过Method运行时内存布局hook方法探索

Posted on 2018-06-30 | Edited on 2018-11-20 | In iOS | Comments:

在iOS开发中, Method Swizzling想必大家都不陌生, 可以以此来对方法进行hook, 做一些我们希望做的事情, 比如页面进入退出, 可以对viewWillAppear及viewWillDisappear进行hook, 从而进行一些埋点日志相关的事情。

那么, Method Swizzling的原理到底是怎样的呢? 这个问题, 即使没自己研究过, 大多数人也有所耳闻, 简单来说, 无非就是修改方法的imp指向, 让其指向我们hook的方法。如果是这样的话, 我们是否可以不用Runtime提供的API如method_setImplementation、method_exchangeImplementation等函数而通过对象及方法的内存布局来实现呢? 答案是肯定的, 下面便是我在此过程中的一些探索和理解。

本文描述大部分内容对开发没有太大帮助, 但是对于更加了解运行时方法调用有一定帮助。

直接赋值Method的IMP进行hook

要想通过方法的内存布局来修改, 一定要对方法的内存布局有所了解, 查看源码可以知道Method的内存布局如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct method_t {
SEL name;
const char *types;
IMP imp;

struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};

上面结构中, 很容易就找到我们想要的东西IMP, 话不多少, 赶紧进行hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation Person

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(sayHello);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);

struct method_t *method = (struct method_t *)originalMethod;
method->imp = (IMP)hookedSayHello;
});
}

- (void)sayHello {
NSLog(@"Hello, everybody!");
}

void hookedSayHello (id self, SEL _cmd, ...) {
NSLog(@"This is hooked sayHello");
}

@end

然后再main.m中调用:

1
2
Person *person = [[Person alloc] init];
[person sayHello];

遇到的问题, 还是调用原来的方法实现

此时却发现, 打印出来的却和我想象不太一样, 仍然是调用了原来的sayHello方法, 而且打个断点发现method的imp指针也确实指向了 void hookedSayHello (id self, SEL _cmd, …) 这个函数, 这确实有些让人捉摸不透。

直接修改方法imp hooked_imp.png

浅尝辄止–method _setImplementation

于是怀疑人生的我, 又使用Runtime提供的API method_setImplementation进行相同操作, 发现和以往一样, 毫无问题, 那么一定是做了一些处理, 查其源码, 发现了一个很可疑的函数 flushCaches, 见名知意, 清除缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static IMP 
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertWriting();

if (!m) return nil;
if (!imp) return nil;

IMP old = m->imp;
m->imp = imp;

// Cache updates are slow if cls is nil (i.e. unknown)
// RR/AWZ updates are slow if cls is nil (i.e. unknown)
// fixme build list of classes whose Methods are known externally?

flushCaches(cls);

updateCustomRR_AWZ(cls, m);

return old;
}


/***********************************************************************
* _objc_flush_caches
* Flushes all caches.
* (Historical behavior: flush caches for cls, its metaclass,
* and subclasses thereof. Nil flushes all classes.)
* Locking: acquires runtimeLock
**********************************************************************/
static void flushCaches(Class cls)
{
runtimeLock.assertWriting();

mutex_locker_t lock(cacheUpdateLock);

if (cls) {
foreach_realized_class_and_subclass(cls, ^(Class c){ // 遍历子类
cache_erase_nolock(c);
});
}
else {
foreach_realized_class_and_metaclass(^(Class c){
cache_erase_nolock(c);
});
}
}

// Reset this entire cache to the uncached lookup by reallocating it.
// This must not shrink the cache - that breaks the lock-free scheme.
void cache_erase_nolock(Class cls)
{
cacheUpdateLock.assertLocked();

cache_t *cache = getCache(cls);

mask_t capacity = cache->capacity();
if (capacity > 0 && cache->occupied() > 0) {
auto oldBuckets = cache->buckets();
auto buckets = emptyBucketsForCapacity(capacity);
cache->setBucketsAndMask(buckets, capacity - 1); // also clears occupied

cache_collect_free(oldBuckets, capacity);
cache_collect(false);
}
}

如上述源码可知, 在flushCaches函数中, 这个函数会把当前类本身, 当前类的元类以及当前类的子类的方法缓存全部清空, 这里我们也可以自己验证一下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(sayHello);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);


// method_setImplementation(originalMethod, (IMP)hookedSayHello); //Runtime API, 可以发现cache被清除了, 可以打开注释, 验证结果

struct method_t *method = (struct method_t *)originalMethod;
// method->imp = (IMP)hookedSayHello; // 直接复制imp指针

struct my_objc_class *clz = (__bridge struct my_objc_class *)aClass;
uint32_t cacheCount = clz->cache.capacity();
NSLog(@"cacheCount : %d", cacheCount);


for (NSInteger i = 0; i < cacheCount; i++) {
char *key = (char *)((clz->cache._buckets + i)->_key);
// 这里设置一下
printf("%ld - %s\n", i, key); // 测试
});
}

当调用Runtime API method_setImplementation, 打印如下图所示:
调用Runtime API method_setImplementation,cache被清除 method_setImplementation_cache.png
当直接给imp指针赋值, 打印如下图所示:
imp指针赋值,cache没被清除 direct_imp_cache.png

可以看出, 当直接给imp指针复制, 不清除方法缓存, 其中打印的sayHello正是我们hook的方法, 之前的疑惑也一扫而空, 虽然方法的imp指向发生了改变, 但是方法缓存中的sayHello对应的imp并没有发生改变。

我们知道, Objective-C通过方法缓存来提升方法调用速度, 缓存中找不到, 再去类对象的方法列表中去查找, 调用后便加入到方法缓存中, 这点也可以通过objc_msgSend的源码来确认, objc_msgSend的源码是汇编实现的, 即使看不懂汇编也没事, 通过旁边的注释, 大概来看出来调用流程: 在方法缓存中寻找, 找到直接返回方法IMP, 否则调用__objc_msgSend_uncached, 去方法列表中查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// objc_msgSend, 除去一些nil验证检测后, 调用 CacheLookup LOOKUP
LLookup_GetIsaDone:
CacheLookup LOOKUP // returns imp


/// CacheLookup
.macro CacheHit
.if $0 == NORMAL
MESSENGER_END_FAST
br x17 // call imp
.elseif $0 == GETIMP
mov x0, x17 // return imp
ret
.elseif $0 == LOOKUP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached

作怪到底–自己修改方法缓存对应的imp

既然都到这里, 不妨尝试自己去修改方法缓存中对应imp。其实从Objective-C Runtime层面来说, 对象、方法、block等都是以结构体的形式存在内存中, 想去改对象的属性, 方法的实现会是block的实现, 都是要对它们的内存布局有所了解。

前面的分析把疑惑基本解决了, 现在要做的就比较简单是了, 只需要将方法缓存以及其他需要用到的结构体如对象、方法等的结构抽出来, 自己声明一个结构体, 把需要用上的成员变量和方法带上即可, 不需要用上可以直接删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct bucket_t {
cache_key_t _key;
IMP _imp;
};

struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();

mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();

static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);

static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};

接下来, 只需要将load方法中添加一点代码进行验证即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
// Class aClass = self; // 不给self发消息, cache不会生成, 结果就和我们的预想一样

SEL originalSelector = @selector(sayHello);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);

// method_setImplementation(originalMethod, (IMP)hookedSayHello); //Runtime API, 可以发现cache被清除了, 可以打开注释, 验证结果

struct method_t *method = (struct method_t *)originalMethod;
method->imp = (IMP)hookedSayHello;

// cache问题, 因为 已经和 imp缓存了, 直接会调用原来方法
// method_setImplementation 中有个函数 flushCache -> cache_erase_nolock, 会重新设置 cache

// 修改cache
struct my_objc_class *clz = (__bridge struct my_objc_class *)aClass;
uint32_t cacheCount = clz->cache.capacity();
NSLog(@"cacheCount : %d", cacheCount);


for (NSInteger i = 0; i < cacheCount; i++) {
char *key = (char *)((clz->cache._buckets + i)->_key);
// 这里设置一下
printf("%ld - %s\n", i, key); // 测试

if (key) {
NSString *selectorName = [NSString stringWithUTF8String:key];

if ([selectorName isEqualToString:@"sayHello"]) {
(clz->cache._buckets + i)->_imp = (IMP)hookedSayHello;
}
}
}
});
}

自己修改cache direct_imp_cache_and_change_cache_imp.png

发现打印的确实是我们希望的实现, 当然这里只是一个简单的类, 对于有子类的情况没做验证, 如果有子类的情况下, 还是比较复杂的, 对于子类是否实现了该方法也是有区别的, 这也许也是 method_setImplementation 直接暴力地将当前类和子类的缓存都清空的原因吧!

总结

通过本次探索, 对方法调用以及底层的一些流程有了一定的了解, 虽然对于开发确实没太大帮助, 但对于理解底层机制有一定帮助。在日常学习中, 可以配合源码, 通过自己的尝试, 一定可以对相关知识有更深刻地理解。

代码地址: https://github.com/LoyalToOrigin/HookMethodWithLayout

iOS自动化打包之重签名导出不同证书ipa探索

Posted on 2018-03-08 | Edited on 2018-11-20 | In iOS | Comments:

在完成基础的自动包打包流程过后,随即也出现了日常中常见的问题,比如我们每次需要打出不同网络环境和不同证书的ipa,由于开发者可以添加的设备只有100个,而公司的几个项目都是用的一个账号,各项目组都是独立的,再加上期间加入设备的员工的离职,真正能参与测试的设备寥寥无几。

所以我司一般测试都是使用企业证书,这样不同的项目都可以公用同一个证书,不仅管理起来方面,而且还摆脱了设备数量限制的烦恼,但另一方面,对于需要测试内购等功能的时候,仍然需要使用adhoc证书的包来进行测试。

我们原先的打包策略是通过执行脚本时输入的参数来打对应的包,这样对于不同测试并行测试,一次就要打出好几个,以我司作为打包服务器 Mac Mini 来说,archive + export 一个包的时间约为20min,对于不同证书不同环境的包随机组合,一次打出4个不同的包的时间就要花费约1h20min,而且在打包的时候,如果其他同事修改了新的bug,也无法打包。

因此,我们寻思能不能通过重签名的方式,只编译一次,对其重签名,打出不同的包。

本文主要介绍我在此过程中的一些探索,旨在提高不同证书不同环境的打包效率。

对ipa进行重签名

起初,我在网上查阅了相关资料,按照相关教程,却最终以失败告终。 如果有同学直接对ipa进行重签名成功的,希望不吝赐教。

我估摸着是不是内部做了什么验证,导致对ipa重签名无法成功。所以,我想可不可以不到ipa这步,更早地对其进行信息的修改以及重签名,权当一次尝试,即使失败也能在探索中学到新知识。最终,成功将原来打4个包需要1h20min的时间压缩到30min不到。

不等导出ipa,修改.xcarchive文件

.xcarchive文件是对项目进行手动archive,或执行以下脚本:

1
xcodebuild archive -workspace ${work_space} -scheme ${scheme} -configuration ${configurationDistribution} -archivePath ${archivePath}

如果对打包命令不是很了解的,可以查看我的上一篇文章文章:关于iOS自动化打包的一些分享

首先,我们进入到 .xcarchive 文件目录,发现里面一个 Info.plist 文件,打开如下显示:
xcarchive_infoplist.png

我们可以看到里面有一些App必需的属性。

1. 修改 .xcarchive 的 Info.plist

此处,如果项目 Bundle Identifier 需要发生改变,则修改 CFBundleIdentifier 对应的值,并将 SigningIdentity 改成 Bundle Identifier 对应的证书,关于此处SigningIdentity的值,可在钥匙串中找到对应的证书,查看其信息,即为下图中(英文系统)的 Common Name 。

certificate_info.png

2. 修改 App Extension 相关信息

此步是对于项目 target 中如 notification extension 等从属 target,如果没有 App Extension ,直接可以跳过此步,查看下一步 修改主target相关信息 。

通过文件夹打开 YourAppName.xcarchive/Products/Applications/YourAppName.app/PlugIns/YourAppNameNotificationServiceExtension.appex ,这里不是标准文件夹,open 命令似乎不起作用,观察其目录结构:

extension_floder.png

2.1 修改 Info.plist 相关信息

App Extension 的 Bundle Identifier 是 App 的 Bundle Identifier 加上其对应后缀,如 notificationserviceextension 。

修改 Bundle Identifier 为对应的值,这里对应的值是指之前修改 .xcarchive 目录中 Info.plist 的 Bundle Identifier 对应,如 com.test.www ,这里便是 com.test.www.notificationserviceextension。

extension_infoplist.png

2.2 替换 Provisioning Profile

将对应的 Provisioning Profile 拷贝到该目录下替换原来的 Provisioning Profile ,改成相同的文件名 embedded.mobileprovision 。

2.3 修改 archived-expanded-entitlements.xcent

我们通过xcode打开archived-expanded-entitlements.xcent,其本质就是plist文件,
格式是 teamId.bundle identifier 。

extension_archived-expanded-entitlements.png

修改图中遮盖的两项值,依旧是要和.xcarchive的Info.plist值对应。

2.4 重签名

用对应的证书对 App Extension 重新签名,这里的 YourCetificateName 依旧是修改 .xcarchive的Info.plist 里的证书名。

1
codesign -f -s "YourCetificateName" YourAppNameNotificationServiceExtension.appex

3. 修改主target相关信息

与上一步修改 App Extension 步骤基本相同,只是少一步,不用修改 archived-expanded-entitlements.xcent 。

3.1 修改Info.plist的相关信息

进入.app目录,修改Info.plist的Bundle Identifier,使其与.xcarchive文件对应。

你也可以修改其他一些值,如网络环境,是测试环境,还是生产环境,这里只是抛砖引玉。事实上,修改网络环境有方便的方法,如通过读取粘贴板的文本来切换,或者写一个辅助程序来打开我们的App,从而通知切换环境。

3.2 替换Provisioning Profile

将对应的 Provisioning Profile 拷贝到该目录下替换原来的 Provisioning Profile ,改成相同的文件名 embedded.mobileprovision 。

3.3 重签名

用对应的证书对 .app文件 重新签名,这里的 YourCetificateName 依旧是修改 .xcarchive的Info.plist 里的证书名。

1
codesign -f -s "YourCetificateName" YourAppName.app

4. 导出包

1
xcodebuild -exportArchive -archivePath YourAppName.xcarchive -exportPath $(pwd) -exportOptionsPlist YourExportOptionsPlistPath

成功后,命令台输出:

export_succeed.png

如果对于 exportOptionsPlist 不了解的,也可以看我的上篇文章:关于iOS自动化打包的一些分享
。

注意点

上述修改的每一步,无论是Bundler Identifer,还是Provisioning Profile,还是重签名用的证书,都是需要相对应的,如果有一步错了,ipa包是导不出来的。

我的表述可能不是那么清楚,相信大家操作一次,一步一步来,修改需要修改的值,其实基本是一目了然的。
大家如果有类似需求,建议先操作一次,成功后再写脚本实现自动化。

总结

经过上述操作,实质上只进行了一次编译,然后修改相关信息,导出对应不同的证书的包,只是多做了几次导出操作,大大地节省了打包时间。大家如果有什么想法或更好的办法,欢迎一起讨论讨论。

关于iOS自动化打包的一些分享

Posted on 2018-01-11 | Edited on 2018-11-20 | In iOS | Comments:

说到自动化打包,相信大家在日常开发中都有所接触,尤其是在多分支并行开发的情况下,自动化打包显得尤为重要,很多时候,我们打包一般是打及成分支的包,开发却在开发分支上,如果采取手动打包,我们需要反复切分支,不仅影响工作效率,而且会打断我们的开发思维,而却在工程较大的情况下,xcode每次indexing需要的时间就很久。

即使对于很多单分支开发的小项目来说,自动化打 包的优势也是不言而喻的,因为在手动打包的同时,基本可以说是什么事都做不了的,你需要一步步等待archive,export这些机械化的步骤。而有了自动化打包,你只需要点击一个按钮,便可以继续自己的开发。所以,自动化打包势在必行。

本文主要记录了我在公司自动化打包布置中的一些探索,及各平台的优缺点和配置过程踩过的坑。

谈到iOS的持续集成,我们首先想到的一定会是jenkins,这里我先介绍下我司采用的Mac OS Server(以下简称Server)这个平台的一些优缺点。

Server相比于jenkins,我总结优点有三:

  1. 相比于jenkins的各种繁琐配置,Server配置简单,全程基本下一步操作即可;
  2. 直接使用xcode就可开始构建项目,而不需要登录网页;
  3. 集成度相当高,没有特别的需求,基本可以不写脚本,只需要配置一个plist文件即可以打包。

这里不做过多的配置介绍,虽然Server没有jenkins热门,但网上的文章也比比皆是,而且如上优点1中所说,Server配置真的很简单,在证书、描述文件齐全的情况下,基本就是一直点下一步操作。

下面我介绍使用过程中需要注意的一些方面:

xcode_integration.png

如上图所示,上图是对bot的各种设置,一个bot对应一个独立工作空间,如果有了解jenkins的话,bot可以类比jenkins的一个项目。

如果对打包没有特别需求,勾选Archive,选择对应Scheme、Configuration,指定一个plist文件,后面的Triggers不需要写任何代码,便可以打出对应的包。

上面所说的plist文件大体结构是这样的:
exportPlist.png

这个plist文件对应一系列的参数,并不需要我们自己写,手动打一次包,即可导出这个文件。这里顺便提一句,Server配置好后,连接此Server后,任意机器都可以上传此plist文件,Server是将上传的plist文件拷贝到当前Bot工作目录下。

在Server配置好后,即使是windows电脑也可以通过ip或者自签证书登录,
https://192.168.0.xxx/xcode/lastest 或 https://xxxdemac-mini.local/xcode/bots/latest,登陆后会显示以下界面,点击integration,便可以开始集成了,但是这里似乎只能够集成,不能配置,不过根据Apple的惯性,想要构造自己的生态,我们也是能理解的。
xcode_page.png

关于jenkins的一些配置注意事项:

以下是我在配置过程中踩到的一些坑:

  1. 8080端口被其他程序占用,启动失败: java -jar jenkins.war —httpPort=8082;
  2. git权限需要告诉jenkins私钥,而不是git上的公钥: cat ~/.ssh/id_rsa;

jenkins_rsa.png

接下来,其他用户直接通过浏览器登录 http://192.168.0.xxx:8082 ,通过账号密码登录,便可以配置和构建项目。

jenkins相对Mac OS Server的优点:

  1. 同一局域网便可以登录,登录之后便可以自行配置项目
  2. 似乎可以并行构建任务

当使用Mac OS Server进行打包,无论进行多少个打包任务,它只开启一个xcodebuild进程
xcodebuild_process_server.png
而使用jenkins进行多项目打包,这里开始构建两个项目就开启两个进程(下图上面两个xcodebuild进程是jenkins开启)
xcodebuild_process_jenkins.png
这里我没有做定量的测试,猜想是jenkins的效率稍优,对于多核处理器,相同的计算能力,对于两个构建来说,应该没多大差距,但对于拉代码等耗时操作,比起Server其他构建任务在排队,这部分就能省上一些时间。

但是jenkins有更方便的打包方式:
jenkins开启token,不需要用户名登录便可以打包:

jenkins_token.png

这样给构建项目设置后还是不行的,因为jenkins觉得这样是不安全的,拿到了token就可以做任何事了。
系统管理->全局安全配置->勾选 Allow anonymous read access
jenkins_allow_anonymous_read_access.png

接着,我们便可以通过命令来打包:

1
curl http://10.24.113.24:8082/job/notification_extension_test/build\?token\=123\&cause\=testBuild

参数 注释
notification_extension_test 项目名称
token 上面设置的token
cause 可选参数,可不传

这样似乎可以用一台服务器,将打包任务部署到指定机器上:
jenkins_servers.png
这样可以在一台机器上集成公司不同端的项目,而且还不影响打包效率。

关于Server和jenkins的一些总结:

  1. 如果仅仅是iOS端的打包,Server是完全够用了,而且操作贴近我们平时的开发风格,虽然网页无法配置,但是对于大部分公司来说,打包配置都是开发在做的,而不是测试;
  2. 对于iOS端小型项目来说,没有特别多的分支,直接可以多建几个bot,从而避开手写脚本;
  3. 如果多端同一服务器,那么jenkins无疑有较大的优势;如果公司有足够的电脑作为分布打包服务器,那么打包效率会更上一层楼。

fastlane及打包脚本简单介绍

说到自动化打包,就不得不谈当下非常流行的fastlane,如果说Server和jenkins是同一维度的,都是打包平台,那么fastlane应该是和shell脚本来作比较,或者可以说,fastlane是在shell的基础上封装了一层,fastlane相比于脚本打包,短暂体验后,我觉得优点主要有:

  1. 避免繁琐的路径拼接,拷贝等
  2. 修改工程配置文件,避免调试时修改配置文件不小心提交到远程分支,导致打包失败

我们来简单看一段fastlane的打包代码:
fastlane_demo.png

上述代码参数基本见名知意,不难看出,这基本就是给之前Server的exportPlist文件的一种包装,只需执行

1
fastlane adhocMyApp version:100000  // 100000是传的版本号

便可以自动打出一个包,并导出dSYM文件,这里我故意把Distribution的provisioning Profile改成企业的
fastlane_configuration.png

发现工程配置文件发生了改变,这里比较暴力,把每种configuration的Provisioning Profile和teamID全都改了
fastlane_change_configuration.png

我们再看终端,看看fastlane究竟做了些啥
fastlane_temernal.png

也确实和上图一样,把所有都改成了AdHoc的。在进行修改配置后,最终也是执行打包的核心脚本:

1
2
3
4
// 对应手动打包archive
xcodebuild archive -workspace ${work_space} -scheme ${scheme} -configuration ${configurationRelease} -archivePath ${archivePath}
// 对应导出步骤
xcodebuild -exportArchive -archivePath ${archivePath} -exportPath ${exportPath} -exportOptionsPlist ${exportOptionsPlist}

上述脚本的参数也基本见名知意,脚本中${work_space}等代表取一个变量的值,这里都是各个配置对应的路径或字符串。

经历上述脚本后,就会在指定的exportPath路径下生成.ipa文件。我们一般是要将ipa和dSYM文件copy到指定的文件夹供测试去取,后面便是一段处理繁琐的路径的脚本,脚本本身没任何难度,但是要格外注意,且测试起来需要花费一定的时间,如果使用fastlane就可以避免这个烦恼。

总结

本文主要是团队中的一次分享后的整理,并不是特别细致的教程,只是对当下的自动化打包的一些尝试及过程中遇到的一些问题和自己的一点思考,如果有说的不对的地方,望不吝赐教。

loyaltoorigin

4 posts
1 categories
© 2021 loyaltoorigin
Powered by Hexo v3.8.0
|
Theme — NexT.Muse v6.3.0