上拉加载组件

flutter中有个很好用的Sliver CupertinoSliverRefreshControl,但是没有提供对应的上拉组件,三方库中有不少不错的支持的刷新组件,比如flutter_easy_refresh。但是基本是需要再滚动视图外包上一个另外的监听组件,似乎没有以Sliver方式实现的简单的上拉组件,所以我按照CupertinoSliverRefreshControl的实现原理,简单实现一个上拉sliver组件

CupertinoSliverRefreshControl实现原理

先回顾下Sliver的布局逻辑:

1、Viewport 将当前布局和配置信息通过 SliverConstraints 传递给 Sliver。
2、Sliver 确定自身的位置、绘制等信息,保存在 geometry 中(一个 SliverGeometry 类型的对象)。
3、Viewport 读取 geometry 中的信息来对 Sliver 进行布局和绘制。

而该段逻辑主要在RenderSliver的performLayout方法中,需要用到SliverConstraints的如下几个属性

1
2
3
4
5
6
7
8
class SliverConstraints extends Constraints {
//当前Sliver理论上(可能会固定在顶部)已经滑出可视区域的总偏移
double? scrollOffset;
//上一个 sliver 覆盖当前 sliver 的长度(重叠部分的长度),通常在 sliver 是 pinned/floating
//或者处于列表头时,距离顶部的距离
double? overlap;
...
}

当用户滑动列表,传递给Sliver的约束会不断变化,设置新的geometry和重新布局子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
const SliverGeometry({
this.scrollExtent = 0.0, // sliver占用主轴的高度,决定Sliver在Viewport可滚动的距离,为0时不影响子组件布局显示,但是不占用滚动高度,比如排列在首尾的Sliver在用户结束滑动时,会回弹出Viewport
this.paintExtent = 0.0, // 可视区域中的绘制长度,会被传递到Boxcontrains中的minHeight
this.paintOrigin = 0.0, // 绘制的坐标原点,相对于自身布局位置,小于0时也能生效
//在 Viewport中占用的长度;如果列表滚动方向是垂直方向,则表示列表高度。主要影响下一个Sliver的位置
//范围[0,paintExtent]
double? layoutExtent,
this.maxPaintExtent = 0.0,//最大绘制长度
//scrollExtent的修正值:layoutExtent变化后,为了防止sliver突然跳动(应用新的layoutExtent)
//可以先进行修正,具体的作用在后面 SliverFlexibleHeader 示例中会介绍。
this.scrollOffsetCorrection,
...
})

CupertinoSliverRefreshControl结构

先看下CupertinoSliverRefreshControl的两个主要组成元素。

  • CupertinoSliverRefreshControl
    一个StatefulWidget,他管理的状态就是RefreshIndicatorMode,并通过transitionNextState方法来更新RefreshIndicatorMode
  • _CupertinoSliverRefresh是个SingleChildRenderObjectWidget,他接受CupertinoSliverRefreshControl的参数配置,并传递给_RenderCupertinoSliverRefresh_RenderCupertinoSliverRefresh是个Sliver,他本身不管理刷新状态,只负责根据目前的sliver约束条件和CupertinoSliverRefreshControl的参数配置来布局和绘制。

通过下面的伪代码,表达下大概是这样的组成方式

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
CupertinoSliverRefreshControl (refreshIndicatorExtent){
// CupertinoSliverRefreshControl的state
_CupertinoSliverRefreshControlState { // 负责管理刷新状态
bool _hasLayoutExtent;
Widget build(context) {
return _CupertinoSliverRefresh( // 负责绘制刷新组件
refreshIndicatorExtent:widget.refreshIndicatorExtent, // 刷新组件的指定高度
hasLayoutExtent:_hasLayoutExtent, // 是否要让子组件常驻显示,默认情况下比如下拉距离不够,子组件还是会回弹的,只有下拉足够距离_hasLayoutExtent才未true
child:LayoutBuilder(builder:(context,constrain){ // 这里加个LayoutBuilder的目的是为了拿到constrain,
latestIndicatorBoxExtent = constraints.maxHeight; // 保存布局高度,据此来判断当前刷新状态
refreshState = transitionNextState(); // transitionNextState的大概逻辑是根据latestIndicatorBoxExtent的高度,和之前保存的状态,得出目前的最新状态(更新_hasLayoutExtent),必要时会触发setstate刷新
return widget.build(context, refreshState...);
})
);
}
}
}

