FMDB 源码(一)

FMDB 简介

FMDB 瞎说123

FMDB 是在 sqlite 的 Objective-C 版本。在 iOS 开发过程中,用到数据库的第三方大部分都会选择这个库,好早之前就用到了,这次抽了时间看看主要实现。

数据库所做的就是 CRUD 操作,那么 FMDB 要做的便是将 sqlite C API 中的 CRUD 操作以及其他数据库的操作转换成 Objective-C 的 API 。

为什么看 FMDB 源码?

(▼へ▼メ)
FMDB 的 REAMDME 大体浏览一遍,其对 ARC 环境编译期间的识别,以及其多线程的安全让我想探究一下他们是如何实现的,是通过什么样的技术去做到的,当然还有一部分就是我想看一下 FMDB 的单元测试是怎么做的,大概总结如下

  • ARC 和 非 ARC 的编译器区分如何实现
  • 多线程安全如何实现
  • UnitTest 如何去做
  • FMDB 如何支持 swift 呢?

尤其是最后这条,我没有看到 FMDB 有 Swift 的版本,那么他是如何支持 Swift 的呢?

FMDB 结构

  1. FMDatabase Class 主要数据库类,线程安全

    FMDB 之 FMDatabase Class

读源码学习知识

这里记录的是学习到 FMDB 解决某些问题的方式

  • ARC 和 非 ARC 区分
  • 可变参数
  • 参数约束
  • 方法丢弃

FMDB 如何支持 非 ARC 和 ARC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if ! __has_feature(objc_arc)
#define FMDBAutorelease(__v) ([__v autorelease]);
#define FMDBReturnAutoreleased FMDBAutorelease

#define FMDBRetain(__v) ([__v retain]);
#define FMDBReturnRetained FMDBRetain

#define FMDBRelease(__v) ([__v release]);

#define FMDBDispatchQueueRelease(__v) (dispatch_release(__v));
#else
// -fobjc-arc
#define FMDBAutorelease(__v)
#define FMDBReturnAutoreleased(__v) (__v)

#define FMDBRetain(__v)
#define FMDBReturnRetained(__v) (__v)

#define FMDBRelease(__v)

#endif

上面这部分代码便是如何实现 ARC 和 非 ARC 情况的宏定义,不错,就是通过宏定义来区分的,看一下实现的代码:

1
2
3
4
5
6
7
+ (instancetype)databaseQueueWithPath:(NSString *)aPath flags:(int)openFlags {
FMDatabaseQueue *q = [[self alloc] initWithPath:aPath flags:openFlags];

FMDBAutorelease(q);

return q;
}

在使用的地方,根据 ARC 或者 非 ARC 的情况,因为通过宏定义,如果是使用了 retain 的地方,那么在 ARC 下就是空的,如果使用了自动释放池,那么在 ARC 下便是原样儿不变,除此之外,还有 dealloc 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)dealloc {
FMDBRelease(_db);
FMDBRelease(_path);
FMDBRelease(_vfsName);

if (_queue) {
FMDBDispatchQueueRelease(_queue);
_queue = 0x00;
}
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}

FMDB 如何实现 Objective-C 方法可变参数(两种方式)

没有格式

1
- (BOOL)executeUpdate:(NSString*)sql, ...;

这个方法是 FMDB 中用来执行 sql 语句的,对于 sql 中的 insert 语句而言可以做如下执行:

1
2
3
4
5
6
7
8
NSString *inputName = @"Tom";
NSInteger inputAge = 34;
NSInteger selectGender = 1;

[db executeUpdate:@"insert into Teacher (name,age,sex) values (?, ?, ?)",
inputName,
@(inputAge),
@(selectGender)];

这里就是可变参数了,而且参数没有限制。怎么实现的呢 ?

1
2
3
4
5
6
7
8
9
- (BOOL)executeUpdate:(NSString*)sql, ... {
va_list args;
va_start(args, sql);

BOOL result = [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args];

va_end(args);
return result;
}

通过 va_list 这种宏定义方式进行解析,而且 sql 语句貌似可以直接接受 va_list 参数

有格式

什么是有格式呢 ?

1
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);

怎么使用呢 ?

1
[db executeUpdateWithFormat:@"insert into Teacher (name,age,sex) values (%@, %d, %d)",inputName,inputAge,selectGender];

可以看到,这种模式我们并不陌生,我们称之为:格式化字符串。像 NSString 就有这么一个方法,我们经常在用:

1
2
+ (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
+ (instancetype)localizedStringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);

这里是在方法声明的后面添加了 NS_FORMAT_FUNCTION(1,2) 一个宏定义便 OK 了,那么如何解析这些参数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)executeUpdateWithFormat:(NSString*)format, ... {
va_list args;
va_start(args, format);

NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
NSMutableArray *arguments = [NSMutableArray array];

[self extractSQL:format argumentsList:args intoString:sql arguments:arguments];

va_end(args);

return [self executeUpdate:sql withArgumentsInArray:arguments];
}

