Unruly的博客

心存善念,必有善行;善念善行,天必佑之。


  • 首页

  • 归档

  • 标签

  • 分类

  • 关于

iOS 面试

发表于 2018-09-25

atomic nonatomic区别和理解

atomic的意思就是setter/getter这个函数,是一个原语操作。如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,可以保证数据的完整性。nonatomic不保证setter/getter的原语行,所以你可能会取到不完整的东西。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结

数据持久化

  • NSKeyedArchiver -> 用户信息
  • NSUserDefaults -> 状态、版本
  • Write􏰴􏱼􏰲􏰳􏲅 -> plist、txt等文件
  • 数据库 -> WCDB、FMDB等
  • CoreData -> 苹果自带的数据库

深、浅拷贝

  • 拷贝需实现 NSCopying协议
  • 浅拷贝: 􏱭􏱙􏰵􏰈􏱊􏱋􏳽􏳄􏱜􏱯􏳾􏴏􏰩􏴐􏱃􏱜􏱯􏱭􏱙􏰵􏰈􏱊􏱋􏳽􏳄􏱜􏱯􏳾􏴏􏰩􏴐􏱃􏱜􏱯􏱭􏱙􏰵􏰈􏱊􏱋􏳽􏳄􏱜􏱯􏳾􏴏􏰩􏴐􏱃􏱜􏱯􏱭􏱙􏰵􏰈􏱊􏱋􏳽􏳄􏱜􏱯􏳾􏴏􏰩􏴐􏱃􏱜􏱯􏱭􏱙􏰵􏰈􏱊􏱋􏳽􏳄􏱜􏱯􏳾􏴏􏰩􏴐􏱃􏱜􏱯copy指针 指向相同地址
  • 深拷贝: 新的对象 新的指针

单例

1
2
3
4
5
6
7
8
+ (AccountManager *)sharedManager
{
static AccountManager *staticInstance = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
staticInstance = [[self alloc] init];
return staticInstance;
}); }

RunLoop

RunLoop 是什么?RunLoop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

源码

一个RunLoop对象,主要包含了一个线程,若干个Mode,若干个commonMode,还有一个当前运行的Mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; //存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //存储的是CFRunLoopModeRef
struct _block_item *_blocks_head;//doblocks的时候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};

详见 RunLoop详解

应用场景

一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoop 的 default mode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode 。而这个时候, Timer 就不会运行

有如下两种解决方案:

  • 设置 RunLoop Mode,例如 NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个 Mode 的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
  • 在另外一个线程执行和处理 Timer 事件,然后在主线程更新 UI

内存管理机制

Autorelease 释放时机

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop(双向链表)

关键字

  • retain(􏰀􏰁􏰂􏰃􏰄引用计数+1)->release􏰅􏰀􏰁􏰂􏰃􏰆1(引用计数-1)
  • alloc(申请内存空间)-> dealloc(释放内存空间)
  • readwrite 既有getter,也有setter(默认)
  • readonly 只有getter,没有setter
  • nonatomic 不考虑线程安全
  • 原子性: atomic 线程操作安全(默认)nonatomic不使用自旋锁
    比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。
    而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。
    不过atomic可并不能保证线程安全。如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。
    保证数据完整性——这个多线程编程的最大挑战之一——往往还需要借助其他手段

线程安全下的 setter、getter

1
2
3
4
5
6
7
8
9
10
11
12
- (NSString *) value
{
@synchronized(self) {
return [[_value retain] autorelease];
} }
- (void)setValue:(NSString *)aValue
{
@synchronized(self) {
[aValue retain];
[_value release];
_value = aValue;
} }
  • retain: release旧的对象 retain新的对象
  • assign:简单赋值 不更改引用计数(默认)
  • copy:copy相同对象(retain指针拷贝,copy:内容拷贝)
  • strong: ARC中 同MRC retain一样
  • weak:同assign一样,weak当指向的内存释放后自动nil话,防止野指针 assign 可以用非 OC 对象,而 weak 必须用于 OC 对象(weak:a.delegate = b, a和b指向同一位置,当b释放 a也释放,如果delegate用assign就会变成野指针)
  • unsafe_unretained:weak修饰的对象被释放后,指向对象的指针会置空,也就是指向nil,不会产生野指针;而unsafe_unretained修饰的对象被释放后,指针不会置空,而是变成一个野指针,那么此时如果访问这个对象的话,程序就会Crash,抛出BAD_ACCESS的异常(它会比weak性能更好,明确对象声明周期时可使用,YYModel内大量使用)

