【疯狂造轮子-iOS】JSON转Model系列之一

news2024/9/23 9:28:07

1. 前言


之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗忘。这段时间准备自己造一些轮子,主要目的还是为了提升自身实力,总不能一遇到问题就Google

之前写i博客园客户端的时候,经常会遇到JSON数据转Model的功能。一般遇到这种问题我都是自己在对应Model类中定义一个+ (instance)initWithAttributes:(NSDictionary *)attributes函数来将NSDictionary*数据转化为对应Model。

下面是i博客园中ICUser的部分代码,其中就使用了initWithAttributes。

// ICUser.h
#import <Foundation/Foundation.h>
 
extern NSString *const kUserId;
extern NSString *const kUserBlogId;
extern NSString *const kUserDisplayName;
extern NSString *const kUserAvatarURL;
@interface ICUser : NSObject
 
@property (nonatomic, copy)     NSString    *userId;
@property (nonatomic, assign)   NSInteger   blogId;
@property (nonatomic, copy)     NSString    *displayName;
@property (nonatomic, strong)   NSURL       *avatarURL;

 + (instancetype)initWithAttributes:(NSDictionary *

)attributes;
 
@end
 
// ICUser.m
#import "ICUser.h"
 
NSString *const kUserId                 = @"UserId";
NSString *const kUserBlogId             = @"BlogId";
NSString *const kUserDisplayName        = @"DisplayName";
NSString *const kUserAvatarURL          = @"Avatar";
@implementation ICUser

+ (instancetype)initWithAttributes:(NSDictionary *

)attributes
{
    ICUser *user = [[ICUser alloc] init];
    user.userId             = attributes[kUserId];
    user.blogId             = [attributes[kUserBlogId] integerValue];
    user.displayName        = attributes[kUserDisplayName];
    user.avatarURL          = [NSURL URLWithString:attributes[kUserAvatarURL]];
return user;
}
 
@end

如果我们需要处理的情况符合下面两个要求:

  1. Model类的个数比较少
  2. 每个Model的成员不是很复杂

这种情况下使用上述方法还可以接受。但是一旦Model这一层急剧膨胀,这时候就会让人苦不堪言:

  1. initWithAttributes函数容易写错,而且出错后不方便排查。
  2. 机械性的代码会比较多,不利于提高效率。

考虑到手动转JSON为Model的种种不便,我决定自己写一个JSON转Model的库。虽然网上已经有很多这方面的第三方库,但是我还是想自己造轮子,目的是为了更深入地学习iOS

2. 设计思路


1.首先要考虑到输入输出是什么?

输入:NSDictionary类型的数据

这里我们先从简,一般我们使用到解析JSON的场合是在网络请求。服务器端返回JSON格式的数据,我们需要转化成本地的Model(此处不讨论直接使用NSDictionary好还是转化为Model好)。并且本篇文章只假设我们网络请求获取到的JSON数据已经在客户端处理成了NSDictionary类型的数据(比较常见)。

输出:Model类型的数据

Model类型的数据。

举例:

目前我实现的一个简单的例子:

#pragma mark - PJXUser
@interface PJXUser : NSObject
@property (nonatomic, copy) NSString* username; // 用户名
@property (nonatomic, copy) NSString* password; // 密码
@property (nonatomic, copy) NSString* avatarImageURL; // 头像的URL地址
@end
 
- (void)runSimpleSample
{
    NSDictionary *userDict = @{@"username"      :@"shuaige",
                               @"password"      :@"",
                               @"avatarImageURL":@"http://www.example.com/shuaige.png"};
 
    PJXUser *user = [[PJXUser alloc] initWithAttributes:userDict];;
 
    NSLog(@"username:%@\n",user.username);
    NSLog(@"password:%@\n",user.password);
    NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);
}

这个例子的输入就是userDict这个NSDictionary数据,输出则是一个PJXUser类的对象user。不知道大家有没有注意到,attributes中的key必须和Model中的property的名称一致,比如上例中PJXUser的username、password等属性(当然,你可以使用一个映射表解决这个问题,不过我们先暂时不想那么多)。

2. 核心算法怎么做(输入转输出)?