是的和上面的解析方式一样,但是我们要看到如何解析到字符串和数字呢?

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {

NSUInteger length = [sql length];
unichar last = '\0';
for (NSUInteger i = 0; i < length; ++i) {
id arg = nil;
unichar current = [sql characterAtIndex:i];
unichar add = current;
if (last == '%') {
switch (current) {
case '@':
arg = va_arg(args, id);
break;
case 'c':
// warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSString stringWithFormat:@"%c", va_arg(args, int)];
break;
case 's':
arg = [NSString stringWithUTF8String:va_arg(args, char*)];
break;
case 'd':
case 'D':
case 'i':
arg = [NSNumber numberWithInt:va_arg(args, int)];
break;
case 'u':
case 'U':
arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)];
break;
case 'h':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
// warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithShort:(short)(va_arg(args, int))];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
// warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))];
}
else {
i--;
}
break;
case 'q':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
break;
case 'f':
arg = [NSNumber numberWithDouble:va_arg(args, double)];
break;
case 'g':
// warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'
arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))];
break;
case 'l':
i++;
if (i < length) {
unichar next = [sql characterAtIndex:i];
if (next == 'l') {
i++;
if (i < length && [sql characterAtIndex:i] == 'd') {
//%lld
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
//%llu
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
}
else if (next == 'd') {
//%ld
arg = [NSNumber numberWithLong:va_arg(args, long)];
}
else if (next == 'u') {
//%lu
arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)];
}
else {
i--;
}
}
else {
i--;
}
break;
default:
// something else that we can't interpret. just pass it on through like normal
break;
}
}
else if (current == '%') {
// percent sign; skip this character
add = '\0';
}

if (arg != nil) {
[cleanedSQL appendString:@"?"];
[arguments addObject:arg];
}
else if (add == (unichar)'@' && last == (unichar) '%') {
[cleanedSQL appendFormat:@"NULL"];
}
else if (add != '\0') {
[cleanedSQL appendFormat:@"%C", add];
}
last = current;
}
}

是的,就是这么一个一个的解析出来的

参数约束

1
2
3
4
5
6
7
NS_ASSUME_NONNULL_BEGIN
+ (instancetype)databaseWithURL:(NSURL * _Nullable)url;

- (instancetype)initWithPath:(NSString * _Nullable)path;

- (instancetype)initWithURL:(NSURL * _Nullable)url;
NS_ASSUME_NONNULL_END

这一对宏中包含的方法中的参数都是必须传入的,否则会报警告,也就是参数不能为 nil。如果,打算让参数可以为 nil 那么就必须在参数类型后面添加 _Nullable, FMDB 初始化方法可以不传入 path ,如上实现。

方法丢弃

这在系统 API 中也经常看到,

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

//.h 声明

- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError * _Nullable*)outErr, ... __deprecated_msg("Use executeUpdate:withErrorAndBindings: instead");;


//.m 实现

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ... {
va_list args;
va_start(args, outErr);

BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:args];

va_end(args);
return result;
}

#pragma clang diagnostic pop

FMDB 如何支持 Swift 呢?

FMDB 的单元测试

如何看源码?

带着问题去看,带着好奇心去看,快乐的看!
看源码是一件很痛苦的事情,因为这就像一场陌生的旅游,或者是到了别人家里一样!比如有一天别人说:

”啊,那么土地真的很美丽,很有魅力,大家都说它多好多好”

你听说了之后,也不知道自己感不感兴趣,也不知道它哪里美,更不知道你去了要做什么,只知道别人说去过那里的人都很 NB ,所以你去了,这不是一场旅行,这是一场灾难,绝对的,因为你对它太陌生了,陌生到你处处碰壁,无路可走。

很多时候,我们觉得漫无目的的浏览风光觉得很好,但是,当你意识到生命的短暂,时间的流逝的时候,让你这么做,那绝对绝对是谋杀了。记得以前总觉得自己总是被枷锁锁住,后来我将枷锁打破,然后自己慢慢的静待时光流逝,我以为这种感觉会很好,但是当时间流逝了一段时间之后,我感到疲惫,感到不安,这就如同我们在看源码的时候,漫无目的的去看,都知道这个框架好,那个代码写的优美,但是你真的懂得怎么去欣赏这里的景色了么?

当你想去买房子的时候,你才会去关注房价,关注房屋设计,关注各个城市之间的区别等等。为此,你猜了解到了,原来房子需要这样那样,原来周边环境也很重要,原来这种材料是一般般的等等的问题。而如果你从未打算去买房子,那么别人邀你一起去看房子,即使那房子再美,我想你看到的也就是那句 “房子不错” 其他的你也许真的真的看不出来了。

所以,如果你打算去看一个源码,那么最好最好准备,你要得到什么,为什么去看,相信我,如果你知道了这些,真的会事半功倍!

就如同现在区块链很火,很多人都去看,算法也很火,很多人都去学,我看到好多人都去分享那些xxxx课程,xxxx讲座,xxxx多少天让你了解,我没有去看过,我只在乎那些我需要的东西。

就如同,我要实现可变参数,我要实现一个库支持 Objective-C 和 Swift 一样,just this!

还有一个原因就是,你永远不可能知道第三方库为什么要写成这样?真的,第三方库为什么要实现这样或者那样的需求,这些事情要问作者,而作者也不会知道,你我都是 coding 的,我们都知道需求写完了之后的事情就是没有问题我们便会会提起!所以好多东西本来便没有了来源,你再怎么看也是那个样子,所以索性让第三方库围绕着你走,你要什么便到里面去寻找就 OK 了。

-------------本文结束谢谢欣赏-------------
Alice wechat