普通视图
iOS应用数据持久化 FMDB
iOS应用数据持久化 SQLite
对象序列化
对象序列化
一、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(一)
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 去测试 ......
参考:
离屏渲染(二)
离屏渲染(一)
启动优化clang插桩(三)
启动优化clang插桩(二)
启动优化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,
结论
这就说明了通过这个方法整个项目里的符号,它都能捕获到。