CJZ's Blog

回忆昨天,珍惜今天,期待明天


  • Home

  • Tags

  • Categories

  • Archives

IGList Diff算法

Posted on 2020-11-27 | In 算法

名词作用解释

  • IGListEntry: 用于记录数据在新数组和老数组出现的情况
  • table :存放所有的 Entry,key-value
  • newResultsArray :存到new数据的元素 Entry
  • oldResultsArray :存到old数据的元素 Entry
  • newArry 存放着原始新数据的array
  • oldArry 存放着原始老数据的array

Entry类

1
2
3
4
5
6
7
8
9
10
struct IGListEntry {
/// 在OldArray出现的次数
NSInteger oldCounter = 0;
/// 在NewArray出现的次数
NSInteger newCounter = 0;
/// 在OldIndex的位置,如果是遍新newArray的话,会往这个栈添加NSNotFound
stack<NSInteger> oldIndexes;
/// 标志是否要更新
BOOL updated = NO;
};

算法流程

第一步

遍历 newArry,创建 newResultsArray 里面存放着 IGListRecord,每个元素的newCounter大于0,并向oldIndexs栈添加NSNotFound

1
2
3
4
5
6
7
8
9
10
11
12
vector<IGListRecord> newResultsArray(newCount);
for (NSInteger i = 0; i < newCount; i++) {
id<NSObject> key = IGListTableKey(newArray[i]);
IGListEntry &entry = table[key];
entry.newCounter++;

// add NSNotFound for each occurence of the item in the new array
entry.oldIndexes.push(NSNotFound);

// note: the entry is just a pointer to the entry which is stack-allocated in the table
newResultsArray[i].entry = &entry;
}

第二步:

遍历 oldArray,创建 oldResultsArray 里面存放着 IGListRecord,每个元素的 oldCounter大于0,并且oldIndexs的top为
当前元素在oldArray的index

1
2
3
4
5
6
7
8
9
10
11
12
vector<IGListRecord> oldResultsArray(oldCount);
for (NSInteger i = oldCount - 1; i >= 0; i--) {
id<NSObject> key = IGListTableKey(oldArray[i]);
IGListEntry &entry = table[key];
entry.oldCounter++;

// push the original indices where the item occurred onto the index stack
entry.oldIndexes.push(i);

// note: the entry is just a pointer to the entry which is stack-allocated in the table
oldResultsArray[i].entry = &entry;
}

第三步

  1. 遍历 newResultArray
  2. 如果当前 oldIndexes pop出来的位置是小于oldCount的话,即表示该元素在oldArray也存在。
  3. 分别从 newArray[i] (注:这是原始数据,并不是上面的newResultArray) 和oldArray[originalIndex] (同样) 取出元素。进行 diff 比较,如果两个不等,设置 updated = YES
  4. 如果 newCount 和 oldCount 且 originalIndex != NSNotFound (为什么第三步不用这个来判断??)。设置 newResultArray和 oldResultArray 里面的 Record 的 index 互为对方的位置;
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
for (NSInteger i = 0; i < newCount; i++) {
IGListEntry *entry = newResultsArray[i].entry;

// grab and pop the top original index. if the item was inserted this will be NSNotFound
NSCAssert(!entry->oldIndexes.empty(), @"Old indexes is empty while iterating new item %li. Should have NSNotFound", (long)i);
const NSInteger originalIndex = entry->oldIndexes.top();
entry->oldIndexes.pop();

if (originalIndex < oldCount) {
const id<IGListDiffable> n = newArray[i];
const id<IGListDiffable> o = oldArray[originalIndex];
switch (option) {
case IGListDiffPointerPersonality:
// flag the entry as updated if the pointers are not the same
if (n != o) {
entry->updated = YES;
}
break;
case IGListDiffEquality:
// use -[IGListDiffable isEqualToDiffableObject:] between both version of data to see if anything has changed
// skip the equality check if both indexes point to the same object
if (n != o && ![n isEqualToDiffableObject:o]) {
entry->updated = YES;
}
break;
}
}
if (originalIndex != NSNotFound
&& entry->newCounter > 0
&& entry->oldCounter > 0) {
// if an item occurs in the new and old array, it is unique
// assign the index of new and old records to the opposite index (reverse lookup)
newResultsArray[i].index = originalIndex;
oldResultsArray[originalIndex].index = i;
}
}

处理Delete的数据

遍历 oldResultArray ,index 为NSNotFound设置为删除

1
2
3
4
5
6
7
8
9
10
11
for (NSInteger i = 0; i < oldCount; i++) {
deleteOffsets[i] = runningOffset;
const IGListRecord record = oldResultsArray[i];
// if the record index in the new array doesn't exist, its a delete
if (record.index == NSNotFound) {
addIndexToCollection(returnIndexPaths, mDeletes, fromSection, i);
runningOffset++;
}

addIndexToMap(returnIndexPaths, fromSection, i, oldArray[i], oldMap);
}

offset的示意图(橙色表示会被删除)

image

Insert、Update、Move处理

添加判断逻辑

在newResultArray 中,代码的判断 record.index == NSNotFound ,那就属于要被添加进去的

更新判断逻辑

同样看下面代码,之前设置了update直接添加到update结果里面就行了。

移动的判断逻辑:

在上面会记录了删除后和插入后的偏移位置,如果 oldIndex - deleteOffset + insertOffset 计算后和当前的位置是一样的,就表示不用移动。就好比:[“1”, “2”, “3”] => [“”2, “3”],2的这个位置在新数组里面是 0 ,他的oldIndex = 1, deleteOffset也是1,经过删除后,就是0,即不用移动。

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
for (NSInteger i = 0; i < newCount; i++) {
insertOffsets[i] = runningOffset;
const IGListRecord record = newResultsArray[i];
const NSInteger oldIndex = record.index;
// add to inserts if the opposing index is NSNotFound
if (record.index == NSNotFound) {
addIndexToCollection(returnIndexPaths, mInserts, toSection, i);
runningOffset++;
} else {
// note that an entry can be updated /and/ moved
if (record.entry->updated) {
addIndexToCollection(returnIndexPaths, mUpdates, fromSection, oldIndex);
}

// calculate the offset and determine if there was a move
// if the indexes match, ignore the index
const NSInteger insertOffset = insertOffsets[i];
const NSInteger deleteOffset = deleteOffsets[oldIndex];
if ((oldIndex - deleteOffset + insertOffset) != i) {
id move;
if (returnIndexPaths) {
NSIndexPath *from = [NSIndexPath indexPathForItem:oldIndex inSection:fromSection];
NSIndexPath *to = [NSIndexPath indexPathForItem:i inSection:toSection];
move = [[IGListMoveIndexPath alloc] initWithFrom:from to:to];
} else {
move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:i];
}
[mMoves addObject:move];
}
}

