2
头图

Author: Wang Zhejian (Zhejian)

After one and a half months of technical refactoring, several months of iteration and heavy volume, the new version of the product evaluation list of Hand Taobao finally ran the entire process steadily with 100% traffic on Double Eleven in 2021. We have not only made a clear improvement in our business, but also precipitated a lot of technical explorations, such as precipitation of a light-mode R&D framework based on DinamicX + event chain orchestration, and promotion of the native language upgrade to Swift/Kotlin, which ultimately made the overall R&D efficiency and There is a relatively large improvement in stability. (Note: DinamicX is an internal self-developed dynamic UI framework)

In this article, I will focus on the part about Swift. If you want to know some questions about how Swift improves R&D efficiency/quality, whether existing projects/modules require Swift as a native language, how to choose, what problems we encountered in the process of product evaluation and implementation of Swift, and what are the benefits and conclusions in the end. , I hope this article can bring you some help.

First of all, why would I choose to learn Swift?

Technological change, the future is here

Because I am very firm in my heart. Compared with OC, Swift is more capable of carrying the future.

Strong backing

The main reason is that it has a strong backing , Swift Apple as the most important future development language, WWDC content of external light output has been up to 73, including, but not limited to, grammar, design, performance, development tool chain , The specific content is shown in the figure:

Looking back at the development of Swift in the past few years, it has been 7 years since its official release in 2014. In the whole process, Apple has invested a lot of energy in building Swift, especially the emergence of the Swift Only framework. It also means that Apple is actively advocating everyone to invest in Swift development.

Three advantages

Secondly, Swift has three clear advantages: faster, safer and more expressive .

faster. means that Swift has made many optimizations in execution efficiency. For example, the Swift system library itself uses many basic types that do not require reference counting, and problems such as memory allocation size, reference counting loss, and static analysis of method dispatch have been effectively improved. The specific details will not be analyzed here, and those who are interested can move to Understanding Swift Performance to understand the details.

The so-called security does not mean that no crash occurs, but it means that any input has a clearer definition of performance. Swift was originally designed to hope that developers can write code without any unsafe data structures. Therefore, Swift has a very robust type system. Developers can complete all development work almost without considering pointer issues. At the same time, it also provides a series of types or functions prefixed with Unsafe, which are used for high-performance interaction with unsafe languages (such as C language) and relatively unsafe operations such as operating raw memory. On the one hand, Unsafe is used to warn developers to use these APIs. , The other aspect is to distinguish types to ensure that most development scenarios use safe types.

Here you can share a piece of data. An App project I participated in before was written in Pure Swift (99%+). Our online crash rate continued to be around 8 per 100,000 all year round. This is for a small team (single-ended 4 people), it is a very impressive result. We hardly use Unsafe APIs, so that most of our problems can be avoided during compilation. The design of optional types and optional binding forces developers to think about how to deal with scenarios where the value is empty, so that before the software is released Just kill the developer's mistakes in the bud.

more expressive Simply put, it uses less code to express a complete logic. There have been 330 proposals in the Swift Evolution project to enhance the expressiveness of Swift, and thanks to these features, the amount of code in Swift is about 30%-50% less than that of OC. Let's give a few practical examples

Builder Pattern

When we define a complex model with many attributes, we don't want the attributes of this model to be changed after initialization. We need to solve it through the builder mode, the code is as follows:

// OCDemoModelBuilder.h
@interface OCDemoModelBuilder : NSObject

@property (nonatomic, copy, nonnull) NSString *a;
@property (nonatomic, copy, nonnull) NSString *b;
@property (nonatomic, copy, nonnull) NSString *c;
@property (nonatomic, copy, nonnull) NSString *d;
@property (nonatomic, copy, nonnull) NSString *e;
@property (nonatomic, copy, nonnull) NSString *f;
@property (nonatomic, copy, nonnull) NSString *g;
@property (nonatomic, copy, nonnull) NSString *h;

@end

// OCDemoModelBuilder.m

@implementation OCDemoModelBuilder

- (instancetype)init {
    if (self = [super init]) {
        _a = @"a";
        _b = @"b";
        _c = @"c";
        _d = @"d";
        _e = @"e";
        _f = @"f";
        _g = @"g";
        _h = @"h";
    }
    return self;
}

@end

// OCDemoModel.h

@interface OCDemoModel : NSObject

