Original: JD retail technology platform

Technology classification: Optimization, APP, analysis

Retrieved on: Github

As the Swift ABI stabilizes and developers’ interest in Swift continues to increase, some open source frameworks are no longer available in ObjC, and some of Apple’s new system libraries are Swift Only.

Under such a background, 360buy’s order business tries to use Swift more in different scenarios, such as:

  • Jd App part of the order business page

  • Jingdong App logistics widget

  • “Jingdong Work Station” provides macOS applications for integrating part of the working environment and development environment as well as part of the workflow for the company

During the transformation, Swift impressed the team with its efficiency, security, convenience and some excellent features. There are many features that developers don’t think much about when writing ObjC. For example, Swift offers static dispatch, use of value types, static polymorphism, Errors+Throws, currying and function composition, and enrichment of higher-order functions. Swift also offers better support for protocol-oriented programming, generic programming, and more abstract functional programming than OOP. Solves many of the pain points faced by developers in the ObjC era.

Combining the similarities and differences between Swift and ObjC, we reexamined and optimized the functional code of the project from the perspective of Swift’s advantages, including but not limited to the following aspects.

Replace some methods of dynamic dispatch with static dispatch

One of the reasons Swift is faster than ObjC is because of its dispatches: static (value types) and function table (reference types). The static ARM architecture can jump to the corresponding function address directly with THE BL instruction, which is the most efficient and beneficial to the inlining optimization of the compiler. The value type cannot inherit from the parent class, and the type can be determined at compile time, satisfying the conditions of static dispatch. For reference types, different compiler Settings can also affect how dispatches are delivered. For example, in WMO full-module compilation, the system automatically fills in keywords such as implicit final to decorate classes that are not inherited by subclasses, thus using static distribution as much as possible.

In our project, we did an overall check for all classes that use Class. Avoid inheriting From NSObject completely unless necessary, and use subclasses of NSObject infrequently. For scenarios where inheritance or polymorphism is not a concern, use keywords such as final or private whenever possible.

Another concern is that ObjC also introduces static dispatch of methods. The latest LLVM integration in Xcode12 has enabled ObjC to change dynamic dispatch to static dispatch by specifying __attribute__((objc_direct)) to methods.

Check that all classes are replaced with structures or enumerations where possible

Structures and enumerations in Swift are value types, and classes are reference types. Whether to use value types or reference types in Swift is something developers need to think about and evaluate.

In our JD.com logistics widget and SwiftUI based macOS app, we are currently using more structures and enumerations. Value types (Struct Enum, etc.)

  • Create on the stack, create fast

  • Small memory usage. The total memory footprint is the size of the internal property memory alignment

  • Memory reclaim fast, with the stack frame control on the stack out of the stack can, no heap memory overhead

  • No reference counting is required (except for structs that use reference types as attributes)

  • Generally static distribution, running fast, but also convenient compiler optimization, such as inlining

  • Deep copy when assigning. The system uses copy-on-write to avoid unnecessary Copy and reduce the Copy overhead

  • There is no implicit data sharing, with independence and immutability

  • Mutating can be used to modify attributes in a structure. In this way, the independence of the value type is guaranteed, and the modification of some attributes is also supported.

  • Thread-safe, generally no race conditions or deadlocks (be careful to ensure that values are copied in each child thread)

  • There is no inheritance support to avoid the problem of OOP subclasses being too coupled to their parents.

  • Abstractions can be implemented through protocols and generics. However, the structure of the implementation protocol is different in body size, so it cannot be directly put into the array. To ensure the consistency of storage, the system introduces the middle layer Of Interface Container when the parameter is assigned. Here, if there are more structure attributes, it will be a little more complicated, but Apple will also have an optimization (Indirect Storage With copy-on-write), which reduces the overhead. In general, polymorphism of value types has a cost, and the system is optimized as much as possible. Developers need to think less about dynamic polymorphism and use the protocol directly as a class, and more about static polymorphism, using more in combination with generic constraints.

Reference types (Class Function Closure, etc.):

  • Reference types are less efficient in memory usage than value types, and are created on the heap and require a stack pointer to point to that region, increasing the overhead of heap memory allocation and reclamation

  • The assignment consumes little and is generally a shallow copy of the pointer. But there are reference counting costs

  • Multiple Pointers can point to the same memory, independence difference, easy to misoperation

  • Non-thread-safe, to consider atomicity, multiple threads require thread locking together

  • Reference counting is required to control memory release, and misuse carries the risk of wild Pointers, memory leaks, and circular references

  • Inheritance is allowed, but the Side effect of inheritance is a tight coupling between a child class and its parent class. For example, the main purpose of the UIStackView system is to use the layout, but it has to inherit all the properties and methods of UIView.