addIndexToMap(returnIndexPaths, toSection, i, newArray[i], newMap);
}

最后生成结果

主要把update的变成 insert和delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// convert move+update to delete+insert, respecting the from/to of the move
// 遍历 又要moves和update的值,(前面设置update的时候是还没delete和insert),所以后面会有可能产生 update + move
const NSInteger moveCount = moves.count;
for (NSInteger i = moveCount - 1; i >= 0; i--) {
IGListMoveIndexPath *move = moves[i];
if ([filteredUpdates containsObject:move.from]) {
[filteredMoves removeObjectAtIndex:i];
[filteredUpdates removeObject:move.from];
[deletes addObject:move.from];
[inserts addObject:move.to];
}
}

// iterate all new identifiers. if its index is updated, delete from the old index and insert the new index
// 同理,把从老数数的Map里面,取出要更新的,变成insert和delete
for (id<NSObject> key in [_oldIndexPathMap keyEnumerator]) {
NSIndexPath *indexPath = [_oldIndexPathMap objectForKey:key];
if ([filteredUpdates containsObject:indexPath]) {
[deletes addObject:indexPath];
[inserts addObject:(id)[_newIndexPathMap objectForKey:key]];
}
}

扩展阅读
  • DeepDiff库(diff算法的另一种实现)
  • https://medium.com/flawless-app-stories/a-better-way-to-update-uicollectionview-data-in-swift-with-diff-framework-924db158db86
  • https://xiaozhuanlan.com/topic/6921308745 Wagner–Fischer算法

卡顿监控笔记

Posted on 2020-11-20

卡顿监控就目前来说有三种方案:

CADisplayLink 计算帧数

CADisplayLink是和屏幕刷新保持同步的,所以可以用这个来展示fps的值。

这种方案有个问题,就是帧率变化也会被当成卡顿。

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
@implementation ViewController {
UILabel *_fpsLbe;

CADisplayLink *_link;
NSTimeInterval _lastTime;
float _fps;
}

- (void)startMonitoring {
if (_link) {
[_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_link invalidate];
_link = nil;
}
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}

self.count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
_fps = _count / delta;
self.count = 0;
_fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}

使用Runloop的状态来判断是否出现卡顿

所有的代码运行都是基于Runloop的,我们就可以通过监听 Runloop的状态,来判断调用方法是否执行时间是否过长。

参考网上的Runloop精简的代码

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
    /// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

/// 5. GCD处理main block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();


/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

我们会向Runloop添加一个Observer,然后在回调的方法时,把状态记录下来。如果在kCFRunLoopBeforeSources或者在kCFRunLoopAfterWaiting这两个状态保持时间太长,我们就可以认为线程受阻了。

举个例子
这个buttonTap里面操作非常耗时,buttonTap这个函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__是在runloop这个方法调用到的。在这个时间,我们保存的状态应该是kCFRunLoopBeforeSources,如果一直长时间在这个状态,就可以认为当时线程受阻。
image

参考戴铭老师的代码如下

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@interface MonitorMain()

@property (nonatomic, strong) dispatch_semaphore_t dispatchSemaphore;
@property (nonatomic, assign) CFRunLoopObserverRef runLoopObserver;
@property (nonatomic, assign) NSInteger timeoutCount;
@property (nonatomic, assign) CFRunLoopActivity runLoopActivity;

@end

@implementation MonitorMain

- (void)start {
self.dispatchSemaphore = dispatch_semaphore_create(0);
// dispatchSemaphore = dispatch_semaphore_create(0);
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopCallBack,
&context);

CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopObserver, kCFRunLoopCommonModes);

dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
// 88 ms后,就会超时。连续三次超时,记启动一次的记录一次卡顿
long semaphoreWait = dispatch_semaphore_wait(self->_dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 88 * NSEC_PER_MSEC));
// 如果是超时了,这里不等于0
if (semaphoreWait != 0) {
// stop
if (!self.runLoopObserver) {
self.timeoutCount = 0;
self.dispatchSemaphore = 0;
self.runLoopActivity = 0;
return;
}

if (self.runLoopActivity == kCFRunLoopBeforeSources ||
self.runLoopActivity == kCFRunLoopAfterWaiting) {
self.timeoutCount ++ ;
if (self.timeoutCount < 3) {
continue;
}
//
NSLog(@"检测到卡顿");
}
}
self.timeoutCount = 0;
}
});
}

void runLoopCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
MonitorMain *monitor = (__bridge MonitorMain*)info;
monitor.runLoopActivity = activity;

switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;

case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
case kCFRunLoopAllActivities:
NSLog(@"kCFRunLoopAllActivities");
break;

}

// 发出信号
dispatch_semaphore_signal(monitor.dispatchSemaphore);
}

- (void)stop {
if (!self.runLoopObserver) {
return;
}

CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.runLoopObserver, kCFRunLoopCommonModes);
CFRelease(self.runLoopObserver);
self.runLoopObserver = NULL;
}

@end

但是这个方法有个问题暂时还没找到答案:

image

从上图可以看到- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath方法是回调在- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath里面。

而这时是走到了kCFRunLoopBeforeWaiting这个状态里,所以卡顿的状态判断是没办法判断这种情况的。

二进制重排笔记

Posted on 2020-11-20

在文章开始之前,先搞明白一个问题,在iOS为什么二进制重排能优化启动时间?

一句话来说,就是减少 Page Fault 带来的性能损失

那么什么是PageFault,我们先从虚拟内存讲起。

为什么会引入虚拟内存

  • 地址空间不隔离,所有程序都可以直接访问物理地址,恶意程序就可以随便修改内存的的值。
  • 内存使用效率低
    • 当A和B读进内存中,忽然要执行c,这时候内存不够,而C差不多要占据整个内存,就要把A和B存回磁盘中。因为这样子大量的数据换出换入,效率低下。
  • 程序运行的地址不确定
    因为在程序编写中,指令和数据的地址是固定的,这样子就涉及了程序的重定位问题。

