阅读视图

发现新文章,点击刷新页面。

iOS应用数据持久化 SQLite

iOS应用数据持久化 SQLite

一、前言

虽然平时开发不会直接使用SQLite,但是作为一个轻量级数据库,理解和掌握其基本原理和SQL语句基本使用还是有必要的。FMDB就是基于SQLite的封装,并且微信团队在自研自己的数据库前也是使用SQLite

点击项目名称 -> TAGETS -> Build Phases -> Link Binary With Libraries 点击添加libsqlite3.tbd

Pasted Graphic.png

二、代码准备

@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:

Pasted Graphic 1.png

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的文件:

Pasted Graphic 4.png

Navicat打开person_info.sqliteperson的表已经创建好了:

Pasted Graphic 3.png

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条记录:

1__#$!@%!#__Pasted Graphic 3.png

还有一个sqlite_sequenceseq10

1__#$!@%!#__Pasted Graphic 4.png

注:如果没有看到数据,点击左边下面的按钮刷新,右键刷新无效。

1__#$!@%!#__Pasted Graphic 1.png

三、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填入夏洛特,age24,点击增:

Pasted Graphic 5.png

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

回到Navicatperson表点击刷新,可以看到新增的一条记录:

Pasted Graphic 6.png

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 的数据删除掉:

Pasted Graphic 7.png 点击删除:

Sqlite3Demo[47564:3319611] 删除数据成功

查看全部数据,发现id5的数据已经没有了:

------------------------------
查到的数据 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里查看:

Pasted Graphic 8.png

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的名字改为赵云:

Pasted Graphic 9.png

查看全部:

------------------------------
查到的数据 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的记录:

Pasted Graphic 10.png

------------------------------
查到的数据 id:4 name:张4 年龄:29

查年龄小于25的记录:

Pasted Graphic 11.png

点击查按钮:

------------------------------
查到的数据 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

查看Navicatperson

Pasted Graphic 12.png

查看squence,点击刷新,id21完全一致:

Pasted Graphic 13.png

点击清空:

/**清空表中数据和自增排序*/
- (IBAction)removeAllData:(UIButton *)sender {
    [self openDatabase];
    const char *deleteSpl = "DELETE FROM person";
    [self updateDataWithSql:deleteSpl success:^{
        NSLog(@"清空表数据成功");
    }];
}

表数据已经被清空:

Pasted Graphic 14.png

这个时候sqlite_sequence还是21,没有归0

Pasted Graphic 15.png

此时点击插入10条数据,发现id21开始:

Pasted Graphic 16.png

并没有真正的清空,需要给数据添加seq0操作:

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(@"自增主键归零");
        }];
    }];
}

再点击清空:

Pasted Graphic 17.png

点击插入多条,id又从1开始:

Pasted Graphic 18.png

四、总结

SQLite接口都是C语言,并不面向对象,所有操作需要自己手写SQL语句。

参考

离屏渲染(二)

离屏渲染是在当前屏幕帧缓冲区外增加了一个临时新的缓冲区对将要进行显示的图片进行渲染操作再写回帧缓冲区,过个过程造成性能损耗。

离屏渲染(一)

离屏渲染是在当前屏幕帧缓冲区外增加了一个临时新的缓冲区对将要进行显示的图片进行渲染操作再写回帧缓冲区,过个过程造成性能损耗。

启动优化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

Pasted Graphic.png

再次运行,点击屏幕打印:

