background JSON is a commonly used application layer data exchange protocol for mobile development. The most common scenario is that the client initiates a network request to the server, the server returns a JSON text, and then the client parses the JSON text and displays the corresponding data on the page. But when programming, processing JSON is a hassle. Without introducing any wheels, we usually need to convert JSON to Dictionary first, and then remember the key corresponding to each data, and use this key to retrieve the corresponding value in the Dictionary for use. In this process, we will make various mistakes:
In order to solve these problems, many open source libraries for processing JSON have emerged. In Swift, these open source libraries mainly work in two directions:
For 1, the most widely used and highly rated library is SwiftyJSON, which represents the core of this direction. It is still based on the JSON structure to get the value, which is easy and clear to use. But because of this, this approach cannot properly solve the above problems, because the key, path, and type still need to be specified by the developer; For option 2, I personally think this is a more reasonable approach. Due to the existence of the Model class, the parsing and use of JSON are subject to the constraints of the definition. As long as the client and the server agree on the Model class, after the client defines it, when using the data in the business, you can enjoy the benefits of syntax checking, property preview, property completion, etc., and once the data definition changes, the compiler will force all used places to change before the compilation passes, which is very safe. In this direction, the work done by open source libraries is mainly to deserialize JSON text into the Model class. This type of JSON library includes ObjectMapper, JSONNeverDie, HandyJSON, etc. HandyJSON is the most comfortable library to use. This article will introduce the use of HandyJSON to convert between Model and JSON. Project address: https://github.com/alibaba/handyjson Why use HandyJSON Before HandyJSON appeared, there were two main ways to deserialize JSON to Model class in Swift:
Both of them have obvious disadvantages. The former requires Model to inherit from NSObject, which is very inelegant and directly negates the way of defining Model with struct; the latter's Mapping function requires developers to customize it and specify the JSON field name corresponding to each attribute, which is highly invasive and still prone to spelling errors and maintenance difficulties. HandyJSON takes a different approach and uses Swift reflection + memory assignment to construct Model instances, avoiding the problems encountered by the above two solutions. Convert JSON to Model Simple Types If a Model class wants to support deserialization through HandyJSON, it only needs to implement the HandyJSON protocol when defining it. This protocol only requires the implementation of an empty init() function. For example, we have agreed with the server on an Animal data with name/id/num fields, so we define the Animal class like this:
Then suppose we get the following JSON text from the server:
After introducing HandyJSON, we can do deserialization like this:
Simple, huh? Support Struct If Model is defined as a struct, since Swift provides a default constructor for struct, there is no need to implement an empty init() function. However, please note that if you specify another constructor for strcut, you need to keep an empty implementation.
More complex types HandyJSON supports various forms of basic properties in class definitions, including optional (?), implicitly unwrapped optional (!), arrays, dictionaries, Objective-C basic types (NSString, NSNumber), various types of nesting ([Int]?, [String]?, [Int]!, ...), etc. For example, the following type looks more complicated:
Just as easy to convert:
Nested Model Class If an attribute in the Model class is another custom Model class, then as long as that Model class also implements the HandyJSON protocol, it can be converted:
Specify a node in the deserialized JSON Sometimes the JSON text returned to us by the server contains a lot of status information that has nothing to do with the Model, such as statusCode, debugMessage, etc., or the useful data is below a certain node, then we can specify which node to deserialize:
Model class with inheritance relationship If a Model class inherits from another Model class, you only need to make the parent Model class implement the HandyJSON protocol:
Custom parsing method HandyJSON also provides an extension capability, which allows you to define the parsing key and parsing method of a field in the Model class. We often have such requirements:
The HandyJSON protocol provides an optional mapping() function, in which we can specify the key of a field or the method to parse its value from JSON. For example, we have a Model class and a JSON string returned by the server:
As you can see, the id attribute of the Cat class does not correspond to the Key in the JSON text; and as for the parent attribute, it is a tuple and cannot be parsed from "Tom/Lily" in JSON. So we need to define a Mapping function to support these two things:
In this way, HandyJSON helps us convert JSON to Model class perfectly. It is so convenient that this is the reason why it is named Handy. Convert Model to JSON text HandyJSON also provides the ability to serialize Model classes into JSON text, which is simply ruthless. Basic Types If you only need to serialize, you don't need to make any special changes when defining the Model class. For any class instance, you can directly call HandyJSON's serialization method to serialize and get a JSON string.
Complex Types Even if there are other Model classes in the Model class, they are all supported.
Summarize With the support of HandyJSON, we can now happily use JSON in Swift. This library supports Swift 2.2+, Swift 3.0+. If you have any requirements or suggestions, you can go to https://github.com/alibaba/handyjson to raise an issue. Why HandyJSON was developed? My iOS team switched to Swift last November. Our server and client data exchange format has always been JSON, and at that time, the only well-known library for processing JSON in Swift seemed to be SwiftyJSON. After the project switched to Swift, we also used this library. After using it, the requirements were met, but for some complex models, the code looked very bad, because each value needs to be in the form of json["akey"]["bkey"]["ckey"].value. When writing, I didn't think there was any problem with the document, but later, when it was separated from the document, the whole article was full of keys expressed in strings, and it was difficult to feel the model structure from the code. So we would write a section of sample data in the comments. But it is still quite messy, and it is also difficult to debug if the key is written incorrectly. Sometimes it takes half a day to debug a case problem. So we evolved a bit, writing the Model first, then writing the convert function in the Model class, and also using KVC to traverse the key assignment. It was much more comfortable to write, but still troublesome, and required every class to inherit from NSObject. Not long after, we got to know the ObjectMapper library, and without hesitation, we switched to it. The world suddenly became much cleaner. But it still feels a little bit off, because ObjectMapper needs to specify the mapping relationship by itself. Usually the key in JSON and the field name in Model are the same, and I have to write a bunch of extra things every time, which always feels redundant, and it is also difficult to change the field. New colleagues who have just come into contact with Swift also expressed that they are not very comfortable, because the JSON deserialization library they used before, whether in Java or Objective-C, naturally uses the Model field name to get the value. So I thought about studying whether this effect can be achieved in Swift. Design ideas of HandyJSON Limitations in Swift Whether it is a JSON deserialization library in Java or Objective-C, it usually obtains the field name set of the Model at runtime, traverses the set, takes the key to get the value in JSON and completes the assignment. These steps can be achieved by Java relying on the reflection mechanism, and Objective-C can also easily achieve them through the class_copyPropertyList method plus the KVC mechanism. However, Swift will get stuck at the last step: unable to assign a value. Swift's reflection is read-only, that is, we can get all the fields and field values of a Model instance at runtime, but we cannot assign values to it. In fact, the value we get is a read-only copy of the original value, and even if we get the address of this copy and write a new value, it is invalid.
Furthermore, Apple's official website still describes the Mirror class that implements the reflection mechanism as follows: Mirrors are used by playgrounds and the debugger. The attitude is very vague and seems not very encouraging, but many libraries in production use it. It can only be said that Apple will not easily remove this capability, but it is unlikely to expect it to make improvements to this capability (such as supporting runtime assignment). How to bypass restrictions The simplest way is to inherit NSObject when defining Model in Swift, so that the instance of this Model exists in the objc runtime, and the above-mentioned class_copyPropertyList method and KVC can be used. All the JSON libraries that do not require the mapping relationship to be specified in Swift that we have seen so far use this method. Then there are libraries represented by ObjectMapper, which complete the assignment when specifying the mapping relationship through operator overloading. There are also many libraries that implement this type. But what I want to do is to support pure Swift classes running in the Swift runtime without explicitly specifying the mapping relationship of each field. So, if reflection assignment is not possible, then write it directly to memory. Specific implementation In Swift, the memory layout of a class instance is regular:
There is basically no official reference in this regard. Some of the above rules are collected from the summaries of other experts on the Internet, some are mined from some Clang documentation, and some are tested in playgrounds. I was not sure at first, but after implementing HandyJSON for so long, there has been no problem, so I think it is reliable. Now that we have a way to calculate the memory layout, the rest is pretty easy. For an example:
Get the starting pointer of the class instance In Swift, the methods for getting the starting pointer of a struct instance and the starting pointer of a class instance are different, and are also related to the language version. In Swift3:
Get field name and type through Mirror
Calculate the size of each attribute field in the Model Swift 3 exposes two interfaces for calculating the size of a type: MemoryLayout.size(ofValue: T) and MemoryLayout.size. Neither of them can be used directly because:
Therefore, for each Model class T that implements the HandyJSON protocol, you can directly call T.size() to get the size of T. Impact of memory alignment The attributes of class instances are not directly arranged in order according to their respective space sizes, otherwise things would be simple. Like C/C++, the instance memory layout in Swift also takes memory alignment into consideration. After reading Swift's docs and some LLVM materials, MemoryLayout provides an interface: MemoryLayout.alignment. The alignment rule is that the starting address of each field must be an integer multiple of the alignment value. I have forgotten the source of the details. After conducting some complex type tests at the time, it was determined that it was true. So the function in HandyJSON to calculate the starting address of the next field is:
Other situations The basic types can be processed in the above way. There are also optional types, array types, and dictionary types. The processing methods are similar through traversal, recursive parsing, etc. For example, arrays:
Get the Array generic parameter type, then construct an array of that type and complete the assignment. Conclusion The main process is like this, and it is relatively simple. The remaining handling of inheritance, combination, etc. is just an implementation issue, so I will not go into details. I always feel that my understanding of the Swift pointer facility is not very good. Maybe there are better uses, for example, there is no need for an empty init() function to initialize an instance of a class. If any students have a deeper understanding in this regard, and have any opinions or suggestions, please feel free to communicate~ refer to
|
<<: Starting from Dash iOS open source, don’t pursue perfect code too much
>>: Android immersive status bar and suspension effect
If there are no operational resources, we can fin...
It has been exactly four years since I graduated....
TikTok 0 to 1 Basic Course Resource Introduction:...
“Drinking tea frequently can lead to kidney stone...
Expert of this article: Wang Xiaohuan, Doctor of ...
At the end of the year, the review process sudden...
The factors affecting the quotation of Heze Elect...
Theoretical physicist Stephen Hawking and others ...
Recently, we are working on optimizing the succes...
Audit expert: Yin Tielun Deputy Chief Physician, ...
The main factors affecting the price of mini prog...
According to the data from the "iiMedia Repo...
People have to sleep every day, but have you ever...
...
Giada D2308U is an upgraded version of D2308. Alth...