核心算法部分其实就是调用initWithAttributes:这个函数。那这个函数该如何设计呢?

既然我们需要所有的Model类都可以调用这个initWithAttributes:来完成JSON转Model的工作。那么我们首先想到的就是将这个函数添加到NSObject的category中,并要求所有Model类都继承自NSObject。

所以我首先新建了一个NSObject+Extension的category。并在其中添加了- (instancetype)initWithAttributes:(NSDictionary *)attributes方法。下面我简单阐述下该函数的实现。

其实我的实现思路基本参照的YYKit(传送门)中的YYModel(传送门)部分其最核心的部分就是调用Model中每个属性的setter方法,并且将传入的attributes中每个元素的value作为setter的参数

好的,到此为止最核心的部分已经讲完了。可能大家会有很多疑问,比如说如何获取到属性的setter方法,获取后又如何调用setter方法,毕竟此时的操作是在NSObject这个父类中进行的,并没有具体子类的信息。这里我简单提一下,既然编译期我们无法解决上述问题,那么我们就需要借助于OC的runtime机制了。当然,下面会具体讲解如何实现。

3. 具体实现


根据上面的核心思路,我觉得实现起来还存在一些问题:

如何获取每个属性的setter方法?如果现在获取到了每个属性的setter方法(注意是SEL类型),怎么给每个属性调用此方法?

现在是在NSObject中操作,所以不指望使用obj.username = attributes[@"username"]。所以需要使用runtime中的objc_msgSend,使用方法举例如下:

((void (*)(id, SEL, id))(void *) objc_msgSend)((id)self, NSSelectorFromString(@"setUsername:"), @"shuaige");

可以看到我们只需要把其中的@"setUsername"和@"shuaige"替换成我们自己的变量就行。具体怎么替换呢?这时候我们就需要创建一些数据结构来处理和保存相关的属性信息。当然,这些数据结构也是我在实现过程中不断修正的结果。至于中间如何修正,就不细说了,直接上结果。

数据结构的构建其实也很符合我们的思考习惯。既然我们需要对某个类进行处理,不可避免的,我们需要新建一个类来存储Class信息(PJXClassInfo),而每个Class是由property、ivar和method组成的,所以针对不同组成,我们需要定义三个类来存储property、ivar、method。但是此处我们只需要property信息,所以只建立了property相关的类(PJXPropertyInfo)。

我首先创建了一个PJXClassInfo的类。这个类目前只存放了一个NSMutableDictionary类型的propertyInfos属性,该属性是用来存储这个Class的property信息。而propertyInfos中每个元素其实就是一个个PJXPropertyInfo对象。而每个PJXPropertyInfo保存的就是property的name,setter方法等等,当然,后期会根据需求为PJXPropertyInfo添加新的属性。

这两个类的关系如下:

下面我们看看具体代码如何实现,如下:

PJXPropertyInfo代码

 

/**
 * @brief 存储Model中每个property的信息
 * @param property 是一个objc_property_t类型变量
 * @param name 表示该property的名称
 * @param setter 是一个SEL类型变量,表示该property的setter方法
 */
@interface PJXPropertyInfo : NSObject
@property (nonatomic, assign) objc_property_t property;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) SEL setter;
@end
 
@implementation PJXPropertyInfo
 
- (instancetype)initWithPropertyInfo:(objc_property_t)property
{
    self = [self init];
 
    if (self) {
        // 以备不时之需
        _property = property;
 
        // 使用property_getName获取到该property的名称
        const char *name = property_getName(property);
        if (name) {
            _name = [NSString stringWithUTF8String:name];
        }
 
        // 目前不考虑自定义setter方法,只考虑系统默认生成setter方法
         // 也就是说属性username的setter方法为setUsername:
        NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:].uppercaseString, [_name substringFromIndex:]];
        _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]);
    }
 
    return self;
}
 
@end

PJXClassInfo代码

/**
 * @brief 存储Model的Class信息,不过目前只存储Class的property信息
 * @param propertyInfos 是一个NSMutableDictionary类型的变量,key存储property的名称,value存储对应的PJXPropertyInfo对象
 */