正是因为上面的3大问题,所以在计算机中引入了虚拟地址的概念。

分段

一开始的方案是使用分段,即把每个程序的对应的虚拟内存地址映身到物理内存中
image
像这种,虽然能解决地址隔离和重定位的问题,但是效率还是不高,因为每次都要把整块地址交换到磁盘中。

分页

分页的基本方法是地址空间人为地等分成固定大小的页,每一页的大小由硬件决定,由操作系统选择决定页的大小。
image
如上图所谓,内存映射表会把每一页虚拟内存映射到 物理内存 中。
如果进程1要访问 Virtual Page 3,这时 page3 不在内存中,就会发生缺页中断,就是 页错误(Page Fault,缺页异常)。然后操作系统接管进程,负现把 Page 3从磁盘中读出来装到内存中。然后内存映射表建立映射关系。

虚拟内存的实现要依靠硬件的实现,一般来说CPU内置着一个叫 MMU (Memory Management Unit)的部件进行页映射。

image

怎么做

  1. 生成Order文件
  2. 在Xcode 的 BuildSetting里配置Order文件
  3. 生成LinkMap文件,查看是否重排成功
  4. 使用System Trace查看page fault的次数
  5. 最终要看启动时间是否减少

其中的难点就在于怎么生成 Oder 文件,把 App 启动时的调用到的符号尽可能放在一个page里

生成Order的三种方法

clang 静态插桩

静态插桩就是利用clang提供的回调方法,在回调方法里面获取符号。

官方文档:https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs

大佬提供的库: https://github.com/yulingtianxia/AppOrderFiles

使用fishhook objc_msgSend 方法

Hook objc_msgSend方法要保证栈平衡。
参考戴老师的方法

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
// 寄的value放到x12寄存器中,通过寄存器寻址跳转到地址
#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");

// 把x0~x9的寄存器保存起来,
// 保存x9据说为了内存对齐
// 如果这里用到浮点数,还要把q0~q9保存起来
#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");

// 从栈里面把数据读取出来,恢复寄存器
// 调整sp指针
#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );

__attribute__((__naked__))
static void hook_Objc_msgSend() {
// Save parameters.
/// 保存寄存器的参数到栈里面
/// Step 1
save()

/// Step 2
// 保存lr寄存器到第三个寄存器中,_before_objc_msgSend的方法,第三个参数就是lr
__asm volatile ("mov x2, lr\n");
__asm volatile ("mov x3, x4\n");

/// 此时的x0、x1,就是objc_msgSend方法里面的id,sel
call(blr, &before_objc_msgSend)

/// 从栈里面取出参数到放到寄存器中
load()

/// 调用原来的msg_send方法
call(blr, orig_objc_msgSend)

/// 保存原来
save()

/// 调用after_objc_msgSend方法,这个方法要把lr寄存器的地址返回。
call(blr, &after_objc_msgSend)

/// 上面调用after_objc_msgSend返回的lr寄存器地址会放到x0里面,所以我们把x0恢复到原来的lr寄存器即可
__asm volatile ("mov lr, x0\n");

/// 恢复上下文
load()

// 调用返回方法
ret()
}

修改静态库的符号表

这个方案来自:静态拦截iOS对象方法调用的简易实现

原理:修改静态库的符号名,在静态链接时就能链接到修改后的方法

定义Person.m 文件

1
2
3
4
5
6
7
@implementation Person

- (void)sayHelloToWorld {
NSLog(@"1+2=3");
}

@end

用clang把.m文件生成.o目标文件,

clang -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk -c Person.m -o Person.o

现在的文件还未链接,跳转NSLog符号的地址还没定,一旦链接到bl跳转的地址就是目标地址

image
image
image
image

这样子就能找到符号了,如果我们把0x0928位置的N字符改成O。就会变成下面OSLog这个方法
image
image

我们在主工程定义了OSLog方法,在静态链接就会链接到这个方法。

fishhook 原理笔记

Posted on 2020-11-20

在OC中想在hook一个函数,绝对是利用runtime swizzle 方法。但是对于c函数我们应该怎么hook呢?
没错,就是利用今天的主角fishhook

使用方法

我们以 hook NSlog() 为例

先定义我们自己的 log 函数 与 NSLog 的函数指针

1
2
3
4
5
6
void mLog(NSString * format, ...) {
format = [format stringByAppendingFormat:@"勾上了!\n"];
sys_nslog(format);
}

static void(*sys_nslog)(NSString * format, ...);

声明 rebinding 的结构体。这个结构体包含了重新绑定的信息。

1
2
3
4
5
6
7
8
struct rebinding nslog;
nslog.name = "NSLog";
nslog.replacement = mLog;
nslog.replaced = (void *)&sys_nslog;
//rebinding结构体数组
struct rebinding rebs[1] = {nslog};

rebind_symbols(rebs, 1);

最终我们调用 NSLog方法,打印如下:

xxx 勾上了!

此处有一个小问题?如果在这个项目里面的 c 函数能hook住吗?答案是不能!

原理解释

新建一个项目,在ViewDidload里面打印文本,把生成的可执行文件拖到 Hopper Disassembler 中。
image
image

这里跳到 imp__stubs_NSLog 这个位置,进来继续看
image
上面是把 0x0c00 位置的值放到x16寄存器上,我们看一下地址

image
找到 0x0C000 的地址,这里是 Data Segment,__la_symbol_ptr Section。这里有两个点很重要:

  • 属于Data段,可读写
  • __la_symbol_ptr section 是存放着动态链接的符号表,这里面的符号会在该符号被调用时,通过dyld的dyld_stb_binder过程进行加载。
    还有一种动态链接相关的表是 __nl_symbol_ptr 表示在动态链接库绑定的时候进行加载的

到这里就很显示了,fishhook 就是通过修改 __la_symbol_ptr 这张符号表里面的值来达到hook c函数的目的。
下面我们来验证一下

原理验证

在一个新项目的ViewController里面写上面代码,然后我们关注 __nl_symbol_ptr 这个符号表里面NSLog 对应地址的变化。把可执行文件拖到 MachOView里面
image
image
位置是这个0xc000,运行程序,进入断点。找到ALSR的偏移地址
image

image
lldb x命令指地址的值读出来,dis -s 以当前赋值的地址反汇编。
可以看出来,我们NSLog 调用的地址,暂时看不出来是什么(实际是调了dyld_stb_binder相关函数,进行动态绑定)。