_CupertinoSliverRefresh {
// _CupertinoSliverRefresh的RenderSliver
_RenderCupertinoSliverRefresh {
double _refreshIndicatorExtent; //
bool _hasLayoutExtent; //
performLayout() {
// 计算子组件的刷新高度
final double layoutExtent = (_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent;
// 判断此时是否需要显示子组件
final bool active = constraints.overlap < 0.0 || layoutExtent > 0.0;

// 计算子组件的高度
final double overscrolledExtent = constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
// 布局子组件
child!.layout(
constraints.asBoxConstraints(
maxExtent: layoutExtent + overscrolledExtent,
),
parentUsesSize: true,
);

if (active) {
// 显示子组件的情况,计算当前sliver的布局
geometry = SliverGeometry(
scrollExtent: layoutExtent,
paintOrigin: -overscrolledExtent - constraints.scrollOffset,
paintExtent: max(
max(child!.size.height, layoutExtent) - constraints.scrollOffset,
0.0,
),
maxPaintExtent: max(
max(child!.size.height, layoutExtent) - constraints.scrollOffset,
0.0,
),
layoutExtent: max(layoutExtent - constraints.scrollOffset, 0.0),
);
} else {
// 不显示子组件的情况,直接隐藏,此时不会触发LayoutBuilder的绘制
geometry = SliverGeometry.zero;
}
}
}
}

CupertinoSliverRefreshControl总结

发现CupertinoSliverRefreshControl的逻辑并不复杂,主要通过Sliver的布局变化,触发刷新还是隐藏sliver,其中主要注意点点是:

  • _CupertinoSliverRefreshControlState中使用了LayoutBuilder,其目的是为了拿到sliver返回的Box布局信息,来判断当前的滚动状态,因为_CupertinoSliverRefresh中并没有给到一个回调机制
  • _CupertinoSliverRefresh中动态修改scrollExtent,其实会引起Viewport的抖动,因为后续的sliver布局都受到自己的高度影响,所以geometry中提供了scrollOffsetCorrection这个修正属性。

实现一个CupertinoSliverLoadMoreControl

CupertinoSliverLoadMoreControl结构

我们的组件和CupertinoSliverRefreshControl的结构保持一致,尽量不更改使用期望,只是增加了自动加载和预加载的功能。
用到的约束条件如下

1
2
3
4
5
6
7
8
9
class SliverConstraints extends Constraints {
//当前Sliver之前的sliver的滚动高度,肯能是double.infinity
double precedingScrollExtent;
//Viewport剩余的绘制距离,相当于最后一个sliver到Viewport底部的距离
double remainingPaintExtent;
// viewport 的高度
double viewportMainAxisExtent;
...
}

另外用到了Viewport的偏移量,从parent中的offset可以读取到,和cacheExtent,表示预加载区或者缓存区,在sliver不在Viewport区域但是在cacheExtent时,任然会回调布局逻辑。

代码层面的主要修改集中在sliver的performLayout()

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
120
121
bool invisible = constraints.precedingScrollExtent <= 0; // 如果前面没有sliver占用空间,认为当前的Viewport是空的,则不显示刷新组件
// 同样,当剩余空间没有了,说明当前sliver已经不在Viewport中,也不需要显示,这里通过小于0.000000001判断,是因为测试中发现,在Viewport向下拉动时,有时候的remainingPaintExtent会变成一个特别小的数字,而且和0交替出现,虽然此时其实应该是0,可能是flutter的bug。
invisible |= (constraints.remainingPaintExtent < 0.000000001);
// 当不展示sliver时,就直接设置为空sliver的布局数据,
if (invisible && !_autoRefresh) {
geometry = SliverGeometry.zero;
child!.layout(
constraints.asBoxConstraints(maxExtent: 0),
parentUsesSize: true,
);
return;
} else if (invisible && _autoRefresh) {
geometry = SliverGeometry.zero;
child!.layout(
constraints.asBoxConstraints(maxExtent: 0),
parentUsesSize: true,
);

// 这里处理自动加载逻辑,
if (parent is RenderViewport) {
RenderViewport port = parent as RenderViewport;
// 这里计算出Viewport的剩余滚动空间,相当于最后一个sliver到Viewport底部的距离,没有使用`remainingPaintExtent`,是因为在这种情况下他的值为0
final remainScrollExtent = constraints.precedingScrollExtent -
port.offset.pixels -
constraints.viewportMainAxisExtent;
// 理论上这里这里永远会成立,因为`cacheExtent`和`remainScrollExtent`永远相等,只有在进入缓冲区布局时才会布局,但是实际测试发现有还未进入缓冲区就布局的情况。
if (remainScrollExtent <=
(port.cacheExtent ??
RenderAbstractViewport.defaultCacheExtent) &&
remainScrollExtent > 0) {
onPreloadZone();
}
}
return;
}

double scrollOffsetOfAllSlivers = (parent as RenderViewport).offset.pixels;

// 这里是有部分sliver占用了空间,但是没有占满Viewport的情况
if (constraints.precedingScrollExtent <
constraints.viewportMainAxisExtent) {
if (_autoRefresh && scrollOffsetOfAllSlivers >= 0) {
final scrollExtent = (constraints.viewportMainAxisExtent -
constraints.precedingScrollExtent);
geometry = SliverGeometry(
scrollExtent: scrollExtent,
paintExtent: scrollExtent,
maxPaintExtent: scrollExtent,
);
child!.layout(
constraints.asBoxConstraints(
minExtent: 0,
maxExtent: min(
constraints.remainingPaintExtent,
geometry!.maxPaintExtent,
)),
parentUsesSize: true,
);
return;
}
// user pulling down
if (scrollOffsetOfAllSlivers < 0) {
geometry = SliverGeometry.zero;
child!.layout(
constraints.asBoxConstraints(maxExtent: 0),
parentUsesSize: true,
);
return;
}

if (_hasLayoutExtent) {
var paintOrigin =
constraints.remainingPaintExtent - scrollOffsetOfAllSlivers;
var fix = paintOrigin +
_refreshIndicatorExtent -
constraints.remainingPaintExtent;
if (fix > 0) {
paintOrigin -= fix;
}

final painExtent = constraints.remainingPaintExtent - paintOrigin;
geometry = SliverGeometry(
scrollExtent: 0,
paintOrigin: paintOrigin,
paintExtent: painExtent,
maxPaintExtent: painExtent,
);
} else {
geometry = SliverGeometry(
scrollExtent: 0,
paintOrigin:
constraints.remainingPaintExtent - scrollOffsetOfAllSlivers,
paintExtent: scrollOffsetOfAllSlivers,
maxPaintExtent: scrollOffsetOfAllSlivers,
);
}
child!.layout(
constraints.asBoxConstraints(
maxExtent: min(
constraints.remainingPaintExtent,
geometry!.maxPaintExtent,
)),
parentUsesSize: true,
);
return;
}

// 这里处理了sliver即将划入到Viewport的情况,此时`invisible`为false,`remainingPaintExtent`也开始>0,此时只需要判断要不要给子组件滚动空间即刻。
child!.layout(
constraints.asBoxConstraints(maxExtent: constraints.remainingPaintExtent),
parentUsesSize: true,
);

final double layoutExtent =
(_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent;
geometry = SliverGeometry(
scrollExtent: layoutExtent,
paintExtent:
min(_refreshIndicatorExtent, constraints.remainingPaintExtent),
maxPaintExtent: _refreshIndicatorExtent,
);

CupertinoSliverLoadMoreControl总结

下拉组件和核心代码说明已经完成,其他部分代码可以查看仓库,或者试下demo(记得在手机上查看,或者打开移动端调试模式,因为桌面滚动视图不支持弹性滚动overscroll)。

NSArrary强引用问题

在iOS的本地通知功能中,有个细节。

一般使用通知功能NSNotification,都需要添加和移除观察者,否则会引起一些问题:

在iOS9以下,如果观察者已经被释放,但是没有移除出通知中心,那么当这个通知发出时,会引起野指针崩溃,iOS9以上对这个问题做了优化,没有手动移除的话,也不会发生什么,估计苹果也认为自动释放会显得智能一点。

关于iOS如何做到在观察者dealloc的时候,自动移除观察者,网上也有现成的案,这里只是通过这个事情联想到,数组强引用的问题。

Foundation中数组在元素被添加的时候(这里的数组指平常使用的NSArray和NSMutableArray)会强引用持有,就算使用__weak修饰也没有用,导致一些奇特的内存泄漏和循环引用问题。

那么除了业务注意的话,就是要找到一些弱引用集合 Weak Reference Collection的实现方法了.

方法0:使用弱引用包装

1
2
NSValue *value1 = [NSValue valueWithNonretainedObject:_obj1];
[_array addObject:value1];

NSValue可以对某对象弱音引用的方法,我们再将value保存到数组中就可以了。
但是不建议使用NSValue,可以使用自定义的类来实现,因为valueWithNonretainedObject并不会判空,同时,释放掉的对象,也不会将引用置为空,导致野指针问题。

方法1:使用CoreFoundation

使用CoreFoundation中的CFArrayCreateMutable,可以实现弱引用数组。
但是需要小心内存释放问题,同时还要注意类型准换。
所以不建议使用此方法。

参考:

https://stackoverflow.com/questions/4692161/non-retaining-array-for-delegates
https://www.jianshu.com/p/5c98ac2dab58
https://www.jianshu.com/p/ed2030920ec4

方法2:使用NSPointerArray

NSPointerArray可以很好的解决数据类型和自动置空的问题

NSPointerArray提供strongObjectsPointerArrayweakObjectsPointerArray工厂,weakObjectsPointerArray就是我们需要的弱引用数组方法。

NSPointerArray也处理了元素释放导致数组元素个数不稳定的问题:添加了allObjects属性,代表所有未释放的对象,而原本的count则表示所有元素个数。

1
2
3
4
5
6
 _storage = [NSPointerArray weakObjectsPointerArray];

[_storage addPointer:(__bridge void *)(obj)];

[_storage pointerAtIndex:0]; //obj 此处返回的是对象指针,如果对象已经释放,则为nil.

参考:

https://yq.aliyun.com/articles/29434

https://blog.csdn.net/kaiyuanheshang/article/details/52944565

方法3:使用NSHashTable和NSMapTable

NSHashTableNSMapTable使用思路类似NSPointerArray,同时也有实现弱引用的工厂方法,用来满足弱引用的需求。

二者同NSPointerArray区别在于NSHashTable可以看成NSSet,NSMapTable可以看成NSDictionary

iOS启动解析

出自此处

基本大纲

  1. 应用的启动分为Pre-main和mian两部分
  2. 在Pre-main中,可以大致分为load dylib->rebase->bind->Objc setup-> initializer,开发能掌握和度量的是initializer部分
  3. 在开发阶段(Xcode)如何查看启动的每个阶段的时间—通过在Xcode中,设置Edit Scheme -> Run -> Argument汇总的环境变量,会在console中输出
  4. 在应用上线后,统计Pre-mian的使用时间。利用的在加载动态库的一个顺序机制,定制自己的动态库,让他在第一个被加载,并在load函数中hook住所有可执行文件,然后统计出最终的每一个的时间,得到最后的时间(后续文章具体讲述)
  5. Class Load 和 Static Initializers(+(void)load; + (void)initialize; 后续文章)
  6. Xcode For Static Initializer

Apple建议

Apple suggest to aim for a total app launch time of under 400ms and you must do it in less than 20 seconds or the system will kill your app.

Apple建议应用的启动时间控制在400ms之下。并且必须在20s以内完成启动,否则系统则会kill掉应用程序。那么话说我们如何知道app的在启动到调用main()方法之前的时间呢?在WWDC 2016的提到了这方面的信息。

Pre-main时间

从在屏幕上点击你的app icon开始,到应用执行到main()方法或者执行到applicationWillFinishLaunching的过程中,app执行了一系列的操作。在iOS10之前的系统中,我们无处得知其中的细节。而在iOS10系统中,可以通过简单在Xcode设置,在控制台就可以打印出Pre-main的具体信息细节。

通过Xcode中的Edit Scheme -> Run -> Argument,设置参数DYLD_PRINT_STATISTICS值为1

设置DYLD_PRINT_STATISTICS
这里使用的Objective-C项目,iPad Air2,系统iOS10.3

1
2
3
4
5
6
7
8
Total pre-main time:  74.37 milliseconds (100.0%)
dylib loading time: 41.05 milliseconds (55.2%)
rebase/binding time: 8.10 milliseconds (10.9%)
ObjC setup time: 9.87 milliseconds (13.2%)
initializer time: 15.23 milliseconds (20.4%)
slowest intializers :
libSystem.B.dylib : 6.58 milliseconds (8.8%)
libBacktraceRecording.dylib : 6.27 milliseconds (8.4%)

上文中可以看出总共消耗的时间为74.37ms。

  • dylib loading time 载入动态库,这个过程中,会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。对于Apple提供的的系统动态库,做了高度的优化。而对于开发者定义导入的动态库,则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们

  • rebase/binding time 重构和绑定,rebase会修正调整处理图像的指针,并且会设置指向绑定(binding)外部的图像指针。所以为了加快rebase/binding,则需要更少的做指针修复。当你的app当中有太多的Objective-C的类,方法选择器,和类别会增加这一部分的启动时间。有一个数据当大于20000个时候,会增加800ms的时间。另一点:当你的app中使用了很少的C++的虚拟函数,使用Swift会更加高效

  • ObjC setup time 在Objective-C的运行时(runtime),需要对类(class),类别(category)进行注册,以及选择器的分配,所以参照rebase/binding time,尽量减少类的数量,可以达到减少这一部分的时间

  • initializer time 这一份指代的是执行+initialize方法的时间。如果你执行了+load方法(不建议),尽量使用+initialize代替。

LeetCode

两数之和

题目:

给定一个整数数列,找出其中和为特定值的那两个数。

你可以假设每个输入都只会有一种答案,同样的元素不能被重用。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

答案:
通过以空间换取速度的方式,我们可以将查找时间从
O(n) 降低到 O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是因为一旦出现冲突,查找用时可能会退化到 O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
var valKeyMap: Dictionary<Int, Int> = Dictionary<Int, Int>()

for (idx,val) in nums.enumerated() {
let other_target = target - val
if let ot = valKeyMap[other_target] {
if ot != idx {
return [idx,ot]
}
}
valKeyMap[val] = idx
}

return []
}
}

