普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-Jony唐

对象序列化

作者 Jony唐
2023年3月24日 18:16

对象序列化

一、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

模拟器添加两个按钮,一个写入,一个读取:

Pasted Graphic 1.png

点击把自定义数据存进本地:

- (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

二、废弃提醒

存档方法废弃提醒:

Pasted Graphic 3.png

修改存档方法:

//    NSData *perData = [NSKeyedArchiver archivedDataWithRootObject:
userArr];
NSError *error = nil;
NSData *perData = [NSKeyedArchiver archivedDataWithRootObject:
userArr requiringSecureCoding:YES error:&error];

解档方法废弃提醒:

Pasted Graphic 4.png

修改废弃提醒:

//    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 方法里面,也是提示方法废弃:

Pasted Graphic.png

object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];

下面的 - (void)setObject:(id<NSSecureCoding>)object forKey:(NSString *)key 方法里面,也是提示方法废弃:

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

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;
}

运行测试,发现找不到自定义的类:

Pasted Graphic 2.png

就是需要指定一个具体的类名,让他做解码操作,由于 YYCachepod 下来的,不能直接导入文件,为了避免相互引用,只用 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 的代码。

Fastlane自动化打包到蒲公英

作者 Jony唐
2023年3月24日 18:04

Fastlane

fastlane是一个旨在简化 Android 和 iOS 部署的开源平台,可以自动化开发和发布工作流程的各个方面。

一、安装Xcode命令行工具

fastlane 安装Xcode命令行工具:

xcode-select --install

Pasted Graphic.png 如果安装过会提示:

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]你想用快车道做什么?

  1. 📸自动截屏
  2. 👩✈️自动测试分发TestFlight
  3. 🚀自动化应用商店分销
  4. 🛠手动设置-手动设置您的项目自动化您的任务

这边选择 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`

项目里多了这三个文件:

Pasted Graphic 1.png

三、安装蒲公英插件

fastlane add_plugin pgyer

注册蒲公英,拿到 API KeyUser Key

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

在上面看到的 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 🎉

可以看到上传蒲公英成功:

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

点击应用信息:

Pasted Graphic 4.png

在项目本地也会生成一个ipa

Pasted Graphic 3.png

四、分发到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插桩(一)

作者 Jony唐
2023年3月21日 20:31

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

结论

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

❌
❌