接下来,我们继续走下一步,NSLog动态符号表对应的值 发生改变,对当前地址反汇编后发现是 Foundation 的NSLog 方法。

image

最后,让代码继续走过rebind_symbols方法,又又发现NSLog动态符号表对应的值 发生改变,对当前地址反汇编后发现是我们自定义的方法

image

结论很明显,__nl_symbol_ptr 处于Data段,可读写。动态符号可通过这里对应的地址值进行跳转,fishhook就是通过修改这里的值达到Hook目的。

fishhook源码解读

拿Mach-O文件和源码的操作一起对着来看。

rebind_symbols 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
// 把要被hook的函数信息用链表保存起来
// 头插法的形式添加到_rebindings_head这里
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
if (retval < 0) {
return retval;
}
// 第一次的话调用,注册加载镜像的方法_dyld_register_func_for_add_image,
// 如果已经加载过的镜像,会直接调用回调方法。其他的会在加载的时候回调)
if (!_rebindings_head->next) {
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}

第一步:preped_rebindings,这部分主要是把 rebindings_entry 以一个链表的形式保存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry));
if (!new_entry) {
return -1;
}
new_entry->rebindings = malloc(sizeof(struct rebinding) * nel);
if (!new_entry->rebindings) {
free(new_entry);
return -1;
}
// 头插法,把数据保存起来
memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
new_entry->rebindings_nel = nel;
new_entry->next = *rebindings_head;
*rebindings_head = new_entry;
return 0;
}

从上面看到,入口函数最终会调到_rebind_symbols_for_image,从页调到rebind_symbols_for_image这个函数里面。

1
2
3
4
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
rebind_symbols_for_image(_rebindings_head, header, slide);
}

rebind_symbols_for_image

其实重头戏也在这个(rebind_symbols_for_image)函数里面,

1
2
3
4
5
6
7
///
/// @param rebindings 需要被交换方法的数组
/// @param header Mach-O文件的header,通过_dyld_get_image_header获取
/// @param slide ASLR的偏移地址(_dyld_get_image_vmaddr_slide获取)
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {

这里可以分三部分来讲解:

第一部分:确定这个header是否合法

dladdr函数主要作用是获取 &info 所在动态库的信息,如果获取不到,就证明这个header地址是不合法的。

1
2
3
4
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}

第二部分:可以看到在查找三个 load_command

分别是
linkedit_segment、symtab_cmd、dysymtab_cmd,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// loadCommand的开始地址在header的后面
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
// 遍历所有的loadCommands
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
// Command为LC_SEGMENT_64有好几个(PAGEZERO, TEXT, DATA_COST, DATA, LINKEDIT)
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
// 根据segmentName 找到linkedit_segment
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}

} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
//如果上面的三个loadCommand有一个有空,就返回
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}

分析一下这三个LoadCommand主要会加载哪些信息

LINKEDIT Commond

image

image

根据地址可以看到,linkedit_segment 这个命令是用于加载 Dynamic Loader Info 相关的信息。

LC_SYMTAB(符号表)

image
image

LC_DYSYMTAB(动态符号表相关)

image

我们能看到这个 IndSym Table Offset 的值是 0x11648,其他的符号表的offset 是0。

IndSym Table Offset 用于存放在字符表的位置

image
通过上面的分析,我们搞明白了上面这几个load_command的作用了。那么回过头业,继续往下看第三部分

第三部分找到各个 符号表地址、字符串表地址、动态符号表地址,然后调用重新绑定函数的方法进行替换

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
// Find base symbol/string table addresses
//链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
//符号表的地址 = 基址 + 符号表偏移量
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
//字符串表的地址 = 基址 + 字符串表偏移量
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

// Get indirect symbol table (array of uint32_t indices into symbol table)
//动态符号表地址 = 基址 + 动态符号表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//非Data段的数据过滤掉,__la_symbol_ptr和__no_la_symbol_ptr(got)这两个section都是放在Data段上
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}

for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
//找懒加载表
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//非懒加载表
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}

image

至于为什么叫 got 段,估计是和 ELF 文件有关系

找到了这 __la_symbol_prt Section的位置后,就进入了查找符号位置,替换方法的逻辑。

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
/// 找到符号表所对应的位置进行绑定
/// @param rebindings rebindings 要交换的数组
/// @param section section_t当前loadcommand的section(__la_symbol_ptr, __no_la_symbol_ptr)
/// @param slide slide alsr程序偏移
/// @param symtab 符号表地址 10608
/// @param strtab 字符串表地址 11708
/// @param indirect_symtab 动态符号表地址 11648
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab) {
//获取__la_symbol_ptr 和 __no_la_symbol的在Indirect Symbols的起始位置
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); // 0xc000 (__DATA, __la_symbol_ptr的地址)

//遍历__la_symbol_ptr/ __no_la_symbol 里面的所有符号
for (uint i = 0; i < section->size / sizeof(void *); i++) {
//找到符号在Indrect Symbol Table表中的值
//读取indirect table中的数据
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
//以symtab_index作为下标,访问symbol table
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//获取到symbol_name
char *symbol_name = strtab + strtab_offset;
//判断是否函数的名称是否有两个字符,为啥是两个,因为函数前面有个_,所以方法的名称最少要1个
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
//遍历最初的链表,来进行hook
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断两个
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
//判断replaced的地址不为NULL以及我方法的实现和rebindings[j].replacement的方法不一致
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}

例子

下面以NSLog为例,一步一步使用 Mach-O对应的把流程走完看看,(NSLog 在 __la_symbol_ptr 符号表里面)

1
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; // 获取 __la_symbol_ptr 符号在 indSymBol的开始位置。

image

下面这图可以看到在Indirect Symbols 偏移是 0x19,也就是第25个。
image

走到这步, 其实就是 __la_symbol_ptr的位置

1
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);

image

进入for 循环第一句,获取在 symtab_index,在 0xE3 这个位置
uint32_t symtab_index = indirect_symbol_indices[i];

image

根据在 符号表里面的Index(0xE3 = 227),取到符号对象,并获取在字符串表 strtab的位置。

1
2
//以symtab_index作为下标,访问symbol table
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;

image

所以我们取到的字符的位置 0x11708 + 0xD4 = 0x117DC
image

然后判断symbol是否大于一,因为前面的 “_”字符是编译期加上的。