Solution().twoSum([1,2,4,5], 9)

两数相加

给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

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
public class ListNode {
public var val: Int
public var next: ListNode?
public init(_ val: Int) {
self.val = val
self.next = nil
}
}

class Solution {

func addTwoNumbers(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {

var outCarrer = l1?.next
var outCarrer2 = l2?.next

var lasttenths = 0
let sum = l1!.val + l2!.val
if sum > 9 {
lasttenths = 1
}
let newList:ListNode? = ListNode(sum % 10)
var inCarrer = newList

while outCarrer != nil || outCarrer2 != nil{

var val1 = 0
var val2 = 0

if let v = outCarrer?.val {
val1 = v
}
if let v = outCarrer2?.val {
val2 = v
}

let sum = val1 + val2 + lasttenths
lasttenths = sum > 9 ? 1 : 0
let sum2 = sum % 10
inCarrer?.next = ListNode(sum2)
outCarrer = outCarrer?.next
outCarrer2 = outCarrer2?.next
inCarrer = inCarrer?.next
}

if lasttenths != 0{
inCarrer?.next = ListNode(1)
}
return newList
}
}

无重复字符的最长子串

给定一个字符串,找出不含有重复字符的最长子串的长度。

示例:

给定 “abcabcbb” ,没有重复字符的最长子串是 “abc” ,那么长度就是3。

给定 “bbbbb” ,最长的子串就是 “b” ,长度是1。

给定 “pwwkew” ,最长子串是 “wke” ,长度是3。请注意答案必须是一个子串,”pwke” 是 子序列 而不是子串。

方法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (allUnique(s, i, j)) ans = Math.max(ans, j - i);
return ans;
}

public boolean allUnique(String s, int start, int end) {
Set<Character> set = new HashSet<>();
for (int i = start; i < end; i++) {
Character ch = s.charAt(i);
if (set.contains(ch)) return false;
set.add(ch);
}
return true;
}
}

方法2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}
else {
set.remove(s.charAt(i++));
}
}
return ans;
}
}

方法3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
Map<Character, Integer> map = new HashMap<>(); // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {
i = Math.max(map.get(s.charAt(j)), i);
}
ans = Math.max(ans, j - i + 1);
map.put(s.charAt(j), j + 1);
}
return ans;
}
}

快速排序

swift版本

1
2
3
4
5
6
7
8
9
10
11
12

func quicksort<T: Comparable>(_ a: [T]) -> [T] {
guard a.count > 1 else { return a }

let pivot = a[a.count/2]
let less = a.filter { $0 < pivot }
let equal = a.filter { $0 == pivot }
let greater = a.filter { $0 > pivot }

return quicksort(less) + equal + quicksort(greater)
}

C++版本

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
// 对arr[l...r]部分进行partition操作
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
template <typename T>
int _partition(T arr[], int l, int r){

// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr[l] , arr[rand()%(r-l+1)+l] );

T v = arr[l];
int j = l;
for( int i = l + 1 ; i <= r ; i ++ )
if( arr[i] < v ){
j ++;
swap( arr[j] , arr[i] );
}

swap( arr[l] , arr[j]);

return j;
}

// 对arr[l...r]部分进行快速排序
template <typename T>
void _quickSort(T arr[], int l, int r){

// 对于小规模数组, 使用插入排序进行优化
if( r - l <= 15 ){
insertionSort(arr,l,r);
return;
}

int p = _partition(arr, l, r);
_quickSort(arr, l, p-1 );
_quickSort(arr, p+1, r);
}

SQLite - C/C++

基于 菜鸟 整理

要了解更多细节,请查看 SQLite 官方文档1, 官方文档2

序号 API & 描述
1 sqlite3_open(const char *filename, sqlite3 **ppDb)
该例程打开一个指向 SQLite 数据库文件的连接,返回一个用于其他 SQLite 程序的数据库连接对象。
如果 filename 参数是 NULL 或 ‘:memory:’,那么 sqlite3_open() 将会在 RAM 中创建一个内存数据库,这只会在 session 的有效时间内持续。
如果文件名 filename 不为 NULL,那么 sqlite3_open() 将使用这个参数值尝试打开数据库文件。如果该名称的文件不存在,sqlite3_open() 将创建一个新的命名为该名称的数据库文件并打开。
2 sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)
该例程提供了一个执行 SQL 命令的快捷方式,SQL 命令由 sql 参数提供,可以由多个 SQL 命令组成。
在这里,第一个参数 sqlite3 是打开的数据库对象,sqlite_callback 是一个回调,data 作为其第一个参数,errmsg 将被返回用来获取程序生成的任何错误。
sqlite3_exec() 程序解析并执行由 sql 参数所给的每个命令,直到字符串结束或者遇到错误为止。
3 sqlite3_close(sqlite3*)
该例程关闭之前调用 sqlite3_open() 打开的数据库连接。所有与连接相关的语句都应在连接关闭之前完成。
如果还有查询没有完成,sqlite3_close() 将返回 SQLITE_BUSY 禁止关闭的错误消息。

连接数据库

下面的 C 代码段显示了如何连接到一个现有的数据库。如果数据库不存在,那么它就会被创建,最后将返回一个数据库对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sqlite3.h>

int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;

rc = sqlite3_open("test.db", &db);

if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stderr, "Opened database successfully\n");
}
sqlite3_close(db);
}

现在,让我们来编译和运行上面的程序,在当前目录中创建我们的数据库 test.db。您可以根据需要改变路径。

1
2
3
$gcc test.c -l sqlite3
$./a.out
Opened database successfully

如果要使用 C++ 源代码,可以按照下列所示编译代码:

1
$g++ test.c -l sqlite3

在这里,把我们的程序链接上 sqlite3 库,以便向 C 程序提供必要的函数。这将在您的目录下创建一个数据库文件 test.db,您将得到如下结果:

1
2
3
-rwxr-xr-x. 1 root root 7383 May  8 02:06 a.out
-rw-r--r--. 1 root root 323 May 8 02:05 test.c
-rw-r--r--. 1 root root 0 May 8 02:06 test.db

创建表

下面的 C 代码段将用于在先前创建的数据库中创建一个表:

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
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

static int callback(void *NotUsed, int argc, char **argv, char **azColName){
int i;
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}

int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;

/* Open database */
rc = sqlite3_open("test.db", &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stdout, "Opened database successfully\n");
}

/* Create SQL statement */
sql = "CREATE TABLE COMPANY(" \
"ID INT PRIMARY KEY NOT NULL," \
"NAME TEXT NOT NULL," \
"AGE INT NOT NULL," \
"ADDRESS CHAR(50)," \
"SALARY REAL );";

/* Execute SQL statement */
rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Table created successfully\n");
}
sqlite3_close(db);
return 0;
}

上述程序编译和执行时,它会在 test.db 文件中创建 COMPANY 表,最终文件列表如下所示:

1
2
3
-rwxr-xr-x. 1 root root 9567 May  8 02:31 a.out
-rw-r--r--. 1 root root 1207 May 8 02:31 test.c
-rw-r--r--. 1 root root 3072 May 8 02:31 test.db

INSERT 操作

