An overview of the

Some time ago, 58 cooperated with Nuggets to hold a technical salon, and I was honored to be one of the sharing guests. The following are the contents I shared this time and all the videos of the salon

background

In 2014, Apple released the new language Swift at WWDC. Since then, it has been constantly updated, iterated and optimized. Major companies at home and abroad have been eager to try, but it has not been commercially or widely used. In 2019, Apple released version 5.0 and announced ABI to be stable. In 2020, It successively launched SwiftUI, CareKit and other Exclusive Swift SDKS, and Apple has been vigorously promoting and encouraging people to use Swift. In this context, more and more developers and open source projects are speeding up the pace of Swift ecological construction. In addition, Swift, as a new language, has huge late-mover advantages compared with Objective-C: security, efficiency, high performance, etc. These features help developers improve development efficiency and APP quality. In Swift 2021 Ecological Research Report, Swift accounts for 91% of the top 100 free apps in App Store outside China. The domestic share is nearly 50%

The status quo

Under such a trend, 58 Group launched Swift co-construction project with the end of 2020, which is internally called Mixed Sky Project. The goal is to build the basic components, assistive tools, and infrastructure for Swift. Develop Swift development specifications and code detection tools for the group, as well as the landing of Swift in various business lines. As the core industry of the group, the real estate business has been deeply involved in the research and development of the Mixed-sky project and the landing of Swift. The following content is mainly about some problems and exploration that Swift encountered in the process of landing the real estate business line from 0 to 1. At present, the company’s projects are all developed in OC language. In such a project with a long history of rapid iteration, it is impossible to rewrite all projects with Swift in a short time, so we adopted the mixed development of Swift and OC in the early stage.

Engineering structure

Before accessing Swift, let’s first understand the engineering structure of group APP and business structure of real estate. The iOS team of 58 Group has maintained more than a dozen apps such as 58.com, Anjuke, Ganji.com, 58.com, Zhaicaimao and Cheshangtong. In order to reduce maintenance costs and development efficiency, the team componentized basic functions, provided several self-developed basic components and SDK, and built an APP factory. But different apps and lines of business rely on the underlying layer differently. Therefore, an intermediate layer is added on this basis to solve the underlying differences between vertical businesses across APP and the sharing between businesses within APP.

Real estate business structure

The group has real estate business in 58.com, Anjuke, Ganji.com and 58.com. In the early stage, each team operated and maintained the business independently. With the business of vertical and industrialization. Real estate launched project Jupiter, the goal is to build a set of code, multiple apps to run. To reduce maintenance costs and development efficiency. Let different teams pay more attention to their business and play to their strengths. Although the efficiency of development and maintenance has been improved, the complexity of the project has also been increased. The following is the business structure of the core business of the real estate

Mix solution

There are two main ways to mix Swift and Objective-C:

Directional bridge

If you are mixing within an App Target, add Bridging files to the host project. Each project creates a Bridging (ProductModuleName-Bridging- header. h) file the first time it creates a Swift file

If Swift class needs to access OC class, it only needs to import the class to be exposed in this bridge file, then it can access the corresponding OC class and method in Swift. Access to Swift’s classes and methods exposed to Objective-C is simple and convenient to use, but there are two drawbacks: 1. As the number of Swift usage scenarios increases, the imported header files become bloated. 2. If the project is managed through Cocoapods, Pod and Pod cannot call each other

Module

Our project project consists of a shell project and several business sub-projects. Each business line sub-project has one or more modules connected together, which are managed through Cocoapods. There is a dependency relationship between modules. We need calls not only between Swift and OC, but also across pods, so bridging is definitely not enough. Another way to do this is with modules. Set the “Build Settings” option to YES and create an umbrella header. If you want to call Swift in ObjC, you also need to set the Build Settings parameter to YES. Then in reference to Swift code ObjC file import compiler to generate the header file # import < ProductName/ProductModuleName – Swift. H > # # # practice Module

background

As you can see from the above, our current engineering architecture is managed through Cocoapods for component/modularity. Each Module is a Module, and directional bridge mode is not able to communicate across modules, so we are suitable for Module mode to mix, then how to mix?

Environment set up

  • Enable Module option

In order for Pod libraries to reference exposed Swift interfaces, the first step is to enable module for the library being accessed by adding ‘DEFINES_MODULE’ => ‘YES’ to xcconfig in podSpec in Pod folder where Swift is located.

  • Add the dependent

Callers need to add moudule dependencies to their podspec, s.dependency ‘called’s POD library’.

  • use