序列化和反序列化

  • 序列化:对象->字节序列
  • 反序列化:字节-> 对象
  • NSCoding协议、Codable协议

多线程

进程、线程

  • 进程:操作系统多个软件在运行(微信、抖音等)
  • 线程:进程内好多线程
  • 线程是进程的基本组成单元

多线程的好处

  • 时间长的任务放到后台处理
  • 程序运行更快
  • 等待任务

    多线程的缺点

  • 大量影响性能、因为操作系统需要在他们之间切换
  • 占用更多的内存空间
  • 线程死锁

多线程实现

  • Thread
  • NSOperation
  • GCD

字符串去重

1
2
3
4
5
6
7
8
9
10
11
12
func testString(str: String) -> String{
var dict: [String: Int] = [:]
var result: String = ""
let _ = str.map { (char) in
let temp = "\(char)"
if dict[temp] == nil {
result += "\(char)"
dict[temp] = 10
}
}
return result
}

UIImage初始化

  • imageNamed 检查系统缓存
  • contentsOfFile: 直接加载
  • cgImage

便利构造器

1
2
3
4
5
extension UIButton {
convenience init(imageName: String) {
self.init()
}
}

继承

iOS 类不可以多继承 可protocol实现

APP启动流程

main函数 -> UIApplication -> rootVC

  • application:willFinishLaunchingWithOptions: - 这个方法是你在启动时的第一次机会来执行代码

  • application:didFinishLaunchingWithOptions: - 这个方法允许你在显示app给用户之前执行最后的初始化操作

  • applicationDidBecomeActive: - app已经切换到active状态后需要执行的操作

  • applicationWillResignActive: - app将要从前台切换到后台时需要执行的操作

  • applicationDidEnterBackground: - app已经进入后台后需要执行的操作

  • applicationWillEnterForeground: - app将要从后台切换到前台需要执行的操作,但app还不是active状态

  • applicationWillTerminate: - app将要结束时需要执行的操作

ViewController 生命周期

init loadView viewDidLoad viewWillAppear viewWillDisAppear viewDidDisappear didReceiveMemoryWarning deinit/dealloc

字典的工作原理

使用hash表来实现key-value之间的映射和存储的

哈希冲突

  • 1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
  • 2.再哈希法
  • 3.链地址法(Java hashmap就是这么做的)
  • 4.建立一个公共溢出区

什么是hash冲突?

假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10

简单计算一下:hash(5)=5, 所以数据5应该放在hash表的第5个槽里;hash(28)=1,所以数据28应该放在hash表的第1个槽里;hash(19)=1,也就是说,数据19也应该放在hash表的第1个槽里——于是就造成了碰撞(也称为冲突,collision)。

常用的Hash冲突解决方法有以下几种:

1.开放定址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:

Hi=(H(key)+di)% m i=1,2,…,n

其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:

线性探测再散列

dii=1,2,3,…,m-1

这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

二次探测再散列

di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )

这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

伪随机探测再散列

di=伪随机数序列。

具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。

如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。

如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。

如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。

2.再哈希法
这种方法是同时构造多个不同的哈希函数:

Hi=RH1(key) i=1,2,…,k

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

3.链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

4.建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

拉链法与开放地址法相比的缺点:
拉链法的优点

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

拉链法的缺点

 拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

反射机制

OC:字符串访问 类和方法
NSClassFromString NSSelectorFromString
Swift: Mirror Class

KVO 实现

热修复

Block

Block和函数指针的区别

函数指针是函数地址的引用 block是函数对象

视音频编解码技术

发表于 2018-08-27

视频播放器的原理:

图片

  • 解协议: 流媒体协议(HTTP,RTMP,或是MMS)的数据解析成标准的相应的封装格式数据

iOS 知识小集(4)

发表于 2018-07-30 | 分类于 知识小集

Color Literal
Image Literal

iOS 知识小集(2)

发表于 2018-07-30 | 分类于 知识小集
  • Write more code
  • Incorporate tests
  • Be honest
  • Contribute to open source
  • Be open to help
  • Pick a personal project
  • Lower your ego
  • Understand the “why”
  • Don’t be lazy
  • Solving coding challenges
  • Encourage the good stuff
  • Do not hide behind the layer