下面的 C 代码段显示了如何在上面创建的 COMPANY 表中创建记录:

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
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

static int callback(void *NotUsed, int argc, char **argv, char **azColName){
int i;
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}

int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;

/* Open database */
rc = sqlite3_open("test.db", &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stderr, "Opened database successfully\n");
}

/* Create SQL statement */
sql = "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " \
"VALUES (1, 'Paul', 32, 'California', 20000.00 ); " \
"INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) " \
"VALUES (2, 'Allen', 25, 'Texas', 15000.00 ); " \
"INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)" \
"VALUES (3, 'Teddy', 23, 'Norway', 20000.00 );" \
"INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)" \
"VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );";

/* Execute SQL statement */
rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Records created successfully\n");
}
sqlite3_close(db);
return 0;
}

上述程序编译和执行时,它会在 COMPANY 表中创建给定记录,并会显示以下两行:

1
2
Opened database successfully
Records created successfully

SELECT 操作

在我们开始讲解获取记录的实例之前,让我们先了解下回调函数的一些细节,这将在我们的实例使用到。这个回调提供了一个从 SELECT 语句获得结果的方式。它声明如下:

1
2
3
4
5
6
typedef int (*sqlite3_callback)(
void*, /* Data provided in the 4th argument of sqlite3_exec() */
int, /* The number of columns in row */
char**, /* An array of strings representing fields in the row */
char** /* An array of strings representing column names */
);

如果上面的回调在 sqlite_exec() 程序中作为第三个参数,那么 SQLite 将为 SQL 参数内执行的每个 SELECT 语句中处理的每个记录调用这个回调函数。
下面的 C 代码段显示了如何从前面创建的 COMPANY 表中获取并显示记录:

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
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

static int callback(void *data, int argc, char **argv, char **azColName){
int i;
fprintf(stderr, "%s: ", (const char*)data);
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}

int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;
const char* data = "Callback function called";

/* Open database */
rc = sqlite3_open("test.db", &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stderr, "Opened database successfully\n");
}

/* Create SQL statement */
sql = "SELECT * from COMPANY";

/* Execute SQL statement */
rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Operation done successfully\n");
}
sqlite3_close(db);
return 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
Opened database successfully
Callback function called: ID = 1
NAME = Paul
AGE = 32
ADDRESS = California
SALARY = 20000.0

Callback function called: ID = 2
NAME = Allen
AGE = 25
ADDRESS = Texas
SALARY = 15000.0

Callback function called: ID = 3
NAME = Teddy
AGE = 23
ADDRESS = Norway
SALARY = 20000.0

Callback function called: ID = 4
NAME = Mark
AGE = 25
ADDRESS = Rich-Mond
SALARY = 65000.0

Operation done successfully

UPDATE 操作

下面的 C 代码段显示了如何使用 UPDATE 语句来更新任何记录,然后从 COMPANY 表中获取并显示更新的记录:

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
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

static int callback(void *data, int argc, char **argv, char **azColName){
int i;
fprintf(stderr, "%s: ", (const char*)data);
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}

int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;
const char* data = "Callback function called";

/* Open database */
rc = sqlite3_open("test.db", &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stderr, "Opened database successfully\n");
}

/* Create merged SQL statement */
sql = "UPDATE COMPANY set SALARY = 25000.00 where ID=1; " \
"SELECT * from COMPANY";

/* Execute SQL statement */
rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Operation done successfully\n");
}
sqlite3_close(db);
return 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
Opened database successfully
Callback function called: ID = 1
NAME = Paul
AGE = 32
ADDRESS = California
SALARY = 25000.0

Callback function called: ID = 2
NAME = Allen
AGE = 25
ADDRESS = Texas
SALARY = 15000.0

Callback function called: ID = 3
NAME = Teddy
AGE = 23
ADDRESS = Norway
SALARY = 20000.0

Callback function called: ID = 4
NAME = Mark
AGE = 25
ADDRESS = Rich-Mond
SALARY = 65000.0

Operation done successfully

DELETE 操作

下面的 C 代码段显示了如何使用 DELETE 语句删除任何记录,然后从 COMPANY 表中获取并显示剩余的记录:

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
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

static int callback(void *data, int argc, char **argv, char **azColName){
int i;
fprintf(stderr, "%s: ", (const char*)data);
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}

int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;
const char* data = "Callback function called";

/* Open database */
rc = sqlite3_open("test.db", &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stderr, "Opened database successfully\n");
}

/* Create merged SQL statement */
sql = "DELETE from COMPANY where ID=2; " \
"SELECT * from COMPANY";

/* Execute SQL statement */
rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Operation done successfully\n");
}
sqlite3_close(db);
return 0;
}

上述程序编译和执行时,它会产生以下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Opened database successfully
Callback function called: ID = 1
NAME = Paul
AGE = 32
ADDRESS = California
SALARY = 20000.0

Callback function called: ID = 3
NAME = Teddy
AGE = 23
ADDRESS = Norway
SALARY = 20000.0

Callback function called: ID = 4
NAME = Mark
AGE = 25
ADDRESS = Rich-Mond
SALARY = 65000.0

Operation done successfully

SQLite

最近要实现对SQLite的管理库,先记录如下:

基于 菜鸟 整理

基本知识


建表

1
2
3
4
5
6
7
CREATE TABLE database_name.table_name(
column1 datatype PRIMARY KEY(one or more columns),
column2 datatype,
column3 datatype,
.....
columnN datatype,
);

删表

1
DROP TABLE database_name.table_name;

插入列

1
2
3
4
INSERT INTO TABLE_NAME [(column1, column2, column3,...columnN)]  
VALUES (value1, value2, value3,...valueN);

INSERT INTO TABLE_NAME VALUES (value1,value2,value3,...valueN);

Update 语句

1
2
3
UPDATE table_name
SET column1 = value1, column2 = value2...., columnN = valueN
WHERE [condition];

Delete 语句

1
2
DELETE FROM table_name
WHERE [condition];

查询

1
2
3
SELECT column1, column2, columnN FROM table_name;

SELECT * FROM table_name;

子句

各个子句的排列顺序

1
2
3
4
5
6
SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY

Where子句

1
2
3
SELECT column1, column2, columnN 
FROM table_name
WHERE [condition]

AND


1
2
3
SELECT column1, column2, columnN 
FROM table_name
WHERE [condition1] AND [condition2]...AND [conditionN];

OR


1
2
3
SELECT column1, column2, columnN 
FROM table_name
WHERE [condition1] OR [condition2]...OR [conditionN]

Like 子句

SQLite 的 LIKE 运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,LIKE 运算符将返回真(true),也就是 1。这里有两个通配符与 LIKE 运算符一起使用:
百分号 (%)
下划线 (_)快速记忆 _ 代表就一个字符
百分号(%)代表零个、一个或多个数字或字符。下划线(_)代表一个单一的数字或字符。这些符号可以被组合使用。

1
2
3
SELECT column_list 
FROM table_name
WHERE column LIKE 'XXXX%'

Glob 子句

Glob类似like,区别是like 不区分大小写,GLob区分大小写

星号 ( * )对应 百分号 (%)问号 (?)对应 下划线 ( _ )


Limit 子句

1
2
3
SELECT column1, column2, columnN 
FROM table_name
LIMIT [no of rows]

1
2
3
SELECT column1, column2, columnN 
FROM table_name
LIMIT [no of rows] OFFSET [row num]

Order By 子句

1
2
3
4
SELECT column-list 
FROM table_name
[WHERE condition]
[ORDER BY column1, column2, .. columnN] [ASC | DESC];

Group By 子句

SQLite 的 GROUP BY 子句用于与 SELECT 语句一起使用,来对相同的数据进行分组。

在 SELECT 语句中,GROUP BY 子句放在 WHERE 子句之后,放在 ORDER BY 子句之前。

1
2
3
4
5
SELECT column-list
FROM table_name
WHERE [ conditions ]
GROUP BY column1, column2....columnN
ORDER BY column1, column2....columnN

Having 子句

HAVING 子句允许指定条件来过滤将出现在最终结果中的分组结果。

WHERE 子句在所选列上设置条件,而 HAVING 子句则在由 GROUP BY 子句创建的分组上设置条件。

在一个查询中,HAVING 子句必须放在 GROUP BY 子句之后,必须放在 ORDER BY 子句之前。下面是包含 HAVING 子句的 SELECT 语句的语法:

1
2
3
4
5
6
SELECT column1, column2
FROM table1, table2
WHERE [ conditions ]
GROUP BY column1, column2
HAVING [ conditions ]
ORDER BY column1, column2

关键字


Distinct 关键字

