阅读视图
iOS应用数据持久化 FMDB
iOS应用数据持久化 SQLite
iOS应用数据持久化 SQLite
一、前言
虽然平时开发不会直接使用
SQLite,但是作为一个轻量级数据库,理解和掌握其基本原理和SQL语句基本使用还是有必要的。FMDB就是基于SQLite的封装,并且微信团队在自研自己的数据库前也是使用SQLite。
点击项目名称 -> TAGETS -> Build Phases -> Link Binary With Libraries 点击添加libsqlite3.tbd:
二、代码准备
@interface ViewController (){
/**数据库*/
sqlite3 *database;
/**准备语句*/
sqlite3_stmt *stmt;
}
@property(nonatomic, strong) NSString *databasePath;
/**id文本框*/
@property (weak, nonatomic) IBOutlet UITextField *idTF;
/**名字文本框*/
@property (weak, nonatomic) IBOutlet UITextField *nameTF;
/**年龄文本框*/
@property (weak, nonatomic) IBOutlet UITextField *ageTF;
模拟器先拉好UI:
1. 生成路径
- (NSString *)databasePath{
if (!_databasePath) {
/**获取Document路径*/
NSArray *Paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *DocumentPath = [Paths firstObject];
/**拼接数据库位置*/
_databasePath = [DocumentPath stringByAppendingPathComponent:@“person_info.sqlite"];
NSLog(@"%@",_databasePath);
}
return _databasePath;
}
2. 打开数据库
/**打开数据库*/
- (void)openDatabase{
/**创建或打开数据库*/
int openFlag = sqlite3_open_v2(self.databasePath.UTF8String, &database, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, NULL);
/**判断是否成功*/
if (openFlag != SQLITE_OK) {
/**失败则关闭数据库*/
sqlite3_finalize(stmt);
sqlite3_close(self->database);
NSLog(@"创建或打开数据库失败 --- %d",openFlag);
}else{
NSLog(@"创建或打开数据库成功");
[self createForm];
}
}
3. 创建表
/** 创建表*/
- (void)createForm{
char *errMsg = NULL;
/** 创建表
1、建表格式: create table if not exists 表名 (列名 类型,....)
2、如需生成默认增加的id: id integer primary key autoincrement
3、数据库名、表名、字段名使用小写,关键字、函数名称使用大写
4、每个sql语句最后都要加上”;“
*/
NSString *sqlStr = [NSString stringWithFormat:
@"CREATE TABLE IF NOT EXISTS 'person'("
"id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
"name TEXT NOT NULL,"
"age INTEGER NOT NULL);"];
/**
第1个参数:数据库对象
第2个参数:sql语句
第3个参数:查询时候用到的一个结果集闭包
第4个参数:用不到
第5个参数:错误信息
*/
int execRst = sqlite3_exec(database, sqlStr.UTF8String, NULL, NULL, &errMsg);
if (execRst == SQLITE_OK) {
NSLog(@"创建表成功");
}else{
NSLog(@"创建表失败:%s",errMsg);
}
}
运行打印:
Sqlite3Demo[46764:3296271] /Users/xxxx/Library/Developer/CoreSimulator/Devices/…/data/Containers/Data/Application/…/Documents/Person.sqlite
Sqlite3Demo[46764:3296271] 创建或打开数据库成功
Sqlite3Demo[46764:3296271] 创建表成功
打开生成路径,可以看到一个person_info.sqlite的文件:
用Navicat打开person_info.sqlite,person的表已经创建好了:
4. 插入多条测试数据
/**插入多条数据*/
- (IBAction)insertMultipleData:(UIButton *)sender{
[self openDatabase];
for (int i = 0; i < 10; i++) {
NSString *name = [NSString stringWithFormat:@"张%d",i+1];
int age = arc4random_uniform(20) + 10;
// 拼接 sql 语句
NSString *sql = [NSString stringWithFormat:@"INSERT INTO person (name,age) VALUES ('%@',%d);",name,age];
// 执行 sql 语句
char *errMsg = NULL;
int result = sqlite3_exec(database, sql.UTF8String, NULL, NULL, &errMsg);
if (result == SQLITE_OK) {
NSLog(@"插入数据成功 - %@",name);
} else {
NSLog(@"插入数据失败 - %s",errMsg);
}
}
}
点击模拟器增多条:
Sqlite3Demo[47220:3310420] 插入数据成功 - 张1
Sqlite3Demo[47220:3310420] 插入数据成功 - 张2
Sqlite3Demo[47220:3310420] 插入数据成功 - 张3
Sqlite3Demo[47220:3310420] 插入数据成功 - 张4
Sqlite3Demo[47220:3310420] 插入数据成功 - 张5
Sqlite3Demo[47220:3310420] 插入数据成功 - 张6
Sqlite3Demo[47220:3310420] 插入数据成功 - 张7
Sqlite3Demo[47220:3310420] 插入数据成功 - 张8
Sqlite3Demo[47220:3310420] 插入数据成功 - 张9
Sqlite3Demo[47220:3310420] 插入数据成功 - 张10
5. 添加查询
/**查询所有记录*/
- (IBAction)retrieveAllData:(UIButton *)sender {
[self openDatabase];
NSString *searchSqlStr = @"SELECT * FROM person";
[self operationData:searchSqlStr.UTF8String];
}
/**操作数据*/
- (void)operationData:(const char *)sql{
int result = sqlite3_prepare_v2(database, sql, -1, &stmt, nil);
if (result != SQLITE_OK) {
NSLog(@"操作失败,%d",result);
} else {
/**打印操作后的数据,每调用一次sqlite3_step,stmt就会指向下一条记录*/
printf("------------------------------\n");
/**找到一条记录*/
while (sqlite3_step(stmt) == SQLITE_ROW) {
/**取出第0列字段的值*/
int ID = sqlite3_column_int(stmt, 0);
/**取出第1列字段的值*/
const unsigned char *name = sqlite3_column_text(stmt, 1);
/*取出第2列字段的值*/
int age = sqlite3_column_int(stmt, 2);
printf("查到的数据 id:%d name:%s 年龄:%d\n",ID,name,age);
}
/*关闭连接*/
[self clearTextField];
sqlite3_finalize(stmt);
sqlite3_close(self->database);
}
}
点击查全部:
------------------------------
查到的数据 id:1 name:张1 年龄:29
查到的数据 id:2 name:张2 年龄:16
查到的数据 id:3 name:张3 年龄:26
查到的数据 id:4 name:张4 年龄:29
查到的数据 id:5 name:张5 年龄:18
查到的数据 id:6 name:张6 年龄:26
查到的数据 id:7 name:张7 年龄:20
查到的数据 id:8 name:张8 年龄:17
查到的数据 id:9 name:张9 年龄:14
查到的数据 id:10 name:张10 年龄:26
到Navicat里看一下,已经有10条记录:
还有一个sqlite_sequence,seq是10:
注:如果没有看到数据,点击左边下面的按钮刷新,右键刷新无效。
三、CRUD增删改查操作
1. 增加一条数据
/**插入一条记录*/
- (IBAction)insertData:(UIButton *)sender {
[self openDatabase];
NSString *insertSplStr = [NSString stringWithFormat:@"INSERT INTO person (name,age) VALUES ('%@',%@);",_nameTF.text,_ageTF.text];
[self updateDataWithSql:insertSplStr.UTF8String success:^{
[self clearTextField];
NSLog(@"插入数据成功");
}];
}
name填入夏洛特,age填24,点击增:
Sqlite3Demo[47564:3319611] 插入数据成功
点击查全部,发现已经插入了一条数据:
------------------------------
查到的数据 id:1 name:张1 年龄:29
查到的数据 id:2 name:张2 年龄:16
查到的数据 id:3 name:张3 年龄:26
查到的数据 id:4 name:张4 年龄:29
查到的数据 id:5 name:张5 年龄:18
查到的数据 id:6 name:张6 年龄:26
查到的数据 id:7 name:张7 年龄:20
查到的数据 id:8 name:张8 年龄:17
查到的数据 id:9 name:张9 年龄:14
查到的数据 id:10 name:张10 年龄:26
查到的数据 id:11 name:夏洛特 年龄:24
回到Navicat的person表点击刷新,可以看到新增的一条记录:
2. 删除一条数据
/**删除一条记录*/
- (IBAction)deleteData:(UIButton *)sender {
[self openDatabase];
NSString *deleteSplStr = [NSString stringWithFormat:@"DELETE FROM person where id = %@;",self.idTF.text];
[self updateDataWithSql:deleteSplStr.UTF8String success:^{
[self clearTextField];
NSLog(@"删除数据成功");
}];
}
把id = 5 的数据删除掉:
点击删除:
Sqlite3Demo[47564:3319611] 删除数据成功
查看全部数据,发现id为5的数据已经没有了:
------------------------------
查到的数据 id:1 name:张1 年龄:29
查到的数据 id:2 name:张2 年龄:16
查到的数据 id:3 name:张3 年龄:26
查到的数据 id:4 name:张4 年龄:29
查到的数据 id:6 name:张6 年龄:26
查到的数据 id:7 name:张7 年龄:20
查到的数据 id:8 name:张8 年龄:17
查到的数据 id:9 name:张9 年龄:14
查到的数据 id:10 name:张10 年龄:26
查到的数据 id:11 name:夏洛特 年龄:24
在Navicate里查看:
3. 修改一条数据
/**更新一条记录*/
- (IBAction)updateData:(UIButton *)sender {
[self openDatabase];
NSString *changeSqlStr = [NSString stringWithFormat:@"UPDATE person SET name = '%@' WHERE id = '%@';",self.nameTF.text,self.idTF.text];
[self updateDataWithSql:changeSqlStr.UTF8String success:^{
[self clearTextField];
NSLog(@"修改成功");
}];
}
/**更新记录操作*/
- (void)updateDataWithSql:(const char *)sql success:(void(^)(void))successBlock {
/**
第1个参数:一个已经打开的数据库对象
第2个参数:sql语句
第3个参数:参数2中取出多少字节的长度,-1 自动计算,\0停止取出
第4个参数:准备语句
第5个参数:通过参数3,取出参数2的长度字节之后,剩下的字符串
*/
int result = sqlite3_prepare_v2(database, sql, -1, &stmt, nil);
if (result != SQLITE_OK) {
NSLog(@"操作失败,%d",result);
sqlite3_finalize(stmt);
sqlite3_close(self->database);
} else {
sqlite3_step(stmt);
successBlock();
}
}
把id = 10的名字改为赵云:
查看全部:
------------------------------
查到的数据 id:1 name:张1 年龄:29
查到的数据 id:2 name:张2 年龄:16
查到的数据 id:3 name:张3 年龄:26
查到的数据 id:4 name:张4 年龄:29
查到的数据 id:6 name:张6 年龄:26
查到的数据 id:7 name:张7 年龄:20
查到的数据 id:8 name:张8 年龄:17
查到的数据 id:9 name:张9 年龄:14
查到的数据 id:10 name:赵云 年龄:26
查到的数据 id:11 name:夏洛特 年龄:24
4. 查找检索数据
/**查询记录*/
- (IBAction)retrieveData:(UIButton *)sender {
[self openDatabase];
/**
查询age < 25也可使用sql= "select * from person where age < 25"
*/
NSString *sqlStr;
if(_idTF.text.length > 0){
sqlStr = [NSString stringWithFormat:@"SELECT id, name, age FROM person WHERE id = '%@';",_idTF.text];
}
else if(_nameTF.text.length > 0){
sqlStr = [NSString stringWithFormat:@"SELECT id, name, age FROM person WHERE name = '%@';",_nameTF.text];
}
else if(_ageTF.text.length > 0){
sqlStr = [NSString stringWithFormat:@"SELECT id,name,age FROM person WHERE age %@;",_ageTF.text];
NSLog(@"sqlStr == %@",sqlStr);
}
[self operationData:sqlStr.UTF8String];
}
查id = 4的记录:
------------------------------
查到的数据 id:4 name:张4 年龄:29
查年龄小于25的记录:
点击查按钮:
------------------------------
查到的数据 id:2 name:张2 年龄:16
查到的数据 id:7 name:张7 年龄:20
查到的数据 id:8 name:张8 年龄:17
查到的数据 id:9 name:张9 年龄:14
查到的数据 id:11 name:夏洛特 年龄:24
查找名字赵云:
------------------------------
查到的数据 id:10 name:赵云 年龄:26
点击增加多条数据测试,再查看全部数据:
------------------------------
查到的数据 id:1 name:张1 年龄:29
查到的数据 id:2 name:张2 年龄:16
查到的数据 id:3 name:张3 年龄:26
查到的数据 id:4 name:张4 年龄:29
查到的数据 id:6 name:张6 年龄:26
查到的数据 id:7 name:张7 年龄:20
查到的数据 id:8 name:张8 年龄:17
查到的数据 id:9 name:张9 年龄:14
查到的数据 id:10 name:赵云 年龄:26
查到的数据 id:11 name:夏洛特 年龄:24
查到的数据 id:12 name:张1 年龄:21
查到的数据 id:13 name:张2 年龄:11
查到的数据 id:14 name:张3 年龄:20
查到的数据 id:15 name:张4 年龄:28
查到的数据 id:16 name:张5 年龄:25
查到的数据 id:17 name:张6 年龄:24
查到的数据 id:18 name:张7 年龄:22
查到的数据 id:19 name:张8 年龄:29
查到的数据 id:20 name:张9 年龄:14
查到的数据 id:21 name:张10 年龄:26
查看Navicat的person:
查看squence,点击刷新,id为21完全一致:
点击清空:
/**清空表中数据和自增排序*/
- (IBAction)removeAllData:(UIButton *)sender {
[self openDatabase];
const char *deleteSpl = "DELETE FROM person";
[self updateDataWithSql:deleteSpl success:^{
NSLog(@"清空表数据成功");
}];
}
表数据已经被清空:
这个时候sqlite_sequence还是21,没有归0:
此时点击插入10条数据,发现id从21开始:
并没有真正的清空,需要给数据添加seq归0操作:
sqlite3 没有truncate关键字:
/**清空表中数据和自增排序*/
- (IBAction)removeAllData:(UIButton *)sender {
[self openDatabase];
const char *deleteSpl = "DELETE FROM person";
[self updateDataWithSql:deleteSpl success:^{
NSLog(@"清空表数据成功");
const char *setSeqSpl = "UPDATE sqlite_sequence SET seq = 0 WHERE name = 'person'";
[self updateDataWithSql:setSeqSpl success:^{
NSLog(@"自增主键归零");
}];
}];
}
再点击清空:
点击插入多条,id又从1开始:
四、总结
SQLite接口都是C语言,并不面向对象,所有操作需要自己手写SQL语句。
参考
对象序列化
对象序列化
一、NSCoding
有时需要对自定义的类对象进行数据持久化存储,但是 NSUserDefaults 只能对系统的数据类型进行存储,而且还有存储延迟的问题,它本质就是一个 plist 文件。自定义的 plist 文件,一般都是保存配置或者是把一些不怎么需要变动的列表写进 plist 里面,然后列表根据 plist 的结构去读取,也实现不了想要的功能。而 NSCoding 是使自定义对象能够被编码和解码以进行归档和分发的协议,可以实现存储的目的。
NSCoding协议声明了一个类必须实现的两个方法,这样该类的实例才能被编码和解码。这种功能为归档(对象和其他结构存储在磁盘上)和分发(对象被复制到不同的地址空间)提供了基础。根据面向对象的设计原则,被编码或解码的对象负责对其实例变量进行编码和解码。编码器通过调用encodeWithCoder:或initWithCoder:来指示对象这样做。encodeWithCoder:指示对象将其实例变量编码到所提供的编码器;对象可以接收此方法任意次数。initWithCoder:指示对象从提供的编码器中的数据初始化自身;因此,它取代了任何其他初始化方法,并且每个对象只发送一次。任何可编码的对象类都必须采用NSCoding协议并实现其方法。
定义一个用户类:
@interface User : NSObject<NSCoding>
@property (nonatomic, strong) NSString *registerName;
@property (nonatomic, strong) NSString *nickname;
@property (nonatomic, strong) NSString *phoneNumber;
@property (nonatomic, assign) BOOL isMember;
@property (nonatomic, assign) int balance;
@end
.m 文件进行归档编码操作:
@implementation User
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:_registerName forKey:@"registerName"];
[coder encodeObject:_nickname forKey:@"nickname"];
[coder encodeObject:_phoneNumber forKey:@"phoneNumber"];
[coder encodeBool:_isMember forKey:@"isMember"];
[coder encodeInt:_balance forKey:@"balance"];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
if(self = [super init]){
if(coder){
_registerName = [coder decodeObjectOfClass:[NSString class]
forKey:@"registerName"];
_nickname = [coder decodeObjectOfClass:[NSString class]
forKey:@"nickname"];
_phoneNumber = [coder decodeObjectOfClass:[NSString class]
forKey:@"phoneNumber"];
_isMember = [coder decodeBoolForKey:@"isMember"];
_balance = [coder decodeIntForKey:@"balance"];
}
}
return self;
}
@end
模拟器添加两个按钮,一个写入,一个读取:
点击把自定义数据存进本地:
- (IBAction)insertData:(UIButton *)sender {
User *user = [User new];
user.registerName = @"孙悟空";
user.nickname = @"猴子";
user.phoneNumber = @"01234";
user.isMember = YES;
user.balance = 123;
User *user2 = [User new];
user2.registerName = @"庄周";
user2.nickname = @"鱼";
user2.phoneNumber = @"01245";
user2.isMember = YES;
user2.balance = 111;
NSArray <User *>*userArr = [NSArray arrayWithObjects:user,user2,
nil];
NSData *perData = [NSKeyedArchiver archivedDataWithRootObject:
userArr];
// 写入本地
NSString *path = [NSHomeDirectory()
stringByAppendingPathComponent:@"User.archiver"];
BOOL result = [[NSFileManager defaultManager]createFileAtPath:path
contents:nil attributes:nil];
if (result){
[perData writeToFile:path atomically:YES];
}
}
点击把数写入本地的自定义数据取出来:
- (IBAction)getData:(UIButton *)sender {
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:
@"User.archiver"];
NSData *data = [NSData dataWithContentsOfFile:path];
NSArray <User *>*userArr = [NSKeyedUnarchiver
unarchiveObjectWithData:data];
NSLog(@"userArr = %@",userArr);
for (User *user in userArr){
NSLog(@"\n - 注册名称:%@\n - 昵称:%@\n - 号码
:%@\n - 是否会员:%d\n - 账户余额:%d", user.registerName,
user.nickname,
user.phoneNumber,
user.isMember,
user.balance
);
}
}
点击写入数据,运行打印:
NSCodingDemo[48440:1221308] 写入数据成功
点击读取数据:
NSCodingDemo[48440:1221308] userArr = (
"<User: 0x600002c89950>",
"<User: 0x600002c885a0>"
)
NSCodingDemo[48440:1221308]
- 注册名称:孙悟空
- 昵称:猴子
- 号码:01234
- 是否会员:1
- 账户余额:123
NSCodingDemo[48440:1221308]
- 注册名称:庄周
- 昵称:鱼
- 号码:01245
- 是否会员:1
- 账户余额:111
二、废弃提醒
存档方法废弃提醒:
修改存档方法:
// NSData *perData = [NSKeyedArchiver archivedDataWithRootObject:
userArr];
NSError *error = nil;
NSData *perData = [NSKeyedArchiver archivedDataWithRootObject:
userArr requiringSecureCoding:YES error:&error];
解档方法废弃提醒:
修改废弃提醒:
// NSArray <User *>*userArr = [NSKeyedUnarchiver unarchiveObjectWith
//Data:data];
NSError *error = nil;
NSArray <User *>*userArr = [NSKeyedUnarchiver unarchivedArrayOfObjects
OfClass:[User class] fromData:data error:&error];
重新运行:
NSCodingDemo[48863:1233999] 写入数据成功
NSCodingDemo[48863:1233999] userArr = (null)
没能读取数据:
研究了一下是要改成
NSSecureCoding,由于这种技术可能是不安全的,因为当您可以验证类类型时,对象已经构造好了,并且如果它是集合类的一部分,则可能插入到对象图中。为了符合NSSecureCoding: 不重写initWithCoder:的对象可以不做任何更改地符合NSSecureCoding(假设它是另一个符合NSSecureCoding的类的子类)。覆盖initWithCoder:的对象必须使用decodeObjectOfClass:forKey:方法解码任何包含的对象。例如:id obj = [decoder decodeObjectOfClass:[MyClass类] forKey: @“myKey”);此外,该类必须重写其supportsSecureCoding属性的getter以返回YES。
三、NSSecureCoding
一种能够以一种健壮的方式对对象替换攻击进行编码和解码的协议。
把 User 遵循协议改成 NSSecureCoding:
@interface User : NSObject<NSSecureCoding>
添加支持安全编码:
+ (BOOL)supportsSecureCoding{
return YES;
}
运行测试:
userArr = (
"<User: 0x600003f6ebe0>",
"<User: 0x600003f6ea30>"
)
NSCodingDemo[54390:1372398]
- 注册名称:孙悟空
- 昵称:猴子
- 号码:01234
- 是否会员:1
- 账户余额:123
NSCodingDemo[54390:1372398]
- 注册名称:庄周
- 昵称:鱼
- 号码:01245
- 是否会员:1
- 账户余额:111
四、YYCacheDemo适配问题
由于 YYCache 很久不更新,在 YYDiskCache 里面,- (id<NSSecureCoding>)objectForKey:(NSString *)key 方法里面,也是提示方法废弃:
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
下面的 - (void)setObject:(id<NSSecureCoding>)object forKey:(NSString *)key 方法里面,也是提示方法废弃:
value = [NSKeyedArchiver archivedDataWithRootObject:object requiring
SecureCoding:YES error:&error];
解档添加版本适配:
if (@available(iOS 12.0, *)) {
NSError *error = nil;
value = [NSKeyedArchiver archivedDataWithRootObject:object
requiringSecureCoding:YES
error:&error];
} else {
// 消除方法弃用(过时)的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// 要消除警告的代码
value = [NSKeyedArchiver archivedDataWithRootObject:object];
#pragma clang diagnostic pop
}
归档添加版本适配:
if (@available(iOS 12.0, *)) {
NSError *error = nil;
object = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet
setWithArray:@[NSObject.class]] fromData:item.value error:&error];
}
else {
// 消除方法弃用(过时)的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// 要消除警告的代码
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
#pragma clang diagnostic pop
}
把 YYCache 里面所有的 NSCoding 改成 NSSecureCoding,在自己自定义的类里面也添加支持安全编码:
+ (BOOL)supportsSecureCoding{
return YES;
}
运行测试,发现找不到自定义的类:
就是需要指定一个具体的类名,让他做解码操作,由于 YYCache 是 pod 下来的,不能直接导入文件,为了避免相互引用,只用 runtime 获取类:
object = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet
setWithArray:@[objc_getClass("ContactsModel")]] fromData:item.value
error:&error]
运行测试:
YYCacheDemo[57594:1453699] disk name = 张0 phoneNumber = 15888899990
YYCacheDemo[57594:1453699] disk name = 张1 phoneNumber = 15888899991
YYCacheDemo[57594:1453699] disk name = 张2 phoneNumber = 15888899992
YYCacheDemo[57594:1453699] disk name = 张3 phoneNumber = 15888899993
YYCacheDemo[57594:1453699] disk name = 张4 phoneNumber = 15888899994
YYCacheDemo[57594:1453699] disk name = 张5 phoneNumber = 15888899995
YYCacheDemo[57594:1453699] disk name = 张6 phoneNumber = 15888899996
YYCacheDemo[57594:1453699] disk name = 张7 phoneNumber = 15888899997
YYCacheDemo[57594:1453699] disk name = 张8 phoneNumber = 15888899998
YYCacheDemo[57594:1453699] disk name = 张9 phoneNumber = 15888899999
没有警告,运行正常。
五、优化代码
如果后面需要添加新的 model,就需要继续给 NSKeyedUnarchiver unarchivedObjectOfClasses: 方法添加解档归档类名,需要经常修改原始框架代码,这样对于维护不利,因为重新添加一个类,专门处理添加解档归档 model 类:
#import "YYModelSet.h"
#import <objc/runtime.h>
@implementation YYModelSet
+ (YYModelSet *)getClasses{
return (YYModelSet *)[NSSet setWithArray:@[objc_getClass
("ContactsModel")]];
}
YYDiskCache 改为:
object = [NSKeyedUnarchiver unarchivedObjectOfClasses:
[YYModelSet getClasses] fromData:item.value error:&error];
这样,后面如果新增需要解档,归档的类,只需要修改自己新增 YYModelSet 类方法即可,不动原来 YYCache 的代码。
YYCache(二)
YYCache(二)
代码测试:
初始化YYCache实例:
#import <YYCache/YYCache.h>
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) YYCache *contactsCache;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.contactsCache = [YYCache cacheWithName:@"Contacts”];
}
添加一个通讯录模型:
@interface ContactsModel : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *phoneNumber;
@end
添加10条数据:
for (int i = 0; i < 10; i ++) {
ContactsModel *model = [ContactsModel new];
model.name = [NSString stringWithFormat:@"张%d",i];
model.phoneNumber = [NSString stringWithFormat:@"1588889999%d",i];
[self.contactsCache setObject:model forKey:
[NSString stringWithFormat:@"kContacts_%d",i]];
}
提示Sending 'ContactsModel *__strong' to parameter of incompatible type 'id<NSCoding> _Nullable:
给ContactsModel添加<NSCoding>协议:
@interface ContactsModel : NSObject<NSCoding>
//通讯类内部的两个属性变量分别转码
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:_name forKey:@"name"];
[coder encodeObject:_phoneNumber forKey:@"phoneNumber"];
}
//分别把两个属性变量根据关键字进行逆转码,最后返回一个Contacts类的对象
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
if (self = [super init]) {
if (coder) {
_name = [coder decodeObjectOfClass:[NSString class]
forKey:@"name"];
_phoneNumber = [coder decodeObjectOfClass:[NSString class]
forKey:@"phoneNumber"];
}
}
return self;
}
提示消失,用循环把值取出来:
for (int i = 0; i < 10; i++) {
ContactsModel *model = (ContactsModel *)[self.contactsCache
objectForKey:[NSString stringWithFormat:@"kContacts_%d",i]];
NSLog(@"name = %@ phoneNumber = %@",model.name, model.phoneNumber);
}
运行打印:
YYCacheDemo[3304:73220] name = 张0 phoneNumber = 15888899990
YYCacheDemo[3304:73220] name = 张1 phoneNumber = 15888899991
YYCacheDemo[3304:73220] name = 张2 phoneNumber = 15888899992
YYCacheDemo[3304:73220] name = 张3 phoneNumber = 15888899993
YYCacheDemo[3304:73220] name = 张4 phoneNumber = 15888899994
YYCacheDemo[3304:73220] name = 张5 phoneNumber = 15888899995
YYCacheDemo[3304:73220] name = 张6 phoneNumber = 15888899996
YYCacheDemo[3304:73220] name = 张7 phoneNumber = 15888899997
YYCacheDemo[3304:73220] name = 张8 phoneNumber = 15888899998
YYCacheDemo[3304:73220] name = 张9 phoneNumber = 15888899999
打印self.contactsCache.memoryCache和self.contactsCache.diskCache,发现数据一样
主要功能:
一、添加限制
二、数据修剪
YYMemoryCache:
YYDiskCach:
测试给内存添加数量限制:
self.contactsCache.memoryCache.countLimit = 5;
YYCacheDemo[4272:102918] name = (null) phoneNumber = (null)
YYCacheDemo[4272:102918] name = (null) phoneNumber = (null)
YYCacheDemo[4272:102918] name = (null) phoneNumber = (null)
YYCacheDemo[4272:102918] name = (null) phoneNumber = (null)
YYCacheDemo[4272:102918] name = (null) phoneNumber = (null)
YYCacheDemo[4272:102918] name = 张5 phoneNumber = 15888899995
YYCacheDemo[4272:102918] name = 张6 phoneNumber = 15888899996
YYCacheDemo[4272:102918] name = 张7 phoneNumber = 15888899997
YYCacheDemo[4272:102918] name = 张8 phoneNumber = 15888899998
YYCacheDemo[4272:102918] name = 张9 phoneNumber = 15888899999
发现前面的
5个数据都被移除了
修剪最大个数为8个:
[self.contactsCache.memoryCache trimToCount:8];
运行:
YYCacheDemo[4461:108978] name = (null) phoneNumber = (null)
YYCacheDemo[4461:108978] name = (null) phoneNumber = (null)
YYCacheDemo[4461:108978] name = 张2 phoneNumber = 15888899992
YYCacheDemo[4461:108978] name = 张3 phoneNumber = 15888899993
YYCacheDemo[4461:108978] name = 张4 phoneNumber = 15888899994
YYCacheDemo[4461:108978] name = 张5 phoneNumber = 15888899995
YYCacheDemo[4461:108978] name = 张6 phoneNumber = 15888899996
YYCacheDemo[4461:108978] name = 张7 phoneNumber = 15888899997
YYCacheDemo[4461:108978] name = 张8 phoneNumber = 15888899998
YYCacheDemo[4461:108978] name = 张9 phoneNumber = 15888899999
清空所有缓存:
[self.contactsCache removeAllObjects];
内存缓存:
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] memory name = (null) phoneNumber = (null)
磁盘缓存:
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
YYCacheDemo[6819:162854] disk name = (null) phoneNumber = (null)
源码:
YYMemoryCache的初始化:
- (instancetype)init {
self = super.init;
pthread_mutex_init(&_lock, NULL);
_lru = [_YYLinkedMap new];
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0;
_shouldRemoveAllObjectsOnMemoryWarning = YES;
_shouldRemoveAllObjectsWhenEnteringBackground = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
[self _trimRecursively];
return self;
}
一个_YYLinkedMap的实例,查看_YYLinkedMap:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
发现_YYLinkedMap是一个双向链表,有两个_YYLinkedMapNode类型节点,还有一个CFMutableDictionaryRef的字典_dic,_dic是真正存放数据的地方。
递归修剪:
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
- (void)_trimInBackground {
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
每隔
_autoTrimInterval秒就自动调用修整内存数据,_autoTrimInterval默认是5秒。
添加数据:
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
….
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
if (node) {
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
[_lru bringNodeToHead:node];
} else {
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
…
pthread_mutex_unlock(&_lock);
}
由于_lru = [_YYLinkedMap new]; ,可以看到就是操作_YYLinkedMap双链表,使用的是pthread_mutex锁。
YYCache(一)
YYCache(一)
前言:
我们一般对网络请求下来的比较大的数据做缓存,如果没有网络,或者是请求到的标识和之前的标识一致,表示数据没有变动,则可以使用缓存加载,不需要重新网络拉取数据,这里一般使用YYCache。
从git上把YYCache pod下来:
可以看到
YYCache的文件结构还是相对简单,除了YYCache这个对外使用的接口文件,还有YYDiskCache这个磁盘缓存,YYKVStorage这个元数据键值存储,还有YYMemoryCache这个内存缓存。
一、YYCache:
YYCache是一个线程安全的键值缓存。 它使用YYMemoryCache将对象存储在一个小而快速的内存缓存中, 并使用YYDiskCache将对象持久化到一个大而慢的磁盘缓存中。
属性列表:
属性就三个:
@interface YYCache : NSObject
/** 缓存名字,只读*/
@property (copy, readonly) NSString *name;
/** 内存缓存.*/
@property (strong, readonly) YYMemoryCache *memoryCache;
/** 磁盘缓存.*/
@property (strong, readonly) YYDiskCache *diskCache;
方法列表:
初始化:
初始化:
/**
用指定的名称创建一个新实例。
*/
- (nullable instancetype)initWithName:(NSString *)name;
/**
用指定的路径创建一个新实例。
*/
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
/**
便捷初始化,用指定的名称创建一个新实例。
*/
+ (nullable instancetype)cacheWithName:(NSString *)name;
/**
便捷初始化,用指定的路径创建一个新实例
*/
+ (nullable instancetype)cacheWithPath:(NSString *)path;
访问方法:
/**
返回一个布尔值,一个指定的键是否在缓存中,可能会阻塞线程直到文件读取完成。
*/
- (BOOL)containsObjectForKey:(NSString *)key;
/**
返回指定键相对应的值。
*/
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
/**
设置缓存中指定键的值。
*/
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
/**
删除缓存中指定键的值,如果为nil,则此方法无效。
*/
- (void)removeObjectForKey:(NSString *)key;
/**
清空缓存。
*/
- (void)removeAllObjects;
/**
用block清空缓存。可以通过参数得到进度。
*/
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
看.m文件的初始化方法initWithName:
- (instancetype)initWithName:(NSString *)name {
if (name.length == 0) return nil;
NSString *cacheFolder = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
NSString *path = [cacheFolder stringByAppendingPathComponent:name];
return [self initWithPath:path];
}
发现initWithName会调用另一个初始化方法initWithPath和YYMemoryCache的初始化方法:
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}
查看initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold发现:
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
…
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
_inlineThreshold = threshold;
…
}
在
YYDiskCache中可以看到内联阈值是20KB,_inlineThreshold被初始化为20KB。
点进YYKVStorageType看,发现有三种存储类型:
一、文件
二、数据库
三、自选
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
/// The `value` is stored as a file in file system.
YYKVStorageTypeFile = 0,
/// The `value` is stored in sqlite with blob type.
YYKVStorageTypeSQLite = 1,
/// The `value` is stored in file system or sqlite based on your choice.
YYKVStorageTypeMixed = 2,
};
再看赋值:
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
…
NSData *value = nil;
if (!value) return;
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
可以看到value.length > _inlineThreshold,filename会被赋值,点击saveItemWithKey方法:
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
…
if (filename.length) {
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
可以看到filename.length如果存在值,就直接写成文件,如果不大于20KB,使用sqlite写入。
作者:NSURLCache、FBDiskCache 都是基于 SQLite 数据库的。基于数据库的缓存可以很好的支持元数据、扩展方便、数据统计速度快,也很容易实现 LRU 或其他淘汰算法,唯一不确定的就是数据库读写的性能,为此我评测了一下 SQLite 在真机上的表现。iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
二、YYDiskCache:
(那些相同的参数和方法就不重新写)
属性列表:
/**
如果对象的数据大小(以字节为单位)大于此值,则对象将
存储为文件,否则对象将存储在sqlite中。
0表示所有对象将存储为分开的文件,NSUIntegerMax表示所有对象
对象将存储在sqlite中。
默认值为20480 (20KB)。
*/
@property (readonly) NSUInteger inlineThreshold;
/**
如果这个块不是nil,那么这个块将被用来存档对象
NSKeyedArchiver。您可以使用此块来支持不支持的对象
遵守' NSCoding '协议。
默认值为空。
*/
@property (nullable, copy) NSData *(^customArchiveBlock)(id object);
/**
如果这个块不是nil,那么这个块将被用来解存档对象
NSKeyedUnarchiver。您可以使用此块来支持不支持的对象
遵守' NSCoding '协议。
默认值为空。
*/
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);
/**
当需要将对象保存为文件时,将调用此块来生成
指定键的文件名。如果块为空,缓存使用md5(key)作为默认文件名。默认值为空。
*/
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key);
#pragma mark - Limit
/**
缓存应容纳的最大对象数。
默认值为NSUIntegerMax,即不限制。
这不是一个严格的限制-如果缓存超过限制,缓存中的一些对象
缓存可以稍后在后台队列中被清除。
*/
@property NSUInteger countLimit;
/**
在开始清除对象之前,缓存可以保留的最大总开销。
默认值为NSUIntegerMax,即不限制。
这不是一个严格的限制-如果缓存超过限制,缓存中的一些对象
缓存可以稍后在后台队列中被清除。
*/
@property NSUInteger costLimit;
/**
缓存中对象的最大过期时间。
>值为DBL_MAX,即无限制。
这不是一个严格的限制-如果一个对象超过了限制,对象可以
稍后在后台队列中被驱逐。
*/
@property NSTimeInterval ageLimit;
/**
缓存应保留的最小空闲磁盘空间(以字节为单位)。
>默认值为0,表示不限制。
如果可用磁盘空间低于此值,缓存将删除对象
释放一些磁盘空间。这不是一个严格的限制——如果空闲磁盘空间没有了
超过限制,对象可能稍后在后台队列中被清除。
*/
@property NSUInteger freeDiskSpaceLimit;
/**
自动修整检查时间间隔以秒为单位。默认值是60(1分钟)。
缓存保存一个内部计时器来检查缓存是否达到
它的极限,一旦达到极限,它就开始驱逐物体。
*/
@property NSTimeInterval autoTrimInterval;
/**
设置“YES”为调试启用错误日志。
*/
@property BOOL errorLogsEnabled;
方法列表:
/**
指定的初始化式。
threshold数据存储内联阈值,单位为字节。如果对象的数据
Size(以字节为单位)大于此值,则对象将存储为
文件,否则对象将存储在sqlite中。0表示所有对象都会
NSUIntegerMax表示所有对象都将被存储
sqlite。如果您不知道对象的大小,20480是一个不错的选择。
在第一次初始化后,您不应该更改指定路径的这个值。
如果指定路径的缓存实例在内存中已经存在,
该方法将直接返回该实例,而不是创建一个新实例。
*/
- (nullable instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;
/**
返回此缓存中的对象数量。
*/
- (NSInteger)totalCount;
/**
以字节为单位的对象开销总数。
*/
- (NSInteger)totalCost;
#pragma mark - 修剪
/**
使用LRU从缓存中移除对象,直到' totalCount '低于指定值。
*/
- (void)trimToCount:(NSUInteger)count;
/**
使用LRU从缓存中移除对象,直到' totalCost '低于指定值,完成后调用回调。
*/
- (void)trimToCost:(NSUInteger)cost;
/**
使用LRU从缓存中删除对象,直到所有过期对象被指定值删除为止。
*/
- (void)trimToAge:(NSTimeInterval)age;
/**
从对象中获取扩展数据。
*/
+ (nullable NSData *)getExtendedDataFromObject:(id)object;
/**
将扩展数据设置为一个对象。
*/
+ (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object;
@end
三、YYMemoryCache:
属性列表:
/** 存中的对象数量(只读)*/
@property (readonly) NSUInteger totalCount;
/** 缓存中对象的总开销(只读)*/
@property (readonly) NSUInteger totalCost;
/**
自动修整检查时间间隔以秒为单位。默认是5.0。
*/
@property NSTimeInterval autoTrimInterval;
/**
如果' YES ',当应用程序收到内存警告时,缓存将删除所有对象。
*/
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
/**
如果是,当应用程序进入后台时,缓存将删除所有对象。
*/
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
/**
当应用程序收到内存警告时要执行的块。
*/
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
/**
当应用程序进入后台时执行的一个块。
*/
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
/**
如果' YES ',键值对将在主线程上释放,否则在后台线程上释放。默认为NO。。
*/
@property BOOL releaseOnMainThread;
/**
如果' YES ',键值对将在主线程上释放,否则在后台线程上释放。默认为NO。
*/
@property BOOL releaseAsynchronously;
总结:
对比一下NSCache:
@interface NSCache <KeyType, ObjectType> : NSObject
@property (copy) NSString *name;
@property (nullable, assign) id<NSCacheDelegate> delegate;
- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
- (void)removeObjectForKey:(KeyType)key;
- (void)removeAllObjects;
@property NSUInteger totalCostLimit;// limits are imprecise/not strict
@property NSUInteger countLimit;// limits are imprecise/not strict
@property BOOL evictsObjectsWithDiscardedContent;
@end
@protocol NSCacheDelegate <NSObject>
@optional
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end
会发现NSCache相对简单很多,YYCache对内存和磁盘缓存给了很多个接口去准备控制缓存的数量和生命周期。
Fastlane自动化打包到蒲公英
Fastlane
fastlane是一个旨在简化 Android 和 iOS 部署的开源平台,可以自动化开发和发布工作流程的各个方面。
一、安装Xcode命令行工具
为 fastlane 安装Xcode命令行工具:
xcode-select --install
如果安装过会提示:
xcode-select: error: command line tools are already installed,
use "Software Update" in System Settings to install updates
二、安装fastlane
提前配置好
HomeBrew管理工具
brew install fastlane
查看 fastlane 版本:
fastlane -v
fastlane installation at path:
/usr/local/Cellar/fastlane/2.206.2/libexec/gems/fastlane-2.206.2/bin/fastlane
-----------------------------
[✔] 🚀
fastlane 2.206.2
将终端导航到项目目录并运行:
fastlane init
命令执行:
[✔] 🚀
[13:13:34]: Sending anonymous analytics information
[13:13:34]: Learn more at https://docs.fastlane.tools/#metrics
[13:13:34]: No personal or sensitive data is sent.
[13:13:34]: You can disable this by adding `opt_out_usage` at the top of your Fastfile
[✔] Looking for iOS and Android projects in current directory...
[13:13:34]: Created new folder './fastlane'.
[13:13:34]: Detected an iOS/macOS project in the current directory: 'FastlaneDemo.xcodeproj'
[13:13:34]: -----------------------------
[13:13:34]: --- Welcome to fastlane 🚀 ---
[13:13:34]: -----------------------------
[13:13:34]: fastlane can help you with all kinds of automation for your mobile app
[13:13:34]: We recommend automating one task first, and then gradually automating more over time
[13:13:34]: What would you like to use fastlane for?
1. 📸 Automate screenshots
2. 👩✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks
[13:13:34]你想用快车道做什么?
- 📸自动截屏
- 👩✈️自动测试分发TestFlight
- 🚀自动化应用商店分销
- 🛠手动设置-手动设置您的项目自动化您的任务
这边选择 4:
[14:29:49]: --- Setting up fastlane so you can manually configure it ---
[14:29:49]: ------------------------------------------------------------
[14:29:49]: Installing dependencies for you...
[14:29:49]: $ bundle update
[14:31:36]: --------------------------------------------------------
[14:31:36]: --- ✅ Successfully generated fastlane configuration ---
[14:31:36]: --------------------------------------------------------
[14:31:36]: Generated Fastfile at path `./fastlane/Fastfile`
[14:31:36]: Generated Appfile at path `./fastlane/Appfile`
[14:31:36]: Gemfile and Gemfile.lock at path `Gemfile`
项目里多了这三个文件:
三、安装蒲公英插件
fastlane add_plugin pgyer
注册蒲公英,拿到 API Key 和 User Key:
在上面看到的 fastlane 文件夹里面的 Fastfile 文件简单配置一下:
default_platform(:ios)
platform :ios do
desc "Description of what the lane does"
# 打包时候用的名称 例如 fastlane app
lane :develop do |options|
target = "FastlaneDemo"
configuration = "Debug"
gym(scheme: target, configuration: configuration, export_method:"development")
pgyer(api_key: "xxxxxxxxxx”)
end
end
终端运行:
fastlane develop
结果:
+------+------------------+-------------+
| fastlane summary |
+------+------------------+-------------+
| Step | Action | Time (in s) |
+------+------------------+-------------+
| 1 | default_platform | 0 |
| 2 | gym | 78 |
+------+------------------+-------------+
[17:39:32]: fastlane.tools finished successfully 🎉
可以看到上传蒲公英成功:
点击应用信息:
在项目本地也会生成一个ipa包
四、分发到Appstore
# 发布到appstore
lane :to_appstore do
# 先获取当前项目中的bundle version + 1
@build_version = get_info_plist_value(path: "#{$info_plist_path}", key: "CFBundleVersion").to_i + 1
# 针对于 iOS 项目开发证书和 Provision file 的下载工具
sigh(
force: true,
output_path: "./fastlane/crets"
)
# 设置 bundle version
set_info_plist_value(
path: "./so/Supporting Files/so-Info.plist",
key: "CFBundleVersion",
value: "#{@build_version}"
)
# 针对于 iOS 编译打包生成 ipa 文件
gym(
workspace: "#{$project_name}.xcworkspace",
scheme: "#{$project_name}",
clean: true,
configuration: "Release",
export_method: "app-store",
output_directory: "ipa_build/release",
output_name: "#{$project_abbreviation}"
)
# 用于上传应用的二进制代码,应用截屏和元数据到 App Store
deliver(
force: true,# 上传之前是否成html报告
submit_for_review: false,# 上传后自动提交审核
automatic_release: true,# 通过审后自动发布
skip_binary_upload: false,# 跳过上传二进制文件
skip_screenshots: true,# 是否跳过上传截图
skip_metadata: false,# 是否跳过元数据
)
end
运行:
[16:03:13]: $ bundle exec fastlane FastlaneDemo
[16:03:13]:
[16:03:13]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
+-----------------------+---------+--------+
| Used plugins |
+-----------------------+---------+--------+
| Plugin | Version | Action |
+-----------------------+---------+--------+
| fastlane-plugin-pgyer | 0.2.4 | pgyer |
+-----------------------+---------+--------+
[16:03:14]: ----------------------------------------
[16:03:14]: --- Step: Verifying fastlane version ---
[16:03:14]: ----------------------------------------
[16:03:14]: Your fastlane version 2.212.1 matches the minimum requirement of 2.68.2 ✅
[16:03:14]: ------------------------------
[16:03:14]: --- Step: default_platform ---
[16:03:14]: ------------------------------
[16:03:14]: Driving the lane 'ios FastlaneDemo' 🚀
[16:03:14]: ----------------------------------
[16:03:14]: --- Step: get_info_plist_value ---
[16:03:14]: ----------------------------------
[16:03:14]: ------------------
[16:03:14]: --- Step: sigh ---
[16:03:14]: ------------------
+-------------------------------------+-------+
| Summary for sigh 2.212.1 |
+-------------------------------------+-------+
| force | true |
| adhoc | false |
| developer_id | false |
| development | false |
| skip_install | false |
| include_mac_in_profiles | false |
| ignore_profiles_with_different_name | false |
| skip_fetch_profiles | false |
| include_all_certificates | false |
| skip_certificate_verification | false |
| platform | ios |
| readonly | false |
| fail_on_name_taken | false |
+-------------------------------------+-------+
[16:03:14]: To not be asked about this value, you can specify
it using 'username'
[16:03:14]: Your Apple ID Username:
输入 Apple ID 去测试 ......
参考:
离屏渲染(二)
离屏渲染(一)
前言
在iOS中图形图像的渲染流程,在平时开发中最常使用的开发框架是UIKit,我们会使用UIKit的框架来绘制界面,而UIKit实际可以看成是集成CoreAnimation和CoreGraphics这两个框架,来方便开发者使用,设置UIKit的一些布局和相关属性来绘制界面。
- 如果界面如果需要动画效果就需要
CoreAnimation这个框架来实现,而CoreAnimation又依赖于OpenGL ES/Metal来做GPU的渲染。 -
CoreGraphics框架是一个高级的绘图引擎,它的主要作用是运行的时候去绘制图像。我们可以使用这个框架来做一些绘图、转换、离屏渲染、阴影、图像的创建等等。 -
CoreGraphics是在CPU是执行的 ,CoreImage和CoreGraphics是相反的,CoreImage是处理创建图像运行前的操作,CoreImage拥有一套现成的图像过滤性,它能对一些已经存在的图像进行高效的处理,这个框架既能在CPU上执行,也能在GPU上执行,我们APP就会使用CoreGraphics、CoreAnimation和CoreImage来绘制一些可视化的内容,这些框架都需要再通过OpenGL ES/Metal来做GPU的绘制,最终再将图像显示到屏幕上面。
一、图像图像渲染流程
上面这张图像就告诉我们图形图像在渲染的时候CPU和GPU分别做了哪些事情:
- 首先,在
Application阶段,是由CPU来处理,CPU会创建我们的视图,它会计算视图的一些数据,进行编解码,绘制纹理等等的操作后再交给GPU,GPU在第一阶段会通过顶点着色器去确定图像在硬件上具体的显示位置; - 然后,通过片源着色器来计算每个像素点的颜色值,最后进行光栅化,这个光栅化会找到图形上像素的点的范围,然后把一个一个的像素点的颜色显示上去,最终会把图形转化为一个个的实际的屏幕像素,当这些操作都处理完之后,就会把渲染完成之后的数据放到帧缓存区里面;
- 最后,再交由显示系统将帧缓存区里的数据给读取出来进行显示 。
二、图像显示流程图
上面就是图像显示流程图,当一个图像在CPU和GPU处理完之后,它就会被存放到FrameBuffer里面,FrameBuffer被称为帧缓存区,然后视频控制器就会往FrameBuffer去读取数据,读取的数据就会交给显示器显示,我们就能看到屏幕上一帧一帧的画面。
三、屏幕扫描
它是通过屏幕扫描的方式,会通过CRT电子枪从上到下逐行扫描,这个扫描的过程就是读取帧缓存区里的数据,当它扫描完的时候,它就会显示一帧的画面, 当显示完一帧画面后,CRT电子枪又回到原来的位置继续从上到下扫描,这样就会显示下一帧的画面,如此循环往复,这就是我们能看到手机屏幕上的一些内容。
当电子枪扫描完一行要换到下面一行的时候,这个时候显示器会发出水平同步信号HSync;当电子扫描完一帧,要回到原来位置,这个时候显示器就会发出垂直同步信号VSync。这个显示器通常都是按照固定的频率来刷新的,这个刷新的频率就是垂直同步信号VSync的频率。苹果手机(除了新出的高刷)的刷新频率是每秒60次,也就是60FPS,这就是我们在进行屏幕卡顿监测或者卡顿优化的时候,会用FPS来作为衡量的指标。
如果屏幕刷新频率不是接近固定刷新频率,那就是出现了掉帧。当一个垂直同步信号过来的时候,如果说CPU和GPU还没有完成渲染的结果去做提交,也就是没有把数据放到FrameBuffer里面,这种情况,未过来提交过来这一帧的画面就会被丢弃,然后等待下一次垂直同步信号过来,再来显示新的画面,这个过程被称为掉帧。 掉帧的时候,屏幕刷新的FPS频率就会减少,这也是界面显示真正卡顿的原因。
接下来是可能造成屏幕卡顿的离屏渲染.
四、离屏渲染
比如我们给图像添加了遮罩,设置了某些圆角,CPU和GPU没有办法把渲染的数据放到FrameBuffer上,它会把渲染的数据先放到FrameBuffer之外的一块缓冲区,在这块区域进行合成渲染,渲染到我们最终想要的画面,再把数据放到FrameBuffer里面,这个过程就是离屏渲染。
先看UIView和CALayer的关系,UIView是基于CALayer的封装,一个View他本身是不能显示的,它想要显示,需要通过内部图层的CALayer来显示,UIView有CALayer的只读属性和遵循CALayer的代理。当View显示什么内容,都要绘制在它内部图层Layer上面,这个Layer就是负责显示,而View负责处理响应的事件。因此,我们看到的界面是View里面的layer层所呈现出来的。
下图可以看出,
layer主要包括三个部分backgroundColor、contents和borderColor。
- 当视图层级比较复杂的时候,
GPU无法直接把渲染数据放到FrameBuffer。比如下面的图例,第三张图是我们最终要看到的画面,它比较复杂,由于CPU和GPU是硬件,它有性能瓶颈,它去读取画面数据需要一定的算法,这个时候由于GPU没有办法通过一次遍历就能拿到一帧的完整数据,这个时候它就会遵循画家算法。 - 要画下面这张图,
GPU它是一个机器,它就一块画布,第一次遍历扫描到图片上的山,这样第一次遍历结束后第二次遍历开始的时候,需要一块新的画布,又扫描出了草地, 以此类推,第三次遍历在新的画布上加上了树。 - 但是,无论是树还是草地或者树都不是我们最终想要的画面效果,这样就不能把任一画布单独放进
FrameBuffer里面,因为画面不够完整,这样就需要开启一块额外的内存缓冲区,然后山、草地和树就在这里面合成,合成最终的效果再把合成的数据存到帧缓存区,这就是离屏渲染的整个流程。
离屏渲染就是硬件的瓶颈,有的图形,它没有办法做到一次渲染完成,于是通过离屏渲染的方式来处理,这就是离屏渲染产生的原因。
启动优化clang插桩(三)
一、获取符号
先把获取符号的代码写在touchBegan里面:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// NSLog(@"%s",__func__);
// 因为不知道有多少个,所有用while循环
while(YES){
// 将node取出来
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
// 取到node为空退出当前循环
if(node == NULL){
break;
}
// 打印拿到符号的信息
Dl_info info;
dladdr(node->pc,&info);
printf("%s\n",info.dli_sname);
}
}
点击运行,会打印出一堆的touchesBegan。
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
…
回到Build setting,将原来标记那里添加一个参数func:
再次运行,点击屏幕打印:
-[ViewController touchesBegan:withEvent:]
-[SceneDelegate sceneDidBecomeActive:]
-[SceneDelegate sceneWillEnterForeground:]
-[ViewController viewDidLoad]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate window]
-[AppDelegate application:didFinishLaunchingWithOptions:]
main
这样就拿到了所有的符号。
二、处理符号
因为队列是先进后出,所以我们需要做一个取反的操作,而且还有一些是重复的符号,我们需要去掉,处理完这些步骤之后的这些符号就是程序启动时候的顺序。
先给函数添加下划线:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// NSLog(@"%s",__func__);
// 初始化一个数组来装载有顺序的数据
NSMutableArray *symbolNames = [NSMutableArray array];
//因为不知道有多少个,所有用while循环
while(YES){
//将node取出来
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
// 取到node为空退出当前循环
if(node == NULL){
break;
}
//打印拿到符号的信息
Dl_info info;
dladdr(node->pc,&info);
printf("%s\n",info.dli_sname);
//转为OC字符串
NSString *name = @(info.dli_sname );
//判断是否是方法
BOOL isMethod = [name hasPrefix:@"+["] ||
[name hasPrefix:@"-["];
//拿到处理后的符号
NSString * symbolName = isMethod? name : [@“_” stringByAppendingString:name];
// 添加进数组
[symbolNames addObject:symbolName];
}
NSLog(@"%@",symbolNames);
}
运行打印,得到:
(
"-[ViewController touchesBegan:withEvent:]",
"-[SceneDelegate sceneDidBecomeActive:]",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate window]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate window]",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"_main"
)
这样main函数就加上了下划线“_”
三、符号逆序
直接反向遍历:
NSEnumerator *em = [symbolNames reverseObjectEnumerator];
NSLog(@"%@",em.allObjects);
运行打印,得到:
(
"_main",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate window]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[SceneDelegate window]",
"-[ViewController viewDidLoad]",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate sceneDidBecomeActive:]",
"-[ViewController touchesBegan:withEvent:]"
)
这就得到我们想要的顺序。
四、符号去重
// 初始化去重后的数组
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
// 定义表示
NSString *name;
// 判断是否数组里已经存在,不存在则添加
while (name = [em nextObject]) {
if(![funcs containsObject:name]){
[funcs addObject:name];
}
}
NSLog(@"%@",funcs);
运行打印,得到:
(
"_main",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[ViewController viewDidLoad]",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate sceneDidBecomeActive:]",
"-[ViewController touchesBegan:withEvent:]"
)
可以发现里面已经没有了重复的符号
五、生成.order文件
// 拼接成一个字符串
NSString *funcsStr = [funcs componentsJoinedByString:@"\n"];
// 文件路径
NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"TraceDemo.order"];
// 文件的内容
NSData *file = [funcsStr dataUsingEncoding:NSUTF8StringEncoding];
// 写入文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
// 打印路径
NSLog(@"%@",NSHomeDirectory());
运行打印:
TraceDemo[31577:752540] /Users/xxxx/Library/Developer/CoreSimulator/Devices/876D0DEB-7AC9-4B67-A877-DB2BC4B5BD10/data/Containers/Data/Application/702BBFFB-D619-4B19-814C-0C9CXXXXX
Tmp文件下可以看到一个.order文件
打开文件:
写入的内容就是我们想要的内容,这样就可以把
.order文件复制进项目里。
Order File添加文件位置:
Link Map File打开:
运行,然后找到这个LinkMap文件:
打开和.order文件对比:
发现完全一致。
重排之后减少多少时间,就需要用
Instruments工具的System Trace去做具体对比。
六、使用swift情况
如果项目使用swift的话,跟重排和使用OC类似。创建一个swift文件:
import Foundation
class SwiftPage: NSObject{
@objc class public func swiftFunc(){
print("我是swift")
}
}
导入头文件:
#import "TraceDemo-Swift.h"
添加方法:
- (void)viewDidLoad {
[super viewDidLoad];
[SwiftPage swiftFunc];
}
运行:
我是swift
点击屏幕打印:
(
"_main",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[ViewController viewDidLoad]",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate sceneDidBecomeActive:]",
"-[ViewController touchesBegan:withEvent:]"
)
发现并没有打印swift方法,因为swift并不是clang编译的,clang插桩只能编译C、C++和OC,这里就需要用在Other Swift Flags添加两个标记:-sanitize-coverage=func、-sanitize=undefined。
再次运行:
(
"_main",
"-[AppDelegate application:didFinishLaunchingWithOptions:]",
"-[SceneDelegate window]",
"-[SceneDelegate setWindow:]",
"-[SceneDelegate scene:willConnectToSession:options:]",
"-[ViewController viewDidLoad]",
"_$s9TraceDemo9SwiftPageC9swiftFuncyyFZTo",
"_$s9TraceDemo9SwiftPageC9swiftFuncyyFZ",
"_$ss27_finalizeUninitializedArrayySayxGABnlF",
"_$sSa12_endMutationyyF",
"_$ss5print_9separator10terminatoryypd_S2StFfA0_",
"_$ss5print_9separator10terminatoryypd_S2StFfA1_",
"-[SceneDelegate sceneWillEnterForeground:]",
"-[SceneDelegate sceneDidBecomeActive:]",
"-[ViewController touchesBegan:withEvent:]"
)
可以看到swift方法,因为swift方法自带混淆,这里swift也捕获到了,这里就完成了OC和swift的二进制重排。在项目需要上线的时候,删除一开始的标记-fsanitize-coverage=func,trace-pc-guard和其他测试代码。
启动优化clang插桩(二)
一、前言
上一篇的方法给到的是个数,但不是符号,个数并没有什么作用,甚至给了全部的符号也没什么用,因为二进制重排仅仅需要的是启动阶段所需要的符号,这就需要下面这个函数:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}
添加断点,点击箭头,可以看到绿色框中的函数调用栈:
函数调用栈跟之前讲到的
app名称.LinkMap-normal-arm64.txt文件里面的数据格式一样。
把上面断点过掉,给touchBegan方法添加断点::
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
///
}
点击上面的绿色箭头:
这里就出现了touchesBegan,那大概推出下面这个函数是系统每调用一个方法,都会调用这个__sanitizer_cov_trace_pc_guard函数:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}
二、调试验证
验证是否为真给下面两个函数添加打印方法:
void test(void) {
NSLog(@"%s",__func__);
}
void (^block) (void) = ^{
NSLog(@"%s",__func__);
};
同时在touchesBegan方法里面为添加打印和调用两个函数:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
test();
block();
}
这个追踪方法也添加打印:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
}
cmd + r启动,先清空打印,点击屏幕:
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] -[ViewController touchesBegan:withEvent:]
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] test
TraceDemo[20273:399962] __sanitizer_cov_trace_pc_guard
TraceDemo[20273:399962] block_block_invoke
可以看出先__sanitizer_cov_trace_pc_guard 再调用方法、函数、block,这说明不管是方法、函数、block它都会去回调这个函数,而且这个函数的调用是我们在调用这个函数之前,也就是这个函数拦截或者hook了所有的方法、函数包括block, 这就搞定了我们没有其他操作就能拦截到app启动时候调用了那些方法和函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
}
我们在程序启动时候__sanitizer_cov_trace_pc_guard拦截到的方法函数:
把这些写入到
.order文件里面这样二进制重排就搞定了
而_sanitizer_cov_trace_pc_guard,这个函数是如何做到这一点的?给_sanitizer_cov_trace_pc_guard添加断点,在Xcode的Debug选择Debug WorkFlow选择显示汇编,选择main:
可以看到在main之前,系统插入了_sanitizer_cov_trace_pc_guard这个符号:
在AppDelegate页面也是插入了这个符号:
在SceneDelegate同样如此:
也就是说,在编译器clang添加下面这个标记后,编译器会给函数方法前面都会调用_sanitizer_cov_trace_pc_guard这个函数:
这样,我们确实在打断点看到启动阶段的所有符号。
三、如何把符号都打印出来
先打开注释的代码:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
// char PcDescr[1024];
// printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
上面的PC指的是上一个函数的地址,有了这个函数地址,就可以拿到这个函数的符号,这里需要用到dladdr函数:
#import <dlfcn.h>
dladdr(const void *, Dl_info *),这个函数可以获得一个函数的名称以及地址,第一个参数传入PC,第二个参数定义一个变量 Dl_info info传进去:
Dl_info info;
dladdr(PC, &info);
查看Dl_info,它是一个结构体:
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
dli_sname就是所需要符号
删除其他打印,把调试代码改为如下:
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
NSLog(@"%d",N);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("dli_sname -- %s\n",info.dli_sname);
}
运行:
dli_sname -- main
dli_sname -- -[AppDelegate application:didFinishLaunchingWithOptions:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[SceneDelegate setWindow:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[SceneDelegate scene:willConnectToSession:options:]
dli_sname -- -[SceneDelegate window]
dli_sname -- -[ViewController viewDidLoad]
dli_sname -- -[SceneDelegate sceneWillEnterForeground:]
dli_sname -- -[SceneDelegate sceneDidBecomeActive:]
得到了启动时候所需要的所有符号和启动顺序,拿到这些符号之后,把它复制粘贴到.order文件中,就可以实现之前需要的目标,就是拿到把启动所需要的符号和顺序加载在前面的page里面,就实现了二进制重排。在放进.order文件之前,需要把重复的符号删掉,并且对于函数或者block,需要在前面加个“_”,这样整个clang插桩就已经完成。
四、获取符号方式优化
但是,上面手动的方法不够灵活,应该让计算机去做这些操作,用代码完成上面的操作。
目标:
- 去掉重复的符号
- 如果是函数就前面加上
“_” - 生成一个
.order文件
那第一步就是要对下面的符号进行收集,然后存储起来:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("dli_sname -- %s\n",info.dli_sname);
}
这就需要有一个全局的容器来存放,并且这里会涉及到多线程的情况,因为一个app启动不大可能只有一个线程在跑,因为函数、方法和
block在哪个线程跑,这个获取符号的回调函数也在那个线程运行,所以在符号收集的时候,需要考虑线程安全问题。
所以,这边使用线程安全的原子队列,导入头文件:
#import <libkern/OSAtomic.h>
定义一个全局容器结构体:
typedef struct {
void * pc;
void *next;
} SYNode;
然后在回调函数里面进行操作:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
//开辟空间
SYNode *node = malloc(sizeof(SYNode));
//赋值
*node = (SYNode){PC,NULL};
//结构体存入原子队列
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next ));
}
offsetof里面的next是下一个存储位置的偏移量,这样我们就把符号都放进了symbolList里面,在合适的位置把它取出来就行。
启动优化clang插桩(一)
启动优化clang插桩(一)
一、了解Clang
首先到Clang地址:Clang Documentation
PCs指的是CPU的寄存器,用来存储将要执行的下一条指令的地址,Tracing PCs就是跟踪CPU将要执行的代码。
二、如何使用
网页下拉有个Example
使用之前要在工程添加标记:
编译器就会在每一行代码的边缘插入这一段函数:
__sanitizer_cov_trace_pc_guard(&guard_variable)
打开实例demo,在Build Settings 搜索 Other c Flag 填入 -fsanitize-coverage=trace-pc-guard
项目会报未定义符号的错:
这就需要去定义这两个符号,先把这两个函数复制过来:
先把代码复制进
ViewController
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
// if(*guard)
// __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
// __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
// If you set *guard to 0 this code will not be called again for this edge.
// Now you can get the PC and do whatever you want:
// store it somewhere or symbolize it and print right away.
// The values of `*guard` are as you set them in
// __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
// and use them to dereference an array or a bit vector.
void *PC = __builtin_return_address(0);
char PcDescr[1024];
// This function is a part of the sanitizer run-time.
// To use it, link with AddressSanitizer or other sanitizer.
__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
把头文件也粘贴进来:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
两个方法里面都有
extern “C”,extern “C”的主要作用是为了能够正确实现C++去调用其他C语言的代码,加上extern “C”就会指示作用域内的代码按照C语言区编译,而不是C++,这个extern “C”在OC项目里没什么用,直接删除
此时还会包一个错误:
这个__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));函数没有什么作用,直接删除即可。
三、代码调试
cmd + r运行,此时终端会打印一些信息:
删除两个函数里面的注释,先注释第二个的内容,然后运行
INIT: 0x1025c5478 0x1025c54f0
这是运行打印得到的地址,就是函数(uint32_t *start, uint32_t *stop)的start和stop两个指针的地址
stop存储的就是我们工程里面符号的个数
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
看一下这个
for循环,start会先复制给*x,x++就是内存平移,按照uint32_t的大小去平移,而uint32_t的定义是typedef unsigned int uint32_t;是无符号整型,占4个字节,所以每次按4个字节平移。
start和stop里面存的是什么,打断点调试:
先看start:
INIT: 0x1042a5278 0x1042a52e0
(lldb) x 0x1042a5278
0x1042a5278: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x1042a5288: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................
(lldb)
由于uint32_t按4个字节来存储发现start就是 0 1 2 3 4…,再看stop,由于stop的已经是结束位置,读取的数据是在start和stop之间的数据,所以需要向前平移4个字节得到其真实数据。
(lldb) x (0x1042a52e0-4)
0x1042a52dc: 1a 00 00 00 00 00 00 00 00 00 00 00 fe f1 29 04 ..............).
0x1042a52ec: 01 00 00 00 00 00 00 00 00 00 00 00 90 40 2a 04 .............@*.
(lldb)
可以得到1a 就是26,也可以循环外面打印结果:
可以得到:
TraceDemo[16814:301325] 26
也是26个符号。
四、测试验证方法
可以验证一下,添加一个函数:
void test(void) {
NSLog(@"%s",__func__);
}
符号变成27:
TraceDemo[16911:304537] 27
再添加一个block:
void (^block) (void) = ^{
NSLog(@"%s",__func__);
};
符号变成28:
TraceDemo[16933:305465] 28
添加一个数据类型属性:
@property (nonatomic ,assign) int age;
由于系统自动生成getter、setter方法,符号变成30
TraceDemo[16975:306816] 30
添加一个对象属性:
@property (nonatomic ,copy) NSString *str;
符号变成33:
TraceDemo[17041:308780] 33
对象属性由于ARC,系统自动除了生成getter、setter方法外还生成了cxx_destruct()析构函数
添加一个方法:
- (void)test{
}
符号变成34:
TraceDemo[17114:311256] 34
在其他类AppDelegate类中添加一个属性:
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) NSString *name;
@end
符号变成37:
TraceDemo[17266:316294] 37
符号变成37,
结论
这就说明了通过这个方法整个项目里的符号,它都能捕获到。