@interface PJXClassInfo : NSObject
@property (nonatomic, strong) NSMutableDictionary *propertyInfos;
@end
 
@implementation PJXClassInfo
 
- (instancetype)initWithClassInfo:(Class)cls
{
    self = [self init];
 
    // 使用class_copyPropertyList获取到Class的所有property(objc_property_t类型)
     unsigned int propertyCount = ;
    objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
 
    _propertyInfos = [NSMutableDictionary dictionary];
 
    // 遍历properties数组
     // 根据对应的objc_property_t信息构建出PJXPropertyInfo对象,并给propertyInfos赋值
    if (properties) {
        for (unsigned int i = ; i < propertyCount; i++) {
            PJXPropertyInfo *propertyInfo = [[PJXPropertyInfo alloc] initWithPropertyInfo:properties[i]];
            _propertyInfos[propertyInfo.name] = propertyInfo;
        }
        // 注意释放空间
        free(properties);
    }
 
    return self;
}
 
@end

现在我们回到之前的问题,即如何获取setter并应用?可以看到有了这两个数据结构,我们就已经解决了如何获取到每个property的setter的问题(使用PJXClassInfo的propertyInfos的属性)。剩下的事情就简单了,调用setter方法进行赋值。这里参考YYModel中的方式,使用了一个Core Foundation函数CFDictionaryApplyFunction


void CFDictionaryApplyFunction(CFDictionaryRef theDict, CFDictionaryApplierFunction applier, void *context);

该函数的作用是对于theDict每个key-value元素都应用applier函数。


所以我们来看看这个applier函数应该怎么设计。

注意这种C语言的applier回调函数不能设计为成员函数,因为成员函数隐藏了一个self参数。此处我们将该回调函数设计成static,并且命名为PropertyWithDictionaryFunction。

// 注意我传入的dictionary就是用户提供的JSON数据
// 比如此处传入的key==@"username",value==@"shuaige"
static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context)
{
    // 先将key和value转化到Cocoa框架下
    NSString *keyStr    = (__bridge NSString *)(key);
    id setValue         = (__bridge id)(value);
 
    // modelSelf其实就是self,不过我这里用的是static函数,所以没有默认参数self
    // 此时我们需要借助context参数来获取到这个self
    // 所以我设计了一个PJXModelContext,用来存储self信息
    // 另外,此函数的参数中也没有保存每个property信息,也得靠context这个参数来传递
    // 所以PJXModelContext还需要存储PJXClassInfo对象信息
    PJXModelContext *modelContext = context;
 
    id modelSelf = (__bridge id)(modelContext->modelSelf);
 
    PJXClassInfo *classInfo = (__bridge PJXClassInfo *)(modelContext->modelClassInfo);
    PJXPropertyInfo *info = classInfo.propertyInfos[keyStr];
 
    ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);
}

最后一步就是在我们的initWithAttributes:函数中构建PJXModelContext并应用到上述函数。

typedef struct {
    void *modelSelf;
    void *modelClassInfo;
}PJXModelContext;
 
- (instancetype)initWithAttributes:(NSDictionary *)attributes
{
    self = [self init];
 
    if (self) {
        // 初始化PJXClassInfo对象,并给modelContext赋值
        PJXModelContext modelContext = {};
        modelContext.modelSelf = (__bridge void *)(self);
        PJXClassInfo *classInfo = [[PJXClassInfo alloc] initWithClassInfo:[self class]];
        modelContext.modelClassInfo = (__bridge void *)classInfo;
 
        // 应用该函数,将得到JSON->Model后的Model数据
        CFDictionaryApplyFunction((CFDictionaryRef)attributes, PropertyWithDictionaryFunction, &modelContext);
    }
 
    return self;
}

4. 测试结果


在2.设计思路这一部分,我们举了一个案例。现在我们运行下,看看NSLog的结果:

成功了!

5. 存在问题


目前的函数整体才100来行,还是存在很多问题没有考虑到。

比如:

  1. 没有考虑用户传入的JSON数据的key值和property的名称不一致
  2. 没有考虑用户传入的JSON数据有嵌套
  3. 没有考虑JSON数据的value值不一定是NSString类型
  4. 没有考虑JSON数据并不一定是NSDictionary类型
  5. 没有考虑用户自定义了Model属性的setter方法

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/498980.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