DISTINCT 关键字与 SELECT 语句一起使用,来消除所有重复的记录,并只获取唯一一次记录。

加了索引之后 distinct 比没加索引的 distinct 快。
加了索引之后 group by 比没加索引的 group by 快。
再来对比 :distinct 和 group by

不管是加不加索引 group by 都比 distinct 快

distinct 和group by都需要排序,一样的结果集从执行计划的成本代价来看差距不大,但group by 还涉及到统计,所以应该需要准备工作。所以单纯从等价结果来说,选择distinct比较效率一些。

1.数据列的所有数据都一样,即去重计数的结果为1时,用distinct最佳

2.如果数据列唯一,没有相同数值,用group 最好

1
2
3
SELECT DISTINCT column1, column2,.....columnN 
FROM table_name
WHERE [condition]

Autoincrement

自增约束

1
2
3
4
5
6
7
sqlite> CREATE TABLE COMPANY(
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME TEXT NOT NULL,
AGE INT NOT NULL,
ADDRESS CHAR(50),
SALARY REAL
);

在AUTOINCREMENT约束的字段没有赋值时,就会自动设定为最大的rowid+1,
自增字段只能是主键。

进阶


PRAGMA

SQLite 的 PRAGMA 命令是一个特殊的命令,可以用在 SQLite 环境内控制各种环境变量和状态标志。一个 PRAGMA 值可以被读取,也可以根据需求进行设置。

pragma 修改的是预置的变量,不可以自定义变量

具体有哪些环境变量查看这里

1
2
3
4
5
6
7
要查询当前的 PRAGMA 值,只需要提供该 pragma 的名字:

PRAGMA pragma_name;

要为 PRAGMA 设置一个新的值,语法如下:

PRAGMA pragma_name = value;

约束

常用约束

1
2
3
4
5
6
7
8
9
NOT NULL 约束:确保某列不能有 NULL 值。

DEFAULT 约束:当某列没有指定值时,为该列提供默认值。

UNIQUE 约束:确保某列中的所有值是不同的。

PRIMARY Key 约束:唯一标识数据库表中的各行/记录。

CHECK 约束:CHECK 约束确保某列中的所有值满足一定条件。

针对check,写个栗子

1
2
3
4
5
6
7
CREATE TABLE COMPANY3(
ID INT PRIMARY KEY NOT NULL,
NAME TEXT NOT NULL,
AGE INT NOT NULL,
ADDRESS CHAR(50),
SALARY REAL CHECK(SALARY > 0)
);

默认值

1
2
3
4
5
6
7
CREATE TABLE COMPANY(
ID INT PRIMARY KEY NOT NULL,
NAME TEXT NOT NULL,
AGE INT NOT NULL,
ADDRESS CHAR(50),
SALARY REAL DEFAULT 50000.00
);

Joins

sqlite 支持的joins 有三种链接

交叉连接 - CROSS JOIN

1
如果不带WHERE条件子句,它将会返回被连接的两个表的笛卡尔积,返回结果的行数等于两个表行数的乘积

内连接 - INNER JOIN

INNER JOIN 产生的结果是AB的交集

1
SELECT * FROM TableA INNER JOIN TableB ON TableA.name = TableB.name

外连接 - OUTER JOIN

LEFT [OUTER] JOIN 产生表A的完全集,而B表中匹配的则有值,没有匹配的则以null值取代。

1
SELECT * FROM TableA LEFT OUTER JOIN TableB ON TableA.name = TableB.name

RIGHT [OUTER] JOIN 不支持
FULL [OUTER] JOINs 不支持


Unions 子句

SQLite的 UNION 子句/运算符用于合并两个或多个 SELECT 语句的结果,不返回任何重复的行。


别名

可以暂时把表或列重命名为另一个名字,这被称为别名

1
2
3
SELECT column1, column2....
FROM table_name AS alias_name
WHERE [condition];
1
2
3
SELECT sum(column) as alias_name
FROM table_name
WHERE [condition];

触发器(Trigger)

SQLite 触发器(Trigger)是数据库的回调函数,它会在指定的数据库事件发生时自动执行/调用。

SQLite 的触发器(Trigger)可以指定在特定的数据库表发生 DELETE、INSERT 或 UPDATE 时触发,或在一个或多个指定表的列发生更新时触发。
SQLite 只支持 FOR EACH ROW 触发器(Trigger),没有 FOR EACH STATEMENT 触发器(Trigger)。因此,明确指定 FOR EACH ROW 是可选的。
WHEN 子句和触发器(Trigger)动作可能访问使用表单 NEW.column-name 和 OLD.column-name 的引用插入、删除或更新的行元素,其中 column-name 是从与触发器关联的表的列的名称。
如果提供 WHEN 子句,则只针对 WHEN 子句为真的指定行执行 SQL 语句。如果没有提供 WHEN 子句,则针对所有行执行 SQL 语句。
BEFORE 或 AFTER 关键字决定何时执行触发器动作,决定是在关联行的插入、修改或删除之前或者之后执行触发器动作。
当触发器相关联的表删除时,自动删除触发器(Trigger)。
要修改的表必须存在于同一数据库中,作为触发器被附加的表或视图,且必须只使用 tablename,而不是 database.tablename。
一个特殊的 SQL 函数 RAISE() 可用于触发器程序内抛出异常。

添加触发器

1
2
3
4
5
CREATE TRIGGER audit_log AFTER INSERT 
ON COMPANY
BEGIN
INSERT INTO AUDIT(EMP_ID, ENTRY_DATE) VALUES (new.ID, datetime('now'));
END;

删除触发器

1
DROP TRIGGER trigger_name;

索引(Index)

索引(Index)是一种特殊的查找表,数据库搜索引擎用来加快数据检索。简单地说,索引是一个指向表中数据的指针。一个数据库中的索引与一本书后边的索引是非常相似的。
索引有助于加快 SELECT 查询和 WHERE 子句,但它会减慢使用 UPDATE 和 INSERT 语句时的数据输入。索引可以创建或删除,但不会影响数据。
索引也可以是唯一的,与 UNIQUE 约束类似,在列上或列组合上防止重复条目。

创建索引

1
CREATE INDEX index_name ON table_name;

单列索引

单列索引是一个只基于表的一个列上创建的索引。基本语法如下:

1
2
CREATE INDEX index_name
ON table_name (column_name);

唯一索引

使用唯一索引不仅是为了性能,同时也为了数据的完整性。唯一索引不允许任何重复的值插入到表中。基本语法如下:

1
2
CREATE UNIQUE INDEX index_name
on table_name (column_name);

组合索引

组合索引是基于一个表的两个或多个列上创建的索引。基本语法如下:

1
2
CREATE INDEX index_name
on table_name (column1, column2);

是否要创建一个单列索引还是组合索引,要考虑到您在作为查询过滤条件的 WHERE 子句中使用非常频繁的列。
如果值使用到一个列,则选择使用单列索引。如果在作为过滤的 WHERE 子句中有两个或多个列经常使用,则选择使用组合索引。

虽然索引的目的在于提高数据库的性能,但这里有几个情况需要避免使用索引。使用索引时,应重新考虑下列准则:

  • 索引不应该使用在较小的表上。
  • 索引不应该使用在有频繁的大批量的更新或插入操作的表上。
  • 索引不应该使用在含有大量的 NULL 值的列上。
  • 索引不应该使用在频繁操作的列上。

indexed by 子句

1
2
3
4
SELECT|DELETE|UPDATE column1, column2...
INDEXED BY (index_name)
table_name
WHERE (CONDITION);

Alter 命令

在 SQLite 中,除了重命名表和在已有的表中添加列,ALTER TABLE 命令不支持其他操作。

用来重命名已有的表的 ALTER TABLE 的基本语法如下:

1
ALTER TABLE database_name.table_name RENAME TO new_table_name;

用来在已有的表中添加一个新的列的 ALTER TABLE 的基本语法如下:

1
ALTER TABLE database_name.table_name ADD COLUMN column_def...;

事务(Transaction)

事务(Transaction)是一个对数据库执行工作单元。事务(Transaction)是以逻辑顺序完成的工作单位或序列,可以是由用户手动操作完成,也可以是由某种数据库程序自动完成。
事务(Transaction)是指一个或多个更改数据库的扩展。例如,如果您正在创建一个记录或者更新一个记录或者从表中删除一个记录,那么您正在该表上执行事务。重要的是要控制事务以确保数据的完整性和处理数据库错误。
实际上,您可以把许多的 SQLite 查询联合成一组,把所有这些放在一起作为事务的一部分进行执行。

