问题

今天遇见了一个多线程死锁的问题。简化代码后大概是这样

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
@interface ViewController (){
pthread_mutex_t _interceptorsLock;
}

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

pthread_mutexattr_t interattr;
pthread_mutexattr_init (&interattr);
pthread_mutexattr_settype (&interattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&_interceptorsLock, &interattr);
pthread_mutexattr_destroy (&interattr);

dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test];
});

// 1
dispatch_async(dispatch_get_main_queue(), ^{
[self test2];
});
}

- (void)test {

NSLog(@"lock 1");
pthread_mutex_lock(&_interceptorsLock);

// 2
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dispachsync");
});

pthread_mutex_unlock(&_interceptorsLock);
NSLog(@"unlock 1");
}

- (void)test2 {

NSLog(@"call test2");
NSLog(@"lock 2");

pthread_mutex_lock(&_interceptorsLock);

NSLog(@"1122---2");

pthread_mutex_unlock(&_interceptorsLock);
NSLog(@"unlock 2");
}

运行之后,日志卡在这里

1
2
3
2023-07-06 17:18:50.897834+0800 AAAA[82542:2621413] lock 1
2023-07-06 17:18:51.016660+0800 AAAA[82542:2620769] call test2
2023-07-06 17:18:51.016700+0800 AAAA[82542:2620769] lock 2

点击 debug 调试后,发现 死锁了

一开始以为,这个锁是 递归锁, 递归所就是可以重复的加锁,不应该有什么问题。后来问了 chatGPT,也说不会有问题,但它给出了一个重要的信息,就是重复加锁,应该是在同一个线程中

而且 Apple 文档也支出,针对的是同一个线程

梳理下调用流程,是这样的

首先我们在 global_queue 中执行 test 方法,其会在子线程,这时子线程 正常加锁,然后里边执行 dispatch_sync。由于是 dispatch_sync 函数,后边的 解锁代码需要等待 这个函数内的 block 执行完毕。
但是在之前,我们通过 dispatch_asyncmain_queue 中执行了 test2
我们知道 dispatch_async 执行其实是把这个 test2 这个任务放在队尾。也就是说 NSLog(@"dispachsync"); 需要等待 test2 执行完才可以执行。

我们来看 test2 函数。这个函数一开始进来先打印,这个没问题,接下来就开始加锁。虽然这个是递归锁,但是由于这是在不同的线程中,它其实就是一个 互斥锁。但这个锁 在之前执行 test 的时候,已经被加锁了。这里只能等 test 中解锁。这样就死锁了

但这个也不是必现的。如果 main_queueNSLog(@"dispachsync");[self test2] 之前的话,则可以正常执行。

进一步思考,既然 递归锁 是为了避免在同一个线程中重复加锁导致的死锁。那既然是同一个线程,则都是 串行 执行的,也不会有资源竞争。那就可以不加锁。那么问题来了。递归锁存在的意义是什么?

其实仔细想下,还是为了避免多线程资源竞争的问题。
虽然 递归锁 避免同一个线程中多次加锁的死锁问题,但在多线程下,其也是一个 互斥锁,也能处理资源竞争问题。

考虑这样一个代码

1
2
3
4
5
6
7
- (void)method {
[lock lock]; // 递归锁
// some code
// possibly call [self method]
a += 1
[lock unlock];
}

如果不加锁的话,两个线程在调用 method 方法后,a 的值可能不能正常处理。当加了这个递归锁之后,可以保证多线程下 a 可以被正确处理

通常我们写代码为了减小体积,为了保证显示效果,只放一张 @3x 的图片,但是在使用 UIGraphics 相关 api 绘制拉伸图片的时候会有问题。

TL;DR:

1. 放两张图片解决问题 
2. 放一张图片时 `UIGraphicsBeginImageContextWithOptions` 的 scale 参数,传 image.scale 来确保显示效果

假如我们有一张这个图片 @3x 的图片