iOS 知识小集(3)

发表于 2018-07-29 | 分类于 知识小集

resizableImage: 该方法可以指定一个矩形区域,这个图片在矩形内的部分会拉伸变形,而这个矩形区域外的部分则会保持原样。该方法有如下两个参数:
(1)withCapInsets:通过设置 UIEdgeInsets 的 left、right、top、bottom 来分别指定这个矩形区域距离左侧、右侧、顶部、底部的距离。

(2)resizingMode:设置矩形区域的拉伸变形模式,具体有如下两种:

  • .stretch:拉伸模式,通过拉伸 UIEdgeInsets 指定的矩形区域来填充图片。
  • .tile:平铺模式,通过重复显示 UIEdgeInsets 指定的矩形区域来填充图片。
1
2
3
4
5
6
let imageView = UIImageView(frame: CGRect(x:10, y:100, width:300, height:66))
let image = UIImage(named: "bg")?
.resizableImage(withCapInsets: UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15),
resizingMode: .stretch) //左右15像素的部分不变,中间部分来拉伸
imageView.image = image
view.addSubview(imageView)

WCDB

发表于 2018-07-25

模型绑定

模型绑定(Object-relational Mapping,简称 ORM),通过对 Swift 类或结构进行绑定,形成类或结构 - 表模型、类或结构对象 - 表的映射关系,从而达到通过对象直接操作数据库的目的。

  • 字段映射
  • 字段约束
  • 索引
  • 表约束
  • 虚拟表映射

字段映射

在类内定义 CodingKeys 的枚举类,并遵循 String 和CodingTableKey。枚举列举每一个需要定义的字段。

1.别名映射 => case identifier = “id”

2.对于变量名与 SQLite 的保留关键字冲突 => case offset = “db_offset”

字段约束

它用于定于针对单个字段的约束,如主键约束、非空约束、唯一约束、默认值等。

1
2
3
4
5
6
7
8
9
ColumnConstraintBinding(
isPrimary: Bool = false, // 该字段是否为主键。字段约束中只能同时存在一个主键
orderBy term: OrderTerm? = nil, // 当该字段是主键时,存储顺序是升序还是降序
isAutoIncrement: Bool = false, // 当该字段是主键时,其是否支持自增。只有整型数据可以定义为自增。
onConflict conflict: Conflict? = nil, // 当该字段是主键时,若产生冲突,应如何处理
isNotNull: Bool = false, // 该字段是否可以为空
isUnique: Bool = false, // 该字段是否可以具有唯一性
defaultTo defaultValue: ColumnDef.DefaultType? = nil // 该字段在数据库内使用什么默认值
)

自增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let autoIncrementObject = Sample()
autoIncrementObject.isAutoIncrement = true

// 插入自增数据
try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
print(autoIncrementObject.lastInsertedRowID) // 输出 1

// 再次插入自增数据
try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
print(autoIncrementObject.lastInsertedRowID) // 输出 2

// 插入非自增的指定数据
let specificObject = Sample()
specificObject.identifier = 10
try database.insert(objects: specificObject, intoTable: "sampleTable")

索引

它用于定于针对单个或多个字段的索引,索引后的数据在能有更高的查询效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
var multiIndexPart1: Int = 0
var multiIndexPart2: Int = 0

enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
case multiIndexPart1
case multiIndexPart2

static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
return [
"_uniqueIndex": IndexBinding(isUnique: true, indexesBy: identifier),
"_descendingIndex": IndexBinding(indexesBy: description.asIndex(orderBy: .descending)),
"_multiIndex": IndexBinding(indexesBy: multiIndexPart1, multiIndexPart2.asIndex(orderBy: .ascending))
]
}
}
}

完整的索引名为表名+索引后缀,如:表 “sampleTable” 的索引分别为 “sampleTable_uniqueIndex”、”sampleTable_descendingIndex” 和 “sampleTable_multiIndex”。

表约束