事务的属性
事务(Transaction)具有以下四个标准属性,通常根据首字母缩写为 ACID:

  • 原子性(Atomicity):确保工作单位内的所有操作都成功完成,否则,事务会在出现故障时终止,之前的操作也会回滚到以前的状态。
  • 一致性(Consistency):确保数据库在成功提交的事务上正确地改变状态。
  • 隔离性(Isolation):使事务操作相互独立和透明。
  • 持久性(Durability):确保已提交事务的结果或效果在系统发生故障的情况下仍然存在

事务控制

使用下面的命令来控制事务:

1
2
3
BEGIN TRANSACTION:开始事务处理。
COMMIT:保存更改,或者可以使用 END TRANSACTION 命令。
ROLLBACK:回滚所做的更改。

事务控制命令只与 DML 命令 INSERT、UPDATE 和 DELETE 一起使用。他们不能在创建表或删除表时使用,因为这些操作在数据库中是自动提交的。

BEGIN TRANSACTION 命令

事务(Transaction)可以使用 BEGIN TRANSACTION 命令或简单的 BEGIN 命令来启动。此类事务通常会持续执行下去,直到遇到下一个 COMMIT 或 ROLLBACK 命令。不过在数据库关闭或发生错误时,事务处理也会回滚。以下是启动一个事务的简单语法:

1
2
3
BEGIN;
or
BEGIN TRANSACTION;

COMMIT 命令

COMMIT 命令是用于把事务调用的更改保存到数据库中的事务命令。
COMMIT 命令把自上次 COMMIT 或 ROLLBACK 命令以来的所有事务保存到数据库。
COMMIT 命令的语法如下:

1
2
3
COMMIT;
or
END TRANSACTION;

ROLLBACK 命令

ROLLBACK 命令是用于撤消尚未保存到数据库的事务的事务命令。
ROLLBACK 命令只能用于撤销自上次发出 COMMIT 或 ROLLBACK 命令以来的事务。
ROLLBACK 命令的语法如下:

1
ROLLBACK;

SQLite 子查询

子查询或内部查询或嵌套查询是在另一个 SQLite 查询内嵌入在 WHERE 子句中的查询。

使用子查询返回的数据将被用在主查询中作为条件,以进一步限制要检索的数据。
子查询可以与 SELECT、INSERT、UPDATE 和 DELETE 语句一起使用,可伴随着使用运算符如 =、<、>、>=、<=、IN、BETWEEN 等。

以下是子查询必须遵循的几个规则:

  • 子查询必须用括号括起来。
  • 子查询在 SELECT 子句中只能有一个列,除非在主查询中有多列,与子查询的所选列进行比较。
  • ORDER BY 不能用在子查询中,虽然主查询可以使用 ORDER BY。可以在子查询中使用 GROUP BY,功能与 ORDER BY 相同。
  • 子查询返回多于一行,只能与多值运算符一起使用,如 IN 运算符。
  • BETWEEN 运算符不能与子查询一起使用,但是,BETWEEN 可在子查询内使用。

SELECT 语句中的子查询使用

子查询通常与 SELECT 语句一起使用。基本语法如下:

1
2
3
4
5
6
SELECT column_name [, column_name ]
FROM table1 [, table2 ]
WHERE column_name OPERATOR
(SELECT column_name [, column_name ]
FROM table1 [, table2 ]
[WHERE])

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

现在,让我们检查 SELECT 语句中的子查询使用:

1
2
3
4
5
sqlite> SELECT * 
FROM COMPANY
WHERE ID IN (SELECT ID
FROM COMPANY
WHERE SALARY > 45000) ;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

INSERT 语句中的子查询使用

子查询也可以与 INSERT 语句一起使用。INSERT 语句使用子查询返回的数据插入到另一个表中。在子查询中所选择的数据可以用任何字符、日期或数字函数修改。

基本语法如下:

1
2
3
4
INSERT INTO table_name [ (column1 [, column2 ]) ]
SELECT [ *|column1 [, column2 ]
FROM table1 [, table2 ]
[ WHERE VALUE OPERATOR ]

实例

假设 COMPANY_BKP 的结构与 COMPANY 表相似,且可使用相同的 CREATE TABLE 进行创建,只是表名改为 COMPANY_BKP。

现在把整个 COMPANY 表复制到 COMPANY_BKP,语法如下:

1
2
3
4
sqlite> INSERT INTO COMPANY_BKP
SELECT * FROM COMPANY
WHERE ID IN (SELECT ID
FROM COMPANY) ;

UPDATE 语句中的子查询使用

子查询可以与 UPDATE 语句结合使用。当通过 UPDATE 语句使用子查询时,表中单个或多个列被更新。

基本语法如下:

1
2
3
4
5
6
UPDATE table
SET column_name = new_value
[ WHERE OPERATOR [ VALUE ]
(SELECT COLUMN_NAME
FROM TABLE_NAME)
[ WHERE) ]

实例

假设,我们有 COMPANY_BKP 表,是 COMPANY 表的备份。

下面的实例把 COMPANY 表中所有 AGE 大于或等于 27 的客户的 SALARY 更新为原来的 0.50 倍:

1
2
3
4
sqlite> UPDATE COMPANY
SET SALARY = SALARY * 0.50
WHERE AGE IN (SELECT AGE FROM COMPANY_BKP
WHERE AGE >= 27 );

这将影响两行,最后 COMPANY 表中的记录如下:

ID NAME AGE ADDRESS SALARY
1 Paul 32 California 10000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 42500.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

DELETE 语句中的子查询使用

子查询可以与 DELETE 语句结合使用,就像上面提到的其他语句一样。

基本语法如下:

1
2
3
4
5
DELETE FROM TABLE_NAME
[ WHERE OPERATOR [ VALUE ]
(SELECT COLUMN_NAME
FROM TABLE_NAME)
[ WHERE) ]

实例

假设,我们有 COMPANY_BKP 表,是 COMPANY 表的备份。
下面的实例删除 COMPANY 表中所有 AGE 大于或等于 27 的客户记录:

1
2
3
sqlite> DELETE FROM COMPANY
WHERE AGE IN (SELECT AGE FROM COMPANY_BKP
WHERE AGE > 27 );

这将影响两行,最后 COMPANY 表中的记录如下:

ID NAME AGE ADDRESS SALARY
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 42500.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

SQLite 日期 & 时间

SQLite 支持以下五个日期和时间函数:

序号 函数 实例
1 date(timestring, modifier, modifier, …) 以 YYYY-MM-DD 格式返回日期。
2 time(timestring, modifier, modifier, …) 以 HH:MM:SS 格式返回时间。
3 datetime(timestring, modifier, modifier, …) 以 YYYY-MM-DD HH:MM:SS 格式返回。
4 julianday(timestring, modifier, modifier, …) 这将返回从格林尼治时间的公元前 4714 年 11 月 24 日正午算起的天数。
5 strftime(format, timestring, modifier, modifier, …) 这将根据第一个参数指定的格式字符串返回格式化的日期。具体格式见下边讲解。

上述五个日期和时间函数把时间字符串作为参数。时间字符串后跟零个或多个 modifier 修饰符。strftime() 函数也可以把格式字符串 format 作为其第一个参数。下面将为您详细讲解不同类型的时间字符串和修饰符。

时间字符串

一个时间字符串可以采用下面任何一种格式:

序号 时间字符串 实例
1 YYYY-MM-DD 2010-12-30
2 YYYY-MM-DD HH:MM 2010-12-30 12:10
3 YYYY-MM-DD HH:MM:SS.SSS 2010-12-30 12:10:04.100
4 MM-DD-YYYY HH:MM 30-12-2010 12:10
5 HH:MM 12:10
6 YYYY-MM-DDTHH:MM 2010-12-30 12:10
7 HH:MM:SS 12:10:01
8 YYYYMMDD HHMMSS 20101230 121001
9 now 2013-05-07

您可以使用 “T” 作为分隔日期和时间的文字字符。

修饰符(Modifier)

时间字符串后边可跟着零个或多个的修饰符,这将改变有上述五个函数返回的日期和/或时间。任何上述五大功能返回时间。修饰符应从左到右使用,下面列出了可在 SQLite 中使用的修饰符:

  • NNN days
  • NNN hours
  • NNN minutes
  • NNN.NNNN seconds
  • NNN months
  • NNN years
  • start of month
  • start of year
  • start of day
  • weekday N
  • unixepoch
  • localtime
  • utc

格式化

SQLite 提供了非常方便的函数 strftime() 来格式化任何日期和时间。您可以使用以下的替换来格式化日期和时间:

替换 描述
%d 一月中的第几天,01-31
%f 带小数部分的秒,SS.SSS
%H 小时,00-23
%j 一年中的第几天,001-366
%J 儒略日数,DDDD.DDDD
%m 月,00-12
%M 分,00-59
%s 从 1970-01-01 算起的秒数
%S 秒,00-59
%w 一周中的第几天,0-6 (0 is Sunday)
%W 一年中的第几周,01-53
%Y 年,YYYY
%% % symbol

实例

现在让我们使用 SQLite 提示符尝试不同的实例。下面是计算当前日期:

1
2
sqlite> SELECT date('now');
2013-05-07

下面是计算当前月份的最后一天:

1
2
sqlite> SELECT date('now','start of month','+1 month','-1 day');
2013-05-31

下面是计算给定 UNIX 时间戳 1092941466 的日期和时间:

1
2
sqlite> SELECT datetime(1092941466, 'unixepoch');
2004-08-19 18:51:06

下面是计算给定 UNIX 时间戳 1092941466 相对本地时区的日期和时间:

1
2
sqlite> SELECT datetime(1092941466, 'unixepoch', 'localtime');
2004-08-19 11:51:06

下面是计算当前的 UNIX 时间戳:

1
2
sqlite> SELECT strftime('%s','now');
1367926057

下面是计算美国”独立宣言”签署以来的天数:

1
2
sqlite> SELECT julianday('now') - julianday('1776-07-04');
86504.4775830326

下面是计算从 2004 年某一特定时刻以来的秒数:

1
2
sqlite> SELECT strftime('%s','now') - strftime('%s','2004-01-01 02:34:56');
295001572

下面是计算当年 10 月的第一个星期二的日期:

1
2
sqlite> SELECT date('now','start of year','+9 months','weekday 2');
2013-10-01

下面是计算从 UNIX 纪元算起的以秒为单位的时间(类似 strftime(‘%s’,’now’) ,不同的是这里有包括小数部分):

1
2
sqlite> SELECT (julianday('now') - 2440587.5)*86400.0;
1367926077.12598

在 UTC 与本地时间值之间进行转换,当格式化日期时,使用 utc 或 localtime 修饰符,如下所示:

1
2
3
4
5
sqlite> SELECT time('12:00', 'localtime');
05:00:00

sqlite> SELECT time('12:00', 'utc');
19:00:00

常用函数

QLite 有许多内置函数用于处理字符串或数字数据。下面列出了一些有用的 SQLite 内置函数,且所有函数都是大小写不敏感,这意味着您可以使用这些函数的小写形式或大写形式或混合形式。欲了解更多详情,请查看 SQLite 的官方文档:

序号 函数 & 描述
1 SQLite COUNT 函数
SQLite COUNT 聚集函数是用来计算一个数据库表中的行数。
2 SQLite MAX 函数
SQLite MAX 聚合函数允许我们选择某列的最大值。
3 SQLite MIN 函数
SQLite MIN 聚合函数允许我们选择某列的最小值。
4 SQLite AVG 函数
SQLite AVG 聚合函数计算某列的平均值。
5 SQLite SUM 函数
SQLite SUM 聚合函数允许为一个数值列计算总和。
6 SQLite RANDOM 函数
SQLite RANDOM 函数返回一个介于 -9223372036854775808 和 +9223372036854775807 之间的伪随机整数。
7 SQLite ABS 函数
SQLite ABS 函数返回数值参数的绝对值。
8 SQLite UPPER 函数
SQLite UPPER 函数把字符串转换为大写字母。
9 SQLite LOWER 函数
SQLite LOWER 函数把字符串转换为小写字母。
10 SQLite LENGTH 函数
SQLite LENGTH 函数返回字符串的长度。
11 SQLite sqlite_version 函数
SQLite sqlite_version 函数返回 SQLite 库的版本。

在我们开始讲解这些函数实例之前,先假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

SQLite COUNT 函数

SQLite COUNT 聚集函数是用来计算一个数据库表中的行数。下面是实例:

1
sqlite> SELECT count(*) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

count(*)
7

SQLite MAX 函数

SQLite MAX 聚合函数允许我们选择某列的最大值。下面是实例:

1
sqlite> SELECT max(salary) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

max(salary)
85000.0

SQLite MIN 函数

SQLite MIN 聚合函数允许我们选择某列的最小值。下面是实例:

1
sqlite> SELECT min(salary) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

min(salary)
10000.0

SQLite AVG 函数

SQLite AVG 聚合函数计算某列的平均值。下面是实例:

1
sqlite> SELECT avg(salary) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

avg(salary)
37142.8571428572

SQLite SUM 函数

SQLite SUM 聚合函数允许为一个数值列计算总和。下面是实例:

1
sqlite> SELECT sum(salary) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

sum(salary)
260000.0

SQLite RANDOM 函数

SQLite RANDOM 函数返回一个介于 -9223372036854775808 和 +9223372036854775807 之间的伪随机整数。下面是实例:

1
sqlite> SELECT random() AS Random;

上面的 SQLite SQL 语句将产生以下结果:

Random
5876796417670984050

SQLite ABS 函数

SQLite ABS 函数返回数值参数的绝对值。下面是实例:

1
sqlite> SELECT abs(5), abs(-15), abs(NULL), abs(0), abs("ABC");

上面的 SQLite SQL 语句将产生以下结果:

abs(5) abs(-15) abs(NULL) abs(0) abs(“ABC”)
5 15 < null > 0 0.0

SQLite UPPER 函数

SQLite UPPER 函数把字符串转换为大写字母。下面是实例:

1
sqlite> SELECT upper(name) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

upper(name)
PAUL
ALLEN
TEDDY
MARK
DAVID
KIM
JAMES

SQLite LOWER 函数

SQLite LOWER 函数把字符串转换为小写字母。下面是实例:

1
sqlite> SELECT lower(name) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

lower(name)
paul
allen
teddy
mark
david
kim
james

SQLite LENGTH 函数

SQLite LENGTH 函数返回字符串的长度。下面是实例:

1
sqlite> SELECT name, length(name) FROM COMPANY;

上面的 SQLite SQL 语句将产生以下结果:

NAME length(name)
Paul 4
Allen 5
Teddy 5
Mark 4
David 5
Kim 3
James 5

SQLite sqlite_version 函数

SQLite sqlite_version 函数返回 SQLite 库的版本。下面是实例:

1
sqlite> SELECT sqlite_version() AS 'SQLite Version';

上面的 SQLite SQL 语句将产生以下结果:

SQLite Version
3.6.20

Shell脚本

最近一段时间用的Cocoapods做项目组件化,意外的打开了以前不太注意的脚本大门,可以让工作更加方便自动化,现在记录如下:

xcode 中的build phrase 用来控制项目的编译过程,你按下CMD+B或者CMD+R,他就按照从上到下的执行一遍,我想要在编译前或者编译后执行一些操作,就可以在合适的位置添加脚本,shell或者Python等都可以。

xcode 内置了一些宏变量方便脚本,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BUILT_PRODUCTS_DIR:  build成功后的,最终产品路径--可以在Build Settings参数的Per-configuration Build Products Path项里设置.
TARGET_NAME: 目标工程名称
SRCROOT:工程文件(比如Nuno.xcodeproj)的路径
CURRENT_PROJECT_VERSION:当前工程版本号

其他
PRODUCT_NAME
SYMROOT
BUILD_DIR
BUILD_ROOT
CONFIGURATION_BUILD_DIR
CONFIGURATION_TEMP_DIR
SDK_NAME
CONFIGURATION
TARGET_NAME
EXECUTABLE_NAME
IPHONEOS_DEPLOYMENT_TARGET
ACTION
CURRENTCONFIG_SIMULATOR_DIR
CURRENTCONFIG_DEVICE_DIR

一个简单的shell脚本可以直接在当前路径下执行 ./xxxx.sh, 执行前要赋权 chmod +x xxxx.sh

如果要在全局要执行这个脚本,则要添加全局变量

全局变量添加在 ~/.bash_profile 下,其他好像不怎么用(就我目前所知😝),写下你的文件路径 export xxxx="/usr/local/bin/xxx"

这个文件可以是脚本文件,也可以是执行文件,然后执行 source ~/.bash_profile,就可以立即让这个路径生效,不然要重启,让系统读下这个文件。

echo $PATH 显示当前PATH环境变量

在Xcode中可以创建command line 工程,创建命令行工具,用C,Objective-c,C++都可以

在C语言中调用shell脚本,可用int system(const char *command)这个函数,也有其他的函数

静态库的拆分合并

由于公司的私有库其他部门不能访问,想要提供的话又不可能提供源码,一般以静态库的方式提供,所以这里记录下,pod打包静态的库相关信息

Cocoapods 支持打包成动态库或者静态库,但是需要插件cocoapods-packager

可通过 gem安装

sudo gem install cocoapods-packager

cocoapods-packager 打包只需要依靠podspec文件即可,所以本地有没有源码文件没有关系。

具体命令

pod package xxxx.podspec

这样顺利的话可以打包出一个framework动态库

下面是这个插件的相关参数:

--force: 如果以前打包过的话,就覆盖同名文件
--no-mangle:当这个pod有依赖其他pod时,插件会默认修改依赖的名称,以防止主工程也依赖了这个pod,但是版本不同的问题,添加这个参数就不会修改名称了。
--embedded: 嵌入?
--library: 静态库
--dynamic: 动态库
--bundle-identifier: 如果是framework的话可以,可以修改Bundle ID
--exclude-deps: 是否把以来的pod打包进来
--configuration: 打包配合,debug 或者 release, 默认release
--subspecs: 要打包的子pod
--spec-sources=默认肯定是从官方的repo仓库找代码仓库,如果是私有的话,就要在这里写明repo仓库地址了
--verbose: 打印出脚本执行过程

栗子:

pod package CDChatList.podspec --no-mangle --library --exclude-deps --verbose --spec-sources=https://chdo.github.git,https://chdo.github2.git

打包完成后,会在当前目录得到个表明pod版本的文件夹,里面包含了,编译好的.a 文件,和各个架构的.o文件等,你可以直接使用这个.a 文件了。

这个.o就是目标文件,.a就是把这些文件都集中到一起,你可以定制自己的.a文件,控制包含的架构,包含的依赖库等,相关命令记录如下:

查看.a包含架构

lipo -info xxxx.a

按架构分离静态库

lipo xxxx.a -thin armv7 -output xxxx.a

在当前目录拆分静态库为目标文件

ar -x xxx.a

将目标文件合并为静态库

libtool -static -o xxxx.a *.o

将不同架构静态库合并为通用静态库

lipo -create xxxxx.a xxx.a -output XXX.a

项目利用Cocoapods组件化

最近公司项目重构,准备利用Cocoapods组件化。
网上这篇文章已经说的非常好了,我也是参照这篇文章,不过也有一些小坑需要趟一下,所以整理如下。

Cocoapods是非常好的项目依赖管理工具,类似的还有Carthage,不过一番权衡,还是用CP了。

既然是公司项目肯定得建个私有库,将项目的不同部分合适的切分成不同的模块,有的模块还有父子依赖关系,再上传到私有库上,定期维护这些库就可以了,这样可以有效和划分人员和代码的职责,方便管理。

最终,项目被分成了三个库,分别是A(工具库)、B(数据库)、C(一个复杂视图封装好的组件库),BC依赖于A,BC之间互相独立。

下面是具体实现记录:

创建并设置一个私有的Spec Repo

上面的ABC就是我们的私有pods, 而Spec Repo就是私有的pods的集合仓库,地位等同于公共的Cocoapods仓库,等我们配置完成后,用命令pod search XXXX,就会依次搜索公共ocoapods仓库和我们的私有仓库。

Spec Repo本质是一个git仓库,仓库中保存着每一个私有pods的索引文件,索引文件记录了各个私有pods的关键信息,比如名称,代码地址,介绍之类的。

首先建一个普通的私有仓库,一般放在公司的服务器上,可以取名叫某某公司的pods就行,让后告诉cocoapod这是我们的私有仓库

1
pod repo add WTSpecs https://coding.net/wtlucky/WTSpecs.git

本地的/Users/xxx/.cocoapods/repo/文件夹下就会多出这个仓库了

创建Pod项目

pods集合仓库建好后,就开始逐个创建私有pods的仓库,因为所有的私有pod都依赖A仓库,所以要先搞A

具体命令是pod lib create A, Cocoapods会根据你的设置创建好模板工程,让后就可以在模板里写代码了,

pod的模板工程中包括以下的几个关键的文件或文件夹,
A/,存放代码和资源的地方,里面包括Class,Assets
A.podspec,这个就是私有pod的身份证了,里面记录了这个pod的所有信息,当我们把这个pod提交到pods集合仓库时,本质就是提交这个podspec文件过去,不然pods集合仓库的空间根本不够用。
Example/,这是在创建模板时,默认会建的文件夹,里面存放了依赖这个pod的demo工程,当让也可以用模板的,手动搞一个。

首先看看podspec的内容:

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
#
# Be sure to run `pod lib lint A.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#

Pod::Spec.new do |s|
s.name = 'A'
s.version = '0.1.0'
s.summary = 'A short description of A.'

# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!

s.description = <<-DESC
TODO: Add long description of the pod here.
DESC

s.homepage = 'https://github.com/chdo002/A'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'chdo002' => '1107661983@qq.com' }
s.source = { :git => 'https://github.com/chdo002/A.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

s.ios.deployment_target = '8.0'

s.source_files = 'A/Classes/**/*'

# s.resource_bundles = {
# 'A' => ['A/Assets/*.png']
# }

# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
end

拣几个关键的说下,

s.name 需要想好,最好有公司或者单位前缀,不然有大坑,而且不好改,后面细说
s.version指定了这个pod的版本,非常重要。
s.homepages.source,指定了仓库的地址,需要填写正确
s.source_files 指向我们的代码路径,这里统配了这个路径下的所有文件
s.resource_bundles 是说明我们的pod所包含的资源文件用的,在工程中,这些资源会被打包成bundle,放在这个pod名的framework下,所以不需要担心资源重名问题
s.frameworkss.dependency是pod的系统依赖和对其他pod的依赖,这里有点需要注意,如果依赖的其他pod是私有库的话,我们也不能在这里说明,需要在外部处理,见下文。

这些属性的验证可以通过命令pod lib lint检查,实际上这个命令也是检查项目代码能否顺利编译。

在哪写私有pod代码呢?

打开example下的项目worksapce文件,可以看到在podstarget下有个Development Pods文件夹,这个就是我们的私有pods存在的地方,删掉里面的ReplaceMe.m文件就可以了。

当写完一部分代码,想要看看是否奏效时,在example路径下执行pod install更新工程,这个和普通的操作没有区别。

一番编写调试完成,我们的私有pods没有问题的话,那就可以提交第一个版本了

向Spec Repo提交podspec

向Spec Repo提交podspec之前还是需要几步操作,

首先向pod远程仓库提交代码,用pod lib lint验证,可以加上--verbose查看具体的编译过程,这一步也是比较容易出错的地方,不过出错的原因都会详细说明。

编译通过的话,就可以commit,并在这个commit打上个tag,tag要与这此pod的版本一致。

比如我这次提交的pod版本是0.0.1,那我就要打个0.0.1的tag,git tag -m "A pod first Release" "0.0.1"

然后都push上去,git push origin --tags

最后就是pod repo push WTSpecs A.podspec , 这一步也是会执行pod lib lint的,你也可以添加--verbose和其他参数。

如果一切顺利,在本地路径/Users/xx/.cocoapods/repo/WTSpecs下可以发现我们提交的pod,这样你就可以在自己的工程引用这个私有的pod了

使用私有pod

Podfile中有两种方法可以引用,

方法一

1
2
3
4
5
6
7
8
use_frameworks!

target 'Test_Example' do
# 这里可以单独指定这个私有pod的源
pod 'A', :source => 'https://coding.net/wtlucky/podTestLibrary.git'

end

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use_frameworks!

# 在这里也可以全局指定pod源,这样在加载pod时,就会先从官方库中找,然后去我们的私有库中找,所以一定要把私有pod的名称和官方库中pod的名称区分开,最好先pod search确定下。

source 'https://github.com/CocoaPods/Specs.git' # 官方库
source 'https://coding.net/wtlucky/podTestLibrary.git' # 私有库

target 'Test_Example' do
# 这里可以单独指定这个私有pod的源
pod 'A'
pod 'FMDB'

end

pod的更新。

对podA的维护更新,也是比较简单的。

改完的代码提交一个commit,然后打上相应的tag,比如说0.0.2,让后都push上去,

pod repo push WTSpecs A.podspec 即可

其他

依赖问题

在做Bpod时,因为依赖了私有库A,所以要写s.dependency 'A',同时这个pod还依赖了其他官方库中的pod,比如说AFNetworking

pod lib lint或者pod repo push,就需要说明这个pod的源了,具体为什么不能再pods spec中指明各个依赖的源还不清楚,

在命令后面加上

1
--sources=https://github.com/CocoaPods/Specs.git,https://coding.net/wtlucky/podTestLibrary.git

就可以了,意思是要指明编译时的源

缓存

pod repo push 提交后一般要清一下pod缓存 pod cache clean --all, 然后调用 pod setup更新

常用命令

删除本地tag git tag -d 0.0.1
删除远程tag git push origin --delete tag 0.0.1