1
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];

进入while循环,把rebindings都过一遍,看看和上面字符相等的函数名,进行方法替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断两个
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
//判断replaced的地址不为NULL以及我方法的实现和rebindings[j].replacement的方法不一致
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}

我们回顾一下上面查找符号的过程:
image

  1. 从 Loadcomamd 的 reserved1 的位置,找到在 Indirect Symbol Table 的开始位置。
  2. 进入循环,从开始位置遍历 Indirect Symbol Table,长度是Section.size
  3. 遍历过程中,拿到Symbol Table的位置,从而知道符号的字符串
  4. 遍历过程中, 字符串和被替换相匹配,如果匹配就进行替换函数。

编码规范

Posted on 2018-12-11 | In 其他

Swift 编码规范

正确性

尽量使代码在没有警告下编译

命名

描述性和一致性的命名会使代码更新易读易懂,使用Swift官方命名约定中描述的 API Design Guidelines。现总结常用几点:

  • 避免不常用的缩写
  • 命名清晰比简洁重要
  • 使用驼峰命名方法
  • 类、协议、枚举不用像Objectice-c那样添加前缀

类前缀

Swift 自动以模块名称作为命名空间,不需要给类添加前缀,例如:RW。如果相同的两个类在不同的命名空间下,可以增加模块名字作为前缀。

1
2
import SomeModule
let myClass = MyModule.UsefulClass()

Delegate

在创建自定义的delegate方法时,第一个参数不带名字的参数应该是delegate原来的对象

Preferred

1
2
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool

Not Preferred:

1
2
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool

使用类型推断上下文

使用类型推断可以使代码整洁 (可参考Type Inference.)

Preferred:

1
2
3
4
let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)

Not Preferred:

1
2
3
4
let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)

泛型

泛型命名应该是可描述性的,使用驼峰命名。当类型名字没有意义时,使用传统的大写字母:T,U 或者 V

Preferred:

1
2
3
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T)

Not Preferred:

1
2
3
struct Stack<T> { ... }
func write<target: OutputStream>(to target: inout target)
func swap<Thing>(_ a: inout Thing, _ b: inout Thing)

类和结构体

应该用哪个

结构体是值类型。使用结构体的对象是不具有唯一性,例如:数组[a,b,c]和另外的一个定义在其他的数组[a,b,c]是可以互换的。不管是第一个数组还是第二个数组,它们所代表的含义是一样的。这就是为什么数组是结构体的原因。

类是引用类型。对拥有自己的id或者有生命周期的事物使用类。常常把人建模为一个类,两个人的对象是不同的东西,就算他们有相同的名字和生日,也不意味着他们是同一个人。但是生日应该为结构体,因为1950年3月的日期跟其他相同日期表示的意思是一样的。

例子

这是一个比较不错的定义Class的例子

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
class Circle: Shape {
var x: Int
var y: Int
var radius: Double
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}

init(x: Int, y: Int, radius: Double) {
self.x = x
self.y = y
self.radius = radius
}

convenience init(x: Int, y: Int, diameter: Double) {
self.init(x: x, y: y, radius: diameter / 2)
}

override func area() -> Double {
return Double.pi * radius * radius
}

//MARK: - CustomStringConvertible

var description: String {
return "center = \(centerString) area = \(area())"
}
private var centerString: String {
return "(\(x),\(y))"
}
}

使用Self

为了简洁起见,避免使用self,因为Swift不强求使用self来访问对象的属性或者调用方法。

只有在编译器要求使用self的时候,(在@escaping闭包里面或者在对象初始化的方法里面为了消除歧义),否则在没有编译器提醒时都应该省略它

计算属性

Preferred:

1
2
3
var diameter: Double {
return radius * 2
}

Not Preferred:

1
2
3
4
5
var diameter: Double {
get {
return radius * 2
}
}

Final关键字

如果你的类不需要派生子类,那就给类定义为final吧

1
2
3
4
5
6
7
// Turn any generic type into a reference type using this Box class.
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}

函数定义

函数定义在一行可以定义完包括大括号

1
2
3
func reticulateSplines(spline: [Double]) -> Bool {
// reticulate code goes here
}

多个参数,让每个参数应该在新的一行

1
2
3
4
5
6
7
func reticulateSplines(
spline: [Double],
adjustmentFactor: Double,
translateConstant: Int, comment: String
) -> Bool {
// reticulate code goes here
}

使用Void表示缺省

Preferred:

1
2
3
4
5
func updateConstraints() -> Void {
// magic happens here
}

typealias CompletionHandler = (result) -> Void

Not Preferred:

1
2
3
4
5
func updateConstraints() -> () {
// magic happens here
}

typealias CompletionHandler = (result) -> ()

函数调用

Mirror the style of function declarations at call sites. Calls that fit on a single line should be written as such:

1
let success = reticulateSplines(splines)

If the call site must be wrapped, put each parameter on a new line, indented one additional level:

1
2
3
4
5
let success = reticulateSplines(
spline: splines,
adjustmentFactor: 1.3,
translateConstant: 2,
comment: "normalize the display")

闭包表达式

只有参数列表末尾有一个闭包表达式参数时,才使用尾随闭包。否则还是应该加上参数名字

Preferred:

1
2
3
4
5
6
7
8
9
UIView.animate(withDuration: 1.0) {
self.myView.alpha = 0
}

UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}, completion: { finished in
self.myView.removeFromSuperview()
})

Not Preferred:

1
2
3
4
5
6
7
8
9
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
})

UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}) { f in
self.myView.removeFromSuperview()
}

对于上下文清晰的单个参数表达式时,使用隐式返回:

1
2
3
attendeeList.sort { a, b in
a > b
}

使用尾随闭包的链式方法上下文应该是易读的。而间隔换行、参数等由作者自觉定义:

1
2
3
4
5
6
let value = numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.index(of: 90)

let value = numbers
.map {$0 * 2}
.filter {$0 > 50}
.map {$0 + 10}

类型

尽量使用Swfit的原生类型表达式

Preferred:

1
2
let width = 120.0                                    // Double
let widthString = "\(width)" // String

Less Preferred:

1
2
let width = 120.0                                    // Double
let widthString = (width as NSNumber).stringValue // String

Not Preferred:

1
2
let width: NSNumber = 120.0                          // NSNumber
let widthString: NSString = width.stringValue // NSString

