设计模式-⑤多代理模式

前言

1、什么是多代理

引文①、环信SDK

用过环信SDK的同学应该对多代理不陌生了,请看下面代码:

1
2
3
4
5
6
7
8
9
/*
@method
@brief 注册一个监听对象到监听列表中
@discussion 把监听对象添加到监听列表中准备接收相应的事件
@param delegate 需要注册的监听对象
@param queue 通知监听对象时的线程
@result
*/
- (void)addDelegate:(id<EMChatManagerDelegate>)delegate delegateQueue:(dispatch_queue_t)queue;

平时我们写得比较多的代理: @property (nonatomic,weak) id<EMChatManagerDelegate>delegate; 写了上面属性后系统会默认生成set方法: - (void)setDelegate:(id<EMChatManagerDelegate>)delegate; 通过对两个接口的比较就不难看出:单代理只能设置一个,而多代理可以设置多个,准确来说应该是多代理可以添加多个

以上摘自:iOS 实现多代理的方法及实例代码

引文②、XMPP

XMPP以及类似IM框架里通常会有这种需求:打开多个聊天窗口,和多个人聊天。然而框架底层消息转发管理器却只有一个。通常是这两个窗口都要收到消息回调,然后取自己有用的消息。。。
大概就这么个意思,我两年前用了下,具体也解释不清楚,欢迎指点交流,反正就是有一个需求需要多代理回调,这种IM框架通常有这种方法
[xmppRoom addDelegate:self delegateQueue:dispatch_get_main_queue()];

以上摘自:iOS 多代理的实现

引文③、类似IM库,当接受到消息,在几个不同地方做回调,比如改变消息数,显示小红点等

参考文章:iOS实现多重代理及应用场景

2、为什么不用NSNotificationCenter

系统不是已经有通知中心NSNotificationCenter了吗?为什么还要自己实现一个呢?

举个例子,现在我们有一个模块需要抛一个通知出来,通知其它模块用户名改变了,我们来看代码大致是怎么写的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发通知一方
NSString *const kUserNameChangedNotification = @"UserNameChangedNotification";

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:UserNameChangedNotification object:nil
userInfo:@{@"oldName":@"zhangsan","newName":"lisi"}];

// 接收通知的一方可以是
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(UserNameChanged1:)
name:kUserNameChangedNotification object:nil];
// 也可以是
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(UserNameChanged2:)
name:kUserNameChangedNotification object:nil];

从例子中可以看到有的缺点:
1.对于接收同一个事件的通知,不同的人可能会用不同的方法名来执行(例子中是UserNameChanged1UserNameChanged1),无法统一。
2.对于多参数支持不方便。

所以本文我们的优化的是统一接收方的执行方法,并为该方法提供明确的参数。

CJProtocolCenter

CJProtocolCenter.m

项目背景举例:

背景描述如下:

vc中所做的某个操作(指一个操作),想要发送给多个人,让他们接收到信息后,自己处理。

我们假设接收者为

1
2
3
4
DelegateReceivedViewModel1 *delegateReceiver1 = [[DelegateReceivedViewModel1 alloc] init];
DelegateReceivedViewModel2 *delegateReceiver2 = [[DelegateReceivedViewModel2 alloc] init];
self.delegateReceiver1 = delegateReceiver1;
self.delegateReceiver2 = delegateReceiver2;

一、正常操作

发送状态变化的信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// OneToManyDelegateNormalViewController1.h
#import "CJUIKitBaseHomeViewController.h"
#import "TSDelegate.h"

@interface OneToManyDelegateNormalViewController1 : CJUIKitBaseHomeViewController {

}
@property (nonatomic, weak) id<TSDelegate> delegate1; // 提供给外部设置
@property (nonatomic, weak) id<TSDelegate> delegate2; // 提供给外部设置

@end


// OneToManyDelegateNormalViewController1.m
loginModule.actionBlock = ^{
[self.delegate1 delegate_didUpdateLoginState:YES];
[self.delegate2 delegate_didUpdateLoginState:YES];
}

②设置代理的地方会是如下:

1
2
3
4
OneToManyDelegateNormalViewController1 *viewController = [[OneToManyDelegateNormalViewController1 alloc] init];
viewController.delegate1 = delegateReceiver1;
viewController.delegate2 = delegateReceiver2;
[self.navigationController pushViewController:viewController animated:YES];

二、优化delegate

依赖注入

依赖注入:把做的事情交给某人。至于依赖的某人有什么能力,依靠外部决定。

参考文档:

注入可以通过对象的初始化(或者可以说构造器)或者通过特性(setter),即构造器注入和setter方法注入。

OneToManyDelegateNormalMediator21

1
2
3
4
5
6
7
8
9
10
11
OneToManyDelegateNormalMediator21 *delegateMediator

// 方法1:构造器注入
- (instancetype)initWithDelegate1:(Delegate1 *)delegate1
delegate2:(Delegate2 *)delegate2;
@property (nonatomic, weak, readonly) Delegate1 *delegate1;
@property (nonatomic, weak, readonly) Delegate2 *delegate2;