是用如下代码进行拉伸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UIImage *img = [UIImage imageNamed:@"ic_camera_desc_right"];
img = [img stretchableImageWithLeftCapWidth:img.size.width / 2 topCapHeight:img.size.height / 2];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, img.size.height), NO, UIScreen.mainScreen.scale);
[img drawInRect:CGRectMake(0, 0, 200, img.size.height)];
UIImage *aimg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

UIImageView *imgV = [[UIImageView alloc] initWithImage:aimg];
[self.view addSubview:imgV];

[imgV mas_makeConstraints:^(MASConstraintMaker *make) {

make.top.left.equalTo(@100);
make.width.equalTo(@(200));
make.height.equalTo(@(img.size.height));
}];

则在 iPhone 8 上显示如下

在 iPhone 13 Pro Max 上符合预期

其原因是因为 UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, img.size.height), NO, UIScreen.mainScreen.scale); 中 缩放比例不对导致的,只有 3x 图时,在 iPhone 8 上 image 的 scale 为 3,而 UIScreen.mainScreen.scale 为 2,二者不一样,导致出现问题。二者一样,即可解决问题。

生成 ssh 秘钥

1
ssh-keygen -t rsa -m PEM -b 4096 -f 'file_name' -C "you_email@email.com"

当有多个 ssh 秘钥对 时,需要配置 ~/.ssh/config 来确保 ssh 可以正确解析

1
2
3
4
5
6
7
8
9
10
11
12
13
Host vpn
user root
hostname 8.8.8.8
Identityfile ~/.ssh/id_rsa_vpn

Host 8.8.8.8
user root
hostname 8.8.8.8
Identityfile ~/.ssh/id_rsa_vpn

Host github.com
IdentityFile ~/.ssh/id_rsa_github

其中的 Host user Identityfile 等 不区分大小写,具体细节在这里,也可以通过 man ssh_config 查看

其中 Identityfile 来指定对应的 ssh 的秘钥
如果按上述配置的话,则 ssh vpn == ssh root@8.8.8.8

记录一些 zsh 的配置

zsh 的配置

主题 Powerlevel10k

zsh 中的插件

1
plugins=(git zsh-syntax-highlighting autojump zsh-autosuggestions git-open)

符号化 iOS 的 crash

Apple

1
2
3
4
5
6
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
alias getUUIDFromDYSM="xcrun dwarfdump --uuid"
alias getUUIDFromCrash="grep 'MyApp arm64'"
alias deCrash="/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash"
# deCrashByLine LoadAddress AddressesToSymbolicate
alias deCrashByLine="atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l"

hexo-asset-image 图片路径问题

index.js 做如下修改即可

1
2
3
4
//var endPos = link.lastIndexOf('.');

var endPos = link.length - 1;

这两天提交 APP Store 被拒审了,排查之后发现是 NSHTTPURLResponse 中的 allHeaderFields 的一个坑。

根据 RFC2616 的描述,header 中的字段是大小写不敏感的,也就是,你写 Foofoo 或者是 fOo 是一样的,但是苹果在的实现是,当你 set 的时候,是按照你写的 key-value 来,当你再次设置的时候,是按照大小写不敏感的方式来查找 set,之后在设置进去。

In Objective-C, the returned dictionary of headers is case-preserving during the set operation (unless the key already exists with a different case), and case-insensitive when looking up keys.
For example, if you set the header X-foo, and then later set the header X-Foo, the dictionary’s key is be X-foo, but the value comes from the X-Foo header.

而苹果在 iOS 13 新增了一个方法,这个方法将会用大小写不敏感的方式来查找 key,然后返回对应的 value。

1
2
3
4
5
6
7
8
9
10
11
12
/*!
@method valueForHTTPHeaderField:
@abstract Returns the value which corresponds to the given header
field. Note that, in keeping with the HTTP RFC, HTTP header field
names are case-insensitive.
@param field the header field name to use for the lookup
(case-insensitive).
@result the value associated with the given header field, or nil if
there is no value associated with the given header field.
*/
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