它用于定于针对多个字段或表本身的约束。

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
class Sample: TableCodable {
var identifier: Int? = nil
var multiPrimaryKeyPart1: Int = 0
var multiPrimaryKeyPart2: Int = 0
var multiUniquePart1: Int = 0
var multiUniquePart2: Int = 0

enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case multiPrimaryKeyPart1
case multiPrimaryKeyPart2
case multiUniquePart1
case multiUniquePart2

static var tableConstraintBindings: [TableConstraintBinding.Name: TableConstraintBinding]? {
let multiPrimaryBinding =
MultiPrimaryBinding(indexesBy: multiPrimaryKeyPart1.asIndex(orderBy: .descending), multiPrimaryKeyPart2)
let multiUniqueBinding =
MultiUniqueBinding(indexesBy: multiUniquePart1, multiUniquePart2.asIndex(orderBy: .ascending))
return [
"MultiPrimaryConstraint": multiPrimaryBinding,
"MultiUniqueConstraint": multiUniqueBinding
]
}
}
}

1.MultiPrimaryBinding: 联合主键约束

2.MultiUniqueBinding: 联合唯一约束

3.CheckBinding: 检查约束

4.ForeignKeyBinding: 外键约束

虚拟表映射

它用于定于虚拟表以进行全文搜索等特性。

增删改查

插入操作

插入操作有 “insert” 和 “insertOrReplace” 两个接口。故名思义,前者只是单纯的插入数据,当数据出现冲突时会失败,而后者在主键一致时,新数据会覆盖旧数据。
propertyConvertibleList 插入指定字段

删除操作

1
2
3
4
5
6
func delete(fromTable table: String, // 表名
where condition: Condition? = nil, // 符合删除的条件
orderBy orderList: [OrderBy]? = nil, // 排序的方式
limit: Limit? = nil, // 删除的个数
offset: Offset? = nil // 从第几个开始删除
) throws

将 table 表内,满足 condition 的数据,按照 orderList 的方式进行排序,然后从头开始第 offset 行数据后的 limit 行数据删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 删除 sampleTable 中所有 identifier 大于 1 的行的数据
try database.delete(fromTable: "sampleTable",
where: Sample.Properties.identifier > 1)

// 删除 sampleTable 中 identifier 降序排列后的前 2 行数据
try database.delete(fromTable: "sampleTable",
orderBy: Sample.Properties.identifier.asOrder(by: .descending),
limit: 2)

// 删除 sampleTable 中 description 非空的数据,按 identifier 降序排列后的前 3 行的后 2 行数据
try database.delete(fromTable: "sampleTable",
where: Sample.Properties.description.isNotNull(),
orderBy: Sample.Properties.identifier.asOrder(by: .descending),
limit: 2,
offset: 3)

// 删除 sampleTable 中的所有数据
try database.delete(fromTable: "sampleTable")

更新操作

update with object
update with row

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let object = Sample()
object.description = "update"

// 将 sampleTable 中所有 identifier 大于 1 且 description 字段不为空 的行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: object,
where: Sample.Properites.identifier > 1 && Sample.Properties.description.isNotNull())

// 将 sampleTable 中前三行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: object,
limit: 3)
1
2
3
4
5
6
7
8
9
10
11
12
13
let row: [ColumnCodableBase] = ["update"]

// 将 sampleTable 中所有 identifier 大于 1 且 description 字段不为空 的行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: row,
where: Sample.Properites.identifier > 1 && Sample.Properties.description.isNotNull())

// 将 sampleTable 中前三行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: row,
limit: 3)

查找操作

  • getObjects
  • getObject
  • getRows
  • getRow
  • getColumn
  • getDistinctColumn
  • getValue
  • getDistinctValue

对象查找

1
2
3
4
5
6
7
8
9
10
// 返回 sampleTable 中的所有数据
let allObjects: [Sample] = try database.getObjects(fromTable: "sampleTable")

// 返回 sampleTable 中 identifier 小于 5 或 大于 10 的行的数据
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable",
where: Sample.Properties.identifier < 5 || Sample.Properties.identifier > 10)

// 返回 sampleTable 中 identifier 最大的行的数据
let object: Sample? = try database.getObject(fromTable: "sampleTable",
orderBy: Sample.Properties.identifier.asOrder(by: .descending))

注意: 由于对象查找操作使用了范型,因此需要显式声明返回值的类型以匹配范型

对象部分查询

1
2
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable", 
on: Sample.Properties.identifier)

注意:这里只获取了 identifier 字段,而没有获取 description 的值。这就可能与 Swift 本身存在冲突。 Swift 规定了对象创建时,必须初始化所有成员变量。而进行对象部分查询时,则可能出现某部分变量没有变查询,因此无法初始化的情况。因此,对于可能不被查询的成员变量,应将其类型定义为可选值。
倘若开发者不确定哪些字段可能会被进行对象部分查询,可以将所有字段都定义为可选。

