SQLite小记

静以修身,俭以养德

一、移动端数据库方案

1、关系型数据库
  • SQLite:轻量级的关系数据库, 占用资源非常少,目前广泛应用于Android、iOS等手机操作系统。iOS使用时SQLite,只需要加入libSQLite3.0.tbd依赖以及引入SQLite3.h头文件即可。
  • Apple内建的CoreData底层的持久化方式可以是SQLite数据库,也可以是XML文件、甚至是内存; 比较流行的第三方框架FMDB是对SQLite操作的封装
2、非关系数据库
  • Realm:适用于iOS (同样适用于Swift&Objective-C)和Android的跨平台移动数据库,是NoSQL框架,官方定位是取代SQLite。具体可参考Realm(Java)那些事
  • Realm非常的特色是数据变更通知,查询,存储性能比SQLite好,但是体积大、存入Realm的对象必须继承RealmObject,侵入性强,Realm中存储对象不允跨线程访问

  • 非关系型数据库还有LevelDB、RocksDB

3、其他
  • 16年左右,Realm兴起,部分客户端团队开始使用Realm,但是更多的团队还是继续使用SQLite及其衍生方案;使用Realm并非就一定Cool,使用SQLite并非就一定Out,方案的选择应该是基于业务本身和团队的技术积累。
  • 在16年时候,微信分享了自己对优化SQLite的源码,具体可见微信iOS SQLite源码优化实践,随后推出了自己的数据库方案WCDB(基于SQLite)

二、SQLite的线程模式

1、三种线程模式
  • 单线程模式(Single-thread):所有互斥锁都被禁用,SQLite连接不能在多个线程中使用(多线程使用不安全)。
  • 多线程模式(Multi-thread):在多线程中使用单个数据库连接是不安全的,否则就是安全的 (不能在多个线程中共享数据库连接)
  • 串行模式(Serialized),是线程安全的(即使在多个线程中不加互斥的使用同一个数据库连接)。

说明:线程模式可以在编译时(通过源码编译SQLite库时)、启动时(使用SQLite的应用程序初始化时)或者运行时(创建数据库连接时)来指定。一般而言,运行时指定的模式将覆盖启动时的指定模式,启动时指定的模式将覆盖编译时指定的模式。但是,单线程模式一旦被指定,将无法被覆盖。默认的线程模式是串行模式。

2、编译时选择线程模式
  • 通过定义SQLite_THREADSAFE宏来指定线程模式。如果没有指定,默认为串行模式。
1
2
3
//0:单线程模式;
//1:串行模式;
//2:多线程模式
  • SQLite3_threadsafe()返回值可以确定编译时指定的线程模式。如果指定了单线程模式,函数返回false。如果指定了串行或者多线程模式,函数返回true。
  • 由于SQLite3_threadsafe()函数要早于多线程模式以及启动时和运行时的模式选择,所以它既不能区别多线程模式和串行模式,也不能区别启动时和运行时的模式。
1
2
3
4
5
//FMDB 中代码
+ (BOOL)isSQLiteThreadSafe {
// make sure to read the SQLite headers on this guy!
return SQLite3_threadsafe() != 0;
}
  • 如果编译时指定了单线程模式,那么临界互斥逻辑在构造时就被省略,因此也就无法在启动时或运行时指定串行模式或多线程模式。
3、启动时选择线程模式
  • 假如在编译时没有指定单线程模式,就可以在应用程序初始化时使用SQLite3_config()函数修改线程模式。

    1
    2
    3
    SQLite_CONFIG_SINGLETHREAD  //单线程模式
    SQLite_CONFIG_MULTITHREAD //多线程模式
    SQLite_CONFIG_SERIALIZED //串行模式
4、运行时选择线程模式
  • 如果没有在编译时 和 启动时指定为单线程模式,那么每个数据库连接在创建时,可单独的被指定为多线程模式或者串行模式,但是不能指定为单线程模式
  • 如果在编译时或启动时指定为单线程模式,就无法在创建连接时指定多线程或者串行模式。
  • 创建连接时可以用SQLite3_open_v2()函数的第三个参数来指定线程模式。
1
2
SQLite_OPEN_NOMUTEX    //创建多线程模式的连接(没有指定单线程模式的情况下)
SQLite_OPEN_FULLMUTEX //创建串行模式的连接
5、模式的选择和处理