实际测试如下

1
2
3
4
5
NSHTTPURLResponse *res = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://www.baidu.com"] statusCode:200 HTTPVersion:@"1.1" headerFields:@{@"foo": @"2", @"Foo": @"3", @"FOo": @"4", @"bar": @"1", @"Bar": @"2"}];

NSLog(@"%@", res.allHeaderFields);

NSLog(@"%@", [res valueForHTTPHeaderField:@"fOO"]);

其输出结果是

1
2
3
4
5
6
{
Bar = 2;
FOo = 4;
}

4

这么看的话,似乎和文档描述的并不太一样,最后打印出来的 key 是 FOo 也就是最后添加进去的,而 Bar 也是最后添加进去的,key 会覆盖,value 也会覆盖。。。。

之前对 runloop 的 common mod 有点模糊,今天看了下 CFRunloop 的源码,感觉清晰了不少。

TL;DR

当你调用 CFRunLoopAddCommonMode 把一个 runloop 的某个 mode 标记为 commonMode 的时候,其实做的操作就是把当前 runloop 的 commonModeItem 中的 source timer observer 注册到这个 mode 中。

当调用 CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName) 之类的方法添加一个 source/timer/observer 时,如果 modeName 是 kCFRunLoopCommonModes 的话,会把这个 source 添加到 commonModeItems 里,然后把这个 source/timer/observer 添加到 runloop 的所有 mode 中(遍历 runloop 的 modes,根据名称找到 mode,然后添加进去)

首先我们看下 CFRunLoopModeRef 的定义

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
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};

我们再看下 CFRunLoop 的定义

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

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes; // 一个 set, 里边存放的是字符串,mode name
CFMutableSetRef _commonModeItems; // 被标记为 common 的 source,一个 set 里边可能是 CFRunLoopSourceRef 或 CFRunLoopObserverRef 或 CFRunLoopTimerRef
CFRunLoopModeRef _currentMode; // 当前的 mode
CFMutableSetRef _modes; // 所有的 mode 集合,是一个 set 里边存的是 CFRunLoopModeRef
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

我们可以看到,每个 runloop 中都有若干个 modes,每个 mode 里边会有诺干个 source,timer,observer

我们可以通过下边的方法把一个 mode 标记为 common mod

1
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);

也可以通过下边的方法,把一个 source 添加或移除到 runloop 的 model 中,当然这个 mode 可以是 kCFRunLoopCommonModes

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