借助PLC-Recorder,西门子PLC S7-200SMART实现2ms周期采集的方法(带时间戳采集)

目录 1、测试条件 2、测试结论 3、PLC的发送程序 4、PLC-Recorder侧的通讯设置 5、PLC-Recorder的通道配置 6、PLC-Recorder的变量配置 7、记录数据的情况 8、再说时间戳 9、小结 高速数据采集要保证速度&#xff0c;也要保证时刻的准确性。在windows系统里&#xff0…

ESP32学习笔记22-TWAI-CAN

22.TWAI-CAN 22.1概述 22.1.1参考博客 ESP32 基于自带控制器实现CAN总线通信(上) - 知乎 (zhihu.com) ESP32 基于自带控制器实现CAN总线通信(下) - 知乎 (zhihu.com) 22.1.2 ESP32 TWAI/CAN外设说明 可以支持标准帧格式(11位ID)和扩展帧格式(29位ID)ESP32 包含 1 个 T…

生信刷题之ROSALIND——Part 4 (MPRT, MRNA, ORF)

目录 写在前面1、Finding a Protein MotifProblemSample DatasetSample OutputCodeOutput 2、Inferring mRNA from ProteinProblemSample DatasetSample OutputexampleCodeOutput 3、Open Reading FramesProblemSample DatasetSample OutputCodeOutput 写在前面 本来打算每周更…

3.Hive基础命令练习

创建表格如下: 部门表&#xff1a; create table if not exists dept(deptno int, -- 部门编号dname string, -- 部门名称loc int -- 部门位置 ) row format delimited fields terminated by \t; 员工表&#xff1a; create table if not exists emp(empno int,…

AI 工具合辑盘点(十三)持续更新 之 面向宠物爱好者的 AI 工具和面向电影爱好者的 AI 工具

亲爱的宠物爱好者&#xff0c;这个部分是专门为你准备的。&#x1f43e; 不论你是爱狗人士还是铲屎官&#xff0c;AI 都能满足你。 访问地址&#xff1a; This Cat Does Not Exist 猫咪生成器 你知道喜欢猫的爱好有个专门的名字吗&#xff1f;在国外被称为ailurophilia&…

操作系统考试复习——第四章 对换 分页存储管理方式

对换技术也成为交换技术&#xff0c;由于当时计算机的内存都非常小&#xff0c;为了使该系统能分时运行多个用户程序而引入了对换技术。 1.对换的引入&#xff1a; 所谓“对换”&#xff0c;是指把内存中暂时不能运行的进程或者暂时不用的程序和数据调出到外存上&#xff0c;…

zk之数据的发布与订阅

数据的发布和订阅&#xff1a; &#xff08;1&#xff09;数据的发布与订阅是一个一对多的关系。多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态发生变化时&#xff0c;会通知所有的订阅者对象&#xff0c;使它们能够自动的更新自己的状态。发布和订阅可以让发…

SpringBoot项目修改application.yml,application-prod.yml配置文件中的端口,数据库链接等信息后,项目突然不能运行

SpringBoot项目修改application.yml&#xff0c;application-prod.yml配置文件中的端口&#xff0c;数据库链接等信息后&#xff0c;项目突然不能运行 问题记录 &#xff0c;SpringBoot项目修改application.yml&#xff0c;application-prod.yml配置文件中的端口&#xff0c;数…

跟姥爷深度学习5 浅用卷积网络做mnist数字识别

一、前言 前面用TensorFlow浅做了一个温度预测&#xff0c;使用的是全连接网络&#xff0c;同时我们还对网上的示例做了调试和修改&#xff0c;使得预测结果还能看。本篇我们更进一步使用CNN&#xff08;卷积&#xff09;网络&#xff0c;不过再预测温度就有点大材小用&#x…

Stable Diffusion Webui 本地部署【踩坑记录】