-[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文件

Pasted Graphic 3.png

打开文件:

Pasted Graphic 4.png 写入的内容就是我们想要的内容,这样就可以把.order文件复制进项目里。

Pasted Graphic 2.png

Order File添加文件位置:

Pasted Graphic 1.png

Link Map File打开:

1__#$!@%!#__Pasted Graphic 3.png

运行,然后找到这个LinkMap文件:

1__#$!@%!#__Pasted Graphic 4.png

打开和.order文件对比:

Pasted Graphic 5.png

发现完全一致。

重排之后减少多少时间,就需要用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插桩只能编译CC++OC,这里就需要用在Other Swift Flags添加两个标记:-sanitize-coverage=func-sanitize=undefined

1__#$!@%!#__Pasted Graphic.png

再次运行:

(
    "_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也捕获到了,这里就完成了OCswift的二进制重排。在项目需要上线的时候,删除一开始的标记-fsanitize-coverage=func,trace-pc-guard和其他测试代码。

启动优化clang插桩(二)

一、前言

上一篇的方法给到的是个数,但不是符号,个数并没有什么作用,甚至给了全部的符号也没什么用,因为二进制重排仅仅需要的是启动阶段所需要的符号,这就需要下面这个函数:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
}

添加断点,点击箭头,可以看到绿色框中的函数调用栈:

Pasted Graphic.png

函数调用栈跟之前讲到的app名称.LinkMap-normal-arm64.txt文件里面的数据格式一样。

把上面断点过掉,给touchBegan方法添加断点::

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
///
}

点击上面的绿色箭头:

Pasted Graphic 1.png

这里就出现了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拦截到的方法函数:

Pasted Graphic 3.png

把这些写入到.order文件里面这样二进制重排就搞定了

_sanitizer_cov_trace_pc_guard,这个函数是如何做到这一点的?给_sanitizer_cov_trace_pc_guard添加断点,在Xcode的Debug选择Debug WorkFlow选择显示汇编,选择main

Pasted Graphic 4.png

可以看到在main之前,系统插入了_sanitizer_cov_trace_pc_guard这个符号:

Pasted Graphic 5.png

AppDelegate页面也是插入了这个符号:

Pasted Graphic 6.png

SceneDelegate同样如此:

Pasted Graphic 7.png

也就是说,在编译器clang添加下面这个标记后,编译器会给函数方法前面都会调用_sanitizer_cov_trace_pc_guard这个函数:

Pasted Graphic 8.png

这样,我们确实在打断点看到启动阶段的所有符号。

三、如何把符号都打印出来

先打开注释的代码:

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插桩就已经完成。

四、获取符号方式优化

但是,上面手动的方法不够灵活,应该让计算机去做这些操作,用代码完成上面的操作。 

目标:

  1. 去掉重复的符号
  2. 如果是函数就前面加上“_”
  3. 生成一个.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 Pasted Graphic.pngPCs指的是CPU的寄存器,用来存储将要执行的下一条指令的地址,Tracing PCs就是跟踪CPU将要执行的代码。

二、如何使用

网页下拉有个Example Pasted Graphic 1.png 使用之前要在工程添加标记: Pasted Graphic 2.png

编译器就会在每一行代码的边缘插入这一段函数:__sanitizer_cov_trace_pc_guard(&guard_variable)

打开实例demo,在Build Settings 搜索 Other c Flag 填入 -fsanitize-coverage=trace-pc-guard

1__#$!@%!#__Pasted Graphic 1.png

项目会报未定义符号的错:

Pasted Graphic 7.png

这就需要去定义这两个符号,先把这两个函数复制过来:

Pasted Graphic 5.png 先把代码复制进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项目里没什么用,直接删除

此时还会包一个错误:

Pasted Graphic 8.png

这个__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));函数没有什么作用,直接删除即可。

三、代码调试

cmd + r运行,此时终端会打印一些信息:

Pasted Graphic 9.png

删除两个函数里面的注释,先注释第二个的内容,然后运行

INIT: 0x1025c5478 0x1025c54f0

这是运行打印得到的地址,就是函数(uint32_t *start, uint32_t *stop)startstop两个指针的地址

stop存储的就是我们工程里面符号的个数

for (uint32_t *x = start; x < stop; x++)
        *x = ++N;

看一下这个for循环,start会先复制给*xx++就是内存平移,按照uint32_t的大小去平移,而uint32_t的定义是typedef unsigned int uint32_t; 是无符号整型,占4个字节,所以每次按4个字节平移。

startstop里面存的是什么,打断点调试:

Pasted Graphic 10.png

先看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_t4个字节来存储发现start就是 0 1 2 3 4…,再看stop,由于stop的已经是结束位置,读取的数据是在startstop之间的数据,所以需要向前平移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,也可以循环外面打印结果: Pasted Graphic 11.png 可以得到:

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

结论

这就说明了通过这个方法整个项目里的符号,它都能捕获到。

❌