@property (nonatomic, readonly, nonnull) NSString *a;
@property (nonatomic, readonly, nonnull) NSString *b;
@property (nonatomic, readonly, nonnull) NSString *c;
@property (nonatomic, readonly, nonnull) NSString *d;
@property (nonatomic, readonly, nonnull) NSString *e;
@property (nonatomic, readonly, nonnull) NSString *f;
@property (nonatomic, readonly, nonnull) NSString *g;
@property (nonatomic, readonly, nonnull) NSString *h;

- (instancetype)initWithBuilder:(void(^)(OCDemoModelBuilder *builder))builderBlock;

@end

// OCDemoModel.m

@implementation OCDemoModel

- (instancetype)initWithBuilder:(void(^)(OCDemoModelBuilder *builder))builderBlock {
    if (self = [super init]) {
        OCDemoModelBuilder * builder = [[OCDemoModelBuilder alloc] init];
        if (builderBlock) {
            builderBlock(builder);
        }
        _a = builder.a;
        _b = builder.b;
        _c = builder.c;
        _d = builder.d;
        _e = builder.e;
        _f = builder.f;
        _g = builder.g;
        _h = builder.h;
    }
    return self;
}

@end

// Usage

OCDemoModel *ret = [[OCDemoModel alloc] initWithBuilder:^(OCDemoModelBuilder * _Nonnull builder) {
    builder.b = @"b1";
}];

// ret = a,b1,c,d,e,f,g

But Swift's Struct supports attribute default values and initialization constructors, making the builder pattern not very meaningful. The code is as follows:

struct SwiftDemoModel {
    var a = "a"
    var b = "b"
    var c = "c"
    var d = "d"
    var e = "e"
    var f = "f"
    var g = "g"
    var h = "h"
}

// Usage

let ret = SwiftDemoModel(b: "b1")

// ret = a,b1,c,d,e,f,g
State Pattern

When the execution result of a function may have many different states, we usually use the state mode to solve the problem.

For example, we define that a function execution result may have three states of finish\failure\none. Because there are some associated values, we cannot use enumeration to solve it. Need to define three different specific types, the specific code is as follows:

///  Executable.h
@protocol Executable <NSObject>

- (nullable NSDictionary *)toFormattedData;

@end

///  OCDemoExecutedResult.h
@interface OCDemoExecutedResult: NSObject<Executable>

/// 构造一个空返回值
+ (OCDemoNoneResult *)none;

/// 构造成功返回值
+ (OCDemoFinishedResult *)finishedWithData:(nullable NSDictionary *)data
                                      type:(nullable NSString *)type;
/// 构造一个错误返回值
+ (OCDemoFailureResult *)failureWithErrorCode:(nonnull NSString *)errorCode
                                     errorMsg:(nonnull NSString *)errorMsg
                                     userInfo:(nullable NSDictionary *)userInfo;

@end

///  OCDemoExecutedResult.m
@implementation OCDemoExecutedResult

/// 构造一个空返回值
+ (OCDemoNoneResult *)none {
    return [OCDemoNoneResult new];
}

+ (OCDemoFinishedResult *)finishedWithData:(nullable NSDictionary *)data type:(nullable NSString *)type {
    return [[OCDemoFinishedResult alloc] initWithData:data type:type];
}