要保证数据库使用安全,一般可以采用如下几种模式

  • SQLite 采用单线程模型,用专门的线程(同时只能有一个任务执行访问) 进行访问
  • SQLite 采用多线程模型每个线程都使用各自的数据库连接 (即 SQLite3 *
  • SQLite 采用串行模型,所有线程都公用同一个数据库连接。
6、SQLite使用建议

​ 写操作的并发性并不好,当多线程进行访问时实际上仍旧需要互相等待,而读操作所需要的 SHARED 锁是可以共享的,所以为了保证最高的并发性,推荐

  • 使用多线程模式
  • 使用 WAL 模式
  • 单线程写,多线程读 (各线程都持有自己对应的数据库连接)
  • 避免长时间事务
  • 缓存 SQLite3_prepare 编译结果
  • 多语句通过 BEGINCOMMIT 做显示事务,减少多次的自动事务消耗

三、SQLite基础操作

1、基础概念
  • :是数据库中一个非常重要的对象,是其他对象的基础。根据信息的分类情况,一个数据库中可能包含若干个数据表
  • 字段:表的“列”称为“字段”,每个字段包含某一专题的信息
  • 记录:是指对应于数据表中一行信息的一组完整的相关信息
  • iOS使用SQLite,需要引入libSQLite3.0.tbd框架,并引入头文件
2、关键API-打开数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//打开数据库连接 定义
SQLite_API int SQLite3_open(
const char *filename, /* Database filename (UTF-8) */
SQLite3 **ppDb /* OUT: SQLite db handle */
);

//使用数据库连接
//db是SQLite3对象,SQLite3 *db = nil;
SQLite3_open([sqlPath UTF8String], &db);

//打开
int SQLite3_open_v2(
const char *filename, /* Database filename (UTF-8) */
SQLite3 **ppDb, /* OUT: SQLite db handle */
int flags, /* Flags */
const char *zVfs /* Name of VFS module to use */
);
  • 参数1:数据库的路径(因为需要的是C语言的字符串,而不是NSString所以必须进行转换)

  • 参数2:SQLite的数据库的操作句柄(指向指针的指针)

3、关键API - 执行sql语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//执行sql语句 定义
SQLite_API int SQLite3_exec(
SQLite3*, /* An open database */
const char *sql, /* SQL to be evaluated */
int (*callback)(void*,int,char**,char**), /* Callback function */
void *, /* 1st argument to callback */
char **errmsg /* Error msg written here */
);

//使用
int result = SQLite3_exec(db, sql.UTF8String, nil, nil, nil);
if (result == SQLite_OK) {
//exec ok
} else {
//exec failed
}
  • 参数1:SQLite3对象
  • 参数2:sql语句
  • 参数3:sql执行后回调函数
  • 参数4:回调函数的参数
  • 参数5:错误信息
4、关键API - 执行查询语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//将sql文本转换成一个准备语句(prepared statement)对象,同时返回这个对象的指针,它实际上并不执行(evaluate)这个SQL语句,它仅仅为执行准备这个sql语句。
SQLite_API int SQLite3_prepare_v2(
SQLite3 *db, /* Database handle */
const char *zSql, /* SQL statement, UTF-8 encoded */
int nByte, /* Maximum length of zSql in bytes. */
SQLite3_stmt **ppStmt, /* OUT: Statement handle */
const char **pzTail /* OUT: Pointer to unused portion of zSql */
);

//使用
result = SQLite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
if (result == SQLite_OK) {
//exec ok
} else {
//exec failed
}
5、关键API - 关闭数据库
1
2
3
4
5
//关闭数据库 定义
SQLite_API int SQLite3_close(SQLite3*);

//使用
SQLite3_close(db);

说明:具体API参考C-language Interface Specification for SQLite,FMDB中对SQLite3的操作做了很好的封装,具体可参考FMDB的FMDatabase文件

四、FMDB

FMDB是iOS平台的SQLite数据库框架,iOS项目中使用十分广泛。

1、源码组成
  • FMDatabase : 对SQLite3的封装,可以看做是SQLite3数据库操作实例,通过它可以对SQLite3进行增删改查等等操作。
  • FMResultSet : FMDatabase执行查询之后的结果集。
  • FMDatabaseAdditions : FMDatabase的Extension,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等等功能。
  • FMDatabaseQueue : 使用GCD串行队列保证线程安全,所有的线程共用一个SQLite Handle(单句柄),在多线程并发时,能够使各个线程的数据库操作按顺序同步进行,但正是因为各线程同步进行,导致后来的线程会被阻塞较长时间,无论是读操作还是写操作,都必须等待前面的线程执行完毕,使得性能无法得到更好的保障
  • FMDatabasePool : 使用任务池的形式,对多线程的操作提供支持。(不过官方对这种方式并不推荐使用,优先选择FMDatabaseQueue的方式)

说明:在FMDB中,SQLite运行在多线程模式,一个数据库连接在同一个时间只能在一个线程操作 ,应该是在编译时候确定的,当然也可以在打开数据库连接时候,指定线程模式是 多线程或串行。

2、数据库创建和打开
  • FMDatabase通过一个 SQLite 数据库文件路径创建的,此路径可以是:

    1
    2
    3
    一个文件的系统路径。磁盘中可以不存在此文件,因为如果不存在会自动为你创建。
    一个空的字符串 `@""`。会在临时位置创建一个空的数据库,当 `FMDatabase` 连接关闭时,该数据库会被删除。
    NULL`。会在内存中创建一个数据库,当 `FMDatabase` 连接关闭时,该数据库会被销毁。
  • FMDatabase必须执行open,在这里才能正在创建并打开SQLite3对象。

    1
    2
    3
    4
    5
    6
    FMDatabase *db = [FMDatabase databaseWithPath:dbpath];
    [db open];
    //...

    //关闭
    [db close];
3、数据库查询
1
2
3
4
5
6
//数据库查询
FMResultSet *rs = [db executeQuery:@"select * from people"];
//利用next函数
while ([rs next]) {
NSLog(@"%@ %@",[rs stringForColumn:@"name"],[rs stringForColumn:@"age"]);
}
  • FMResultSet通过调用 -executeQuery... 方法之一执行 SELECT 语句返回数据库查询结果FMResultSet 对象,然后就可以遍历查询结果了。
4、数据库更新
  • SQL 语句中除过 SELECT 语句都可以称之为更新操作。包括 CREATEUPDATEINSERTALTERCOMMITBEGINDETACHDROPENDEXPLAINVACUUMREPLACE 等。

  • 执行更新语句后会返回一个 BOOL 值,返回 YES 表示执行更新语句成功,返回 NO 表示出现错误,可以通过调用 -lastErrorMessage-lastErrorCode 方法获取更多错误信息。

    1
    2
    3
    4
    5
    //创建表
    [db executeUpdate:@"CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER DEFAULT 1)"];

    //插入操作
    [db executeUpdate:@"INSERT INTO people(name,age) VALUES (?,?)", @"LiLei",[NSNumber numberWithInteger:28]]
5、多线程数据库访问
  • FMDatabase 本身不是线程安全的,不要实例化一个 FMDatabase 单例来跨线程使用,但是可以通过FMDatabaseQueue保证跨线程操作是同步的,是线程安全的。
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
FMDatabaseQueue *databaseQueue = [FMDatabaseQueue databaseQueueWithPath:dbpath];
[databaseQueue inDatabase:^(FMDatabase *db) {
//
[db executeUpdate:@"CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER DEFAULT 1)"];
}];

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{
[databaseQueue inDatabase:^(FMDatabase *db) {
BOOL isSuccess = [db executeUpdate:@"INSERT INTO people(name,age) VALUES (?,?)", @"LiLei",[NSNumber numberWithInteger:28]];
if (isSuccess) {
NSLog(@"插入成功1");
}
}];
});

dispatch_async(queue, ^{
[databaseQueue inDatabase:^(FMDatabase *db) {
BOOL isSuccess = [db executeUpdate:@"INSERT INTO people(name,age) VALUES (?,?)", @"LiLei",[NSNumber numberWithInteger:28]];
if (isSuccess) {
NSLog(@"插入成功2");
}
}];
});
  • FMDatabaseQueue 将块代码 block 运行在一个串行队列上,即使在多线程同时调用 FMDatabaseQueue 的方法,它们仍然还是顺序执行。这种查询和更新方式不会影响其它,是线程安全的。

五、其他

1、GYDataCenter
2、WCDB
3、Realm

ReactiveCocoa-信号基础

一、背景

ReactiveCocoa火了一段时间(大概15,16年),在谈及MVVM的双向绑定时候,必然提及ReactiveCocoa,但是这几年,ReactiveCocoa似乎不怎么火了,之前没有用过,最近在一个项目中遇到,简单了解下。

1、概述
  • ReactiveCocoa(简称为RAC),是一个开源的,可应用于iOS开发的函数式 响应式编程框架,它提供了一系列用来组合和转换值流的 API

  • 函数响应式编程(Functional reactive programming,简称FRP

  • RAC 5.0 对自身项目结构的也进行了大幅度的调整。这个调整就是将 RAC 拆分为四个库:ReactiveCocoa、ReactiveSwift、ReactiveObjC、ReactiveObjCBridge,Swift版本是ReactiveSwift,OC版本是ReactiveObjc。

2、编程思想
  • 面向过程:处理事情以过程为核心,一步一步的实现。

  • 面向对象: 万物皆对象

  • 链式编程:是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好,代表作 : masonry

  • 响应式编程:不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆流。代表KVO

  • 函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用

说明ReactiveCocoa结合函数式编程(Functional Programming)和 响应式编程(Reactive Programming)编程思想,简记为 函数响应式编程(Functional reactive programming,简称FRP) ; 所以使用RAC时候,不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。

3、竞品
  • RxSwift 是 Rx的 Swift 版本,RxReactiveX(Reactive Extensions)的简写,它致力于提供一致的编程接口,帮助开发者更方便的处理异步数据流,Rx库支持.NET、JavaScript、C++、Swift, 但是没有支持Objective。

  • EasyReact 美团18年开源的响应式编程框架,从官方纰漏信息说,比RAC性能更好 (ReactiveCocoa vs RxSwift)

二、信号RACSignal

RACSignal是RAC的核心,RACSignal是RACStream子类。本质是是数据流,可以用来传递和绑定

1、基础操作
  • 创建信号(createSignal):创建了一个 RACDynamicSignal 类型的信号,并将传入的代码块保存起来,留待以后调用。

  • 订阅信号(subscribeNext):创建了一个 RACPassthroughSubscriber 类型的订阅者,并将传入的代码块保存起来,留待以后调用,同时调用了第一步创建信号中保存的代码块,并传入创建的订阅者。

  • 发送信号(sendNext):执行订阅信号时对应的block

  • 取消订阅(disposable): 把订阅信号获得的disposable进行dispose,即可在调度器调度该部分代码之前禁止调用。

2、Code
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
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber ) {

//3.发送信号
[subscriber sendNext:@"123"];
//3.发送完成并取消订阅
[subscriber sendCompleted];

//4、用于取消订阅时清理资源用,比如释放资源
return [RACDisposable disposableWithBlock:^{
NSLog(@"信号被取消订阅了");
}];
}];

// 2.订阅信号
[signal subscribeNext:^(id _Nullable x) {

NSLog(@"xxxx-%@",x);

}];

// 2.订阅信号
[signal subscribeNext:^(id x) {
NSLog(@"yyyy-%@",x);
}];


// xxxx-123
// 信号被取消订阅了
// yyyy-123
// 信号被取消订阅了

三、信号操作源码解读

img

1、源码-创建信号
  • createSignal创建信号,实际上是创建 RACDynamicSignal 类型的信号,并把定义的block存储到didSubscribe

create.png

2、源码-订阅信号

subscribeNext (订阅信号)分为两步:第一步创建一个订阅者subscriber,存入nextBlock, 第二步该subscriber订阅信号

subscribe.png

  • 第一步代码如下

subscribe-next.png

  • 第二步:有三个关键点,一个是创建一个RACCompoundDisposable对象,这里存储的是取消订阅后需要做的清除工作;另一个是 创建RACPassthroughSubscriber对象,将subscriber、信号和disposable处理存储下来,完成订阅者转换( 可以理解 RACPassthroughSubscriber 是订阅者的装饰器)。最后一个是使用在RACScheduler.subscriptionScheduler schedule: 来执行didSubscribe(这是创建信号穿过来的block), 最后返回RACCompoundDisposable对象

subscribe-next2.png

3、源码-发送信号
  • 在发送信号里,执行didSubscribe时候传入的是RACPassthroughSubscriber对象,在执行sendNext: 时候,先执行RACPassthroughSubscriber对象的sendNext: 方法

  • 在这里:先检查disposable状态,如果已经disposed直接返回;没有disposed才调用内部的innerSubscriber的sendNext:方法(RACSubscriber对象方法)

send.png

send2.png

说明

  • 发送信号就是执行相应block,此处执行的就是第二步中保存的相应的block

  • 对于 sendError 和 sendCompleted 都是先取消订阅,再执行相应的代码块,而 sendNext 并未取消订阅,所以,一般sendNext: 和 sendCompleted组合出现。

4、源码-取消订阅
  • 在 执行sendCompleted后,RACPassthroughSubscriber对象执行dispose方法,完成整个过程

  • 如果不调用sendCompleted,当订阅者被销毁的时候,RACPassthroughSubscriber对象也会执行dispose方法。

四、RACSubscriber & RACDisposable

1、RACSubscriber
  • 订阅者协议,主要遵守这个协议,并且实现方法才能成为订阅者。设计者特意设计同名的RACSubscriber类,实现RACSubscriber协议

  • RACSubscriber协议定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@protocol RACSubscriber <NSObject>
@required
/// Sends the next value to subscribers.
- (void)sendNext:(nullable id)value;

/// Sends the error to subscribers.
- (void)sendError:(nullable NSError *)error;

/// Sends completed to subscribers.
- (void)sendCompleted;

/// Sends the subscriber a disposable that represents one of its subscriptions.
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end
  • RACSubscriber类定义如下
1
2
3
4
5
6
7
// A simple block-based subscriber.
@interface RACSubscriber : NSObject <RACSubscriber>

// Creates a new subscriber with the given blocks.
+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed;

@end
  • RACPassthroughSubscriber也是实现了RACSubscriber协议,而不是继承RACSubscriber类

RACPassthroughSubscriber.png

2、RACDisposable及其子类
  • 用于取消订阅或者清理资源,当信号发送完成 或者 发送错误的时候,就会自动触发它。

  • 使用场景:不想监听某个信号时,可以通过它主动取消订阅信号。

  • 子类:RACCompoundDisposable、RACScopedDisposable、RACKVOTrampoline、RACSerialDisposable等。

  • RACCompoundDisposable可以存放多个RACDisposable 。当RACCompoundDisposable 执行dispose方法时,它所存放的disposable都会被释放。

五、信号组合

1、concat

组合信号,让信号按照顺序去执行。

模拟发送两个请求,第一个请求返回数据后执行第二个请求

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
//创建一个信号A
RACSignal *signalA = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"发送请求1...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"请求1返回的数据"];
[subscriber sendCompleted];
});
return [RACDisposable disposableWithBlock:^{
NSLog(@"取消订阅1...");
}];
}];

//创建一个信号B
RACSignal *siganlB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

NSLog(@"发送请求2...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"请求2返回的数据"];
[subscriber sendCompleted];
});

return [RACDisposable disposableWithBlock:^{
NSLog(@"取消订阅2...");
}];
}];

//串联信号A和B
RACSignal *concatSignal = [signalA concat:siganlB];
[concatSignal subscribeNext:^(id _Nullable x) {
NSLog(@"value = %@",x);
}];

output:
//发送请求1...
//value = 请求1返回的数据
//取消订阅1...
//发送请求2...
//value = 请求2返回的数据
//取消订阅2...

说明:使用concat 连接信号时,第一个信号发送后,一定要执行sendCompleted方法,否则不会发送第二个信号。

2、concat源码实现

concat.png

  • 当调用concat时,会创建一个拼接信号(RACDynamicSignal对象)

  • 当拼接信号被订阅,就会执行拼接信号的didSubscribe

  • didSubscribe中会先订阅第一个信号(signalA),此时会执行第一个信号(signalA)的didSubscribe。

  • 第一个信号(signalA)didSubscribe中,发送值,就调用第一个信号(signalA)订阅者的nextBlock,通过拼接信号的订阅者把值发送出来。

  • 第一个信号(signalA)didSubscribe中发送完成,就会调用第一个源信号(signalA)订阅者的completedBlock,订阅第二个源信号(signalB)这时候才激活(signalB)。

  • 订阅第二个信号(signalB),执行第二个源信号(signalB)的didSubscribe

  • 第二个信号(signalB)didSubscribe中发送值,就会通过拼接信号的订阅者把值发送出来.

3、then

用于连接连个信号,当第一个信号完成,才会连接then返回的信号,then底层也是使用了concat实现。

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
//创建信号A
RACSignal*signalA = [RACSignal createSignal:^RACDisposable*(id subscriber) {
NSLog(@"send data A");
//发送信号
[subscriber sendNext:@"dataA"];
[subscriber sendCompleted];
return nil;
}];

//创建信号B
RACSignal *signalB = [RACSignal createSignal:^RACDisposable*(id subscriber) {
//发送请求
NSLog(@"send data B");
//发送信号
[subscriber sendNext:@"dataB"];
[subscriber sendCompleted];
return nil;
}];
//创建组合信号
//then会忽略点第一个信号的所有值
RACSignal *signalThen = [signalA then:^RACSignal*{
//返回的信号就是需要组合的信号,这里回将signalA信号忽略掉
return signalB;
}];

//订阅信号
[signalThen subscribeNext:^(id x) {
NSLog(@"%@",x);
}];

//send data A
//send data B
//dataB
4、then源码实现

then.png

  • 先过滤掉之前的信号发出的值。

  • 使用concat连接then返回的信号

5、其他信号组合
  • merge:把两个信号合并为一个信号,任何一个信号有新值的时候就会调用。

  • zipWith:把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发事件。当一个界面有多个网络请求时,要等所有请求完成才更新UI,这时我们就可以用zipWith

  • combineLatest:将多个信号合并起来,拿到各个信号的最新的值,每个合并的signal至少都有过一次sendNext,才会触发合并的信号,底层也是用 zipWith实现的。 一般拿来跟 reduce 一起使用。

六、应用

老说源码比较枯燥,说下信号是怎么让开发变得更有意思。

1、监听Control事件
1
2
3
4
5
@weakify(self);
[[self.btn1 rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *btn) {
@strongify(self);
NSLog(@"点击事件了");
}];
2、监听 textField 的 text 改变
1
2
3
4
self.textField.frame = CGRectMake(15, 200, 300, 40);
[[self.textField rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
NSLog(@"textField input = [%@]",x);
}];
3、把 label 的属性 text 绑定在 UITextField 上
1
2
//self.label1.text 随着 self.textField.text 的改变而改变
RAC(self.label1,text) = self.textField.rac_textSignal;
4、告别繁琐的KVO
  • 不需要手动移除了
1
2
3
[RACObserve(self.person, name) subscribeNext:^(id  _Nullable x) {
NSLog(@"name = %@",x);
}];
5、监听通知
  • 不需要手动移除了
1
2
3
4
//监听通知
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillChangeFrameNotification object:nil] subscribeNext:^(NSNotification * _Nullable notification) {
NSLog(@"-----%@", notification.description);
}];
6、数组 & 字典遍历
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
//数组遍历
NSLog(@"array filter:");
NSArray *array = @[@(1), @(2), @(3), @(4), @(5)];
[array.rac_sequence.signal subscribeNext:^(id _Nullable x) {
NSLog(@"x = %@",x);
}];


NSLog(@"after array filter:");
//过滤 filter,并获取过滤后的数组
NSArray *filter = [[array.rac_sequence filter:^BOOL(id _Nullable value) {
return [value integerValue] > 2;
}] array];

//
[filter.rac_sequence.signal subscribeNext:^(id _Nullable x) {
NSLog(@"y = %@",x);
}];


//匹配、映射 map,变换元素并获取新数组
NSArray *map = [[array.rac_sequence map:^id _Nullable(id _Nullable value) {
NSInteger a = [value integerValue] * [value integerValue];
return [NSNumber numberWithInt:a];
}] array];

[map.rac_sequence.signal subscribeNext:^(id _Nullable x) {
NSLog(@"z = %@",x);
}];


//字典
NSDictionary *dic = @{@"name": @"lion", @"age": @18};
[dic.rac_sequence.signal subscribeNext:^(id _Nullable x) {

RACTupleUnpack(NSString *key, NSString *value) = x;
NSLog(@"\r\nkey: %@\r\nvalue: %@", key, value);


}];​
7、按钮是否点击
1
2
3
4
// 先联合两个信号,再解析信号结果,最后把结果绑定到信号上
RAC(self.btn1, enabled) = [RACSignal combineLatest:@[self.textField.rac_textSignal] reduce:^id _Nonnull(NSString *username){
return @(username.length > 0);
}];
8、节流throttle
1
2
3
4
5
//节流(0.5 秒内 text 没有改变时,才会进行搜索请求)
[[[self.textField rac_textSignal] throttle:0.5] subscribeNext:^(NSString * _Nullable x) {
//发送请求
NSLog(@"开始搜索请求==%@", x);
}]

End

1、总结
  • ReactiveCocoa提供了一个单一的、统一的方法去处理异步的行为,包括delegate方法,blocks回调,target-action机制,notifications和KVO.
  • 冷信号和热信号:冷信号是被动的,只会在被订阅时向订阅者发送通知;热信号是主动的,它会在任意时间发出通知,与订阅者的订阅时间无关;也就是说冷信号所有的订阅者会在订阅时收到完全相同的序列;而订阅热信号之后,只会收到在订阅之后发出的序列。
2、参考

iOS Reactivecocoa(RAC)知其所以然

iOS ReactiveCocoa详解

ReactiveCocoa信号发送详解

RAC 之引起你的兴趣

ReactiveCocoa信号发送详解

ReactiveCocoa代码分析之UITextField


图片解码小记

KeyPoints

  • 图片如何显示到屏幕上的

  • 为什么要图片解码

  • 图片解码方案及其对比

一、图像知识

1、图片
  • 计算机能以矢量图(vector)或位图(bitmap)格式显示图像,其中矢量图 使用线段和曲线描述图像,同时图像还包含了色彩位置信息;而位图 使用像素点来描述图像,也称为点阵图像,位图图片格式有RGB、CMYK等颜色模式;其中RGB是最常用的颜色模式,它通过红(R)、绿(G)、蓝(B)三个颜色通道的数值表示颜色。手机显示屏使用自带Aphal通道(RGBA)的RGB32格式。

  • 我们平时接触到的JPG或PNG图片格式,他们是压缩的位图图形格式,其中 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。JPG或PNG图片显示到屏幕之前,需要将JPG/PNG格式的图片解码位图图像。

2、16位色、24位色、32位色、真彩色
  • 16位色表示描绘图像时有2^16种颜色可供选择, 颜色总数65536。

  • 24位色表示描绘图像时有2^24种颜色可供选择;颜色总数是16777216。通常也被简称为1600万色或千万色;24位色被称为真彩色,它可以达到人眼分辨的极限

  • 32位色在1677万多色基础上,不过它增加了256阶颜色的灰度,为了方便称呼,就规定它为32位色。

  • 真彩色指用三个或更多字节描述颜色,24位色、32位色都是真彩色。

3、动图GIF
  • GIF格式的图像只有256种颜色用以描绘图片,并且只能通过抖动、差值等方式模拟较多丰富的颜色。

  • GIF的alpha通道只有1bit,一个像素要么完全透明,要么完全不透明,而不像现在PNG的RGBA的8bit alpha通道,alpha值也可以和RGB一样都有255个透明值。

  • 所有GIF的图片带上透明度以后,边缘会出现明显的锯齿。如果客户端需要展示带透明度的动图,不考虑GIF。

二、iOS背景知识

1、图像显示到屏幕

img

图像显示到屏幕上,是CPU和GPU协作完成渲染的。具体工作如下:

  • CPU: 计算视图frame,图片解码,需要绘制纹理图片通过数据总线交给GPU

  • GPU: 纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区。

  • 时钟信号:垂直同步信号V-Sync / 水平同步信号H-Sync。

  • iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制

2、图片的显示流程
  • 假设本地从本地加载一张图片,将值赋给UIImageView

  • 一次Runloop结束后,CATransaction遍历所有layer的contents(寄宿图),发现UIImageView layer的contents的变化(CGImage类型);

  • 发现图片不是位图,解码成位图;

  • 如果图像数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐;

  • Core Animation提交渲染树CA::render::commit,将渲染任务和数据交给Render server线程去处理;

  • Render server调用Open GL、Core Graphics相关程序,最终由GPU完成图像渲染并显示到屏幕。

说明:图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。

3、为什么要解压图片
  • JPEG 和 PNG 图片是位图的压缩格式

  • 本质上,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点

  • 将磁盘中/网络上获取的 图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作。

参考https://www.jianshu.com/p/4008ec3cacaa

三、Image I/O: iOS图片解码方案

1、Image I/O是什么
  • Image/IO是Apple提供的一套用于图片编码解码的系统库,详细参考 Apple Image/IO

  • Image/IO的解码,支持了常见的图像格式,包括PNG(包括APNG)、JPEG、GIF、BMP、TIFF(具体的,可以通过CGImageSourceCopyTypeIdentifiers来打印出来,不同平台不完全一致)。在iOS 11之后另外支持了HEIC(即使用了HEVC编码的HEIF格式);

  • Image/IO支持的解码和编码格式可通过以下方法查询

1
2
3
4
5
6
7
8
9
- (void)printImageSupportTypes {
CFArrayRef mySourceTypes = CGImageSourceCopyTypeIdentifiers();
//支持解码的图片格式
CFShow(mySourceTypes);

//支持编码的图片格式
CFArrayRef myDestinationTypes = CGImageDestinationCopyTypeIdentifiers();
CFShow(myDestinationTypes);
}
2、解码流程
  • 静态图(PNG、JPG)解码流程
1
2
3
4
创建CGImageSource
读取图像格式元数据(可选)
**解码得到CGImage**
CGImage转成UIImage,资源清理
  • 动态图(GIF、APNG)解码流程
1
2
3
静态图的步骤1
遍历所有图像帧,重复静态图的步骤2-4
生成动图UIImage
3、解码关键API
  • CGImageSourceCreateWithData:从一个内存中的二进制数据(CGData)中创建ImageSource。ImageSource代表一个待解码数据,还可以通过CGImageSourceCreateWithURL、CGImageSourceCreateWithDataProvider分别从URL、DataProvide中创建ImageSource,DataProvider提供了很多种输入,包括内存,文件,网络,流等。很多CG的接口会用到这个来避免多个额外的接口。

  • CGImageSourceCreateImageAtIndex: 获取CGImage,对于静态图来说,index始终是0。

4、动态图解码Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CGImageSourceRef source = **CGImageSourceCreateWithData**((__bridge CFDataRef)data, NULL);
if (!source) { // 一般这时候都是输入图像数据的格式不支持
return nil;
}
NSUInteger frameCount = CGImageSourceGetCount(source); //帧数
NSMutableArray <UIImage *> *images = [NSMutableArray array];
double totalDuration = 0;
for (size_t i = 0; i < frameCount; i++) {
NSDictionary *frameProperties = (__bridge NSDictionary *) CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; // GIF属性字典
double duration = [gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime] doubleValue]; // GIF原始的帧持续时长,秒数
CGImagePropertyOrientation exifOrientation = [frameProperties[(__bridge NSString *)kCGImagePropertyOrientation] integerValue]; // 方向
CGImageRef imageRef = **CGImageSourceCreateImageAtIndex**(source, i, NULL); // CGImage
UIImageOrientation imageOrientation = [self imageOrientationFromExifOrientation:exifOrientation];
UIImage *image = [[UIImage imageWithCGImage:imageRef scale:[UIScreen mainScreen].scale orientation:imageOrientation];
totalDuration += duration;
[images addObject:image];
}
// 最后生成动图
UIImage *animatedImage = [UIImage animatedImageWithImages:images duration:totalDuration];
5、说明
  • Image/IO所有的方法都是线程安全的,而且基本上也都是同步的;

  • 通过CGImageSourceCreateImageAtIndex生成的CGImage,其实它的Bitmap还没有立即创建,他只是一个包含了一些元信息的空壳Image。这个CGImage,在最终需要获取它的Bitmap Buffer的时候(即,通过相应的API,如CGDataProviderCopyData,CGDataProviderRetainBytePtr),才会触发最后的Bitmap Buffer的创建和内存分配;

  • 图片的解码默认发生在主线程,在图片多或图片过大的情况下,第一次加载会导致滚动帧率下滑,后续帧率会好些,解码完成后的Bitmap Buffer会复用;

  • Image/IO是Appple提供的图片编码解码库,使用简单,性能也有保证, 但是对于不支持的格式如webp,编解码是无能为力。

四、子线程解码方案

1、空间换时间
  • 通过CGContext创建一个位图画布 CGBitmapContextCreate

  • 通过CGContextDrawImage绘制位图,CGContextDrawImage在执行过程中会触发Image/IO进行解码并分配Bitmap内存。得到的产物用来真正产出一个CGImage-based的UIImage,交由UIImageView渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = **CGBitmapContextCreate**(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
**CGContextDrawImage**(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = **CGBitmapContextCreateImage**(context);
CFRelease(context);
return newImage;
2、特点
  • 可以提升图第一次渲染到屏幕上的性能和滚动帧率;

  • 因为解码后的位图要保留在内存中,会给内存带来压力,要注意内存的清理;

  • 注意控制处理图片解码的子线程数量,子线程过多同样会影响性能。

1
解码后的图像大小和图片的宽高像素有关,宽高像素越大,位图图像就越大。假设一个3MB的图片,其宽高像素为2048 * 2048 的图片,解码后的位图图像大小是16MB(2048 * 2048 * 4)
3、其他
  • SDWebImage中有两个解码方法decodedImageWithImage 和 decodedAndScaledDownImageWithImage ,分别对应处理普通图,大图(默认位图大小超过60MB),对于大图,建议使用后者。其主要思路是:将大的原图切块,按块缩放成指定大小的图片填充到目标图片中去。

  • Apple大大没有采用此类方案,猜测原因是:因为早期设备内存有限,UIKit整套渲染机制很多地方采用时间换空间的策略。

  • 现在,大部分业务使用的是小图,大图的场景少,导致SDWebImage这类子线程解码方案很欢迎,内存开销比较稳定,性能也能提升。

五、后续

  • 子线程解码最终还是利用Image I/O解码,对于Image I/O不支持的图片格式,第三方解码方案?

  • 图片编解中遇到的图像方面的知识


iOS架构小记

一、概述

1、背景
  • 谈及iOS的架构,绕不开组件化、MVC、MVVM这些关键词
  • 对于几个人的小团队,组件化未必是适合的方案,当然遇到业务扩张,新App需要落地,将相同的功能沉库,可以汲取组件化的精髓,提高生产效率。
  • 组件化是大中型App团队的选择。
  • 对于百人以上的客户端超级大团队,组件化非常值得拥有,当然实施起来也非常复杂,包括业务划分,业务组件和基础组件的设计和开发、测试、集成和发版。
2、MVC和MVVM
  • 好像一说起Controller代码臃肿,就把锅求给MVC,就鼓吹MVVM,其实MVC虽然有明显的缺点,但是合理使用,绝对满足大部分需求,造成代码的臃肿和难以管理,很大原因和开发者认知有关系。
  • MVVM可以用,使用MVVM不必要一定引入RAC,
3、组件化
  • 业务快速发展、复杂的业务场景、团队的快速扩张等带来的变革诉求,希望通过组件化来提高团队的协作能力、减低开发成本,提高开发质量。
  • 组件化直接效果:代码解耦,功能模块化;代码的复用性高;代码管理更加科学。
  • 蘑菇街(蘑菇街 App 的组件化之路)、支付宝(从支付宝红包揭秘亿级APP的移动开发)等团队公开的信息来看:业务的增加、开发团队的扩张、快速迭代的要求,催生组件化方案(可能有更好的架构方案)快速落地。
  • 蘑菇街和支付宝等团队实现组件化方案,一是业务发展的必然选择;二是其技术沉淀深,能为自己和兄弟团队打造出质量上乘的组件化服务。

二、组件化实施

1、实施步骤
  • 剥离产品公共库和基础库
1
将网络请求、数据存储、图片下载和存储、第三方SDK(微信,支付宝)、UI基础组件、自定义相机等、UIKit和Foundation的扩展等,拆分出来,各自沉库,使用cocopods管理。
  • 独立业务模块单独成库
1
将登录、分享、支付、日志上报等模块封装成组件,也可以将一些通用模块,如资讯详情页、XX模块拆分出来,拆分粒度可以先粗后细,将相对独立的功能封装成组件,统一对外提供服务,保证体验一致性。
  • 对外服务最小化
1
在前两步都完成的情况下,根据组件被调用的需求来抽象出组件对外的最小化接口(遵循SOLID原则中的接口分离原则)
2、组件通信

组件之间的通信,更多是指业务组件之间的通信吧。目前蘑菇街团队公开的方案是:URLRouterProtocol Class BindingTarget-Action这三类方案。

1)URLRouter
  • 简介:蘑菇街团队实现MGJRouter库,可以根据URL处理执行对应的Block;其核心在于,先注册URL 和 服务Block & 参数字典的对应关系(保存在router字典中),然后利用URL找到对应的Block,将参数字典交给Block,唤起对应的服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 注册
    [MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
    [self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
    }];

    //传参
    [MGJRouter openURL:@"mgj://foo/bar" withUserInfo:@{@"param1":@"hello world"} completion:nil];

    //同步获取object
    NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]
  • 优势:解耦方便;各个组件依赖MGJRouter就可以;打破组件间的相互依赖;

  • 不足:组件本身依赖中间件,但是分散注册又使得耦合较多
    需要专门维护URL(蘑菇街使用后台维护,自动生成URL短链的方式);

  • 补充MGJRouter是URL Router的OC版本实现,URLNavigator是URL Router的Swift版本实现,核心都是:注册URL和对应的处理,然后根据URL解析去做事情,如页面跳转。

2)Protocol Class Binding(协议和类绑定)
  • 简介:核心在于,为组件定义Protocol,Protocol指定返回的数据,然后在组件中新建Class实现Protocol,如此将Protocol和Class关联起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //以购物车组件为例
    //1、组件定义MGJCart,执行返回订单数方法
    @protocol MGJCart <NSObject>

    - (NSInteger)orderCount;
    @end

    //2、MGJCartImpl 实现MGJCart ,实现略

    //3、关联
    [ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)],

    //4、获取MGJCartImpl,接下来可以访问到参数了
    [ModuleManager classForProtocol:@protocol(MGJCart)]
  • 优势:把公共的协议统一放到同一文件中,组件依赖该文件即可。

  • 补充:阿里的beehive属于此类方案实现,具体参考BeeHive —— 一个优雅但还在完善中的解耦框架,核心思想涉及:各个模块间从直接调用对应模块,变成以Service的形式,避免了直接依赖;App生命周期的分发,将耦合在AppDelegate中的逻辑拆分,每个模块以微应用的形式独立存在。

3)Target-Action方案
  • 简介: CTMediator方案,在此类中对外提供明确参数类型的接口,接口内部通过performTarget方法调用服务方组件的Target、Action。由于CTMediator类的调用是通过runtime主动发现服务的,所以服务方对此类是完全解耦的。但如果CTMediator类对外提供的方法都放在此类中,将会对CTMediator造成极大的负担和代码量。解决方法就是对每个服务方组件创建一个CTMediator的Category,并将对服务方的performTarget调用放在对应的Category中,这些Category都属于CTMediator中间件,从而实现了感官上的接口分离。
  • 特点: 侵入小,但硬编码较多,Runtime编译阶段不检查,运行时才检查对应类或者方法是否存在,对开发要求较高。
3、其他

需要一个组件的管理平台,管理这些组件的单元测试、集成和发版

参考文章:iOS App组件化开发实践iOS组件化方案选型

三、MVC

1、苹果推荐的MVC
  • 我们常说的MVC主要包括以下三部分
1
2
3
控制器(Controller)- 数据的加工者
视图(View) - 数据显示
模型(Model)- 数据管理
  • 在苹果推荐的MVC中,View和Model是没有通信的,但是Controller可以Model和View通信;

MVC

  • Controller是可以直接访问Model,然后Model不知道Controller是谁,当Model发生变化时候,利用通知、代理或KVO等方式通知Controller
  • Controller也可以直接访问View,Controller可以直接根据Model来决定View的展示。View接收到响应事件, 通过delegate、target-action、block等方式告诉Controller的状态变化。Controller进行业务的处理,然后再控制View的展示。

  • 随着Controller和Model、View的交互(通信)越来越多,Controller中的代码就越来越多,造成Controller中代码过于臃肿。

2、MVC的优化实践
  • MVC是非常经典的设计模式,虽然有很多问题,但是使用得当的话,其实能搞定大部分项目。很多时候,代码的臃肿很大原因是开发者代码不规范造成的,比如将网络请求逻辑,数据的存储逻辑都放在Controller中,或者Controller中展示聚集太多的UI元素,这如何不造成Controller的代码臃肿呢。
  • 在使用MVC模式,需要划分好MVC的职责
1
2
3
4
5
6
7
8
9
10
11
12
13
Model应该做的事:
1.给ViewController提供数据
2.给ViewController存储数据提供接口
3.提供经过抽象的业务基本组件,供Controller调度

Controller应该做的事:
1.管理View Container的生命周期
2.负责生成所有的View实例,并放入View Container
3.监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。

View应该做的事:
1.响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
2.界面元素表达
  • 对软件结构做好分层,将软件分成若干个水平层,每一层都有清晰的角色和分工,不需要知道其他层的细节。层与层之间通过接口通信。

四、MVVM

1、介绍

MVVM.png

  • 在MVVM中,将和视图、数据的交互处理从Controller中移到ViewModel中;VM负责和Model通信,可以直接访问Model,当Model改变时候通知VM;同时,VM负责和视图的通信,直接访问视图,当视图接收到响应事件时,通知VM去处理。而Controller仅仅是协调各个部分的绑定关系以及必要的逻辑处理。
  • MVVM的核心是:实现MVVM的双向绑定,很多项目组因此引入RAC框架(函数响应式框架),其实使用KVO机制也可以实现双向绑定也可以,直接使用KVO,会有一些问题,建议直接使用facebook的KVOController
2、MVVM和MVC的对比

mvvm和mvc.png

  • MVVM优点:方便测试,VM可以方便做单元测试,MVC下的Controller里面逻辑太多,无法做单元测试;耦合度低; 复用性高; 层次更清晰; 重构成本低;
  • MVVM缺点:类文件增多(每个VC多一个ViewModel),ViewModel的代码复杂度增大(处理各种交互)
  • MVC优点:通用架构; 处理耦合度高的逻辑方便;
  • MVC缺点: 耦合度高; 复用性差; 测试性差;
3、补充个MVP
  • MVP分别是ModeViewPresenter,对应数据视图主持者,在Android开发中使用普遍。

MVP.png

  • MVC模式下,Android的Activity承担繁杂的业务逻辑,导致代码臃肿,使用MVP模式,将View层和Mode层隔离开,增加Presenter层,作为Mode层和View层通讯的桥梁
  • Presenter同时持有Mode层和View层的引用,在需要数据改变 或 视图显示时直接改变数据或者视图的显示状态。同样View层持有Presenter层的引用,这样就能将一些处理事件的逻辑放在Presenter层中进行处理,处理完成后通知View层改变显示状态。
  • 具体参考 MVP模式的经典封装

五、SOLD原则

1、介绍
  • 根据项目的复杂度来决定选择MVC或MVVM,但是无论哪种模式,都做好代码设计,结构分层,类设计遵守SOLID原则
缩写 全称 中文 备注
S The Single Responsibility Principle 单一责任原则 一个类只应承担一种责任
O The Open Closed Principle 开放封闭原则 可扩展,不可修改
L Liskov Substitution Principle 里氏替换原则 子类可以在任意地方替换基类且软件功能不受影响
I The Interface Segregation Principle 接口分离原则 将接口拆分成更小和更具体的接口,有助于解耦
D The Dependency Inversion Principle 依赖倒置原则 一个方法应该遵从“依赖于抽象而不是一个实例”。依赖注入 是该原则的一种实现方式。
2、IOC和DI

控制反转(IOC)和依赖注入(DI)是Spring中最重要的核心概念之一,而两者实际上是一体两面的。

  • 依赖注入
    • 一个类依赖另一个类的功能,那么就通过注入,如构造器、setter方法等方式将这个类的实例引入。
    • 侧重于实现。
  • 控制反转
    • 创建实例的控制权由一个实例的代码剥离到IOC容器控制,如xml配置中。
    • 侧重于原理。
    • 反转了什么:原先是由类本身去创建另一个类,控制反转后变成了被动等待这个类的注入。

瘦身优化小记

闲散心如月,风光好自知

一、安装包组成分析

1、组成情况

​ 将IPA包修改后缀名为ZIP,解压缩后,获取payload中的App文件,查看App文件的内容,你会发现该文件主要包含以下内容

  • Exectutable: 可执行文件
  • Resources:资源文件
    • 图片资源:Assets.car/bundle/png/jpg 等
    • 视频/音频资源:mp4/mp3 等
    • 静态网页资源:html/css/js 等
    • 视图资源:xib/storyboard 等
    • 其他:文本/字体/证书 等
  • Framework
    • SwiftSupport: libSwiftxxx 等一系列 Swift 库
    • 其他依赖库:Embeded Framework
  • Pulgins:Application Extensions
    • appex:其组成大致与 ipa 包组成一致
2、组成分析
  • 一般来说,可执行文件、图片资源(asset.car)和动态库的占比最大,如果是Swift和OC混编,可执行文件比纯OC大很多
  • 从优化的效果上看,优化图片资源的ROI比较大,如果是首次优化,建议从图片资源的优化开始。
  • 项目中使用Swift,会增加安装包大小,因为FrameWork中会加入为了支持 Swift 的动态库集合,如果纯Swift项目,不会引入这些东西。

二、资源文件优化

​ 理论上,资源文件包括:图片视频音频和字体等;实际上,视频和音频文件一般不会集成到安装包中,在安装包中的资源文件主要是图片。

1、优化手段1:App Slicing
  • iOS 9之后提供了App Thinning三件套:App SlicingOn Demand ResoucesBitcode
App Thinning 理想 现实
App Slicing 将App Bundle资源根据不同的设备特性分为不同的版本。对于图片资源,会根据设备所需图片分辨率不同分发给对应设备所需对应的图片资源。 主要是图片资源的Slicing,我们有自己的方案,没有采用
On Demand Resources App的资源只有要使用时才下载,如果其他资源需要空间这些资源可以被移除 更适合游戏类App,项目没有使用
Bitcode Bitcode可以作为中间产物一起提交AppStore。包含Bitcode配置的程序将会在AppStore上被编译和链接。Bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到AppStore上 使用BitCode的要求所有代码都支持BitCode,改动项目较大,没有使用

说明:可以充分利用App Slicing实现图片资源的瘦身

  • 在项目中引入图片时候,直接在 Assets.xcassets中添加就可以(资源文件用Asset Catalog管理),这样能使用到App Slicing功能,这样当用户从App Store上下载App时,可以只下载适用于其设备的App架构版本和所需资源,从而减少App所占的空间。
  • 在实践中发现,有的新同学在Assets.xcassets中引入@1x的图片,iPhone手机目前需要的@2x和@3x图片,所以@1x的图片显然是不需要的。
  • 在实践中还发现,有的图片资源游离在Assets.xcassets之外,这些可以考虑是否可以放入Assets.xcassets中(大部分情况下是可以放入的)
2、优化手段2:Xcode编译项
  • 因为绝大部分引入的图片是PNG格式,Xcode 提供的给我们两个编译选项来帮助压缩 PNG 资源:

  • Compress PNG Files:设置为YES,打包的时候自动对图片进行无损压缩,使用的工具为 pngcrush,压缩比还是相当高的,比较流行的压缩软件 ImageOptim 也是使用 pngcrush 进行压缩 PNG 的。

  • Remove Text Medadata From PNG Files:设置为YES,能帮助我们移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息。
  • 引入项目的PNG资源自动被 Xcode 进行压缩了,但是如果是使用Bundle管理的资源,不会被Xcode压缩,可以使用tinypng压缩。
3、优化手段3:清理无用的资源
  • 及时清理不使用的图片资源。使用类似LSUnusedResources 清理旧的图片文件。
  • LSUnusedResources的思路是,先获取图片文件(imageset, jpg, png, gif)集合A,然后搜索代码文件中所有字符串名称得到B,然后从A集合中排除集合B就得到未使用的图片资源。
4、优化手段4:图片文件去重
  • 遍历图片文件,计算每个文件的MD5值,然后以MD5值为key,文件路径存入key对应的数组;
  • 遍历字典values,将value的数组大小大于1的路径输出,这样就找到重复图片的路径了。
5、优化手段5:更适合的图片格式
  • iconfont代替项目中纯色小图标,也省去很多@2x和@3x的图片切图。
  • PNG切图的替换方案,如PDF矢量图来代替大部分简单的png切图;然后在代码中自己解码并展示出来,一套PDF矢量图可以等效大部分2x和3x的png图片;
  • 网络图片选择压缩比更好的图片格式,如webp

说明:PNG切图不可能被完全替换,在表现颜色丰富图片时候,PNG效果很不错,其他详见浅谈iOS图片优化

三、可执行文件优化

1、优化手段1:编译器优化
  • Xcode 支持编译器层面的一些优化优化选项,可以让我们介于更快的编译速度更小的二进制大小更快的执行速度之间自由选择想要进行的优化粒度;
  • 在Xcode中,使用Clang来编译Objective-C,可以在 Build Setting -> Apple Clang - Code Generation -> Optimization Level 设置,Release下为Fastest Smallest[-Os]。编译器会开启除了会明显增加包大小以外的所有优化选项;
  • 在Xcode中,使用SwiftLang来编译Swift语言,同样也是基于 LLVM 后端的。Xcode 9.3 版本之后可以在Build Setting -> Optimization Level 设置,Release下为Optimize for Speed[-O],这可能会增加安装包大小
1
2
3
No optimization[-Onone]:不进行优化,能保证较快的编译速度。
Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。
Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率

说明:Xcode 9.3/Swift4.1编译器不是特别稳定,特别是开启 Osize 选项之后,编译器很多情况下会莫名其妙的崩溃(Segmentation fault),目前放弃 [-Osize],选择[-O]

2、优化手段2:去除符号信息
  • 可执行文件中的符号:程序中的所有的变量、类、函数、枚举、变量和地址映射关系,以及一些在调试的时候使用到的用于定位代码在源码中的位置的调试符号,符号和断点定位以及堆栈符号化有很重要的关系。
  • Strip Style表示的是我们需要去除的符号的类型的选项,可以在Build Setting -> Strip Style设置, Release下为All Symbols,其分为三个选择项:
1
2
3
4
5
All Symbols: 去除所有符号,一般是在主工程中开启。

Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符号,Debug Symbols 同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库/动态库的建议选项。

Debug Symbols: 去除调试符号,去除之后将无法断点调试。

说明:iOS 的调试符号是 DWARF 格式的,使用 Xcode 编译打包的时候会先通过可执行文件的 Debug Map 获取到所有对象文件的位置,然后使用 dysmutil 来将对象文件中的 DWARF 提取出来生成 dSYM 文件。

  • Strip Linked Product去除不必要的符号信息,去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。Release下为YES。
  • Strip Linked Product 选项在 Deployment Postprocessing 设置为 YES 的时候才生效,而在 Archive 的时候 Xcode 总是会把 Deployment Postprocessing 设置为 YES,Debug下,Deployment Postprocessing 设置为 NO。
  • Strip Debug Symbols During Copy将那些拷贝进项目包的三方库、资源或者 Extension 的 Debug Symbol 去除掉,在Build Settings -> Strip Debug Symbols During Copy设置,Release下设置为YES。
  • Cocoapods 管理的动态库(use_framework!)的情况就相对要特殊一点,因为 Cocoapods 中的的动态库是使用自己实现的脚本 Pods-xxx-frameworks.sh 来实现拷贝的,所以并不会走 Xcode 的流程,当然也就不受 Strip Debug Symbols During Copy 的影响。当然 Cocoapods 是源码管理的,所以只需要将源码 Target 中的 Strip Linked Product 设置为 YES 即可。
  • Strip Swift Symbols能帮助我们移除相应 Target 中的所有的 Swift 符号,这个选项也是默认打开的。Strip Swift symbols需要在打包的发布选项中勾选(默认勾选),在Swift ABI 稳定之前,Swift 标准库是会打进目标文件的。
3、优化手段3:BitCode
  • Bitcode可以作为中间产物一起提交AppStore。包含Bitcode配置的程序将会在AppStore上被编译和链接。Bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到AppStore上。
  • 开启 BitCode 之后编译器后端(Backend)的工作都由 Apple 接管了。所以假如以后苹果推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化,我们也不再需要为其发布新的安装包了。
  • 工程开启 BitCode 之后必须要求所有打进 Bundle 的 Binary 都需要支持 BitCode,也就是说我们依赖的静态库和动态库都是含有 BitCode 的,不然就会打包失败。对于 Cocoapods 等源码管理工具来管理的依赖库来说操作会比较简单,我们只需要开启 Pods 工程中的 BitCode 就行。但是对于一些三方的闭源库,我们就无能为力了。
  • 开启 BitCode 之后,由于最终的可执行文件是 Apple 自动生成的,同时产生新的符号表文件,所以我们使用原本打包生成的 dSYM 符号化文件是无法完成符号化的。所以我们需要在上传至 App Store 时需要勾选 Include app symbols for your application to receive symboilcated crash logs from Apple:勾选之后 Apple 会给我们生成 dSYM,然后就可以在 Xcode -> Organizer 或者 iTunes Connect 中下载对应的 dSYM 来进行符号化了。
4、优化手段4:清除无用代码
  • Dead Code Stripping:Xcode 默认会开启此选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。
  • 扫描查找无用代码:基本思路都是查找已经使用的方法/类和所有的类/方法,然后从所有的类/方法当中剔除已经使用的方法/类剩下的基本都是无用的类/方法,但是由于 Objective-C 是动态语言,可以使用字符串来调用类和方法,所以检查结果一般都不是特别准确,需要二次确认。目前市面上的扫描的思路大致可以分为 3 种:
    • 基于 Clang 扫描
    • 基于可执行文件扫描
    • 基于源码扫描
  • 及时下线不需要的功能,如完成使命的ABTest代码、被产品抛弃的功能代码等。
  • 移除不需要的系统库和第三方库。
5、优化手段5:代码重构
  • 功能合并:相似功能的代码,只需维护一份就可以了。如定制通用UI组件,大家可以有类似需求,可以给通用UI组件的开发提,没必要自己单独实现。

End

1、优化之后
  • 保持良好的开发习惯。及时清理无用代码和无效库

  • 持续关注安装包大小的变化,

  • 定期Review安装包大小变化
  • 建议预警机制,监控每个版本的体积大小,体积图片突然变大,要去找原因。
2、参考资料

iOS App 瘦身实践总结

iOS 安装包瘦身(上篇)

iOS 安装包瘦身(下篇)


推荐迷雾(一):话说ABTest实验

雄兔脚扑朔,雌兔眼迷离;双兔傍地走,安能辨我是雄雌?

一、概述

1、什么是ABTest实验

  • 今天ABTest已经是一门显学了,主流的产品,无论是算法模型优化,还是UI或体验的调整,上线后都会做ABTest实验,这种线上实验离用户更近,收益大于离线评估方案;
  • ABTest实验是为了避免盲目决策带来不确定性和随机性,将各种不同的实验同时放到线上,然后利用数据分析来辅助决策,总之一句话,让数据说话 (data talk)。

2、ABTest实验的条件

  • 比较好的两个及以上备选方案,毕竟 ABTest实验不是银弹,它只是辅助我们做更好的选择
  • 量化的指标,比如App中PV、UV、CTR、CVR、CPM等
  • 用户群体稳定 且 用户量足够

3、ABTest实验需要注意的问题

  • 实验流量合理分配: 保证每组实验流量分配的正交性、均匀性和充足性
  • 排查实验自身干扰:实验中可能引入不确定因素,导致结果不可预估

参考你的AB测试平台和方案,真的可靠么

二、单层模型和分层模型

1、单层模型

  • 不同组实验在同一层拆分流量,不同组的流量是不重叠的
  • 只能支持少量的实验,不利于迭代
  • 实验之间不独立,策略之间可能相互影响
  • 分流方式不灵活

2、分层模型

  • 主流的流量分配方案,来自2010年谷歌公布的的《Overlapping Experiment Infrastructure More, Better, Faster Experimentation》论文;
  • 谷歌提出将实验空间横向、纵向划分,纵向流量可以独占实验区域,可以独享实验流量,不被其他实验影响;横向分若干层,每一个可以做同一组的实验,每个独立实验为一层,层与层之间流量是正交的,一份流量穿越每层实验时,都会再次随机打散,且随机效果离散。

参考一文搞懂AB Testing的分层分流

三、分层模型方案

1、技术关键点

  • 分流函数(流量如何在每层被打散)如何设计,如果保证每层流量分配的均匀性和正交性

  • 如何处理实验样本的过滤(如 只选取某个地区的用户、只选取新用户)

  • 分配多大的流量可以使实验置信

2、设计

  • 域(domain):划分的一部分流量
  • 层(layer):系统参数的一个子集
  • 实验(exp):在一个域上,对一个或者多个参数修改,改变请求路径的过程
  • 相关联的策略参数位于同一实验层;
  • 相互独立的策略参数分属于不同的实验层;
  • 一个实验参数只能在一个实验层中出现;
  • 不同实验层间进行独立的流量划分和独立的实验,互不影响。

  • 每一实验层享有 100% 流量,可以避免流量切分过细,保证实验间的可对比性、客观性;

  • 不同实验层之间流量正交,可以避免不同试验间的流量依赖和流量不均匀情况的出现。为了更好地评估实验的效果,每一实验层还引入了基准实验。该基准实验会采用该实验层的默认策略取值,流量配比会设定在一个合适的水平。

四、业内ABTest实验平台

基于Google的分层模型,美团和微博的ABTest平台实现

1、美团点评 的 Gemini

https://www.csdn.net/article/2015-03-24/2824303

2、微博的 Faraday

微博广告法拉第(Faraday)全流量分层实验平台。该实验平台支持大规模广告策略并发实验,提供了多种流量均匀分流模式,全面的广告指标跟踪评估,实验效果实时反馈等。

http://www.yunweipai.com/archives/19535.html


推荐迷雾(一):再见推荐系统

序言

花非花,雾非雾。夜半来,天明去。来如春梦不多时,去似朝云无觅处。

​ 用白居易的诗作为《推荐迷雾》系列的开始,本人非推荐算法工程师,但是工作中也和算法打过些交道,也曾于13年粗读过项亮博士的《推荐系统实战》,这本12年出版的“旧”书如今再读起来,更多一番体会;虽然近些年,机器学习尤其是深度学习给传统推荐学习带来新的变化,但是这些新兴的推荐技术依旧有传统的推荐算法模型的影子。

江湖

一、开篇

  • 推荐产生的背景:信息过载,用户需求不确定
  • 推荐三步骤:召回,预估和排序

二、推荐系统的评估指标和方法

1、评估指标

1-1、评分预测的评估
  • RMSE (均方根误差,加大预测不准物品评分的惩罚)

  • MAE(平均绝对误差)

1-2、TopN推荐的评估
  • 在这里用到了准确率召回率F-1 Score三个度量值
  • 召回率(Recall):用户消费的内容,是由推荐提供的占比
  • 准确率(Precision):推荐的内容中,用户消费的占比
  • F-1 Score

$$
F_1 Score = \frac{2 x recall * precision}{recall + precision}
$$

召回率表示在原始样本的正样本中,最后被正确预测为正样本的概率;

1
2
3
4
5
6
7
8
9
10
11
#计算召回率和准确率
def PrecisionRecall(test,N):
hit = 0
n_recall = 0
n_precision = 0
for user,item in test.items():
rank = Recommend(user,N)
hit += len(rank & items)
n_recall += len(items)
n_precision += N
return [hit/(1.0 * n_recall),hit/(1.0*n_precision)]
  • 为了全面评估TopN推荐的准确率和召回率,一般选取不同推荐列表长度N,计算一组准确率/召回率,然后画出准确率/召回率曲线。
  • 在工程实践中,TopN推荐更合适,因为对于推荐的内容来说,预测用户会不会看,比预测用户看了内容后给多少分更重要。
1-3、覆盖率

推荐系统对物品长尾的发掘能力,简单的定义是推荐物品占总物品集合的比例。当然还有更好的指标来定义覆盖率。

  • 信息熵:
  • 基尼系数:
1-3、其他指标
  • 多样性、新颖性、惊喜度、信任度、实时性、健壮性,商业目标等等,具体参考《推荐系统实战》中的内容

2、评估方法

  • 离线评估:速度快,不需要用户参与;在用户的历史数据上做评估,和线上真实效果有偏差;只能评估少数指标。

  • 用户调查:主要的形式是问卷调查,成本高

  • 在线实验:目前最普遍的做法是ABtest,目前主采用多层重叠实验设计,这些在后面的文章重点介绍

三、推荐系统架构

  • 推荐系统是产品的核心,而推荐算法仅仅是推荐系统中的一部分;以NetFlix的推荐系统架构为例,介绍经典的推荐系统架构。
  • 在NetFlix推荐系统架构中,分为三层:Offline层(离线层)、Nearline层(近线层)和 Online层(在线层)

1、Offline层(离线层)

  • 这一层批量、周期性地抽取数据训练模型;训练得到的模型可以用于为用户计算推荐结果。协同过滤、矩阵分解一般在这层做,Hadoop、Spark分布式计算也在这一层。
  • Offline阶段的推荐结果或模型在Nearline层被更新,产生最终的推荐结果,呈现给用户。

2、Nearline层(近线层)

  • Nearline要处理处理实时数据流(流计算),执行计算任务:从事件队列中获取最新的一个或少许几个用户反馈行为,将这些用户已经反馈过的物品从离线推荐结果中剔除,然后用这几个反馈行为作为样本,以小批量梯度下降的优化方法去更新融合模型的参数。

3、Online层(在线层)

  • 用户使用App/浏览Web,消费展示内容,产生行为事件数据,如页面曝光,按钮点击等,实时被收集走,一边进入分布式文件系统中做存储,给Offline使用,一边流向Nearline的消息队列,供Nearline的流计算使用。
  • 用户发出请求,等待推荐结果;Online层必须实施响应用户请求,要快,要有兜底,Online层处理的一般是已经预处理后的推荐结果。

四、推荐系统中的冷启动和EE问题

  • 在推荐系统中有两个经典问题:冷启动EE(探索和利用问题)。
  • Bandit算法提供了一种有效的解决办法;其中冷启动的本质是:推荐系统没有历史数据,无法预测用户偏好;可分为用户冷启动,物品冷启动和系统冷启动。EE问题是指,是选择现在不确定的一些方案,但未来可能会有高收益的方案;还是选择现在可能最佳的方案;本质是一个选择的问题。
  • Bandit 算法来源于历史悠久的赌博学,它要解决这样的问题:一个赌徒去摇老虎机,赌场中有的老虎机一模一样,但是每个老虎机吐钱的概率不一样,他不知道每个老虎机吐钱的概率分布是什么,那么每次该选择哪个老虎机可以做到最大化收益呢?这就是多臂赌博机问题(Multi-armed bandit problem, K-armed bandit problem, MAB)

MAB问题

  • 假设我们已经经过一些试验,得到了当前每个老虎机的吐钱的概率,如果想要获得最大的收益,我们会一直摇哪个吐钱概率最高的老虎机,这就是Exploitation。但是,当前获得的信息并不是老虎机吐钱的真实概率,可能还有更好的老虎机吐钱概率更高,因此还需要进一步探索,这就是Exploration问题。

  • Bandit解决MAB或者EE问题的策略是:有策略地走一步看一步,这些策略就是Bandit算法,经典的Bandit算法分别是:朴素Bandit、汤普森采样、UCB和Epsilon贪婪算法

1、朴素Bandit

原理:先随机试若干次,计算每个臂的平均收益,一直选均值最大那个臂。

2、汤普森采样(Thompson sampling)算法

原理

  • 每个臂维护一个beta(a,b)分布,每次用现有的beta分布产生一个随机数,输出随机数最大的臂(较快,随机性高)

beta(a,b)分布特点

  • a+b值越大,分布曲线就越窄,分布就越集中;
  • a/(a+b)值越大,分布中心越靠近1,反之越靠近0;

采样过程

  • 假设a是用户的点击次数,b是没有得到用户的点击次数

  • 每次取出所有候选的参数a和b,用贝塔分布产生一个随机数

  • 随机数排序,输出最大值对应的候选

  • 如果用户点击,对应的候选a加1,反之b加1

分析

  • 如果一个候选被选中的次数很多(a+b很大),分布变窄,对应的分布产生的随机数基本在中心位置,接近平均收益。
  • 如果a+b很大,a/(a+b)也很大,那么分布产生的随机数越接近1,平均收益很好,进入利用阶段;
  • 如果a+b很小,说明候选的好坏不能确定,分布很宽,可能得到一个较大的随机数,排序时候可能被优先输出,起到了探索的目的。

3、UCB算法

原理

  • 以每个候选的平均收益作为基准线进行选择
  • 对于每次被选择不足的给与照顾
  • 选择倾向于那些确定收益较好的候选

简言之,均值越大,标准差越小,被选中的概率会越来越大 (相对慢一点,确定性高)

4、Epsilon贪婪(Epsilon-Greedy)算法

原理:

  • 先选一个(0,1)之间较小的值,作为Epsilon,然后每次以1-epsilon的概率选取当前收益最大的臂,以epsilon的随机概率选取一个臂。(后期不需要较大探索,epsilon需要衰减)
  • Epsilon可以控制探索和利用的程度,Epsilon越接近0,在探索上就越保守;Epsilon越接近1,在探索上就越激进。

Bandit算法算法模拟试验效果

总结:UCB算法和汤普森采样算法效果更好些。

五、Bandit算法的工程实现

​ 上面介绍的朴素Bandit、汤普森采样、UCB和Epsilon贪婪算法都是经典的Bandit算法,在工程中很少使用;实际中,我们采用的是上下文Bandit算法,比较常见的是LinUCB算法和COFIBA算法

1、LinUCB

概述

  • 传统的 Bandit 算法并没有考虑臂的特征信息,也就是说并没有考虑上下文信息,而yahoo在2010年提出的,一种结合上下文的 Bandit算法——LinUCB (linear UCB)算法。

  • LinUCB 算法可以将当前用户的特征、物品特征构成所有的相关特征,然后根据每个臂维护的特征系数,计算出预估收益。由于加入了特征,所以收敛速度比 UCB 更快。

  • LinUCB的不足:同时处理的候选臂数量不能太多,不超过几百个最佳。因为每一次要计算每一个候选臂的期望收益和置信区间,一旦候选太多,计算代价将不可接受。其实这也是所有的 Bandit 算法的缺点。

原理:

  • LinUCB 假设一个物品推送给用户之后,获得的收益与相关特征呈线性关系,这里的相关特征就是指上下文信息。LinUCB 有两个版本:DisjointHybrid,Disjoint 表示不同臂之间的不相关,也就是说参数不共享,Hybrid 表示臂之间共享一些参数。

  • Disjoint 模型:假设每个臂包含一个物品,我们在每一次选择时,用户与物品的的特征构成了上下文信息,表示为 x,维度为 d,每个臂维护了一个 d 维的表示特征系数的向量 θ,使用 c 表示本次选择的收益,如果用户点击了就为 1,否则为 0。我们假定:

根据p’ + ∆来选择合适的臂。p’的计算基于有监督的学习方法。我们为每个老虎机维护一个特征向量D,同时上下文特征我们写作θ,然后通过收集的反馈进行有监督学习:

  • 加入特征信息,用User和Item的特征预估回报及其置信区间,选择置信区间上界最大的Item推荐,观察回报后更新线性关系的参数,以此达到试验学习的目的。

  • 岭回归的求解,岭回归适合样本数少于特征的数据集。

  • 参考:结合上下文信息的Bandit算法—LinUCB算法

2、COFIBA

  • 2016年提出,COFIBA算法的不同有两个:
    1. 基于用户聚类挑选最佳的Item(相似用户集体决策的Bandit)。
    2. 基于用户的反馈情况调整User和Item的聚类(协同过滤部分)。
  • 在时刻t,用户来访问推荐系统,推荐系统需要从已有的候选池子中挑一个最佳的物品推荐给他,然后观察他的反馈,用观察到的反馈来更新挑选策略。 这里的每个物品都有一个特征向量,所以这里的Bandit算法是context相关的。 这里依然是用岭回归去拟合用户的权重向量,用于预测用户对每个物品的可能反馈(payoff),这一点和linUCB算法是一样的

3、其他

  • Exploit-Explore这一对矛盾一直客观存在,Bandit算法是公认的一种比较好的解决EE问题的方案。但解决Explore,势必就是要冒险,势必要走向未知,而这显然就是会伤害用户体验的:明知道用户肯定喜欢A,你还偏偏以某个小概率给推荐非A。
  • 实际上,很少有公司会采用这些理性的办法做Explore,反而更愿意用一些盲目主观的方式。究其原因,可能是因为:
1
2
3
4
1、互联网产品生命周期短,而Explore又是为了提升长期利益的,所以没有动力做;
2、用户使用互联网产品时间越来越碎片化,Explore的时间长,难以体现出Explore 的价值;
3、同质化互联网产品多,用户选择多,稍有不慎,用户用脚投票,分分钟弃你于不顾;
4、已经成规模的平台,红利杠杠的,其实是没有动力做Explore的。
  • 所以做Explore要精心设计,必须保证质量。

TODO

  • bandit添加对应的源码实现

  • ABTest的分层实验设计

  • 召回中的协同过滤和隐语义模型, 矩阵分解
  • CTR预估的进化,特征工程 + LR -> GBDT + LR -> FM ->FFM -> DeepFM/Wide&Deep/…
  • 排序怎么做

机器学习中的树模型

一、开篇

1、概述

  • 在数据结构中,有树这种结构;在机器学习中,有决策树;咋一看,感觉是一回事,其实不然;
  • 数据结构的树关注的事查找,插入,删除的效率,二叉树,平衡二叉树都是为了解决这些效率而产生的;

  • 机器学习中的决策树关注的是,如何找到最佳分解节点,ID3算法(按最大信息增益划分),C4.5(按信息增益比划分),CART算法(按最小基尼指数划分)都是为了达到最佳分裂的效果。

  • 最大信息增益会倾向于可取值较多的特征,最大信息增益比会倾向于可取值较少的特征

2、决策树和集成学习

  • 决策树模型很简单,简单到即使你不懂机器学习,也能很快了解他,可能有人会说,这么简单的模型有什么用。哈哈,数学的伟大,在于复杂的问题简单化,深度学习的强大在于利用将简单的神经网络不断加深;决策树虽然简单,但是很多个决策树在一起,就足够让很多浅层机器学习算法忘而却步。
  • 这个将许多决策树整合一起的方式就是集成学习,在集成学习中,决策树是个体学习器,当然个体学习器可以是别的弱学习器。

  • 而根据个体学习器生成方式的不同,目前集成学习方法大致可分为两大类,即个体学习器间存在强依赖关系、必须串行生成的序列化方法,以及个体学习器间不存在强依赖关系、可同时生成的并行化方法;前者的代表是Boosting 梯度提升树(GBDT),后者的代表是和Bagging随机森林(Random Forest)

二、决策树

1、决策树中需关注问题

  • 一棵树是如何构建的?建树过程中,树分裂节点时,如何选出最优的属性作为分裂节点。

  • 如何用树的减枝来避免过拟合问题。

  • 对于含有空值的数据,如何构建树。

  • 构建树可能存在的问题,过拟合问题,如何解决

2、如何构建一棵树

简言之:选择最优划分属性作为分裂结点,使得分支结点中所包含的样本尽可能属于同一类。树的生成算法有三种:ID3、C4.5和CART.

1、ID3

选择最大信息增益(Information Gain)规则去寻找最优分裂节点

1-1、基础概念
  • 信息熵(Information entropy):表示随机变量不确定性的度量;熵越大,随机变量的不确定性就越大;(我们希望分类后的结果熵越小越好)。

  • 信息增益(Information gain):表示因特征X的信息而使得类Y信息不确定性减少的程度。(当然是越大越好);
    $$
    g(D,A) = H(D) - H(D|A)
    $$

在划分过程中,找到信息增益最大的特征将样本根据此特征划分不同的结点中,新的结点中继续划分。

1-2、不足
  • 信息增益反应的是:给点条件后,不确定性减少的程度,特征取值越多,意味着确定性越高,也就是条件熵越小,信息增益越大。这就造成信息增益对可取值较多的属性有所偏好。
  • ID3只能处理离散型变量,只能处理分类任务
  • 对样本特征缺失值比较敏感
2、C4.5

选择最大信息增益比(Information Gain Ratio)规则去寻找最优分裂节点

2-1、基础概念
  • 信息增益比(Information Gain Ratio): 特征A对训练数据集D的信息增益比为其 信息增益与训练数据集D关于特征A的值的熵之比。
    $$
    g_R(D,A) =\frac{g(D,A)}{H_A(D)}
    $$

在划分过程中,找到信息增益比最大的特征将样本根据此特征划分不同的结点中,新的结点中继续划分。

2-2、不足
  • C4.5 对ID3做了优化,使用信息增益比在一定程度上对取值较多的特征进行了惩罚,避免ID3出现的过拟合特性,提升了决策树的泛化能力。
  • C4.5 能处理连续型变量和离散型变量,但是只能处理分类任务。
3、CART

选择最小基尼指数(Gini index)规则去寻找最优分裂节点

3-1、基础概念
  • 基尼指数:表示数据的纯度
    $$
    Cini(D) = 1 - \sum_{k=1}^n(\frac{|C_k|}{|D|})^2
    $$

利用基尼指数最小选择最优分裂点,采用二元切割法

3-2、特点
  • 不仅能处理分类任务,还能处理回归任务
  • 不仅能处理离散型变量,还能处理连续型变量
  • 能够处理样本特征数据缺失的情况

3、树的剪枝

3-1、预剪枝

阈值、

3-2、后剪枝

三、梯度提升树 和 随机森林


机器学习杂谈

高能预警:本人非算法开发,以下是我的一些浅见。

一、开篇

自有机器学习以来,算法模型中就有了区分;分为监督学习,无监督学习,半监督学习和强化学习等。

1、监督学习

  • 机器学习中大部分任务都是监督学习任务;在监督学习中,分类和回归是其两大主题。分类中名声最大的是:逻辑回归(Logistic regression)、朴素贝叶斯(Naive Bayes) 和 支持向量机(Support Vector Machine,SVM) 。

  • 在工业界曾经有一招LR打天下的”传说”,这里的LR就是逻辑回归;早些年做CTR预估时候,人工特征海洋 + LR用得非常多,很多算法工程师大部分时间埋头搞特征工程;

  • 朴素贝叶斯(Naive Bayes)是个开挂的存在,它将贝叶斯原理应用到机器学习中,而后机器学习中贝叶斯学派声明鹊起,与旧贵族统计学派在机器学习中鼎足而立;它强假设输入的数据的特征都是独立的,在文本分类中表现出很好的效果,可谓是入门nlp都必须了解的算法之一了。
  • 支持向量机(Support Vector Machine,SVM) 当之无愧是监督学习中的无冕之王,自上世纪90年代诞生之后,在机器学习界掀起“腥风血雨”,它利用核技巧,在属于两个不同类别的两组数据之间找到良好的分类边界,将线性不可分问题转为高纬空间的线性可分问题,借此达到了分类的目的。因为SVM的存在,将多层神经网络的研究打进冷宫;多层神经网络是深度学习的前身。直到2012年,因为深度学习在ImageSet竞赛中的完胜,神经网络的研究才再次复苏,直至形成今天的野火燎原之势。
  • 监督学习中另一个主题是回归,最基础的的当然是线性回归(Linear regression),模型虽然简单,但是预测个房价,股票价格这种高大上的事情也还是有着不错的表现。
  • 非监督学习比较尴尬,在工业界,更多起到打辅助的左右;有聚类,降维和关联规则三大主题,聚类中用的较多反而是简单的K-means,PCA和SVD用来做数据降维,关联规则那块好像在推荐系统用的稍微多些。

2、决策树和集成学习

  • 决策树是监督学习中比较特殊的模型,说他特殊,是因为它不像LR、SVM和NB直接上手,大家喜欢使用将决策树作为集成学习的基础学习器,比较著名的是随机森林(Random Forest)、梯度提升树(GBDT),决策树的实现经历了ID3, C4.5,CART三个阶段,在随机森林、梯度提升树里面用的是CART这个分类回归树。从CART的名字也看出来,不仅仅可以做回归,也能做分类。粗暴理解就是,分类时候好多CART投票分类结果,回归时候,取CART预测值得平均数。
  • GBDT有个超级经典的实现,XgBoost。在Kaggle比赛中,XgBoost近乎霸主级别的存在;江湖有传言,图片图像等机器学习问题用Keras(深度学习库),浅层机器学习用XgBoost。

3、神经网络和深度学习的渊源

  • 我认为,没有神经网络,就没有深度学习;虽然业界有说法,不用神经网络就也能实现深度学习;但是神经网络真的真的很厉害,虽然很长一段时间不被看好,因为两层的神经网络连个异或都处理不了,的确有理由被看不上;但是随着反向传播算法发现,神经网络层数不断加深,在工业界发挥牛逼闪闪的光芒。
  • 你可能想不到,权重,阈值,激活函数这些简简单单的东西,怎么能解决计算机视觉、语音识别这种非常难的问题,这些问题连当年风光无限的SVM都败下阵来。

4、特征工程

  • 有句老话:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。在传统机器学习领域,做好特征工程很重要;工业界有句老话,70%多的时间在做特征工程;

  • 比较尴尬的是,市面上多是讲解机器学习算法模型的书,讲解算法和模型的奥妙是他们的主题,提供的试验数据也都是已经处理好的,并不需要特征工程处理,但是实际工程中不是这样。特征工程的处理依赖领域知识和经验依旧是主流,但是业界很多项目处于安全、隐私等考虑,不会透露底层的特征工程的处理。

5、特征工程 & 深度学习

  • 深度学习有自动获取特征的能力,可以对输入的低阶特征进行组合、变换,得到高阶特征,但是这个能力只是对于某些领域(如图像、语音)有比较好的效果。
  • 在其他领域,如自然语言处理中,输入的字或词都是离散、稀疏的值,不像图片一样是连续、稠密的。输入原始数据进行组合、变换得到的高阶特征并不是那么有效。而且有的语义并不来自数据,而来自人们的先验知识,所以利用先验知识构造的特征是很有帮助的。
  • 总的来说,在深度学习中,特征工程仍然适用;神经网络能对特征自动进行排列组合,所以只要输入一阶特征就行,省去了手动构造高阶特征的工作量。

6、模型评估

  • 模型评估很重要,是向别人证明你的模型怎么好的依据,我也就知道过拟合、欠拟合、交差验证、ROC、分类中查全率,查准率这些概念,没有个比较整体的认识,后面抽时间了解后再来补充。

二、常见的监督学习算法模型

哈哈,先留坑,后面慢慢填

1、LR(逻辑回归)

  • 线性模型的基础上增加了sigmoid函数,处理二分类问题,广义的线性模型,在CTR预估中有大作用

2、SVM(支持向量机)

  • 可以处理非线性问题,深度学习未火之前,是学术界和工业界的热点,

    数据规模较小时,能够构建出数据间的非线性关系,

    1、SVM的原始公式是如何由实际问题产生,算法的灵魂

    2、SVM原始问题到对偶问题的数学推导公式

3、NB(朴素贝叶斯)

  • 基于贝叶斯定理的一组有监督学习算法,朴素:“简单”地假设每对特征之间相互独立;
  • 在很多实际情况下,朴素贝叶斯工作得很好,特别是文档分类垃圾邮件过滤。这些工作都要求 一个小的训练集来估计必需参数。

http://sklearn.apachecn.org/cn/0.19.0/modules/naive_bayes.html


NSURLProtocol基础

一、概述

1、URL Loading System

URL-loading-system.png

  • URL Loading System 是一系列用来访问通过 URL 来定位的资源的类和协议。这项技术的核心在于基于 NSURL 这个类来访问资源,除了加载 URL 的类 NSURLSession 之外,我们把其他相关辅助类分为 5 类:
1
2
3
4
5
协议支持(protocol support)
认证和证书(authentication and credentials)
Cookie 存储(Cookie storage)
请求配置(configuration management)
缓存管理(cache management)
2、NSURLProtocol
  • NSURLProtocol是URL Loading System的重要组成部分;NSURLProtocol能拦截所有基于URL Loading System的网络请求;NSURLProtocol是一个抽象类。使用NSURLProtocol时候,需要创建它的子类。

  • NSURLProtocol可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。

  • 基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的。

  • 现在主流的iOS网络库,例如AFNetworking,Alamofire等网络库都是基于NSURLSession的,所以这些网络库的网络请求都可以被NSURLProtocol所拦截。

  • 至于还有一些年代比较久远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。

二、使用 NSURLProtocol

定义NSURLProtocol的子类,然后在子类(MYURLProtocol)中处理好注册—>拦截—>转发—>回调—>结束

1、注册

对于基于NSURLConnection或者使用[NSURLSession sharedSession]创建的网络请求,调用registerClass方法即可。

1
[NSURLProtocol registerClass:[NSClassFromString(@"MYURLProtocol") class]];

对于基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性。

1
2
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[[NSClassFromString(@"MYURLProtocol") class]];
2、拦截
  • NSURLProtocol可以在这个方法中拦截到网络请求,比如过滤掉(不处理)白名单的URL请求,比如只允许http/https请求。
  • 请求拦截Code Demo
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
//利用MYHTTPIdentifier避免 canInitWithRequest 和 canonicalRequestForRequest 出现死循环
static NSString * const MYHTTPIdentifier = @"MYHTTPIdentifier";

//每一个请求的时候都会调用这个方法,在这个方法里面判断这个请求是否需要被处理拦截,
//如果返回YES就代表这个request需要被处理,反之就是不需要被处理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:MYHTTPIdentifier inRequest:request] ) {
return NO;
}

NSString *scheme = [[request.URL scheme] lowercaseString];
if ([whiteUrlList contains:request.URL.absoluteString];){
return NO;
}

if ([scheme isEqual:@"http"] || [scheme isEqual:@"https"]) {
return YES;
}
return NO;
}

//返回规范的request的对象,我们可以对request进行处理。例如修改头部信息等。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

NSMutableURLRequest *mutableReqeust = [request mutableCopy];
//修改头部信息
NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy];
[headers setObject:@"ttf" forKey:@"i am ttf"];
request.allHTTPHeaderFields = headers;
//
[NSURLProtocol setProperty:@YES
forKey:MYHTTPIdentifier
inRequest:mutableReqeust];
return [mutableReqeust copy];
}
3、转发
  • 在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去,核心方法startLoading。
  • 在该方法中,我们把处理过的request重新发送出去。至于发送的形式,可以是基于NSURLSession。
1
2
3
- (void)startLoading {
//
}
4、回调
  • 在拦截了请求后,不能影响到原来网络请求的逻辑。所以将网络请求转发出去以后,当收到网络请求的返回后,要再将返回值返回给原来发送网络请求的地方。需要用到以下五个方法
1
2
3
4
5
6
7
8
9
[self.client URLProtocol:self didFailWithError:error];

[self.client URLProtocolDidFinishLoading:self];

[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

[self.client URLProtocol:self didLoadData:data];

[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
  • 假设我们在转发过程中是使用NSURLSession发送的网络请求,那么在NSURLSession的回调方法中,我们做相应的处理即可。并且我们也可以对这些返回,进行定制化处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {

[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
if (response != nil){
[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
}
5、结束
  • 在一个网络请求完全结束以后,NSURLProtocol回调用到stopLoading方法,在此方法中,完成在结束网络请求的操作。
1
2
3
- (void)stopLoading {
//
}

三、NSURLProtocol的应用

1、Mock网络请求
2、网络监控和相关数据统计
  • APM工具,如Bugly
3、其他
  • URL重定向
  • 实现HTTPDNS

四、NSURLProtocol的坑

1、死循环

原因:偶尔会出现canInitWithRequest方法多次调用的情况,不注意处理会发生死循环的问题,需要在canInitWithRequest方法中会判断是否拦截过请求。

解决办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) { //句1
return NO;
}
//....
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

//...
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request]; //句2
//...
return [request copy];
}

//句1和句2是为了防止死循环的,主要用于对request进行标记,如果被标记了,就不再次进行处理了,如果没有标记过就要进行处理。
2、多NSURLProtocol嵌套使用
  • 若一个项目中存在多个NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册的方式和顺序有关,对于使用registerClass方法注册的情况:多个NSURLProtocol拦截顺序为注册顺序的反序,即后注册的的NSURLProtocol先拦截。

  • 对于通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册的情况:protocolClasses这个数组里只有第一个NSURLProtocol会起作用,所以为了保证自定义的protocolClass有效,采用的办法是:把自己的NSURLProtocol插入到protocolClasses的第一个,进行拦截。拦截完成之后,又进行移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig {
// Runtime check to make sure the API is available on this version
if ([sessionConfig respondsToSelector:@selector(protocolClasses)]
&& [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]){
//
NSMutableArray *urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];

Class protoCls = OHHTTPStubsProtocol.class;
if (enable && ![urlProtocolClasses containsObject:protoCls]){
[urlProtocolClasses insertObject:protoCls atIndex:0];
}else if (!enable && [urlProtocolClasses containsObject:protoCls]){
[urlProtocolClasses removeObject:protoCls];
}
sessionConfig.protocolClasses = urlProtocolClasses;
}else{
NSLog("")
}
}
3、NSURLProtocol不能拦截WKWebView中请求
  • WKWebView中的请求走得是WebKit内核,不走在App进程中,一般情况下,在App中不能通过NSURLProtocol拦截请求;
  • 实践中,通过WebKit2的私有方法,让WKWebView的请求被NSURLProtocol拦截,但是有post请求body数据被清空的坑。
4、NSURLSession的坑

在NSURLProtocol中使用NSURLSession有很多莫名其妙的问题,基本上都是系统的bug,比较明显的就是:

  • 拦截到的Request中的HTTPBody为nil;
  • startLoading在某些特殊情况会出现死锁;
  • 关于注册registerClass方法只适用于sharedSession创建的网络请求;

五、参考

NSURLProtocol全攻略

黑魔法NSURLProtocol

iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求

####