// 方法2:Setter 注入
@property (nonatomic, weak) id<Delegate1> delegate1;
@property (nonatomic, weak) id<Delegate2> delegate2;

参考的其他文档:

EXTConcreteProtocol 虽然没有直接叫做依赖注入,而是叫做混合协议,但是充分使用了 OC 动态语言的特性,不侵入项目,高度自动化,框架十分轻量,使用非常简单。

1、优化思路一:从vc中抽离delegate到Mediator

为了优化代码,我们从vc中抽离delegate,则此时

①设置代理的地方会是如下:

1
2
3
4
5
6
7
OneToManyDelegateNormalMediator21 *delegateMediator = [[OneToManyDelegateNormalMediator21 alloc] init];
delegateMediator.delegate1 = self.delegateReceiver1;
delegateMediator.delegate2 = self.delegateReceiver2;

OneToManyDelegateNormalViewController21 *viewController = [[OneToManyDelegateNormalViewController21 alloc] init];
viewController.delegateMediator = delegateMediator;
[self.navigationController pushViewController:viewController animated:YES];

②发送状态变化的信息如下:

1
2
3
4
5
6
// OneToManyDelegateNormalViewController21.m
loginModule.actionBlock = ^{
//[self.delegate1 delegate_didUpdateLoginState:YES];
//[self.delegate2 delegate_didUpdateLoginState:YES];
[self.delegateMediator delegate_didUpdateLoginState:YES];
};

③此时delegateMediator中的delegate_didUpdateLoginState:方法如下:

1
2
3
4
5
6
7
8
9
10
#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
if (self.delegate1 && [self.delegate1 respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegate1 delegate_didUpdateLoginState:YES];
}

if (self.delegate2 && [self.delegate2 respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegate2 delegate_didUpdateLoginState:YES];
}
}

2、优化思路二:在Mediator中管理delegate数组

为了减少设置代理的变量,我们使用delegate数组,则此时

①设置代理的地方会是如下:

1
2
3
4
5
6
7
OneToManyDelegateArrayMediator31 *delegateMediator = [[OneToManyDelegateArrayMediator31 alloc] init];
[delegateMediator addDelegate:self.delegateReceiver1];
[delegateMediator addDelegate:self.delegateReceiver2];

OneToManyDelegateArrayViewController31 *viewController = [[OneToManyDelegateArrayViewController31 alloc] init];
viewController.delegateMediator = delegateMediator;
[self.navigationController pushViewController:viewController animated:YES];

②发送状态变化的信息如下:不变

③此时delegateMediator中的delegate_didUpdateLoginState:方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  OneToManyDelegateArrayMediator31.m
_delegates = [NSPointerArray weakObjectsPointerArray];

- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}

#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if (delegate && [delegate respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[delegate delegate_didUpdateLoginState:YES];
}
}
}

三、优化状态变化的发送方法

1、未优化时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vc.m
loginModule.actionBlock = ^{
if (self.delegateMediator && [self.delegateMediator respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegateMediator delegate_didUpdateLoginState:YES];
}
};

// Mediator.m
#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if (delegate && [delegate respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[delegate delegate_didUpdateLoginState:YES];
}
}
}

为了能够在Mediator中减少我们要实现每个代理的每个方法,我们进行如下优化:

2、增加个发送的封装方法,将协议要执行的方法改为参数等传进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// vc.m
loginModule.actionBlock = ^{
[self.delegateMediator broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};

// Mediator.m
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];
if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

3、如果你不用上述的封装方法,你还可以利用消息转发

对Mediator对象调用delegate_didUpdateLoginState方法时,因为含delegates的Mediator对象并没有实现协议中的方法,如delegate_didUpdateLoginState,所以,我们只能要么补充实现,要么不实现的话,就利用消息转发,将协议中的方法转发到自己delegate链中的对象

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
// vc.m
loginModule.actionBlock = ^{
if (self.delegateMediator && [self.delegateMediator respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegateMediator delegate_didUpdateLoginState:YES];
}
};

// Mediator.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (signature) {
return signature;
}
[_delegates addPointer:nil]; //不加上这句的话,直接调用compact,并不能清除 _delegates 数组中的 nil 对象。
[_delegates compact]; //注意 [_delegates compact],这个方法可以帮助你去掉数组里面的野指针,避免你在快速遍历的时候拿到一个指向不存在对象的地址

for (id delegate in _delegates) {
if (!delegate) {
continue;
}
signature = [delegate methodSignatureForSelector:aSelector];
if (signature) {
break;
}
}
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
BOOL responded = NO;
for (id delegate in _delegates) {
if (delegate && [delegate respondsToSelector:selector]) {
[anInvocation invokeWithTarget:delegate];
responded = YES;
}
}
if (!responded) {
[self doesNotRecognizeSelector:selector];
}
}

此方式的问题:由于[self.delegateMediator delegate_didUpdateLoginState:YES];,所以delegateMediator需要在@interface支持,同样的因为这个支持,我们需要在.m文件中去实现它里面的方法,否则会有警告。除非说协议里的那些方法全部都是设置为了@optional

结论:基于此方式的问题,所以为了减少对其他的约束,我们这里最后采用2的优化方式。