With the above dependencies configured, you can call the POD library that opens the Module. The Components can also call the OC method in WBLOCO and the exposed Swift interface across pods. Of course, if you want to expose the Swift interface to the OC environment, The @objc declaration is required, and the interface should be declared public

#import "WBListVC.h"

@import WBLOCO;
@interface WBListVC ()<LCListViewDelegate>
@property (nonatomic, strong) LCListView  *listV;
@end
Copy the code

Engineering change

WBLOCO After module is enabled, two additional files wbloco. modulemap and wbloco-umbrella. H are generated

By default, umbrella. H in WBLOCO exports all OC header files. You can fix this problem by adding the export of shielded header files to your PodSpec’s private_header_files

Components Pod changes to project files after adding WBLOCO dependencies

Precautions for external exposure of Swift type

To expose the Swift interface to the OC environment, whether in the current Pod or across the Pod, first of all, the Swift class wants to be defined as public, and the exposed interface needs to be declared with @objc, and the interface needs to be defined as public

import Foundation
@objc public enum LCListItemSelectionStyle: Int {
   case single
   case multiple
}
public class LCListItemModel:NSObject {
   @objc public var list_selected:Bool = false
   @objc public var list_selection_style:LCListItemSelectionStyle = .single
   @objc public var text:String = ""
   @objc public var data:[LCListItemModel] = []
   @objc public convenience init(modelWithDict: [String:Any]) {
        self.init()
        LCListItemModel.init(dict: modelWithDict)
    }
Copy the code

On the pit case

Environment configuration is good, in access to Swift mixed development is the time to encounter a variety of problems pit, the following is the property in access to Swift some relatively common problems #### repeated definition problems after creating Swift files, the project directly failed to compile, the error is as follows:

According to the prompt screen found because project code name or the name of the Block is repeated in the agreement, as long as the two files in the pure OC no reference each other, the compiler testing relative to less strict, so the compiler will not complain, but access to the Swift, the compiler to detect more strictly, compile would have failed, the independent out of a solution way, ####LLDB debugging problem We are in the process of mixed programming development, but when we debug the console Po, we found that the variable name can not be seen, the error message is similar to the following:

(lldb) po self warning: Swift error in fallback scratch context: <module-includes>:1:9: note: in file included from <module-includes>:1: #import "WBLOCO-umbrella.h" ^ /Users/xxxx/... /WBLOCO-umbrella.h:70:9: note: in file included from /Users/xxxx/... /Components-umbrella.h:70: #import "LGBaseNode.h" ^ /Users/xxxx/... /LGBaseNode.h:9:9: note: in file included from //Users/xxxx/... /LGBaseNode.h:9: #import "LGDefines.h" ^ error: could not build Objective-C module 'WBLOCO' <module-includes>:1:9: note: in file included from <module-includes>:1: #import "WBLOCO-umbrella.h"Copy the code

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

#import "WBLOCO.h"
Copy the code

The modified

#import <WBLOCO/WBLOCO.h>
Copy the code

This can debug, but there are many such irregularities in the code, we can use script replacement, so that all the irregularities in the project can be unified modification

Reflection problem and principle of Swift and OC mixed programming

Background of reflection problem when Swift and OC are mixed

Reflection is often used in everyday development. The iOS development system also provides us with the corresponding API, we can use these apis to perform the string to Class, SEL, etc. Due to the dynamic nature of the OC language, these operations occur at run time.

FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector); FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector); FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName); FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass); FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName); FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0); FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0); FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);Copy the code

With these methods, we can create an instance of a string at run time and dynamically select the method to invoke

Class cls = NSClassFromString(@"ViewController");
ViewController *vc = [[cls alloc] init];
SEL selector = NSSelectorFromString(@"initWithData");
[vc performSelector:selector];
Copy the code

The main core pages of real estate are all flow layout, and the business is characterized by high similarity of sub-business, fast update frequency, and certain dynamic and flexibility. Based on the above reasons, we designed a solution with built-in cells in the client. Each Cell is bound to a Key, and the data Key is delivered through the Server to reflect the corresponding Cell, so as to display different contents and control the sequence of contents displayed. However, recently we have encountered the problem of NSClassFromString reflection after accessing Swift and OC to mix.

Reflection in Swift and OC combination

We will create Swift class TestClass. The current Module name is HouseTest

import Cocoa
@objc public class TestClass: NSObject {

}
Copy the code

The OC code gets the TestClass object by reflection. The CLS output in the first way is empty. The second way is to concatenate the Module name before the class name