在使用CG开头的相关类型,使用CGFloat会使代码更加清晰易读

常量

常量可以应该用let关键字定义

Tip: 最好是全部都使用let,只有要编译有问题时才使用var

定义类常量比实例常量会更好。定义类常量使用static let关键字。

Preferred:

1
2
3
4
5
6
enum Math {
static let e = 2.718281828459045235360287
static let root2 = 1.41421356237309504880168872
}

let hypotenuse = side * Math.root2

Not Preferred:

1
2
3
4
let e = 2.718281828459045235360287  // pollutes global namespace
let root2 = 1.41421356237309504880168872

let hypotenuse = side * root2 // what is root2?

Optionals 可选类型

如果定义变量和函数返回值有可能为nil,应该定义为可选值?

使用!定义强制解包类型,只有在你接下来会明确该变量被会初始化。例如:将会在viewDidLoad()方法实例的子view。

访问可选类型时,如果有多个类型或者只访问一次,可以使用语法链

1
textContainer?.textLabel?.setNeedsDisplay()

多处使用应该使用一次性绑定

1
2
3
if let textContainer = textContainer {
// do many things with textContainer
}

是否可以类型不应该出现在命名中,例如:optionalString、maybeView、unwrappedView,因为这些信息已经包含在类型声明中

Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
13
var subview: UIView?
var volume: Double?

// later on...
if let subview = subview, let volume = volume {
// do something with unwrapped subview and volume
}

// another example
UIView.animate(withDuration: 2.0) { [weak self] in
guard let self = self else { return }
self.alpha = 1.0
}

Not Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var optionalSubview: UIView?
var volume: Double?

if let unwrappedSubview = optionalSubview {
if let realVolume = volume {
// do something with unwrappedSubview and realVolume
}
}

// another example
UIView.animate(withDuration: 2.0) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.alpha = 1.0
}

类型推断

让编译器推断变量或者常量的类型,当真需要时,才会指定特定的类型,例如: CGFloat和 Int16

Preferred:

1
2
3
4
let message = "Click the button"
let currentBounds = computeViewBounds()
var names = ["Mic", "Sam", "Christine"]
let maximumWidth: CGFloat = 106.5

Not Preferred:

1
2
3
let message: String = "Click the button"
let currentBounds: CGRect = computeViewBounds()
var names = [String]()

语法糖

尽量使用较短快捷的定义版本

Preferred:

1
2
3
var deviceModels: [String]
var employees: [Int: String]
var faxNumber: Int?

Not Preferred:

1
2
3
var deviceModels: Array<String>
var employees: Dictionary<Int, String>
var faxNumber: Optional<Int>

内存管理

代码无论在什么时候都不应该产生循环引用,使用weak和unowned引用防止产生循环引用,或者使用值类型。

延长对象寿命

延长对象寿命使用[weak self] 和 guard let = self else { return } 语法。

Preferred

1
2
3
4
5
6
7
resource.request().onComplete { [weak self] response in
guard let self = self else {
return
}
let model = self.updateModel(response)
self.updateUI(model)
}

Not Preferred

1
2
3
4
5
// might crash if self is released before response returns
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}

Not Preferred

1
2
3
4
5
// deallocate could happen between updating the model and updating UI
resource.request().onComplete { [weak self] response in
let model = self?.updateModel(response)
self?.updateUI(model)
}

控制流

Prefer the for-in style of for loop over the while-condition-increment style.

Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for _ in 0..<3 {
print("Hello three times")
}

for (index, person) in attendeeList.enumerated() {
print("\(person) is at position #\(index)")
}

for index in stride(from: 0, to: items.count, by: 2) {
print(index)
}

for index in (0...3).reversed() {
print(index)
}

Not Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
13
var i = 0
while i < 3 {
print("Hello three times")
i += 1
}


var i = 0
while i < attendeeList.count {
let person = attendeeList[i]
print("\(person) is at position #\(i)")
i += 1
}

三元表达式

Preferred:

1
2
3
4
5
let value = 5
result = value != 0 ? x : y

let isHorizontal = true
result = isHorizontal ? x : y

Not Preferred:

1
result = a > b ? x = c > d ? c : d : y

黄金路径

当使用条件编写代码时,应该及时return。也就是说,不要嵌套if语句,关键字guard你值得了解

Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}

// use context and input to compute the frequencies
return frequencies
}

Not Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {

if let context = context {
if let inputData = inputData {
// use context and input to compute the frequencies

return frequencies
} else {
throw FFTError.noInputData
}
} else {
throw FFTError.noContext
}
}

如果是多个条件的情况下,使用guard更加清晰明了。例子:

Preferred:

1
2
3
4
5
6
7
8
guard 
let number1 = number1,
let number2 = number2,
let number3 = number3
else {
fatalError("impossible")
}
// do something with numbers

Not Preferred:

1
2
3
4
5
6
7
8
9
10
11
12
13
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// do something with numbers
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}

分号

Swift 不要求在每句语句后面加分号,只要在多句语句在同一行的时才需要分号

不要把多行语句在同一行用分号隔开来

Preferred:

1
let swift = "not a scripting language"

Not Preferred:

1
let swift = "not a scripting language";

括号

在条件语句中括号不是一定要的,应该省略掉。

Preferred:

1
2
3
if name == "Hello" {
print("World")
}

Not Preferred:

1
2
3
if (name == "Hello") {
print("World")
}

很长的表达式中,使用括号可以使代码更加清晰

Preferred:

1
let playerMark = (player == current ? "X" : "O")

多行字符串

当创建很长字符串时,应该使用多行字符串语法

Preferred:

1
2
3
4
5
6
7
let message = """
You cannot charge the flux \
capacitor with a 9V battery.
You must use a super-charger \
which costs 10 credits. You currently \
have \(credits) credits available.
"""

Not Preferred:

1
2
3
4
5
6
let message = """You cannot charge the flux \
capacitor with a 9V battery.
You must use a super-charger \
which costs 10 credits. You currently \
have \(credits) credits available.
"""

Not Preferred:

1
2
3
4
5
let message = "You cannot charge the flux " +
"capacitor with a 9V battery.\n" +
"You must use a super-charger " +
"which costs 10 credits. You currently " +
"have \(credits) credits available."

References

  • The Official raywenderlich.com Swift Style Guide.
  • The Swift API Design Guidelines

iOS持续集成(四)——jenkins

Posted on 2018-09-18