四、发现潜在问题

1、如果一个类同时依赖两个protocal,且刚好两个protocal中有同名方法怎么办?

举例:在vc.m中想要告诉大家去实现协议1中的showMessage方法,但是如果不说明是协议1中的方法,可能有个协议2也有同样的方法,导致执行的方法错了。

分析:这种情况是可能出现的,但是这种情况出现的时候,不会被引用到同一个监听者,不然分不清delegateReceiver中现在执行的是哪一个。

如原本名字不一样,我们区分得开。

1
2
3
4
5
6
7
8
9
10
// viewModel.m
#pragma mark - TSUserDelegate
- (void)userDelegate_didUpdateLoginState:(BOOL)loginState {
NSLog(@"vm收到userDelegate:登录状态发生变化,您已%@", loginState ? @"登录" : @"登出");
}

#pragma mark - TSMessageDelegate
- (void)messageDelegate_didUpdateMessageState:(BOOL)messageState {
NSLog(@"vm收到messageDelegate:信息状态发生变化");
}

我们直接在上述原本实现的基础上看问题,修改后为:

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
// vc.m
loginModule.actionBlock = ^{
[self.delegateMediator broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};

// Mediator.m
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

同理:添加delegate的时候,也

1
2
3
if (![delegateReceiver conformsToProtocol:protocol]) {  // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
// continue or return or ...;
}

2、如何保证delegates操作的数据安全

场景:B页面发送一个信号,B页面的viewModel会去执行,当然还有其他地方也有可能执行。

那么当B发送信号出去,还在遍历执行的时候,突然某个从B页面返回A页面,B页面释放,B的viewModel也跟着释放,即这个信号的其中一个接收者B的viewModel被释放了,那么当执行的时候相当于给nil发送信息,不会崩溃。

1
2
3
4
5
6
7
8
9
10
11
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}

需要注意数据安全的地方,应该是避免重复添加和重复删除

所以,我们使用@synchronized来保护。

1
2
3
@synchronized(xxx) {

}

五、delegateMediator升级为中心单例

发送时候的核心代码,大概如下:

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
// vc.m
loginModule.actionBlock = ^{
[[TSNotificationCenter defaultCenter] broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};


// 该发送方法在`TSNotificationCenter.m`中的代码如下
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

接收时候的核心代码,大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// receiver.m
[[TSNotificationCenter defaultCenter] addDelegate:self.diffDelegateReceiver
forProtocol:@protocol(TSUserDelegate)];
[[TSNotificationCenter defaultCenter] addDelegate:self.diffDelegateReceiver
forProtocol:@protocol(TSMessageDelegate)];

// 该添加方法在`TSNotificationCenter.m`中的代码如下
- (void)addDelegate:(id __nonnull)listener forProtocol:(Protocol * _Nonnull )protocol {
NSAssert(listener, @"listener is nil");
NSAssert(protocol, @"Protocol is nil");
#ifdef DEBUG
NSAssert([listener conformsToProtocol:protocol], @"This listener does not conform to the protocol");
#endif

@synchronized(protocol) {
[self.hashTable addObject:listener];
}
}

说说上面我们应该用什么来管理遵守协议的类(NSHashTable)?

答:因为delegate本身为了避免循环应用,所以其是弱引用对象。而要保存弱引用对象,我们不能够用NSArray,因为NSArray是强引用,而应该用NSHashTable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法1:NSPointerArray
_delegates = [NSPointerArray weakObjectsPointerArray];
- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}

// 方法2:NSHashTable
_hashTable = [NSHashTable weakObjectsHashTable];
- (void)addDelegate:(id)delegate {
[_hashTable addObject:delegate];
}


@property (nonatomic, strong, readonly) NSMutableSet *listenerLists; // 保存多个NSHashTable,即其是数组的数组,里面的元素是 NSHashTable *listeners

这边用了NSHashTable来存储遵守协议的类,NSHashTable和NSSet类似,但又有所不同,总的来说有这几个特点:

NSHashTable中的元素可以通过Hashable协议来判断是否相等.

NSHashTable中的元素如果是弱引用,对象销毁后会被移除,可以避免循环引用.

附一:block 或 delegate 等弱引用对象用什么保存

如果你需要保存一堆block,并且希望它们能够响应特定的事件或操作,你通常会使用数组来存储这些block。然而,由于block在Objective-C中是对象,并且默认情况下它们会被强引用,你需要考虑循环引用的问题,特别是当block内部捕获了它们的捕获环境(例如,捕获了它们的创建者对象的强引用)时。

以下是几种处理block存储的方法:

  1. 使用__weakweak修饰符:如果你的block捕获了它们的创建者对象的引用,你可以在block内部使用__weak(在Objective-C中)或weak(在Swift中)来避免强引用循环。
  2. **使用NSHashTableNSPointerArray**:如果你需要存储block的弱引用,可以使用NSHashTableNSPointerArray,并设置它们的pointerFunctions属性以使用NSPointerFunctionsWeakMemory,这样它们就会存储block的弱引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@property (nonatomic, strong) NSPointerArray *blocks;