However, there are two obvious defects when this scheme is actually used in the project. 1.Swift class we must know the name of Module, while OC does not have the name of Module, so we need to determine whether it is Swift or OC class to do special processing. Each time we add a new Swift Class, we have to judge that the code is not elegant. 2. Under multiple pods, if the Class is moved to another POD, then the Class will not be found, and the compiler will not error. According to the data sent by the Server, the corresponding Cell Class is reflected to achieve dynamic layout. In the case of mixed programming, the above scheme has a large difference in processing, which cannot meet our needs. We finally use another scheme: add custom Class name after @ojbc in Swift Class, and the code is as follows:

@objc(TestClass)
public class TestClass: NSObject {
}
Copy the code

If @objc(TestClass) is added, we don’t need to care about the Swift Class in OC

Class cls =  NSClassFromString(@"TestClass");
Copy the code

Let’s verify that

Here we have TestClass, which in this way solves two of the drawbacks of the above approach. Why does Swift require a Module name for reflection? What does @objc(TestClass) do?

Swift and OC mix when reflection breaks the casserole to the end

Since OC source code is not open source, there is no way to directly look at the source code, first next symbolic breakpoint, look at the assembly code (x86)

Foundation`NSClassFromString: -> 0x181a3c43c <+0>: pacibsp 0x181a3c440 <+4>: stp x28, x27, [sp, #-0x40]! 0x181a3c444 <+8>: stp x22, x21, [sp, #0x10] 0x181a3c448 <+12>: stp x20, x19, [sp, #0x20] ...... . 0x181a3c4f8 <+188>: bl 0x181d41f00 ; symbol stub for: objc_msgSend 0x181a3c4fc <+192>: mov x21, x0 0x181a3c500 <+196>: mov x0, x21 0x181a3c504 <+200>: bl 0x181d41ef0 ; symbol stub for: objc_lookUpClassCopy the code

Looking at the key information through the above assembly code, we finally see that objc_lookUpClass is called to simulate the pseudo-code through the above assembly

Class _Nullable MY_NSClassFromString(NSString *clsName) { if (! clsName) { return Nil; } NSUInteger classNameLength = [clsName length]; char buffer[1000]; if ([clsName getCString:buffer maxLength:1000 encoding:NSUTF8StringEncoding] && classNameLength == strlen(buffer)) { return objc_lookUpClass(buffer); } else if (classNameLength == 0) { return objc_lookUpClass([clsName UTF8String]); } for (int i = 0; i < classNameLength; i++) { if ([clsName characterAtIndex:i] == 0) { return Nil; } } return objc_lookUpClass([clsName UTF8String]); }Copy the code

The verification results

2021-06-23 21:13:58.750828+0800 HouseTest[25683:4936266] my_cls = TestClass
Copy the code

If @objc(TestClass) is added or not, there must be something wrong with the process. That can only debug the source code (currently objC-781). We trace the call flow of the source code: objc_lookUpClass -> look_up_class -> getClassExceptSomeSwift

Finally, we see that it is fetched from NXMapGet(gDB_objC_realized_classes, name, CLS). Gdb_objc_realized_classes holds all classes loaded from Mach-O. Are Swift classes not loaded? Insert the gDB_objC_realized_classes method

So we see here we add a Log and print it out

TestClass is not what it looks like, it’s _TtC6KCObjc9TestClass so when we call NSStringFromClass(“TestClass”), the key we pass in is TestClass, The key stored in mapTable is _TtC6KCObjc9TestClass. So the return is empty. Why is NSClassFromString(“ModuleName.ClassName”) ok? Let’s track the flow

If I go here, result is still empty, so I go down copySwiftV1MangledName

So why do I add @objc(ClassName)

This is going to be the actual Class name so you can get the address of the Class directly

@objc(TestClass) and @objc(TestClass)

Don’t add @ objc (TestClass)

TestClass = _TtC6KCObjc9TestClass ####@objc ModuleName differentiates the first Pod

Import Foundation class TestClass: NSObject {var name = "I am One Pod"}Copy the code

The second Pod

Import Foundation class TestClass: NSObject {var name = "I am Tow Pod"}Copy the code

The Module names are different, the class names are the same, and the final concatenation is different, so it will compile properly. What if we add @objc(TestClass) to both classes? I can see that the compilation fails directly

###Swift and OC injection binding problems and optimization #### Background the previous article also said that the main core pages of real estate are streaming layout, we designed the solution client built-in Cell, Each Cell is bound to a Key, and the data Key is delivered through the Server to reflect the corresponding Cell. So how do cells and keys bind? In the era of pure OC, there are many ways to bind key-class. At the beginning, a relatively simple and direct way was adopted. When entering real estate business, key-cellname and key-model were bound through NSDictionary.

NSMutableDictionary *classNames = [NSMutableDictionary dictionary]; [classNames setObject:@"HSListHeaderCell" forKey:@"list_header_data"]; [classNames setObject:@"HSListFootCell" forKey:@"list_foot_data"]; . NSMutableDictionary *modelNames = [NSMutableDictionary dictionary]; [classNames setObject:@"HSListHeaderModel" forKey:@"list_header_data"]; [classNames setObject:@"HSListFootModel" forKey:@"list_foot_data"];Copy the code

However, after a period of iteration, we found that this method has some drawbacks. Every time new cells are added, the code needs to be modified here. In addition, the simultaneous development of multiple business lines is difficult to manage and maintain, and prone to code conflicts, which violates the open and closed principle of design principles. So we tried to solve this problem later in the refactoring. ####OC The first solution we thought of was to inject the key-cellname into the +Load method of each class. The code is as follows:

+(void)load{
    [HSBusinessWidgetBindManager.sharedInstance setWidgetKey:@"list_header_data" widgetClassName:@"HSListHeaderWinget"];
}
+ (NSString *)cellName {
    return NSStringFromClass(HSListHeaderCell.class);
}

+ (NSString *)cellModelName {
    return NSStringFromClass(HSListHeaderModel.class);
}

Copy the code

The downside of this approach is that the +Load method has some impact on the startup time of the application. We have hundreds of cells, so the +Load method is not a very good method. (Here we directly bind the Widget to simplify the process of external processing, and the binding of Cell and Model and data-related processing are completed by the Widget.) ####OC Injection binding scheme 2 Finally, the way we realize now is to directly write the bound data into Macho during the program precompilation stage. When the program enters the line of business, it writes to the memory, and then finds the corresponding WidgetName by using the Key delivered by the Server. The code is as follows:

typedef struct { const char * cls; const char * protocol; } _houselist_presenter_pair; #define _HOUSELIST_SEGMENT "__DATA" #define _HOUSELIST_SECTION "__houselist" #define HOUSELIST_PRESENTER_REGIST(PROTOCOL_NAME,CLASS_NAME)\ __attribute__((used, section(_HOUSELIST_SEGMENT "," _HOUSELIST_SECTION))) static _houselist_presenter_pair _HOUSELIST_UNIQUE_PAIR = \ {\ #CLASS_NAME,\ #PROTOCOL_NAME,\ }; \Copy the code

When a Widget needs to be bound, it imports the macro defined above, passing in the corresponding Key and WidgetName:

HOUSELIST_PRESENTER_REGIST(list_header_data, HSHeaderWidget)
Copy the code

When entering the real estate business, read the DATA stored before the DATA section in Macho and save it to the memory, the code is as follows:.h file

@interface HouseListPresenterKVManager : NSObject
+ (instancetype)sharedManager;
- (Class)classWithProtocol:(NSString *)key;
@end
Copy the code

.m files

#import "HouseListDefines.h" #import "HouseListPresenterKVManager.h" #import <mach-o/getsect.h> #import <mach-o/loader.h> #import <mach-o/dyld.h> #import <dlfcn.h> @interface HouseListPresenterKVManager () @property (nonatomic, strong) NSMutableDictionary<NSString*, NSString*> *presenterKV; @end @implementation HouseListPresenterKVManager static HouseListPresenterKVManager *_instance; + (instancetype)sharedManager { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [[HouseListPresenterKVManager alloc] init]; [self loadKVRelation]; }); return _instance; } + (void)loadKVRelation { #if DEBUG CFTimeInterval loadStart = CFAbsoluteTimeGetCurrent(); #endif Dl_info info; int ret = dladdr((__bridge const void *)(self), &info); if (ret == 0) return; #ifndef __LP64__ const struct mach_header *mhp = (struct mach_header *)info.dli_fbase; unsigned long size = 0; uint32_t *memory = (uint32_t *)getsectiondata(mhp, _HOUSELIST_SEGMENT, _HOUSELIST_SECTION, &size); #else /* defined(__LP64__) */ const struct mach_header_64 *mhp = (struct mach_header_64 *)info.dli_fbase; unsigned long size = 0; _houselist_presenter_pair *memory = (_houselist_presenter_pair *)getsectiondata(mhp, _HOUSELIST_SEGMENT, _HOUSELIST_SECTION, &size); /* defined(__LP64__) */ #endif #if DEBUG CFTimeInterval loadComplete = CFAbsoluteTimeGetCurrent(); NSLog (@ "= = = = > houselist_loadcost: % @" ms, @ (1000.0 * (loadComplete - loadStart))); if (size == 0) { NSLog(@"====>houselist_load:empty"); return; } #endif for (int idx = 0; idx < size / sizeof(_houselist_presenter_pair); ++idx) { _houselist_presenter_pair pair = (_houselist_presenter_pair)memory[idx]; [_instance.presenterKV setValue:[NSString stringWithCString:pair.cls encoding:NSUTF8StringEncoding] forKey:[NSString stringWithCString:pair.protocol encoding:NSUTF8StringEncoding]]; } #if DEBUG NSLog(@"====>houselist_callcost:%@ms", @(1000.0 * (CFAbsoluteTimeGetCurrent() -loadComplete))); #endif } - (Class)classWithProtocol:(NSString *)key; { NSString* protocolName = key; if (! ValidStr(protocolName)) { return [NSObject class]; } Class res = ValidStr(self.presenterKV[protocolName]) ? NSClassFromString(self.presenterKV[protocolName]) : [NSObject class]; return res ? : [NSObject class]; } - (NSMutableDictionary *)presenterKV { if (! _presenterKV) { _presenterKV = [NSMutableDictionary dictionaryWithCapacity:10]; } return _presenterKV; } @endCopy the code

This approach avoids the drawbacks of centralized binding and does not have the performance problems of +Load, but when we introduce Swift code mixing, we find that there is neither +Load nor precompilation mechanism in Swift. So how do we solve the key-Wdiget binding problem and still work seamlessly with our current mechanism?

Swift and OC mixed injection binding problems and solutions

After trying various solutions, we finally chose to create a BindKVCenter Class because Swift and OC have the characteristics of OC runtime. But each time we create a new Widget, we add an Extension to BindKVCenter. The Extension implements an Enter method to bind key-widgets. Finally, when entering the real estate business, get the Enter method in all extensions of BindKVCenter and directly call the function to achieve the binding effect. The specific code is as follows: bindkvend.swift

@objc(BindKVCenter) Public class BindKVCenter: NSObject {private Class func Enter () {}}Copy the code

Widget1

private extension BindKVCenter {
    @objc class func enter() {
        HouseListPresenterKVManager.shared().bindKV(withKey: "list_header_data", value: "HSHeaderWidget")
    }
}

@objc(HSHeaderWidget)
class HSHeaderWidget: NSObject {
}

Copy the code

Widget2

private extension BindKVCenter {
    @objc class func enter() {
        HouseListPresenterKVManager.shared().bindKV(withKey: "list_foot_data", value: "HSFootWidget")
    }
}

@objc(HSFootWidget)
class HSFootWidget: NSObject {
}
Copy the code

The core code for distributing bindings

Class currentClass = [BindKVCenter class]; if (currentClass) { typedef void (*fn)(id,SEL); unsigned int methodCount; Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount); IMP imp = NULL; SEL sel = NULL; for (NSInteger i = 0; i < methodCount; i++) { Method method = methodList[i]; NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method)) encoding:NSUTF8StringEncoding]; if ([@"enter" isEqualToString:methodName]) { imp = method_getImplementation(method); sel = method_getName(method); if (imp ! = NULL) { fn f = (fn)imp; f(currentClass,sel); } } } free(methodList); }Copy the code

Get all the methods in BindKVCenter, which won’t be overwritten by any of the methods with the same name. Find all the enter methods in methodList, and call them directly from the function pointer. Bind to implement an injection method. This method can seamlessly connect with the binding method of MACHo in OC, and avoid the conflict with the original design. Performance comparison and benefits We tested the key performance indicators in the case of Swift and OC co-programming, and compared the page of the rote chart function realized by Swift with the page of the rote chart function before OC. The test plan is to load 100 times and average the data performance index every 10 times. The results are as follows: FPS is not bad, CPU performance consumption Swift has a detailed advantage with the increase of business volume, and THE memory consumption of Swift is higher than that of OC, mainly due to the current project engineering or mixed environment, Swift needs to be compatible with OC features. The number of codes in Swift decreased by 38% compared with OC.

conclusion

Swift is an excellent language, integrating the advantages and features of various languages. Compared with OC, Swift has greatly improved performance, security, efficiency and other aspects. Although there are many pits in the process of access, they are finally broken through one by one. After settling in the real estate for more than half a year, Swift is now used to develop the categories page of rental/commercial real estate, details page and live streaming business. From zero to 50% of the team developers have Swift development capability. In the future, we will continue to increase investment in Swift and fully embrace Swift

reference

Stackoverflow.com/questions/2… Stackoverflow.com/questions/2… Tech.meituan.com/2015/03/03/… Swifter. Tips/objc – dynami…

For a replay of the salon, those interested can watch juejin.cn/post/702290…