+ (OCDemoFailureResult *)failureWithErrorCode:(nonnull NSString *)errorCode errorMsg:(nonnull NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo {
    return [[OCDemoFailureResult alloc] initWithErrorCode:errorCode errorMsg:errorMsg userInfo:userInfo];
}

- (nullable NSDictionary *)toFormattedData {
    return nil;
}

@end

///  OCDemoNoneResult.h
@interface OCDemoNoneResult : OCDemoExecutedResult

@end

///  OCDemoNoneResult.m
@implementation OCDemoNoneResult

@end

///  OCDemoFinishedResult.h
@interface OCDemoFinishedResult: OCDemoExecutedResult

/// 类型
@property (nonatomic, copy, nonnull) NSString *type;
/// 关联值
@property (nonatomic, copy, nullable) NSDictionary *data;

/// 初始化方法
- (instancetype)initWithData:(nullable NSDictionary *)data type:(nullable NSString *)type;

@end

///  OCDemoFinishedResult.h
@implementation OCDemoFinishedResult

- (instancetype)initWithData:(nullable NSDictionary *)data type:(nullable NSString *)type {
    if (self = [super init]) {
        _data = [data copy];
        _type = [(type ?:@"result") copy];
    }
    return self;
}

- (NSDictionary *)toFormattedData {
    return @{
        @"type": self.type,
        @"data": self.data ?: [NSNull null]
    };
}

@end

///  OCDemoFailureResult.h
@interface OCDemoFailureResult: OCDemoExecutedResult

/// 错误码
@property (nonatomic, copy, readonly, nonnull) NSString *errorCode;
/// 错误信息
@property (nonatomic, copy, readonly, nonnull) NSString *errorMsg;
/// 关联值
@property (nonatomic, copy, readonly, nullable) NSDictionary *userInfo;

/// 初始化方法
- (instancetype)initWithErrorCode:(NSString *)errorCode errorMsg:(NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo;

@end

///  OCDemoFailureResult.m
@implementation OCDemoFailureResult

- (OCDemoFailureResult *)initWithErrorCode:(NSString *)errorCode errorMsg:(NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo {
    if (self = [super init]) {
        _errorCode = [errorCode copy];
        _errorMsg = [errorMsg copy];
        _userInfo = [userInfo copy];
    }
    return self;
}

- (NSDictionary *)toFormattedData {
    return @{
        @"code": self.errorCode,
        @"msg": self.errorMsg,
        @"data": self.userInfo ?: [NSNull null]
    };
}

@end

But if we use Swift's enum feature, the code will become much more concise:

public enum SwiftDemoExecutedResult {
    /// 正确返回值
    case finished(type: String, result: [String: Any]?)
    /// 错误返回值
    case failure(errorCode: String, errorMsg: String, userInfo: [String: Any]?)
    /// 空返回值
    case none
    /// 格式化
    func toFormattedData() -> [String: Any]? {
        switch self {
        case .finished(type: let type, result: let result):
            var ret: [String: Any] = [:]
            ret["type"] = type
            ret["data"] = result
            return ret
        case .failure(errorCode: let errorCode, errorMsg: let errorMsg, userInfo: let userInfo):
            var ret: [String: Any] = [:]
            ret["code"] = errorCode
            ret["msg"] = errorMsg
            ret["data"] = userInfo
            return ret
        case .none:
            return nil
        }
    }
}
Facade Pattern

When we define an input parameter that needs to conform to multiple protocol types, we usually use the facade pattern to solve the problem.

For example, we have four protocols JSONDecodable, JSONEncodable, XMLDecodable, XMLEncodable and a method with two input parameters. The input parameter 1 is json which requires both JSONDecodable and JSONEncodable protocols, and the input parameter 2 is xml which meets both XMLDecodable and XMLEncodable. When we use OC to solve problems, we usually write like this:

@protocol JSONDecodable <NSObject>
@end

@protocol JSONEncodable <NSObject>
@end

@protocol XMLDecodable <NSObject>
@end

@protocol XMLEncodable <NSObject>
@end

@protocol JSONCodable <JSONDecodable, JSONEncodable>
@end

@protocol XMLCodable <XMLDecodable, XMLEncodable>
@end

- (void)decodeJSON:(id<JSONCodable>)json xml:(id<XMLCodable>)xml {

}

Two additional protocols JSONCodable and XMLCodable are defined to solve this problem. But in Swift, we can use & to solve this problem, no need to define additional types, the code is as follows:

protocol JSONDecodable {}
protocol JSONEncodable {}
protocol XMLDecodable {}
protocol XMLEncodable {}

func decode(json: JSONDecodable & JSONEncodable, xml: XMLDecodable & XMLEncodable) {
}

The above are some of Swift more expressive aspects of 161d6b6a8a62e9. Of course, the advantages are far more than these, but the space is limited and I will not expand here.

All in all, thanks to the high expressiveness of Swift, developers can express a complete logic with less code, which reduces development costs to a certain extent and also reduces the occurrence of problems.

Unstoppable

In addition to having a strong backing and three advantages, Swift has also developed relatively good trends in recent years.

First, according to Githut, the activity of Swift language on Github (Pull request) has surpassed OC, as shown in the following figure: (Data as of October 25, 2021)

At the same time, the domestic Top 100 Swift mixed applications have also increased significantly, from 22% in 19 years to 59%: (data as of 2021/04/22)

The improvement here is that on the one hand, many first-tier Internet companies in China have begun to deploy, and on the other hand, the emergence of Swift Only frameworks such as WidgetKit is also prompting everyone to start building Swift infrastructure.

Of course, foreign data is even more eye-catching. It has reached 91%. It can be said that almost all have been used. Why do you say that? Because 8 of the top 100 Google apps in the US version did not use Swift.

Here is another piece of data to share with you. When we organized the recruitment of "WWDC Internal Reference" authors in our spare time, we collected the author's technology stack and points of interest, and finally found that more than half of the authors have relatively rich Swift development experience, and 2 /3 people are more interested in the content of Swift (a total of 180 people sample). It can be seen that the community's enthusiasm for Swift is still very high. From a long-term perspective, whether to use Swift for development will also become one of the reasons why everyone chooses to work.

Why choose the product evaluation list?

Maybe after seeing the first part, many people will have an urge to use Swift in our project immediately. In order to prevent you from paying for your "impulse", let me share the journey of choosing Swift in the "hand-shopping product evaluation list".

Let’s briefly talk about my own hands-on shopping experience. At first, I joined the hands-on shopping infrastructure group. One of my main job responsibilities was to build Swift infrastructure. However, due to organizational needs, I joined a new business architecture group and worked. The focus has also changed from the original Swift-based upgrade-driven business to a pilot-driven basic technology upgrade. In this process, we mainly experienced three technical decisions:

  • 1. The project that the team first took over: the hand-sale order agreement was upgraded to Xinaochuang
  • 2. Based on the domain understanding of business research and development, the team proposed a new event chain orchestration capability and built it together with DX
  • 3. Product evaluation reconstruction, including evaluation list, interaction, etc.

At each stage, I thought about whether I would use Swift, but in the end I gave up using Swift, which I am good at, for the first two times, mainly for the following considerations:

Need to have the prerequisites to use Swift

The biggest problem with the order Xinaochuang project did not use Swift as the main development language was that the basic infrastructure at that time was not complete. Most of the dependent modules do not support Module. It is almost impossible to harden Swift and will increase a lot of work. For a project with a relatively short schedule, it is not a wise move. Under consideration, I temporarily gave up the idea of using Swift.

What kind of business is more suitable for refactoring using Swift

When the basic conditions are complete, Swift will be a better choice for a business refactoring project. Regardless of the trend of the general environment or the unique advantages of Swift, it is no longer suitable to continue to use OC to refactor a business module.

For large projects that want to try Swift, it is recommended to give priority to small burdens and small businesses involved in pilot projects. At that time, another important reason why we gave up using Swift in the order for the Xin Alchuang project is that the overall structure of Alchuang is relatively complex, the construction and data are mixed together, and the cost of local changes is too high, which will cause problems that affect the whole body. The openness and tolerance of end-to-side new technology interaction is limited. However, there is no such problem in the evaluation of hand-made products, and there are more options. Therefore, we firmly chose Swift as the main development language on the end-side.

Need to adapt to local conditions and get support

When the project has the conditions to use Swift, it must be considered comprehensively based on the current situation of the team.

First of all, the team needs to train or equip a person with Swift development experience in advance to ensure that complex problems are tackled and code quality is controlled. Especially the code quality. Most people who first came into contact with Swift from OC will experience a period of "discomfort". During this period, it is easy to write Swift code with "OC flavor", so someone who is passionate and relevant People with experience and technical ability come to practice and set an example.

At the same time, we also need to get the support of the supervisor. This is very important. It is difficult to continue an event with the love of technology. It is also a very interesting process to keep communicating with the supervisor based on the project situation, and constantly upgrade one's thinking about a technology during the communication process, so that the supervisor can go from the initial questioning to the final support.

Need to have a certain technical foundation support

First of all, in terms of the completeness of the infrastructure, we have done a large-scale module adaptation work to solve the core problem of mixed editing. At the same time, DevOps has been upgraded, and the package management tool tpod has been upgraded to 1.9.1. The static library version framework project at the source level is supported. The tpodedit mode is also provided to solve the header file dependency problem, and some core card ports have been added in the release link. Check to prevent engineering deterioration.

Secondly, based on the existing technical solutions of hand-scoring, after weighing issues such as performance and efficiency, we finally combined our understanding of the pain points of business research and development, and carried out the research and development model upgrade exploration based on event chain orchestration, and considered the initial cost in the initial stage. DX is built internally and exported to Xinaochuang. The overall structure is as follows:

At the UI layer, we use XML as a DSL to ensure dual-ended consistency while reducing dual-ended development costs.

In terms of logical arrangement, we designed the event chain technical solution to atomize each end-side basic capability as much as possible, so as to ensure that end-side capability developers can focus on the development of capabilities.

Based on the support of the above framework, developers can decide on their own the development language used for a single basic ability, and the cost of getting started with Swift for novices can be reduced by a level, and there is no longer a need to struggle with a complex environment.

What problems did you encounter?

Frankly speaking, although we did in-depth thinking when making technical decisions, we still encountered a lot of problems when we actually implemented it.

The basic library API does not adapt to Swift

Although Xcode provides the ability to "automatically" generate bridge files, due to the large difference between OC and Swift syntax, most of the automatically generated Swift APIs do not follow the "API Design Guidelines", which will lead to the writing of currently connected Swift business libraries A lot of code is poorly readable and not easy to maintain.

At the same time, due to the optional value design of Swift, it is necessary to sort out the optional settings of each external API input parameter and output parameter when OC SDK is provided to Swift. A basic SDK heavily relied on for product evaluation did not do this well, so that we encountered many problems.

Unnecessary compatibility caused by incorrect derivation

Let's take a look first, the following code:

// DemoConfig.h

@interface DemoConfig : NSObject

/* 此处已省略无用代码 */

- (instancetype)initWithBizType:(NSString *)bizType;

@end

// DemoConfig.m

@implementation DemoConfig

- (instancetype)initWithBizType:(NSString *)bizType {
    if (self = [super init]) {
        _bizType = bizType;
    }
    return self;
}

/* 此处已省略无用代码 */

@end

Since the DemoConfig class does not indicate whether the return value of the initialization method is optional, the API derived by Xcode by default is changed.

// 自动生成的 Swift API
open class DemoConfig : NSObject {
    /* 此处已省略无用代码 */
    public init!(bizType: String!)
}

Developers have to think about how to solve the scenario where the initialization is empty, which is obviously redundant.

In addition to the optional semantic adaptation of the SDK, we can also add a category to provide an OC method with a non-empty return value. The code is as follows:

/// DemoConfig+SwiftyRateKit.h

NS_ASSUME_NONNULL_BEGIN

@interface DemoConfig (SwiftyRateKit)

- (instancetype)initWithType:(NSString *)bizType;

@end

NS_ASSUME_NONNULL_END

/// DemoConfig+SwiftyRateKit.m
#import <SwiftyRateKit/DemoConfig+SwiftyRateKit.h>

@implementation DemoConfig (SwiftyRateKit)

- (instancetype)initWithType:(NSString *)bizType {
    return [self initWithBizType:bizType];
}

@end

Insecure API

It is inherently unsafe to bridge the OC API to Swift without clearly specifying the optional settings. Why do you say that?

Let's take a real case of online Crash as an example, the stack is as follows:

Thread 0 Crashed:
0x0000000000000012 Swift runtime failure: Unexpectedly found nil while implicitly unwrapping an Optional value DemoEventHandler.swift
0x0000000000000011 handle DemoEventHandler.swift
0x0000000000000010 handle <compiler-generated>
0x0000000000000009 -[XXXXXXXXXX XXXXXXXXXX:XXXXXXXXXX:XXXXXXXXXX:] XXXXXXXXXX.m
0x0000000000000008 -[XXXXXXXX XXXXXXXX:XXXXXXXX:XXXXXXXX:] XXXXXXXX.m
0x0000000000000007 +[XXXXXXX XXXXXXX:XXXXXXX:XXXXXXX:] XXXXXXX.m
0x0000000000000006 -[XXXXXX XXXXXX:] XXXXXX.m
0x0000000000000005 -[XXXXX XXXXX:] XXXXX.m
0x0000000000000004 -[XXXX XXXX:] XXXX.m
0x0000000000000003 -[XXX XXX:XXX:] XXX.m
0x0000000000000002 -[XX XX:]
0x0000000000000001 -[X X:]

The implementation code of the client is as follows:

class DemoEventHandler: SwiftyEventHandler {

    override func handle(event: DemoEvent?, args: [Any], context: DemoContext?) {

        guard let ret = context?.demoCtx.engine.value else {
            return
        }
        /// 此处省略无用代码
    }
}

The cause of the Crash is the context?.demoCtx.engine.value code.

The essential reason is that demoCtx does not specify optional semantics, which causes implicit unwrapping by default when OC is bridged to Swift. During the reading process, if the value does not have a value, it will directly generate a Crash of Unexpectedly found nil while implicitly unwrapping an Optional value due to forced unpacking.

To solve this problem, in addition to the optional semantic adaptation of the SDK, we can also change the calling code to optional calls to avoid the problem of forced unwrapping:

Destructive inheritance

The biggest problem encountered when using the above basic SDK is the destructive inheritance of DemoArray.

DemoArray inherits from NSArray and rewrites many methods, among which is the objectAtIndex: method.

Clearly defined in the NSArray header file

objectAtIndex: The return value of this method must not be empty, but when the SDK implements objectAtIndex: in the DemoArray subclass, it actually returns nil. The code is as follows:

This makes it impossible to develop SDK custom EventHandler using Swift.

The core reason is that to implement an SDK custom EventHandler, it must first comply with the DemoEventHandler protocol, and it must be implemented to comply with the protocol-(void)handleEvent:(DemoEvent )event args:(NSArray )args context:(DemoContext *)context; This method is due to the protocol The above agreement is the NSArray type, so when converted to Swift API args, it becomes the [Any] type, as shown in the following figure:

But the type passed by the SDK to DemoEventHandler is essentially a DemoArray type:

If there are [Null null] objects in DemoArray, it will cause Crash of attempt to insert nil object from objects[0], as shown in the following figure:

The specific reason is that when handleEvent(_:args:context:) is called, Swift internally calls static Array._unconditionallyBridgeFromObjectiveC(_:) to convert the args input parameter from NSArray to Swift Array, and when calling the bridge function, it will first Perform a copy operation on the original array, and call -[__NSPlaceholderArray initWithObjects:count:] during NSArray Copy. Since the NSNull of DemoArray is changed to nil, the initialization will fail and crash directly.

To avoid this problem, it is obviously unrealistic for the SDK to modify DemoArray. Because there are too many callers, neither the impact nor the regression test cost can be evaluated in the short term. Therefore, only an intermediate layer can be added to solve this problem. We first designed an OC class called DemoEventHandlerBox for packaging and bridging. The code is as follows:

/// DemoEventHandlerBox.h

@class SwiftyEventHandler;

NS_ASSUME_NONNULL_BEGIN

@interface DemoEventHandlerBox : NSObject<DemoEventHandler>

-(instancetype)initWithHandler:(SwiftyEventHandler *)eventHandler;

@end

NS_ASSUME_NONNULL_END

/// DemoEventHandlerBox.m
#import <SwiftyRateKit/DemoEventHandlerBox.h>
#import <SwiftyRateKit/SwiftyRateKit-Swift.h>

@interface DXEventHandlerBox ()

/// 处理事件对象
@property (nonatomic, strong) SwiftyEventHandler *eventHandler;

@end

@implementation DemoEventHandlerBox


-(instancetype)initWithHandler:(SwiftyEventHandler *)eventHandler {

    self = [super init];

    if (self) {
        _eventHandler = eventHandler;
    }

    return self;
}

- (void)handleEvent:(DemoEvent *)event args:(NSArray *)args context:(DemoContext *)context {
    [self.eventHandler handle:event args:args context:context];
    return;
}

@end

There is a type of SwiftyEventHandler in DemoEventHandlerBox for logic processing, the code is as follows:

@objcMembers
public class SwiftyEventHandler: NSObject {

    @objc
    public final func handle(_ event: DemoEvent?, args: NSArray?, context: DemoContext?) {
        var ret: [Any] = []
        if let value = args as? DemoArray {
            ret = value.origin
        } else {
            ret = args as? [Any] ?? []
        }
        return handle(event: event, args: ret, context: context)

    }

    func handle(event: DemoEvent?, args: [Any], context: DemoContext?) {
        return
    }

}

The method exposed to OC by SwiftyEventHandler is set to final and the logic to convert DemoArray back to NSArray is compatible. Finally, all EventHandler implementation classes on the Swift side inherit from SwiftyEventHandler and override the handle(event:args:context) method. In this way, problems caused by destructive inheritance can be perfectly avoided.

Clang Module build error

The second category of problems is mainly related to dependence. Although it is mentioned in the previous article that the current basic infrastructure is complete, there are still some problems.

Dependent updates are not timely

When many people first started writing Swift, they often encountered a problem: Could not build Objective-C module. Under normal circumstances, the reason is because the module you depend on does not adapt to the Module, but the infrastructure is basically complete due to the manual Taobao. , Most libraries have already completed the Module adaptation, so you may only need to update the module dependencies to solve this type of problem.

For example, for the STD library, the version currently relied on by Taobao is 1.6.3.2, but when your Swift module needs to rely on STD, using 1.6.3.2 will cause it to fail to compile. At this time, your Swift module may need to be upgraded to 1.6.3.3 to solve this problem. In essence, the difference between 1.6.3.3 and 1.6.3.2 is modular adaptation, so you don't have to worry about side effects.

Dependency problems caused by mixed editing

Although the Module adaptation mentioned above solves most of the problems, there are still some abnormal cases. Let's expand on it here.

In the process of refactoring the product evaluation, in order to ensure that the project can gradually increase the volume, we have done the physical isolation of the code, and created a new module called SwiftyRateKit which is a Swift module. But the entry class of the evaluation list is in an OC module called TBRatedisplay. Therefore, in order to cut the flow, TBRatedisplay needs to rely on some implementations of SwiftyRateKit. But when we relied on SwiftyRateKit to compile TBRatedisplay, we encountered the following problem:

Xcode writes the header file declaration of the Swift class ExpandableFastTextViewWidgetNode exposed to OC into SwiftyRateKit-Swift.h. ExpandableFastTextViewWidgetNode is inherited from the TBDinamic class DXFastTextWidgetNode.

Because TBRatedisplay did not turn on the Clang Module switch (CLANG_ENABLE_MODULES) at the time, the following macro definition in SwiftyRateKit-Swift.h did not take effect, so I don’t know where the ExpandableFastTextViewWidgetNode class is defined:

But when we turned on the Clang Module switch of TBRatedisplay, something more terrifying happened. Since TBDinamic did not turn on the Clang Module switch, @import TBDinamic failed to compile and passed and entered an "endless loop". Finally, all OC module exports that did not support the Clang Module switch had to be temporarily removed.

The concept here is more abstract, I use a picture to show the dependency:

First of all, for a Swift module, as long as the module has DEFINES_MODULE = YES and Umbrella Header is provided, it can import dependencies by import TBDinamic. Therefore, SwiftyRateKit can display dependencies when TBDinamic does not turn on the Clang Module switch, and can be compiled through.

But for an OC module, there are two cases for importing another module.

  • The first is the module with DEFINES_MODULE = YES turned on. We can import it through #import <TBDinamic/TBDinamic_Umbrella.h>.
  • The second is when the Clang Module switch is turned on, we can import it through @import TBDinamic

Since TBRatedisplay relies on SwiftyRateKit, the SwiftyRateKit-Swift.h header file automatically generated by Xcode uses @import TBDinamic to import modules, which causes the above problems.

So I personally suggest that at this stage, try to avoid or reduce the use of a Swift module's API to OC, otherwise it will cause the OC module that the Swift external API needs to depend on to open the Clang Module, and at the same time rely on the OC module of the Swift module. Also need to open Clang Module. Moreover, because Swift and OC grammars are not equivalent, the interface layer capabilities developed by Swift are very limited, which causes Swift's external API to become quite uncoordinated.

The class name is the same as Module

In theory, there is no problem with calling each other between Swift modules. However, due to the large number of hand-sell modules and the heavy historical burden, we encountered a bitter problem of "the class name is the same as Module" when we were doing product evaluation and transformation.

Our SDK is called STDPop, the module name of this SDK is also called STDPop, and there is also a tool class called STDPop. What problems can this cause? All Swift modules that rely on STDPop cannot be used by another Swift module, and a magical error will be reported:'XXX' is not a member type of class'STDPop.STDPop' The main reason is that the Swift module that depends on STDPop is generated The .swiftinterface file will add a STDPop prefix to each STDPop class. For example, PopManager will become STDPop.PopManager, but because STDPop itself has a class called STDPop, the compiler cannot understand whether STDPop is a Module name or a class name.

The only way to solve this problem is to need the STDPop module to remove or modify the STDPop class name.

What are the specific benefits?

After careful consideration, we embarked on the thorny road of Swift landing. Although we encountered many unprecedented challenges in the whole process, looking back now, our original technology selection was relatively correct. Mainly reflected in the following aspects:

The amount of code is reduced, and the efficiency of Coding is improved

Thanks to the expressiveness of Swift, we can use less code to implement a logic originally implemented with OC, as shown in the following figure, we no longer need to write too much defensive programming code, and we can express clearly Out the logic we want to implement.

At the same time, we rewrite the original 13 expressions implemented with OC in Swift, and the overall code volume changes are as follows:

Less code means less time to invest in development and fewer opportunities for bugs.

Significantly reduce the cost of cross-review

The peculiar grammar of OC makes it impossible for most other developers to understand the specific logic at all, which leads to the high cost of the iOS and Android dual-end cross-review, and also makes many libraries often have dual-end logic inconsistencies.

When I was doing the order migration, Neologism faced many inconsistencies in the dual-end API, and the taste of some code logic was more complicated. There were too many temporary troubleshooting issues on the project that affected the rhythm.

Therefore, we took a different approach and adopted the Swift & Kotlin model for development. Since the syntax of Swift and Kotlin is extremely similar, there is no pressure for us to cross Review.

At the same time, thanks to the scaffolding used in product evaluation, subsequent iterations of demand have also dropped significantly. Let’s take the "evaluation item add share button" as an example:

If you use the OC & Java mode, because you can't understand the double-ended code. Therefore, the requirements review needs to be sent to each of the two ends, and it takes about 0.5 man-days to discuss various matters. Then, after discussing the plan at both ends, one person develops the template, which takes about 1 person-day. In the end, each of the two ends realizes the native atomic sharing capability, which requires about 2 person-days each (1 person-day needs to investigate how to access the sharing SDK), a total of 2 0.5 + 1 + 2 2 = 6 person-days.

But if we adopt the Swift & Kotlin model, we only need 1 person to take part in the demand review, 0.5 person-day. It takes about 3 man-days to complete technical research and template development by a single person. Finally, show the written code to the other end, and the other end can directly copy the code and adapt it for about 1 person day according to the characteristics of the end. The total is 0.5 + 3 + 1 = about 4.5 person-days. Approximately 25% time savings.

Project stability has improved

Because there is no better quantitative index, I can only talk about my feelings.

First of all, due to coding problems, the problem of test promotion has been significantly reduced. The basic hook flow is benefited from the optional value design of Swift, which has been clearly considered in the development stage. The overall test problem is obviously much less than when using OC. .

Secondly, online problems have also dropped significantly, except for the Crash problem mentioned above. There are basically no online problems in the product evaluation reconstruction project.

Priority enjoyment of technology dividends

Whether it is WidgetKit or DocC, it can be clearly seen that Apple’s internal upgrade of new features and development toolchain must be that Swift takes precedence over OC, so all students who use Swift can quickly use all newly developed Apple Features and tools.

At the same time, thanks to the open source of Swift, we can not only learn some good design patterns through the source code, but also locate some intractable diseases, and no longer need to fight with jerky and difficult assembly code.

Summary and outlook

The above can be regarded as a summary of our exploration of Swift's business landing on Taobao. We hope that we can give you some help when choosing technology or exploring pitfalls. Of course, this is just the beginning, and there are many things worth doing. First, we need to work together to improve and standardize the Swift coding standards, and even precipitate a series of best practices to guide everyone from OC to Swift at a lower cost; second, we also need to promote the foundation for the mixed compilation issues mentioned above. The SDK builds the Swift Layer and continues to optimize the existing Swift toolchain; finally, we also need to introduce some excellent open source libraries to avoid duplication of wheels, and make good use of the capabilities provided by Apple (such as DocC), and finally find a Swift in hand Best practices.

Finally, if you are more interested in what we do, welcome to join us to build a Swift/Kotlin ecosystem. My contact information is: zhejian.wzj@alibaba-inc.com, and I look forward to your joining.

[Document Reference]

, 3 mobile technology practices & dry goods for you to think about every week!


阿里巴巴终端技术
336 声望1.3k 粉丝

阿里巴巴移动&终端技术官方账号。