值查询操作

identifier description
1 “sample1”
2 “sample2”
3 “sample3”
4 “sample4”
5 “sample5”
6 “sample6”
  • getRows 接口获取整个矩阵的所有内容,即返回值为二维数组。
  • getRow 接口获取某一横行的数据,即返回值为一维数组。
  • getColumn 接口获取某一纵列的数据,即返回值为一维数组。
  • getDistinctColumn 与 getColumn 类似,但它会过滤掉重复的值。
  • getValue 接口获取矩阵中某一个格的内容。
  • getDistinctValue 与 getValue 类似,但它会过滤掉重复的值。
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
// 获取所有内容
let allRows = try database.getRows(fromTable: "sampleTable")
print(allRows[row: 2, column: 0].int32Value) // 输出 3

// 获取第二行
let secondRow = try database.getRow(fromTable: "sampleTable", offset: 1)
print(secondRow[0].int32Value) // 输出 2

// 获取 description 列
let descriptionColumn = try database.getColumn(on: Sample.Properties.description, fromTable: "sampleTable")
print(descriptionColumn) // 输出 "sample1", "sample1", "sample1", "sample2", "sample2"

// 获取不重复的 description 列的值
let distinctDescriptionColumn = try database.getDistinctColumn(on: Sample.Properties.description, fromTable: "sampleTable")
print(distinctDescriptionColumn) // 输出 "sample1", "sample2"

// 获取第二行 description 列的值
let value = try database.getValue(on: Sample.Properties.description, offset: 1)
print(value.stringValue) // 输出 "sample1"

// 获取 identifier 的最大值
let maxIdentifier = try database.getValue(on: Sample.Properties.identifier.max(), fromTable: "sampleTable")

// 获取不重复的 description 的值
let distinctDescription = try database.getDistinctValue(on: Sample.Properties.description, fromTable: "sampleTable")
print(distinctDescription.stringValue) // 输出 "sample1"

数据库、表、事物

数据库 - Database、表 - Table 和 事务 - Transaction

  • 支持增删查改的便捷接口
  • 支持链式接口
  • 数据和状态共享
  • 线程安全

数据和状态共享,意味着对于 同一个路径的数据库 的 不同基础类,它们的标签、数据库是否打开、是否在进行读写操作等所有状态和数据都始终保持一致。

线程安全,意味着开发者可以在 任意线程 对 任意基础类 调用 任意接口,而不需要考虑数据库本身的线程安全问题。同时,WCDB Swift 会根据调用情况,并发执行操作,以达到更高的性能

文档

杭州游玩美食攻略

发表于 2018-07-25

自古就有“上有天堂,下有苏杭”的美誉。

鱼米之乡、丝绸之府、 文物之邦,

都是世人对杭州的唯美印象。

整理了杭州的旅游攻略,想去杭州旅游的小伙伴们看过来吧!

阅读全文 »

组件化实践

发表于 2018-07-25

组件化大体思路
持续集成

框架说明:

  • 持续集成: 主工程(壳工程),包含所有内容,用于发布打包等
  • 基础组件: 不依赖其他任何组件,独立完成功能。主要有:与业务无法的功能(如string或data的加密,category的封装)对第三方库的封装(如AFNetworking,SDWebImage的封装)
  • 业务公用组件: 依赖基础组件或UIKit等系统组件,创建业务共同使用的功能(如分享,支付,网络访问)
  • 中间组件: 连接业务公用组件和业务组件,及业务组件之间的互相调用。(如Mediator的组件)
  • 业务组件:单独的业务功能,不依赖其他业务组件。

##基础组件:
没有业务场景 不允许依赖业务组件

###公共基础
多项目可用

###非公共基础
ProjectA依赖,但是ProjectB不依赖

桥接层

  • 桥接层的主要作用是为上层业务层和下层基础框架层做桥接,主要考虑底层业务层的开放接口变动频繁以及不适配上层业务。
  • 想要达到每个组件之间相对低耦合,比较常用的方案就是断掉横向依赖,使用中间人模式将依赖下沉至中间件,而且组件对中间件是单向依赖

##业务组件
业务之间不允许相互依赖