- (void)addBlock:(void (^)(void))block {
if (!self.blocks) {
self.blocks = [NSPointerArray weakObjectsPointerArray];
}
[self.blocks addPointer:(__bridge void *)(block ? block : [NSNull null])];
}

- (void)executeBlocks {
for (NSUInteger i = 0; i < self.blocks.count; i++) {
void *pointer = [self.blocks pointerAtIndex:i];
if (pointer != [NSNull null]) {
void (^block)(void) = (__bridge void (^)(void))pointer;
block();
}
}
}

附:不常见集合NSHashTable和NSMapTable

不常见集合NSHashTable和NSMapTable

5框架设计模式-⑦组件化.md

END

其他参考文章:

iOS 实现多代理的方法及实例代码

编译优化hmap

一、查看编译时间

Clean后,执行Command+B,查看编译时间为24.0seconds

image-20211124154515792

而如果编译后,不Clean,继续执行编译,即使用之前的编译缓存,得到的新的编译时间为0.6seconds

image-20211124154752653

二、hmap优化的结果

系统的Pods中Headers里有Public和Private目录:

image-20211124155017822

使用 plugin ‘cocoapods-hmap-prebuilt’ 后

image-20211124155617317

在Pods中Headers里会多出一个HMap目录,里面的文件为hamp,如下截图所示:

image-20211124155527016

此时,我们重新Clean,然后编译,得到的优化后的编译时间为16.8seconds

image-20211124155414548

如果编译后,不Clean,继续执行编译,得到的时间为0.5seconds,也比之前的0.6seconds短

image-20211124155945171

三、hmap优化怎么使用

我们直接使用 cocoapods-hmap-prebuilt

1、cocoapods-hmap-prebuilt的使用方法

直接在你的Podfile中,添加以下一行代码就行

1
2
3
4
5
6
# this is part of Podfile
target 'XXX' do
plugin 'cocoapods-hmap-prebuilt' # 添加此行代码就行
pod 'XXX'
...
end

之后,你和平时一样执行pod install就行。

2、安装cocoapods-hmap-prebuilt

如果你没安装cocoapods-hmap-prebuilt,则会在执行pod install的时候,发生如下错误:

image-20211124161907639

所以请先安装,安装方法如下:

1
gem install cocoapods-hmap-prebuilt

image-20211124162055386

之后,你和平时一样执行pod install就行,执行过程中,会多出如下输出:

image-20211124165627186

三、coapods-hmap-prebuilt介绍

以下内容摘自:一款可以让大型iOS工程编译速度提升50%的工具

cocoapods-hmap-prebuilt 是美团平台迭代组自研的一款 cocoapods 插件,以 Header Map 技术 为基础,进一步提升代码的编译速度,完善头文件的搜索机制。

四、Header Map 是个啥?

Header Map是二进制文件,其包含头文件和头文件路径的映射表。

其核心功能是让编译器能够找到相应的头文件的位置,相较于使用header search path提供的路径查找会快很多

1、Hmap在编译后的文件路径

我们编译后,通过Products,找到生成的app的目录,如下图:

image-20211124165159982

在其中,我们找到Intermediates.noindex目录下的Pod.build

image-20211124165909673

2、hmap查看工具

我们通过hmap查看工具来输出下hmap文件的内容。

1
hmap print Masonry-all-target-headers.hmap

输出内容如下:

image-20211124170212021

附:该hmap工具的安装方法,如其github上的How to use一样,使用如下一行命令,在终端中执行即可。

1
brew install milend/taps/hmap

五、Xcode 编译的其他相关知识

Xcode 编译相关

End

第3节:Hexo frontmatter 日期管理

背景

博客文章多了之后,发现 front matter 中的 date 字段存在一些问题:

  1. 部分文章的 date 不准,有的是手动填的随意日期
  2. 缺少 updated 字段,无法知道文章最后更新时间
  3. 文章排序(按时间)不够准确

方案

利用 Git 历史统一校正所有文章的日期:

字段 来源 含义
date Git 首提交时间 文件进入版本库的创建时间
updated Git 末提交时间 文件最后修改的提交时间

规则:

  • updated 仅在末提交时间 > 首提交时间时才写入,从未修改过的文章不写
  • 未来新文章只写 date,不写 updated(等以后修改了再补)

主题改造

在 landscape 主题的 date.ejs 中添加判断:如果文章有 updated 字段,则显示「发表于 X · 更新于 Y」;否则保持原样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<% if (post.updated){ %>
<span class="<%= class_name %>">
<a href="<%- url_for(post.path) %>">
<time datetime="<%= date_xml(post.date) %>" itemprop="datePublished">
发表于 <%= date(post.date, date_format) %>
</time>
</a>
<span class="article-updated">
· 更新于 <%= date(post.updated, date_format) %>
</span>
</span>
<% } else { %>
<a href="<%- url_for(post.path) %>" class="<%= class_name %>">
<time datetime="<%= date_xml(post.date) %>" itemprop="datePublished">
<%= date(post.date, date_format) %>
</time>
</a>
<% } %>

批量更新脚本