1、安装python Python Release Python 3.10.6 | Python.org 2、安装git git是一个代码管理工具&#xff0c;通过它可以将开源项目仓库克隆到本地 下载地址&#xff1a;Git - Downloading Package 3、下载stable-diffusion-webui 可以新建一个目录&#xff0c;在文件夹内单…

代数余子式怎么求

代数余子式是矩阵中每个元素的代数余数&#xff0c;可以通过以下步骤求得&#xff1a; 1. 找到该元素所在的行和列&#xff0c;将其删除&#xff0c;得到一个新的矩阵。 2. 计算新矩阵的行列式&#xff0c;乘以(-1)^(行号列号)&#xff0c;即为该元素的代数余子式。 例如 对…

vulnhub靶场之nasef1

1.信息收集 探测存活主机&#xff0c;发现192.168.239.176存活 对目标主机192.168.239.176进行端口扫描&#xff0c;发现存活22、80端口 浏览器访问http://192.168.239.176/&#xff0c;发现为apache2的页面&#xff0c;查看源码&#xff0c;未发现异常。 对http://192.16…

-笔记 tps qps

页面请求异步处理 将请求 扔进 kafka, Mq等 MQ单机抗几万并发也是ok的 底层批量处理 sql 处理 尽量批量处理&#xff0c;减少耗时 分库分表&#xff0c; 可能到了最后数据库层面还是免不了抗高并发的要求&#xff0c;好吧&#xff0c;那么就将一个数据库拆分为多个库&#xf…

Java ---System类

System 类位于 java.lang 包&#xff0c;代表当前 Java 程序的运行平台&#xff0c;系统级的很多属性和控制方法都放置在该类的内部。由于该类的构造方法是 private 的&#xff0c;所以无法创建该类的对象&#xff0c;也就是无法实例化该类。 System 类提供了一些类变量和类方…

PBDB Data Service:Thumbnail images of lifeforms(生命形式的缩略图)

Thumbnail images of lifeforms&#xff08;生命形式的缩略图&#xff09; 描述用法参数方法响应值格式术语表 描述 此操作返回表示指定分类的图像&#xff0c;或关于图像的信息。如果后缀是 .png&#xff0c;则返回图像内容数据。否则&#xff0c;将以指定的格式返回一个描述…

2023年全国硕士研究生入学统一考试英语(二)试题

2023年全国硕士研究生入学统一考试英语&#xff08;二&#xff09;试题 Section I Use of English Directions: Read the following text. Choose the best word ( s) for each numbered blank and mark A, B , C or D on the ANSWER SHEET. ( 10 points) Here’s a common …

OSS文件打包下载

前言 OSS 存放了很多项目&#xff08;项目是 TMagic 低代码平台编辑生成&#xff0c;自动上传 OSS&#xff09;&#xff0c;现在需要在管理后台将项目打包ZIP下载&#xff0c;并不在本地生成文件。 OSS 要下载项目文件&#xff1a; 一、思路实现 创建 OSSClient 实例获取 Bu…

K8s基础6——应用配置管理方案、调度策略、污点和污点容忍

文章目录 一、应用配置管理方案1.1 ConfigMap1.1.1 注入变量1.1.2 挂载数据卷 1.2 Secret 二、调度策略2.1 nodeSelector定向调度2.1.1 正例2.1.2 反例 2.2 nodeAffinity亲和力调度2.2.1 In硬策略2.2.2 NotIn硬策略2.2.3 软策略 2.3 PodAffinity亲和力调度2.3.1 pod共存2.3.2 p…

【机器学习】信息量、香农熵、信息增益

这节可以搭配 【机器学习】Logistic回归&#xff08;重新整理&#xff09;信息量&#xff08;信息&#xff09;信息量公式的推理过程 香农熵信息增益 【机器学习】Logistic回归&#xff08;重新整理&#xff09; B站视频&#xff1a;“交叉熵”如何做损失函数&#xff1f;打包…

RabbitMQ、RabbitMQ发布/订阅模式

1.RabbiMQ RabbitMQ是一个消息中间件 MQ的基本结构 1.1RabitMQ安装 参考&#xff1a;Docker安装 Docker中部署RabbitMQ 2.入门案例 2.1.publisher实现 package cn.itcast.mq.helloworld;import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; im…