简介

Jenkins是开源的自动构建服务器,一般被用于各种各样的构建任务、测试和发布软件等等。因为是图形化界面,所以对于一些黑盒测试人员来说,非常友好。Jenkins+Fastlane简直就是我们客户端的福音。

安装 && 配置

在 macOS 安装非常简单,可以直接到官网找到下载包,直接跟我们安装应用差不多。

当然还支持Docker、brew、war包等一些方式

具体可以查看官网介绍

安装完成后,访问 http://localhost:8080就能第一次访问Jenkins。

然后会出现下面的界面:

在此过程中文件夹secrets由于没有访问权限。那只能手动改文件夹的权限,从里复制密码填进去。

接下面会要求安装推荐插件,或者自定义安装。我们这可以点击推荐安装。

第一个账户:

第一个项目

点击左边的 新建任务

选择 构建一个自由风格的软件项目,输入项目名字

由于我们用的是Git管理代码,所以配置一个ssh key

由于项目里面多个分支,为了可以选择分支构建。

另外,为了区别Fir和TestFlight两个渠道

我们添加两个参数:

这里的Git Parameter需要手动到插件管理上装好

然后在下面的 Git选项中使用branch变量

由于使用的是Fastlane构建,那么在构建选项上,选择执行shell

这样子就算完成的一个简单项目的配置

Jenkins 节点(分布式构建)

构建使用的电脑一般都比较古老,为了把构建的压力分担出去,可能会选多台Mac共同构建,这样子就形成了分布式。

添加一个节点

点击 系统管理 -> 管理节点 进入节点页面

点击确定,配置节点

然后点击节点,进去节点页面,启动即可。

回到首页就可以看到节点数量。

这样子一个节点配置完成

总结

本文仅仅是持续集成的入门,更多个性化的配置,需要根据不同的需求,通过Jenkins的插件完成。通过Jenkins集成可以完成的内容,还有单元测试、代码规范检查等等。

能偷懒,别自已动手,机器就是我们的最好帮手。写代码不是搬砖,是创造具有更多生产力的工具。

iOS持续集成(三)—— fastlane 自定义插件

Posted on 2018-09-17

fastlane的强大带我们不少的便利,但事无人愿。总有些不一样的需求,今天就给大家带来的是fastlane的action和插件。

这也是fastlane精髓部分,它使fastlane具有强大扩展性,以保证变化不断的个性化需求。

自定义本地action

在项目中,可以创建自定义的action扩展fastlane的功能性。创建的这个action跟fastlane内置的action在使用上面来说没多大区别。下面来个例子:

创建本地action

更新 build 版本号,格式就以年月日时分。在终端输入下面命令:

1
fastlane new_action

action实现分析

在后面会被要求输入action的名字,输入update_build_version按回车后,fastlane会在fastlane/actions目录下面创建后缀为.ruby文件。请看下面的文件内容

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
67
68
69
70
module Fastlane
module Actions
module SharedValues
UPDATE_BUILD_VERSION = :UPDATE_BUILD_VERSION_CUSTOM_VALUE
end

class UpdateBuildVersionAction < Action

def self.run(params) # 这个方法为Action的主方法,在这里咱们写入更新版本号的内容

if params[:version_number]
new_version = params[:version_number]
else
# 格式化时间
new_version = Time.now.strftime("%Y%M%d")
end

command = "agvtool new-vresion -all #{new_version}" #使用苹果的 agvtool 工具更新版本号

Actions.sh(command) #执行上面的 shell 命令
Actions.lane_context[SharedValues::UPDATE_BUILD_VERSION] = new_version # 更新全局变量,供其他的Actions使用

end

def self.description # 对于该Action小于80字符的简短描述
"A short description with <= 80 characters of what this action does"
end

def self.details # 对于该Action的详细描述
# Optional: 可选
end

def self.available_options # 定义外部输入的参数,在这里咱们定义一个指定版本号的参数

[
FastlaneCore::ConfigItem.new(key: :version_number, # run方法里面根据该key获取参数
env_name: "FL_UPDATE_BUILD_VERSION_VERSION_NUMBER", # 环境变量
description: "Change to a specific version", # 参数简短描述
optional: true),
]
end

def self.output # 输入值描述,如果在 run 方法更新 SharedValues 模块里面自定义的变量,供其他的 Action 使用,可选
[
['UPDATE_BUILD_VERSION_CUSTOM_VALUE', 'A description of what this value contains']
]
end

def self.return_value # 返回值描述, 指的 run 方法会有返回值。可选
end

def self.authors # 作者
["ChenJzzz"]
end

def self.is_supported?(platform) # 支持的平台
# you can do things like
#
# true
#
# platform == :ios
#
# [:ios, :mac].include?(platform)
#

platform == :ios
end
end
end
end

从上面的方法上来看,主要的还是run方法和available_options方法。如果看不懂上面的代码,那去补一下ruby相关的语法。OK,这个action跟其他的action一样,在Fastlane直接使用就可以了。在终端输入fastlane action update_build_version,会像下面一样,打印出action的相关信息

image

顺便提一下要在另外的项目上使用,直接复制过去就行了。至于要提交到fastlane的官方库,还是相对来说门槛较高。

自定义插件

上面的action在共享这方面,只能靠复制这一手段,相当之不优雅。那么插件是我们最好的选择。

创建插件

进入一个新的目录

1
fastlane new_plugin [plugin_name]
  • fastlane 创建Ruby gem库目录
  • lib/fastlane/plugin/[plugin_name]/actions/[plugin_name].rb这个文件是我们要实现的action文件

插件跟action都是同样的写法。在这里就不重复描述了。

在当前目录下, 可以运行fastlane test,测试插件是否正确

使用方法

安装已发布到RubyGems的插件
1
fastlane add_plugin [name]

fastlane会执行以下步骤

  • 添加插件到fastlane/Pluginfile
  • 使./Gemfile文件正确引用fastlane/Pluginfile
  • 运行fastlane install_plugins安装插件以及需要的依赖
  • 如果之前未安装过插件,会生成三个文件:Gemfile、Gemfile.lock和fastlane/Pluginfile
安装其他插件

正如上面所说,在项目里面的fastlane/Pluginfile添加下面内容

1
2
3
4
# 安装发布到 Github 的插件
gem "fastlane-plugin-example", git: "https://github.com/fastlane/fastlane-plugin-example"
# 安装本地插件
gem "fastlane-plugin-xcversion", path: "../fastlane-plugin-xcversion"