隐性依赖库 -> Podfile.lock

##过程

开源组件->基础组件->平台中间件->业务组件->壳工程

副作用:因为多库开发在发布与集成时 很多依赖冲突lock文件冲突

自动发版与自动集成

壳工程分离

拾遗 - 多线程

发表于 2018-07-03 | 分类于 拾遗

对比

  • GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象
  • GCD只支持FIFO队列
  • NSOperationQueue可设置最大并发数、设置优先级、设置依赖关系等调整执行顺序
  • NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销

死锁

一定是发生在一个或多个线程之间的。那么死锁和线程阻塞的关系呢,可以这么理解,双向的阻塞导致了死锁

多个进程因循环等待资源而造成无法执行的现象

主队列同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override func viewDidLoad() {
super.viewDidLoad()

DispatchQueue.main.sync {
print("deadlock")
}
}
或者
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

dispatch_sync(serialQueue, ^{

NSLog(@"deadlock");
});
});

解决: 异步或者其它队列

GCD

执行顺序:串行队列先异步后同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

NSLog(@"1");

dispatch_async(serialQueue, ^{

NSLog(@"2");
});

NSLog(@"3");

dispatch_sync(serialQueue, ^{

NSLog(@"4");
});

NSLog(@"5");

打印顺序是13245

首先先打印1
接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5

所以最终顺序就是13245。
这里的任务4在主线程中执行,而任务2在子线程中执行。
如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行(可以添加多个任务看效果)

dispatch_barrier_sync(栅栏函数)

场景多读单写:可以多个读者同时读取数据,而在读的时候,不能取写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (id)readDataForKey:(NSString *)key
{
__block id result;

dispatch_sync(_concurrentQueue, ^{

result = [self valueForKey:key];
});

return result;
}

- (void)writeData:(id)data forKey:(NSString *)key
{
dispatch_barrier_async(_concurrentQueue, ^{

[self setValue:data forKey:key];
});
}

dispatch_group_async

场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

for (NSInteger i = 0; i < 10; i++) {

dispatch_group_async(group, concurrentQueue, ^{

sleep(1);

NSLog(@"%zd:网络请求",i);
});
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

NSLog(@"刷新页面");
});

Dispatch Semaphore(信号量)

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

dispatch_after(延时函数)

dispatch_once(单例)

面试题

执行顺序

NSThread+runloop实现常驻线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+ (NSThread *)shareThread {

static NSThread *shareThread = nil;

static dispatch_once_t oncePredicate;

dispatch_once(&oncePredicate, ^{

shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest2) object:nil];

[shareThread setName:@"threadTest"];

[shareThread start];
});

return shareThread;
}

+ (void)threadTest
{
@autoreleasepool {

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];
}
}

锁

自旋锁

是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

互斥锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

对比

自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
  缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

  • 自旋锁:atomic、OSSpinLock、dispatch_semaphore_t
  • 互斥锁:pthread_mutex、@synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock

atomic和nonatomic的对比

  1. atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。
  2. atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的,比如上面的例子。
    也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。
    atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。
  3. nonatomic:就没有这个保证了,nonatomic返回你的对象可能就不是完整的value。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。但仅仅使用atomic并不会使得对象线程安全,我们还要为对象线程添加lock来确保线程的安全。
  4. nonatomic的速度要比atomic的快。
  5. atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同

拾遗 - 内存

发表于 2018-07-02 | 分类于 拾遗

Autorelease Pool

@autoreleasepool结构

单个 autoreleasepool 的运行过程可以简单地理解为 objc_autoreleasePoolPush()、[对象 autorelease] 和 objc_autoreleasePoolPop(void *) 三个过程

1
2
3
4
5
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

组成

每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)

1
2
3
4
5
6
7
8
9
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};

双向链表

自动释放池中的 AutoreleasePoolPage 是以双向链表的形式连接起来的

小结

  • 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
  • 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
  • 调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
  • 每个线程有一个Runloop 每个Runloop有一个Autorelease Pool

参考资料

  • 自动释放池的前世今生 —- 深入解析 autoreleasepool
  • 黑幕背后的Autorelease

什么时候释放

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop (drain)

循环引用

  • delegate
  • Timer
  • Block
1234
Unruly

Unruly

40 日志
4 分类
71 标签
GitHub 微博
Links
  • kalman
© 2016 — 2019 Unruly
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4