用 Node.js 脚本遍历所有 477 个文件,通过 git log --follow --format="%ai" 获取每篇文章的首末提交时间,批量写入 front matter。

iOS功能_Transform

关于CATransform3D

参考文章:关于CATransform3D

CATransform3DMakeRotation的使用

1
2
3
4
5
6
7
/* Returns a transform that rotates by 'angle' radians about the vector
* '(x, y, z)'. If the vector has length zero the identity transform is
* returned. */

CA_EXTERN CATransform3D CATransform3DMakeRotation (CGFloat angle, CGFloat x,
CGFloat y, CGFloat z)
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

angle:旋转角度
后面的x,y,z三个值组合表示坐标系里面的一个点(x,y,z),由原点和该点(x,y,z)形成一条旋转轴

CATransform3DMakeRotation是按最短路径来选择的

示例:CATransform3DMakeRotation逆时针

Git Commit规范

Git Commit规范

4. Commit 规范

摘自:https://mini.ant.design/

开发过程中,提交 commit 时,需要遵循以下原则:

  • 最小化代码变更:保证每个 commit 的逻辑独立、原子化。一个 commit 只做一件事,哪怕这件事只改了 1 行代码,你也应该独立 commit 这次变更;
  • 遵循 Conventional Commits 原则:避免没有意义、看不懂的 Commit Message 引入,并且在代码提交的时候会有 Commit Message 格式校验。一般情况下,你会大量的使用到:
    1. 使用 feat(scope): xxx 来描述一个 feature 的 commit;
    2. 使用 fix(scope): xxx 来描述一个 bug fixes 的 commit;
    3. 使用 chore(scope): xxx 来来描述一个无关 feature 和 bug fixes 的小调整;
  • issue:如果改动与 issue 相关,请在 Commit Message 中带上 issue 参数,如:fix(scope): [#1] xxxxxx;

Commitizen, Husky, Commitlint

 先简要地介绍这三个工具的作用,让大家对整个校验过程如何实现有一个大体的了解。

  • Commitizen:是一个命令行提示工具,它主要用于帮助我们更快地写出规范的commit message
  • Commitlint:用于校验填写的commit message是否符合设定的规范
  • Husky:是一个git hook工具,用于在提交过程中的某个特定时刻触发commitlint

SVN

目录

1.Mac下搭建SVN服务器

1

SVN中trunk,branches,tags用法详解

svn://proj/|+-trunk+-branches+-tags
这是一个标准的布局,trunk为主开发目录,branches为分支开发目录,tags为tag存档目录(不允许修改)。但是具体这几个目录应该如何使用,svn并没有明确的规范,更多的还是用户自己的习惯。

对于这几个开发目录,一般的使用方法有两种。我更多的是从软件产品的角度出发(比如freebsd),因为互联网的开发模式是完全不一样的。 1.第一种方法,使用trunk作为主要的开发目录
一般的,我们的所有的开发都是基于trunk进行开发,当一个版本/release开发告一段落(开发、测试、文档、制作安装程序、打包等)结束后,代码处于冻结状态(人为规定,可以通过hook来进行管理)。此时应该基于当前冻结的代码库,打tag。当下一个版本/阶段的开发任务开始,继续在trunk进行开发。
此时,如果发现了上一个已发行版本(Released Version)有一些bug,或者一些很急迫的功能要求,而正在开发的版本(Developing Version)无法满足时间要求,这时候就需要在上一个版本上进行修改了。应该基于发行版对应的tag,做相应的分支(branch)进行开发。
例如,刚刚发布1.0,正在开发2.0,此时要在1.0的基础上进行bug修正。

SVN 中trunk、tags、branches使用

svn中的revert和update

iOS 用CornerStone配置SVN,HTTP及svn简单使用说明

Mac下搭建SVN服务器

有些MAC用户想在自己的电脑中建立一个SVN服务器,那么该怎么做呢?
具体步骤:

  • 第一步,启动终端。命令如下:svnadmin create /Users/lichaoqian/Project/SVN,这样我们就在自己的主目录下建立了一个名为“SVN”的代码库。 得到的目录为:
    SVN创建后的目录结构
  • 第二步,配置这个代码仓库。
  • ①首先修改conf文件夹中的svnserve.conf文件,只要把#password-db = passwd这句话的#去掉,使得这个配置项生效即可。保存退出。
    SVN配置代码库1
  • ②接着修改passwd文件,这个里面存着这个代码库接受的用户名和密码
      采用的形式是[用户名]=[密码]。可以增加一个用户“user”,密码“pass”,形如:user=pass,
      SVN配置代码库2
      这里改为只接受用户名为lichaoqian,密码也为lichaoqian的用户。
      SVN配置代码库3
      保存退出。
  • 第三步,启动服务器。终端中继续,命令如下:
    svnserve -d -r /Users/lichaoqian/Project/SVN
    没有任何提示就说明启动成功了。附停止服务器:killall -9 svnserve
    以上就是在MAC中建立SVN服务器的具体步骤了,想要自己建立SVN服务器的用户,快来看看吧

CornerStone的使用

svn的merge使用例子
Cornerstone Merge操作详解
CornerStone的使用

Git2

Git

目录

  • 一、https 和 SSH 的区别
  • 二、在 github 上添加 SSH key 的步骤
  • 三、git撤销

前言

很多朋友在用github管理项目的时候,都是直接使用https url克隆到本地,当然也有有些人使用 SSH url 克隆到本地。然而,为什么绝大多数人会使用https url克隆呢?

这是因为,使用https url克隆对初学者来说会比较方便,复制https url 然后到 git Bash 里面直接用clone命令克隆到本地就好了。而使用 SSH url 克隆却需要在克隆之前先配置和添加好 SSH key 。

因此,如果你想要使用 SSH url 克隆的话,你必须是这个项目的拥有者。否则你是无法添加 SSH key 的。

GitHub 主页只能搜到个人账户下的仓库,却搜不到新创建组织里的仓库

一、https 和 SSH 的区别:

1、前者可以随意克隆github上的项目,而不管是谁的;而后者则是你必须是你要克隆的项目的拥有者或管理员,且需要先添加 SSH key ,否则无法克隆。

2、https url 在push的时候是需要验证用户名和密码的;而 SSH 在push的时候,是不需要输入用户名的,如果配置SSH key的时候设置了密码,则需要输入密码的,否则直接是不需要输入密码的。

二、在 github 上添加 SSH key 的步骤:

1、首先需要检查你电脑是否已经有 SSH key

进入~/.ssh目录,检查是否已经存在 id_rsa.pubid_dsa.pub 文件,如果文件已经存在,那么你可以跳过步骤2,直接进入步骤3。

2、创建一个 SSH key

$ ssh-keygen -t rsa -C "your_email@example.com"

1
2
3
4
代码参数含义:
-t 指定密钥类型,默认是 rsa ,可以省略。
-C 设置注释文字,比如邮箱。
-f 指定密钥文件存储文件名。

以上代码省略了 -f 参数,因此,运行上面那条命令后会让你输入一个文件名,用于保存刚才生成的 SSH key 代码,如:

1
2
Generating public/private rsa key pair.
# Enter file in which to save the key (/c/Users/you/.ssh/id_rsa): [Press enter]

当然,你也可以不输入文件名,使用默认文件名(推荐),那么就会生成 id_rsa 和 id_rsa.pub 两个秘钥文件。

接着又会提示你输入两次密码(该密码是你push文件的时候要输入的密码,而不是github管理者的密码),

当然,你也可以不输入密码,直接按回车。那么push的时候就不需要输入密码,直接提交到github上了,如:

1
2
Enter passphrase (empty for no passphrase): 
# Enter same passphrase again:

接下来,就会显示如下代码提示,如:

1
2
3
4
Your identification has been saved in /c/Users/you/.ssh/id_rsa.
# Your public key has been saved in /c/Users/you/.ssh/id_rsa.pub.
# The key fingerprint is:
# 01:0f:f4:3b:ca:85:d6:17:a1:7d:f0:68:9d:f0:a2:db your_email@example.com

当你看到上面这段代码的收,那就说明,你的 SSH key 已经创建成功,你只需要添加到github的SSH key上就可以了。

3、添加你的 SSH key 到 github上面去

进入账号的SSH key添加处,将id_rsa.pub 文件的内容复制上去添加即可。(记得 SSH key 代码的前后不要留有空格或者回车。)

4、测试一下该SSH key

在git Bash 中输入以下代码

$ ssh -T git@github.com
当你输入以上代码时,会有一段警告代码,如:

1
2
3
The authenticity of host 'github.com (207.97.227.239)' can't be established.
# RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
# Are you sure you want to continue connecting (yes/no)?

这是正常的,你输入 yes 回车既可。如果你创建 SSH key 的时候设置了密码,接下来就会提示你输入密码,如:

Enter passphrase for key ‘/c/Users/Administrator/.ssh/id_rsa’:
当然如果你密码输错了,会再要求你输入,知道对了为止。

注意:输入密码时如果输错一个字就会不正确,使用删除键是无法更正的。

密码正确后你会看到下面这段话,如:

1
2
Hi username! You've successfully authenticated, but GitHub does not
# provide shell access.

如果用户名是正确的,你已经成功设置SSH密钥。如果你看到 “access denied” ,者表示拒绝访问,那么你就需要使用 https 去访问,而不是 SSH 。

github上如何修改别人的开源项目并提交

github上的开源项目是人人都可download下来修改,并贡献源码的,但不是无原则的,如果开源作者提交的代码,被任何人都download下来修改并且随意提交的话,那会有很多恶意修改源码的行为出现,也就没有github良好的开源生态, 那么github上是怎么修改别人的源码并且提交,让自己成为同一项目的源码贡献者呢。有一下三种机制:

1. 开源作者把你加入项目合作者里面

首先他会进入自己项目的settings里,如下:

Collaborators
然后在Collaborators里面搜索你的github用户名,add进去,之后你会在github上受到一个invitation提示,确认一下之后,你就可以在任何IDE里面用git clone到本地,URI选择这个github的开源地址就行,相当于这个github地址作为一个git的远程代码服务器。

2. 第二种方法是fork到本地,然后pull request

找到这个开源项目,点击右上角fork按钮,然后在你自己的github里面就会出现这个项目,修改完之后,在你的github里点击pull request, 之后原作者会收到这个请求,通过之后你就贡献了自己的源码

Fork&pull request

3. 建立organization

登录自己github账号后,进入github.com, 点击右上角,创建一个开源项目组织,人人都可贡献源码

Organization

三、git撤销

git reset HEAD^
拉取最近一次提交到版本库的文件到暂存区 改操作不影响工作区

四、git删除远程提交

这是一个之后需要删除掉的远程commit

在需要删除掉的远程commit后,我又提交了一个commit

在需要删除掉的远程commit后,我又提交了第二个commit

git 删除远程仓库的某次提交

git删除远程提交

1
2
3
4
5
6
7
8
9
10
11
删除上一次远程仓库的提交,修改上次提交的代码,做一次更完美的commit。
>
git reset commitId (注:不要带–hard)到上个版本
>
git stash 暂存修改
>
git push --force 强制push,远程的最新的一次commit被删除
>
git stash pop 释放暂存的修改,开始修改代码
>
git add . -> git commit -m "massage" -> git push

git删除远程提交

End

CocoaPods

官网: https://cocoapods.org/

最新ruby源

1
2
3
gem sources --remove https://gems.ruby-china.org
gem sources -l
gem sources --add https://gems.ruby-china.com

一、cocoapods 的安装和升级

1、查看pod版本和安装路径

1
2
3
pod --version

which pod

2、安装 CocoaPods

1
2
sudo gem install -n /usr/local/bin cocoapods  # OS X 版本 >  10.11,使用此命令安装
sudo gem install cocoapods # OS X 版本 <= 10.11,使用此命令安装

其他参考:百度地图中的CocoaPods文章

3、升级

1
2
3
4
5
6
7
8
sudo gem update cocoapods
# 以下该行命令可升级cocoapods
gem install cocoapods

# 移除现有pod
rm -rf /usr/local/bin/pod
# 重新下载pod
sudo gem install cocoapods

五、spec repo

1、cocoaPod 拉下来的库的本地位置

1
open ~/.cocoapods/repos
image-20240909145311054

2、拉库

常见:直接Podfile中指定source,然后执行 pod install 即可。

结果为:gitee-dvlproad-dvlproadspecs

也可以自己添加库

1
2
cd ~/.cocoapods/repos
pod repo add dvlproad https://gitee.com/dvlproad/dvlproadSpecs.git

结果为:dvlproad

iOS Xcode 14 创建新项目Pod init及Pod install 报错

in `initialize_from_file’: [Xcodeproj] Unknown object version (56). (RuntimeError)

原因:Xcode与cocoapods不兼容。

解决:修改compatibilityVersion,将Project Format 改为Xcode 13.0-compatible

给 Pod 添加资源文件

1
2
3
4
5
6
# 利用 resources 属性,这些资源文件在 build 时会被直接拷贝到 client target 的 mainBundle 里。这样就实现了把图片、音频、NIB等资源打包进最终应用程序的目的。
s.resources = ['CQImagePickerSampleView/Modules/HealthCer/NDM/SimulateApiJSON/*']
# 利用 resource_bundles 属性,这些资源文件在 build 时会被拷贝到 指定的 customBundle 里。这样就实现了把图片、音频、NIB等资源打包进最终应用程序的目的。
s.resource_bundles = {
'CQImagePickerSampleView' => ['CQImagePickerSampleView/**/*.{png,jpg}'] # CQImagePickerSampleView 为生成boudle的名称,可以随便起,但要记住,库里要用
}

End

iconFont

一、Iconfont

阿里巴巴矢量图库:https://www.iconfont.cn/

设计师将图标上传到Iconfont平台,用户可以自定义下载多种格式的icon,平台也可将图标转换为字体,便于前端工程师自由调整与调用。

1、iconfont VS 图片 优势

iconfont和图片相比有如下优势:

  • 1.体积小:可以减小安装包大小。
  • 2.矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
  • 3.可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
  • 4.可以通过TextSpan和文本混用。

iconfont即“字体图标”,它是将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。

效果如下图:控制不同颜色和大小的icon。

image-20220318235832369

2、iconfont 资源下载方式

Iconfont 图标需要使用SVG资源,获取资源的几种方式

下载选择图片

img

二、iconfont在Flutter中的使用

Flutter中,可以像Web开发一样使用iconfont。

1、下载iconfont

2、将文件中的 .json .ttf 添加到项目中

实际建议将整个iconfont解压出来的文件都添加到项目中。主要原因有:

①更新时候,直接整个覆盖,避免内部子文件不对等

②设置icon时候,除了打开json看到图标名,还可以打开html,看到所有图片的整体。

img

3.配置(配置时候注意格式)

这里 family后面的IconFont是自定义的名字,可以自行改成想要的,后续需要用到这个family。

img

4.生成dart文件

1、手动方式:

1
2
3
4
5
6
7
8
9
import 'package:flutter/material.dart';

class AppIcon {
static const __FONT_NAME__ = 'iconfont';
static const IconData icon_dianzan = IconData(0xe612, fontFamily: __FONT_NAME__); // 点赞
static const IconData icon_zhuanfa = IconData(0xe60c, fontFamily: __FONT_NAME__); // 转发
}

// 其中 IconData(0x 后的 e612 的这个值可以从json或html文件中或取

2、自动方式(脚本):

①、脚本内容

iconfont_dart.py(其中变量IconDart,IconCss 根据实际情况可做更改)

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
# coding: u8
import re
from pathlib import Path
import time
ROOT = Path(__file__).resolve().parent
MAIN = ROOT
# 生成的IconFont.dart 文件路径
IconDart = 'lib/common/style/IconFont.dart'
# iconfont css 文件存放路径
#IconCss = 'assets/iconfont/iconfont.css'
IconDart = 'IconFont.dart'
IconCss = 'iconfont.css'
# 将 iconfont 的 css 自动转换为 dart 代码
def translate():
print('Begin translate...')

code = """
import 'package:flutter/widgets.dart';

/// @author: hsc
/// @date: {date}
/// @description 代码由程序自动生成。请不要对此文件做任何修改。

class IconFont {

static const String FONT_FAMILY = 'IconFont';

{icon_codes}

}
""".strip()
strings = []
content = open(MAIN / IconCss).read().replace('\n content', 'content')
matchObj = re.finditer( r'.icon-(.*?):(.|\n)*?"\\(.*?)";', content)
for match in matchObj:
name = match.group(1)
name = name.replace("-","_")
string = f' static const IconData {name} = const IconData(0x{match.group(3)}, fontFamily: IconFont.FONT_FAMILY);'
strings.append(string)
strings = '\n'.join(strings)
code = code.replace('{icon_codes}', strings)
code = code.replace('{date}', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
open(MAIN / IconDart, 'w').write(code)
print('Finish translate...')

if __name__ == "__main__":
translate()


②、脚本执行python3 iconfont_dart.py

image-20220319012403867

执行结果如下,生成了IconFont.dart文件:

image-20220319012857584

其他脚本参考:

二、png转svg格式并上传至iconfont

实现方案:

  1. 下载转换工具inkscape
  2. 登录阿里图标上传svg

一、inkscape

https://inkscape.org

image-20220318192601125

image-20220318192634755

image-20220318192650833

image-20220318204002050

image-20220318205057886

image-20220318205202062

image-20220318205304361

image-20220318205348852

Mock基础知识

一、常用整理

常用整理

类型 占位符 输出值
生日 @date 1990-01-01
性别 @integer(0,2) 0-2之间的随机值
身高 @float(140,220,1,1) 小数位数为1个的140-220之间的随机值
学校 @pick([‘清华大学’, ‘北京大学’, ‘哈尔滨工业大学’, ‘国防科技大学’])
家乡 @city(true) 省+市

二、常用占位符功能详述

@integer(min?, max?)

返回一个随机的整数。

  • min:可选参数,整数最小值。
  • max:可选参数,整数最大值。

例:

1
2
3
4
5
@integer
//--> 1686805249110932
@integer(10,100)
//--> 26
1234

@float( min?, max?, dmin?, dmax? )

返回一个随机浮点数。

  • min:可选,整数部分最小值。
  • max:可选,整数部分最大值。
  • dmin:可选,小数部分最小长度。
  • dmax:可选,小数部分最大长度。

例:

1
2
3
4
5
@float
//--> 1924364975802932.5
@float(60, 100, 2, 6)
//--> 68.1258
1234

@string

返回随机字串,有如下几种使用方式:

1
2
3
4
5
6
@string
@string(length)
@string(pool, length)
@string(min, max)
@string(pool, min, max)
12345
  • length: 字串长度
  • min: 字串最短长度
  • max: 字串最大长度
  • pool:表示字符池。如果传入 ‘lower’、‘upper’、‘number’或’symbol’,表示从内置的字符池从选取。对应关系如下:
1
2
3
4
5
lower: "abcdefghijklmnopqrstuvwxyz",
upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
number: "0123456789",
symbol: "!@#$%^&*()[]"
1234

例:

1
2
3
4
5
6
7
@string(6)
//--> kV(qj4
@string('upper',2,6)
//--> NJKU
@string('123abc!@#', 2,6)
//--> #ab!21
123456

@url(protocol?, host?)

随机生成一个 URL。

  • protocol:指定 URL 协议。例如 http。
  • host:指定URL域名和端口号。

例:

1
2
3
@url('https', '123.sogou.com')
//--> https://123.sogou.com/ccdkttr
12

@ip

随机生成一个IP地址。

@timestamp

返回当前unix时间戳

@datetime(format?)

返回一个随机的日期和时间字符串。

  • format:生成的日期和时间字符串的格式。默认值为yyyy-MM-dd HH:mm:ss。

例:

1
2
3
4
5
@datetime()
//--> 1977-11-17 03:50:15
@datetime('y-M-d H:m:s')
//--> 02-4-23 2:49:40
1234

@pick(arr)

从数组中随机选取一个元素,并返回。

例:

1
2
@pick(['this', 'is', 'picker', 'test'])
//--> picker