在终端运行fastlane/Pluginfile(或者 bundle exec fastlane/Pluginfile),安装插件以及相关依赖

总结

action的出现,大大的增强了fastlane的扩展性。使我们适应自己的业务,定制所需要action。另外,Plugin使fastlane在有强大的扩展性同量,使用更加灵活。

总的来说,如果是单单的项目,action可以解决问题。如果是多个项目,使用plugins是不二选择。

小Tips:如果看不懂,去补一下Ruby的语法。还有就是多点看一下网上action和plugin写法。

参考文档:

Create Your Own Plugin(官方文档)

Available Plugins

iOS持续集成(二)——证书管理神器match

Posted on 2018-09-10

对于iOS的开发者来说,一定都会遇到被证书与测试设备烦到不行的时候。后台的证书乱七八糟,添加设备后打包的出来的ipa总是装不上,证书无效等等问题。这些问题一搞就是浪费了大部分时间。工程师的世界里怎么能忍受这些重复而且毫无意义的工作?这不,fastlane里面的match解决上面的所有问题。

Read more »

iOS持续集成(一)——fastlane 使用

Posted on 2018-09-02

开篇

回想一下我们发布应用,要进行多少步操作。一旦其中一步失误了,又得重新来。这完完全全不是我们工程师的风格。在软件工程里面,我们一直都推崇把重复、流程化的工作交给程序完成,像这种浪费人生的工作,实在是不应该浪费我们的人生。这次的文章主角就是为了解放我们而来—— fastlane。这个明星库在 github 已经高达 1w 多的start量。

Fastlane

fastlane 是 iOS (还有 Android ) 布署和发布最好的一套工具。它处理了所有重复的工作,例如生成截图,处理签名和发布应用。

安装

fastlane实际是由Ruby写的,使用Ruby的Gem安装是我们的不二选择

1
sudo gem install fastlane -NV

接着在终端进入项目里面(目前fastlane swift 正在测试,就以之前的版本讲解)

1
fastlane  init

按照提示初始化完成之后,在项目下面生成 fastlane 文件夹

基本介绍

先普及两个重要的文件,初始化后在./fastlane文件件即可找到

Appfile

存放着 AppleID 或者 BundleID 等一些fastlane需要用到的信息。基本上我们不需要改动这个文件的内容。
它放到你项目下面的 ./fastlane文件夹下面,默认生成的文件如下:

1
2
3
4
5
6
7
8
9
10
app_identifier "net.sunapps.1" # The bundle identifier of your app
apple_id "felix@krausefx.com" # Your Apple email address

# 如果账号里面有多个team,可以指定所有的team
# team_name "Felix Krause"
# team_id "Q2CBPJ58CA"

# 指定 App Store Connect 使用的team
# itc_team_name "Company Name"
# itc_team_id "18742801"

更多详细的配置,可以参考一下文档
Appfile Doc

FastFile

一开始生成的Fastlane文件大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
platform :ios do
before_all do

end

desc "Runs all the tests"
lane :test do
scan
end

# You can define as many lanes as you want

after_all do |lane|

end

error do |lane, exception|
# slack(
# message: "Error message"
# )
end
end

Fastfile里面包含的块类型有四种:

  • before_all 用于执行任务之前的操作,比如使用cocopods更新pod库
  • after_all 用于执行任务之后的操作,比如发送邮件,通知之前的
  • error 用于发生错误的操作
  • lane 定义用户的主要任务流程。例如打包ipa,执行测试等等

如下面,来讲解一下lane的组成。

1
2
3
4
5
desc "Push a new beta build to TestFlight"   //该任务的描述
lane :beta do //定义名字为 beta 的任务
build_app(workspace: "expample.xcworkspace", scheme: "example") //构建App,又叫gym
upload_to_testflight //上传到testfilght,
end

该任务的作用就是构建应用并上传到 TestFilght。下面有两个 Action

  • build_app 生成 ipa 文件
  • upload_to_testflight 把 ipa 文件上传到 TestFilght

在控制台进入项目所在的文件夹下面,执行下面命令

1
fastlane beta

即可执行任务,按照上面的任务,会生成 ipa 并上传到 TestFilght。其实很简单,定义好任务,控制台执行任务即可。

实践

那么如何写一个我们属于自己的 lane 呢? 就以发布 ipa 到 fir 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
desc "发布到Fir"
lane :pulish_to_fir do
# 运行 pod install
cocoapods
# 构建和打包ipa
gym(
clean: true,
output_directory: './firim',
scheme: 'xxxx',
configuration: 'Test',
export_options: {
method: 'development',
provisioningProfiles: {
"xxx.xxx.xxx": "match Development xxx.xxx.xxx"
},
}
)
# 上传ipa到fir.im服务器,在fir.im获取firim_api_token
firim(firim_api_token: "fir_token")
end

下面解释一下上面的内容

1
cocoapods

在项目里执行 pod install,详细例子可见 Doc

1
sh "./update_version.sh"

这是由作者本地写的更新版本号的脚本

1
gym (又名build_app)

gym 是fastlane的里面一部分,它可以方便生成和签名ipa,能为开发者省下不少功夫。

Doc

1
firim

firim 是一个插件,执行 fastlane add_plugin firim 即可把插件装好

总结

fastlane里面内置很多常用的Action,具体的使用方法建议多看一下官方文档。

fastlane项目里面也有很多其他公司的 例子,在不清楚怎么使用的时候,看看这些例子也未尝不是一种方法。

2017与2018

Posted on 2018-02-22

春节过后第一天上班,外面的天气跟我的心情一样黑暗。昨晚带着不舒服的老婆开车上来深圳,今晚我还要回到茂名喝喜酒,把怀孕的老婆一个人放在深圳两天,总有点不放心。

去年一年,完成了结婚这件重要的事,在这件事了花了我不少心思,同时把我的积蓄也花光了。这都不重要,在今年的1月份老婆怀孕了,听到这个消息喜乐参半,喜的是老婆怀上BB了,忧的是这BB来得有点快,打乱我的计划。

2017年,迷糊的一年。
2018,给自己定个目标。

  • 锻炼身体,为了能照顾好老婆与孩子
  • 搞点副业,增加收入
123

ChenJz

30 posts
8 categories
19 tags
RSS
© 2020 ChenJz
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4