首先我们来看下 CFRunLoopAddCommonMode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
if (!CFSetContainsValue(rl->_commonModes, modeName)) { // 如果 commonModes
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL; // 根据 commonModeItems 创建一个 set,
CFSetAddValue(rl->_commonModes, modeName); // 把 当前的 modeName 存储到 commonModes 里边
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode */
/*
这里相当于一个 for 循环,遍历 set 然后调用 __CFRunLoopAddItemsToCommonMode 方法
__CFRunLoopAddItemsToCommonMode 第一个参数是 set 中的 item,第二个参数是 context
*/
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {}
__CFRunLoopUnlock(rl);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 这个方法就很简单了,判断 item 的类型,根据不同的类型调用不同的方法,把 item 添加到对应的 runloop 的对应的 model 中
static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx) {
CFTypeRef item = (CFTypeRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}

我们看 CFRunLoopAddSource observer 和 timer 的操作 大同小异。observer 的话由于是个数组,会有优先级的问题。 倒序遍历数组,插入到把 observer 插入到数组中,结束时,数组里的 observer 的 order 是从小到大排列的。

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
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {	/* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rls)) return;
Boolean doVer0Callout = false;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) { // 如果指定的 modeName 是 kCFRunLoopCommonModes 的话
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; // 创建一个 set,这 set 里边是那些被标记为 common 的 mode
if (NULL == rl->_commonModeItems) { // 如果 _commonModeItems 为空,则创建一个
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rls); // 把当前的 source 加到 _commonModeItems 中
if (NULL != set) { // 如果 set 不为空的话,则遍历这个 set,调用 __CFRunLoopAddItemToCommonModes 方法
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else { // 如果不是 modeName 不是 kCFRunLoopCommonModes
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); // 找到这个 mode
if (NULL != rlm && NULL == rlm->_sources0) { // 没有 source0 的话,创建
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) { // 如果 source0 和 source1 都不存在的话,根据 source 类型,添加到对应的 set 中
if (0 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources0, rls);
} else if (1 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources1, rls);
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
__CFRunLoopSourceLock(rls);
if (NULL == rls->_runLoops) { // 如果 source 没有 runloop 的话,创建一个 CFMutableDictionary
rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
}
CFBagAddValue(rls->_runLoops, rl); // 把 runloop 加入到字典中,key 和 value 是一样的
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) { // 查看 source0 是否有 callOut
if (NULL != rls->_context.version0.schedule) {
doVer0Callout = true;
}
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
}
}

总的来说呢,就是当你调用 CFRunLoopAddCommonMode 把一个 runloop 的 mode 标记为 commonMode 的时候,其实做的操作就是把当前 runloop 的 commonModeItem 中的 source timer observer 注册到这个 mode 中。在 runloop 的循环中,直接处理 mode 中的 source, timer, observer 即可

最近做了一些和 NSOperation 有关的事情,感觉这个类还是挺有意思的,这里记录一下细节和遇到的一些坑。

我们一般使用 NSOperation 的时候,通常会配合 NSOperationQueue 来进行管理,通过配置 NSOperationQueue 的 maxConcurrentOperationCount 来设置为并行或者串行。而对于一个 NSOperation 来说,我们也可把它处理成同步或异步的,针对不同的情况,我们也需要做不同的处理。

我们打开 NSOperation.h 会首先看到 start 方法和 main 方法。接下来我就详细说下自己的理解。

Concurrency Programming Guide

main 方法

看文档的注释发现,NSOperation 的默认实现是什么都没做,也就是个空方法,当子类去重写的时候,也不用调 super,而且它会在一个由 NSOperation 提供的 autorelease pool 中执行,子类也不需要创建自己的 autorelease pool

当我们实现一个同步的方法的时候,我们只需要重写 main 函数,处理我们的业务逻辑即可,不需要处理除 canceled 之外的任何状态。关于 canceled,后边再说。

start 方法

这个方法默认实现是处理了一些状态,检查这个 operation 是否可以正常执行,如 ready 是否为 YES, cancelled 是否为 YES, finished 是否为 YES ,然后调用 main 方法。如果不满足条件,直接 return,就结束了。

如果要执行一个异步的 Operation,例如发送一个网络请求,那么,需要重写 start 方法,并自己手动设置一些状态(不能调用 super),处理这些状态的时候,需要手动触发一些 KVO 来通知其他可能监听的对象,如 isCancelled, isExecuting, isFinished 等。当异步处理结束时,需要更新 finished 和 executing 状态。

如果是异步的话,我们也应该在 处理 asynchronous 属性,返回 YES

cancelled

我们应该一直关注 canceled 状态,当被标记为 YES 的时候,我们需要取消我们现在所执行的任务,而 不只是startmain 函数中对 canceled 状态进行判断。当有一个循环,或者说是耗时比较长的操作中,我们应该在比较好处理的位置,判断 canceled 状态,在 YES 的情况下,结束我们的 operation。

  1. 如果是 YES 的话,应该立即结束
  2. 在每个循环中,至少判断一次,如果一个迭代比较耗时,建议多检查几次
  3. 在任何处理比较方便的位置,取消任务

说了那么多,那我们看看如何定义一个子类,来处理同步和异步

同步

1
2
3
4
// MySyncOperation.h
@interface MySyncOperation : NSOperation

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MySyncOperation.m
@implementation MySyncOperation

- (void)main {
// 处理自己的业务逻辑
// 注意处理 cancelled
}

- (BOOL)isAsynchronous {
return NO;
}

@end

异步

1
2
3
4
// MyASyncOperation.h
@interface MyAsyncOperation : NSOperation

@end
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
// MyASyncOperation.m
@interface MyAsyncOperation ()
{
BOOL executing;
BOOL finished;
BOOL cancelled;
}

@end

@implementation MyAsyncOperation

// 只需重写 start 方法,不能重写 main。
- (void)start {
if (!self.cancelled) {
[self setOperationToExecutingState];
[self handleBusiness];
} else {
[self setOperationToFinishState];
}
}

- (void)cancel {
if (self.isFinished) {
return;
}
[super cancel];
[self setOperationToCancelState];
}

- (void)handleBusiness {
// 处理自己的业务逻辑
// 注意处理 cancelled
// 在异步处理结束的时候调用 [self setOperationToFinishState] 来结束 operation,耗时操作中,注意处理 cancel

}

- (void)setOperationToExecutingState {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = YES;
finished = NO;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}

- (void)setOperationToFinishState {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}

- (void)setOperationToCancelState {
[self willChangeValueForKey:@"isCancelled"];
cancelled = YES;
[self didChangeValueForKey:@"isCancelled"];
}

- (BOOL)isExecuting {
return executing;
}

- (BOOL)isFinished {
return finished;
}

- (BOOL)isCancelled {
return cancelled;
}

- (BOOL)isAsynchronous {
return YES;
}
@end

👉Observer

当一个事物发生变化时,另一个可以获得通知,做 iOS 的都知道,KVO 就是观察者模式的体现。但是别忘了在 dealloc 中移除。

👉Decorator

以我的理解就是包装,类似于俄罗斯套娃,包装原有的类,来提供更多的功能,避免继承造成类膨胀的问题。通常是用来扩展一个已有类的功能。

Decorator is used to add new features of an existing object to create a new object. There is no restriction of freezing the object until all its features are added.

👉Factory

定义了一个创建对象的接口,由子类决定要初始化的是那个类。把类的实例化推迟到子类中。

👉Builder

One should use builder if he wants to limit the object creation with certain properties/features. For example there are 4-5 attributes which are mandatory to be set before the object is created or we want to freeze object creation until certain attributes are not set yet. Basically, use it instead of constructor

Decorator VS Builder

Patterns like builder and factory(and abstract factory) are used in creation of objects. And the patterns like decorator (also called as structural design patterns) are used for extensibility or to provide structural changes to already created objects.

读完代码整洁之道,感觉书中写的很多地方都很棒,这里着重记录几点

  1. 像写诗一样写代码
  2. 规范清晰有意义的变量名
  3. 每个函数只做一件事,尽量保持短小
  4. 减少函数的参数
  5. 函数无副作用,干什么就是干什么,别乱做其他功能
  6. 删掉不必要的注释
  7. 写注释的话一定清晰易懂,不要写的含糊不清晰的,有误导性的
  8. 减少代码的跳跃,在接下来的地方实现功能
  9. 做单元测试
  10. 抽象事物,如 类,函数等
  11. 使用设计模式
  12. 类应该短小,高内聚,低耦合
  13. AOP

上次去吃火锅,被测试 call 说无法打包了,原来是上次有人用电脑,用完之后,忘记连接电源,导致电脑自动关机了,电脑再次开启的时候,Jenkins 反向代理没有开,导致无法打包(我们的 Jenkins 是部署在 阿里云上的,通过反向代理,代理到公司的一个 Mac 电脑上,从而进行打包)。当时也没有带电脑,导致我只好打车回去,去启动反向代理。搞好之后就配置了下开机自动启动,在这里记录一下,以免有人需要。

通过查阅文档可知,Mac 通过launchd 来启动服务

我们只需要在对应的目录,配置好 plist 文件即可,当 Mac 开机或者登录账户的时候,将会按照 plist 文件来加载我们指定的程序。

存放 plist 文件的目录有以下几个

1
2
3
4
5
6
/System/Library/LaunchDaemons/ // 开机后加载
/Library/LaunchDaemons/ // 开机后加载
/System/Library/LaunchAgents/ // 用户登录后
/Library/LaunchAgents/ // 用户登录后
/Library/LaunchDaemons/ // 用户登录后
~/Library/LaunchAgents/ // 用户登录后,自定义的 plist 可以放在这里

详细的 plist 文件的 key 和 value 可以通过 man launchd.plist 来查看,但必须包含以下几项

Label 一个字符串,用来标识要启动的程序,要求唯一
Program 指定一个 sh 脚本,或者一个可执行文件等
ProgramArguments 如果不指定 Program 的话,将依据这个参数来
KeepAlive BOOL 是否一直存活

通过 launchctl load ~/Library/LaunchAgents/com.your.launchjob.plist 来启动程序

通过 launchctl unload ~/Library/LaunchAgents/com.your.launchjob.plist 来终止程序

com.package.jenkinsAgent.plist 内容如下

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/package/.rvm/gems/ruby-2.5.3/bin:/Users/package/.rvm/gems/ruby-2.5.3@global/bin:/Users/package/.rvm/rubies/ruby-2.5.3/bin:/Users/package/.fastlane/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/package/.rvm/bin</string>
</dict>
<key>Label</key>
<string>com.package.jenkinsAgent</string>
<key>Program</key>
<string>/Users/package/autoRun/jenkinsAgent.sh</string>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/package/autoRun/jenkinsAgent.stdout.log</string>
<key>StandardOutPath</key>
<string>/Users/package/autoRun/jenkinsAgent.stdout.log</string>
<key>WorkingDirectory</key>
<string>/Users/package/jenkins_workspace</string>
</dict>
</plist>

在上述的 plist 文件中,我们指定了,在用户登录的时候,去执行 /Users/package/autoRun/jenkinsAgent.sh 脚本,并且设置了一些 log 输出到指定文件,设置了工作目录和环境变量。

/Users/package/autoRun/jenkinsAgent.sh 内容如下

1
2
3
4
5
6
7
8
#!/bin/zsh --login
export LC_ALL=en_US.UTF-8
echo $SHELL
echo $PATH
ruby -v
rvm use 2.5.3
cd /Users/package/autoRun
java -jar agent.jar -jnlpUrl http://jenkins-test-int.igetcool.com/computer/ios-package-node/slave-agent.jnlp -secret dce8bc948f8992da8499926eafcd88d80c395a62c24cf15b2a3adaeeaed593c2 -workDir "/Users/package/jenkins_workspace"

如果 sh 脚本不能执行,请添加权限 chmod +x jenkinsAgent.sh

我们这边用的 Jenkins + fastlane 来进行打包
由于使用的 fastlane,当我通过系统直接启动 Jenkins 反向代理,在 Jenkins 上打包的时候,会发现找不到 fastlane,其实电脑上已经装好了。在 Jenkins 打包的时候,我打印了下环境变量发现和直接在终端里打印的是不一样的,后来想可能是系统启动只会加载默认的环境变量,所以我在 com.package.jenkinsAgent.plist 加上了 EnvironmentVariables 相关的配置。后来在打包,就找到了 fastlane,但又发现ruby版本不对,原来系统启动的时候,使用的系统自带的 ruby,导致 和 bundler 不匹配,这里切换下用户手动安装的 ruby 就可以了。

1
2
3
rvm list
ruby -v
rvm use 2.5.3

当配置完成后,重启系统,Jenkins 反向代理将自动启动****

0%