Thus, Swift provides more powerful value types in an attempt to address the typical pain points of OOP in the ObjC era, such as tight coupling between subclasses and superclasses, implicit data sharing of objects, non-thread-safety, and reference counting. If you look at the Swift library, you’ll find that it consists primarily of value types, and that collections of basic types such as Int, Double, Float, String, Array, Dictionary, Set, and Tuple are also structs. Of course, although value types have many advantages, this is not to say that Class should be completely abandoned, or according to the actual situation, the actual Swift development is more of a combination of ways, not using OOP is not practical.

Optimized structure in vivo

As with C constructs, the size of a Swift structure is the size of the internal attribute memory alignment. The order in which the properties are placed in the structure affects the final memory size. You can use the MemoryLayout provided by the system to view the memory size of the corresponding structure.

We reviewed some details, such as not using Int in cases where Int32 is fully satisfied, not using String or Int instead of Bool in cases where it should be used, keeping small memory properties as late as possible, etc.

struct GameBoard { var p1Score: Int32 var p2Score: Int32 var gameOver: Bool }struct GameBoard2 { var p1Score: Int32 var gameOver: Bool var p2Score: Int32}// Based on CPU addressing efficiency, MemoryLayout<GameBoard>.self.size //4 + 4 + 1 = 9(bytes)MemoryLayout<GameBoard2>.self.size //4 + 4 + 4 = 12(bytes)Copy the code

Use static polymorphism instead of dynamic polymorphism

When we mentioned value types above, we mentioned static polymorphism, which is the type that the compiler can determine at compile time. This allows the compiler to type degrade and produce methods of a specific type at compile time.

The definition of generics as conforming to the constraints of a certain protocol can avoid directly passing the protocol as a class, otherwise the compiler will report an error, which is equivalent to the interface support polymorphism, but the invocation should be made with a specific type, thus achieving the purpose of static polymorphism. For static polymorphism, the compiler optimizes to take full advantage of its static nature and tries to control the resulting code growth with WMO Whole Module Optimization in place.

In short, developers should consider static polymorphism as much as possible. For example, generics can be introduced when a protocol is used as an argument to a function. Here’s a classic discussion from WWDC:

protocol Drawable { func draw()}struct Line: Drawable { var x: Double = 0 func draw() { }}func drawACopy<T: Drawable>(local: T) {Drawable local.draw()}let line = line ()drawACopy(local: T) Let line2: Drawable = line ()drawACopy(local: drawACopy)drawACopy(local: drawACopy)drawACopy(local: drawACopy) Line2)//Error, the compiler does not allow the Drawable protocol as an incoming parameterCopy the code

Protocol-oriented provides an extended default implementation for a protocol

Swift prefers the parent class of a class to the protocol of compliance. Structs/Enums + Protocols + Protocol Extensions + Generics can be used to implement logical abstractions in both ObjC and Swift.

We minimized the use of OOP in our projects, using only value type protocol orientation and generics as much as possible, so that the compiler can do more static optimization and reduce the tight coupling caused by OOP superclasses.

Also, Protocol Extension provides a default implementation for Protocol, which is an important optimization that distinguishes it from ObjC.

Note that the method in Protocol Extension should be called with a specific type, not with the Protocol derived from type inference. When invoked using Protocol, if the method is not defined in Protocol, the default implementation in Protocol Extension will be called, even if the corresponding method is implemented in the specific type. Because the compiler can only find the default implementation at this point.

Optimizing error handling

The apparent benefits of Swift’s more sophisticated handling of errors and throws than ObjC are a friendlier API, improved readability, and reduced Error probability with editor detection. In the ObjC era, exceptions are often not considered, and this is something programmers used to ObjC code need to take care of when encapsulating low-level apis. An Enum that inherits the Error protocol is common.

enum CustomError: Error {   case error1   case error2}
Copy the code

An Error can also be thrown for external processing. Methods that support the throw are strongly checked by the compiler to see if they are handling the throw. () throws -> Void; () throws -> Void; () throws -> Void;

//(Int)->Void throws->Voidlet a: (Int)throws->Void = {n in} (Int) -> Void = { n throws in}Copy the code

Rethrows: If a function takes a function that supports throws, rethrows an Error. When this function is used, the compiler checks to see if a try-catch is needed.

This is something we need to consider when encapsulating basic functionality. There are many friendly examples in the system, such as the definition of the map function in the system:

public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]let a = [1, 2, 3]enum CustomError: Error {case error1 case error2}do {let _ = try A.ap {n -> Int in guard n >= 0 else {// If there is an Error in the closure that map received, Error1} return n * n}} catch CustomError. Error1 {} catch {}Copy the code

Use Guard to reduce if nesting

Guard can be used for critical checks, which has the advantage of increased readability and less if nesting. With Guard, the else would normally be return, throw, continue, break, and so on.

If (XXX){if (XXX){if (XXX){if (XXX){}}}} let dict: Dictionary = ["key": "0"]guard let value1 = dict["key"], value == "0" else { return}guard let value2 = dict["key2"], value == "0" else { return}print("\(value1) \(value2)")Copy the code

Use the Defer

The closure modified as defer will be called when the current scope exits. This is mainly to avoid adding code that needs to be executed before returning, thus improving readability.

For example, if we have a file read/write operation in our macOS application, use defer to make sure we don’t forget to close the file.

func write() throws { //... guard let file = FileHandle(forUpdatingAtPath: filepath) else { throw WriteError.notFound } defer { try? file.close() } //... }Copy the code

Other common scenarios are when a lock is released, and non-escaping closure callbacks.

But don’t overuse defer, and be aware of the closure’s capture variables and scope when you use it.

For example, if you use defer in the if statement, it will be executed when the if pops out.

Replace all forced unpacking with optional bindings

For optional values, try to avoid forced unpacking as much as possible, or even completely. Most of the time if encountered need to use! Is likely to show that the original design was unreasonable. When downCasting is included, avoid using as! Because the cast itself may fail. , as? And, of course, the try! Also avoid.

For optional values, always use optional binding detection to ensure that the real value of the optional variable exists before you proceed:

var optString: String? if let _ = optString {}Copy the code

Consider lazy loading

Change the properties in the project that do not need to be created to lazy load. Swift’s Lazy load is much more readable and easier to implement than ObjC’s, so use Lazy.

lazy var aLabel: UILabel = {    let label = UILabel()    return label}()
Copy the code

Use functional programming to reduce state variable declaration and maintenance

Declaring too many state variables in a class is bad for later maintenance. Swift functions can be used as function parameters, return values, and variables, making it a good support for functional programming. Using functions can effectively reduce global variables or state variables.

Imperative programming focuses more on the steps to solve the problem. Direct response to machine instruction sequence. There are variables (corresponding to storage units), assignment statements (corresponding to obtain and store instructions), expressions (corresponding to instruction arithmetic calculation), and control statements (corresponding to jump instructions).

Functional programming is more concerned with the mapping of data and the flow of data, i.e. input and output. Functions are treated as variables, either as arguments (input values) to other functions or as returns (output values) from functions. The calculation is described as the evaluation of an expression, the mapping f(x)->y of the independent variable, given x, will be stably mapped to y. Functions do not access variables outside the function scope as much as possible, only rely on the input parameters, reduce the declaration and maintenance of state variables. And use less mutable variables (objects) and more immutable variables (structures). This way, there will be no other side effects.

The function that accepts multiple parameters is transformed into one that accepts a single parameter by using currying, and some parameters are cached inside the function. Function composition is also used to increase readability. For example, to do addition and multiplication, we can encapsulate the addition and multiplication functions and call each of them:

func add(_ a: Int, _ b: Int) -> Int { a + b }func multiple(_ a: Int, _ b: Int) -> Int { a * b }let n = 3multiple(add(n, 7), 6) //(n + 7) * 6 = 60
Copy the code

You can also use a functional:

Func add(_ a: Int)-> (Int)->Int {{$0 + a}} func multiple(_ a: Int)->Int Int) -> (Int) -> Int {{$0 * a}} infix operator > : AdditionPrecedencefunc >(_ f1: @escaping (Int)->Int, _ f2: @escaping (Int)->Int) -> (Int)->Int {{f2(f1($0))}}// To generate the new function newFnlet n = 3let newFn = add(7) > multiple(6) (Int)->Intprint( newFn(n) ) //(n + 7) * 6 = 60Copy the code

As you can see, from using multiple(add(n, 7), 6) to let newFn = add(7) > multiple(6), newFn(n), the overall picture is clearer, especially in more complex scenarios, the advantage is more obvious.

conclusion

Swift offers a wealth of simple syntax-sugar and powerful type inference that makes it easy to get started. But from a performance point of view or a better API design point of view, more practice is needed. The order team is trying to use Swift and SwiftUI as much as possible in the development of iOS widgets, AppClips, JINGdong workstation (macOS desktop application) and other scenarios, and the development efficiency and project stability have achieved good performance. At present, JD Group’s internal infrastructure for Swift is being gradually improved. We believe and hope that more students in the group will participate in the development of Swift in the future.

Recommended favorites: Dry goods: Github