Swift's practice in 58 anjuke real estate


01

Background

In 2014, Apple released a new language Swift at WWDC. Since then, it has been continuously updated, iterated and optimized. Major companies at home and abroad have been eager to try, but they have not been used commercially or on a large scale. Until 2019, Apple released version 5.0 and announced that ABI was stable. In 2020, Swift exclusive SDK s such as SwiftUI and CareKit were successively launched, and apple has been vigorously promoting and encouraging everyone to use Swift. In this context, more and more developers and open source projects have accelerated the construction of Swift ecology.
In addition, as a new language, Swift has great late development advantages compared with Objective-C: safety, efficiency, high performance and so on. These features help developers improve development efficiency and APP quality. In the Swift 2021 Ecological Research Report, Swift accounts for 91% of the top 100 free apps in the App Store outside China. Domestic accounting for nearly 50%


02

Present situation

Under such a trend, 58 group launched the swift co construction project at the end of 2020, which is internally called the mixed sky project. The goal is to build Swift's basic components, auxiliary tools and infrastructure. Formulate swift development specifications and code detection tools of the group and the implementation of swift in each business line.
As the core industry of the group, the real estate business has deeply participated in the R & D of the mixed sky project and the landing of Swift. The following contents are mainly some problems and exploration encountered by Swift in the process of landing the real estate business line from 0 to 1.
At present, the company's projects are 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 the short term, so we used the mixed development of Swift and OC in the early stage.

03

Engineering Architecture

Before accessing Swift, let's first understand the engineering architecture of the group APP and the business structure of the real estate. The iOS team of 58 group has maintained more than a dozen apps such as 58 in the same city, anjuke, ganji.com, 58 in the same town, Zhaocai cat and cheshangtong. In order to reduce the maintenance cost and development efficiency, the team componentized the basic functions, provided multiple self-developed basic components and SDK s, and built an APP factory. However, different apps and business lines rely on the bottom layer differently. On this basis, an intermediate layer is added to solve the underlying differences between business vertical services across apps and the sharing of services within apps.

Real estate business structure

The group has real estate business in 58 cities, anjuke, ganji.com and 58 towns. In the early stage, each team operated and maintained relatively independently. With the vertical and industrialization of business. Real estate launched the Jupiter plan, with the goal of creating a set of codes and running multiple apps. To reduce maintenance costs and development efficiency. Let different teams pay more attention to their business and give full play to their advantages. Although the development and maintenance efficiency has been improved, it also increases the complexity of the project. The following is the business structure of the current core business of real estate:


04

Mixed scheme

At present, Swift and Objective-C are mixed in two ways:

Directional bridging

If it is mixed within an App Target, it is mixed by adding a bridge file in the host project. When each project creates a Swift file for the first time, the system will create a bridge (productmodulename bridging header. H) file When Swift class needs to access OC classes, it only needs to import the exposed classes in this bridging file to access the corresponding OC classes and methods in Swift
When OC accesses Swift, import ProductName Swift in OC class H (hidden file), you can access the classes and methods exposed to Objective-C in Swift
This method is very simple and convenient to use, but it has two defects:
  • With more and more Swift usage scenarios, the imported header file will become bloated.
  • If the project is managed through Cocoapods, Pod and Pod cannot call each other


Module

Our project consists of a shell project and multiple business sub projects. Each business line sub project has one or more modules connected together and managed through Cocoapods. There are dependencies between modules. We need not only calls between Swift and OC, but also cross Pod calls, so the bridging method can't be satisfied. At this time, another method is Module. Set the definitions Module option in the Build Settings to YES, then create a umbrella header, and then import the header file of the OC that needs to be exposed to Swift calls into this umbrella header
If you want to call Swift in ObjC, also set the "definitions module" option in "Build Settings" to YES, and then import the compiler generated header file #import < ProductName / productmodulename Swift. In the ObjC file where you want to reference Swift code h>


05

Modular practice

From the above, we can see that our engineering architecture is component / modular management through Cocoapods Each Module is a Module, and directional bridging cannot communicate across modules, so we are suitable for Module mixing. What if mixing?

Environment construction

  • Enable Module option
    In order to reference the exposed Swift interface between Pod libraries, the first step is to enable the accessed library to open the module. You need to add 'definitions' under xcconfig in podspec under the Pod folder where Swift is located_ MODULE’ => ‘YES’

  • Add dependency
    The caller needs to add a moudule dependency in his own podspec, s.dependency 'the called party's pod Library'

  • Mode of use
    After configuring the above dependency configuration, you can call to open the pod Library of the Module. Both Swift files and OC files can be referenced through @ import. At the same time, Components can also call OC methods in WBLOCO and exposed Swift interfaces across pod. Of course, if exposed Swift interfaces want to be exposed in the OC environment, they need to be declared with @ objc, and the interfaces should be declared as public

#import "WBListVC.h"
@import WBLOCO;@interface WBListVC ()<LCListViewDelegate>@property (nonatomic, strong) LCListView *listV;@end


Engineering change

Changes and precautions of project directory after opening moudule in Business Library:
After WBLOCO opens the module, an additional WBLOCO is generated Modulemap and WBLOCO umbrella H two documents Umbrella. In WBLOCO by default H will export all OC header files. To solve this problem, you can use private in podspec_ header_ Add export of mask header file in files Components Pod changes in project files after WBLOCO dependency is added

Swift type external exposure precautions

If the Swift interface wants to be exposed in the OC environment, whether in the current Pod or across pods, first of all, the Swift class needs to be defined as public. At the same time, the exposed interface needs to be declared with @ objc, and the interface should also 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)    }

Case of stepping on pit

The environment is well configured. Various problems are encountered when accessing Swift mixed development. The following are some relatively common problems encountered by the real estate when accessing Swift.

Duplicate definition problem

After the Swift file is created, the direct compilation of the project fails with the following error: According to the prompt, it is found that the protocol name or Block name in the engineering code is duplicate. In pure OC, as long as the two files do not reference each other, the compiler detection is relatively less strict, so the compilation will not report errors. However, after accessing Swift, the compiler detection is more strict, and the compilation fails. The solution is to separate one, After deleting the duplicate definitions or modifying the corresponding protocol name and Block name, the compilation is successful again.

LLDB debugging problem

We are in the process of mixed programming development, but when we debug on the console po, we find that the variable name cannot be seen, and the similar error message is as follows:
(lldb) po selfwarning: 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"       
This error is caused by the introduction of nonstandard OC header files referenced across Pod. We need to All files imported across pods in h files need to be imported in a full path way, for example:
The Pod class of WBLOCO is introduced into the Pod of Components
Before modification:
#import "WBLOCO.h"
After modification:
#import <WBLOCO/WBLOCO.h>

In this way, we can debug, but there are many such nonstandard writing methods in the code. We can uniformly modify all nonstandard in the project through script replacement.


Research on reflection problem and principle when Swift and OC are mixed

Reflection problem background when Swift and OC are mixed

In daily development, we often use reflection as a mechanism. In iOS development, the system also provides us with corresponding API s through which we can convert strings to Class, SEL and other operations. Due to the dynamic nature of OC language, these operations occur at run time.
// SEL and string conversion FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);// Class and string conversion FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);// Protocol and string conversion 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);

Through these methods, we can create corresponding instances through strings at run time, and dynamically select and call corresponding methods.

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

The main core pages of real estate are flow layout. The business is characterized by high similarity of sub businesses, fast update frequency, and certain dynamics and flexibility.

Based on the above reasons, the client of our scheme has built-in cells, each Cell is bound with a Key, and the Server sends the data Key to reflect the corresponding Cell, so as to display different contents and control the order of displayed contents.
But recently, after we connected Swift and mixed OC, we encountered the problem of NSClassFromString reflection.

Reflection when Swift and OC are mixed

Let's first create the Swift class TestClass. The current Module name is HouseTest. Let's look at the printout results of NSClassFromString.
import Cocoa@objc public class TestClass: NSObject {}
The TestClass is obtained through reflection in the OC code. In the first way, the output cls is empty. In the second way, the Module name is spliced in the early requirements of the class name. We see that the cls instance is obtained However, this scheme has two obvious defects when we actually use it in the project
1. For Swift class, we must know the Module name, but OC has no Module name. We need to judge whether Swift or OC class is used for special processing. Every time you add a Swift class, you have to judge that the code is not elegant.
2. In the case of multiple pods, if the Class is moved to another pod, the Class will not be found and the compilation will not report an error.
The core pages in our project are all in streaming layout. According to the data distributed by the Server, the corresponding Cell Class is obtained through reflection to realize dynamic layout. In the case of mixed compilation, the above schemes have great differences, which can not meet our needs.
We finally used another scheme: add the custom class name in @ojbc the Swift class, and the code is as follows:
@objc(TestClass)public class TestClass: NSObject {}
After adding @ objc(TestClass), we don't need to care about the Module name when obtaining Swift's Class during reflection in OC. The underlying processing method is the same as that of OC, which smoothes out the underlying differences.
Class cls =  NSClassFromString(@"TestClass");
Let's verify: Here we see TestClass, which solves the two defects of the above method.
Then think through the above question: why does Swift class need to add the Module name when reflecting, and what does @ objc(TestClass) do at the bottom?

When Swift and OC mix, the reflection breaks the casserole and asks to the end

Because the OC source code is not open source, there is no way to directly look at the source code. First, go to the next symbolic breakpoint and 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_lookUpClass

Through the above assembly code, check the key information. Finally, we see that objc is called_ Lookupclass simulates 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]);}

Verification results:

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

Through the pseudo code, we find that there is no exception here. It is the same whether @ objc(TestClass) is added or not. That must be a problem in the later process. That can only debug the source code (currently objc-781)
We trace the calling process of the source code: objc_ lookUpClass -> look_ up_ class -> getClassExceptSomeSwift

Finally, we see that it is obtained from NXMapGet(gdb_objc_realized_classes, name, cls). gdb_ objc_ realized_ Classes save all the classes loaded from Mach-O. is it because the classes written by Swift have not been loaded? Let's see how it is added when adding. We find the program startup class and insert it into GDB_ objc_ realized_ Classes method

Let's add a Log here and print it

The testclass class is not what we see, it has become_ Ttc6kcobjc9testclass, so when we call nstringfromclass ("testclass"), the key passed in is testclass, and the key stored in maptable is testclass_ TtC6KCObjc9TestClass
So it returns empty. Why is NSClassFromString("ModuleName.ClassName") ok? Let's track the process

After that, the result is still empty. Go down and execute copySwiftV1MangledName


After processing the results here
Then why add @ objc(ClassName) and verify it

This becomes the real Class name, so you can directly get the address of the corresponding Class

Let's take a look at the Swift bridge with @ objc(TestClass) and @ objc compiled and the same @ objc(TestClass)

No @ objc(TestClass)

Here we see that one className is testclass and the other is testclass_ TtC6KCObjc9TestClass

@objc follow up

From here, we can also see that different pods in Swift can have the same Class. Distinguish the first Pod by ModuleName

import Foundation
class TestClass: NSObject { var name = "I am One Pod"
}
Second Pod
import Foundation
class TestClass: NSObject { var name = "I am Tow Pod"
}

The names of the two modules are different and the class names are the same. After the final splicing, they are different, so they can be compiled normally. What if we add @ objc(TestClass) to both classes? I can see that the compilation fails directly.

This also explains why modules that cannot be in Swift can have the same name

Binding problem and optimization of Swift and OC injection

The previous article also said that the main core pages of the real estate are of streaming layout. The solution we designed is that the client has built-in cells, each Cell is bound with a Key, and the data Key is distributed through the Server to reflect the corresponding Cell. How are Cell and Key bound?
In the era of pure OC, there are many ways to bind key class. At the beginning, a relatively simple and direct way is adopted. When entering the real estate business, key cellname and key model are bound through NSDictionary. The binding methods are as follows:
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"];

However, after a period of iteration, we found that this method has some disadvantages. Each new Cell needs to come here to modify the code. Moreover, it is not easy to manage and maintain when multiple business lines are developed at the same time, and it is easy to conflict with the code, which violates the opening and closing principle in the design principles. So we want to solve this problem in the later refactoring.

OC injection binding scheme I

The first solution we think of is injection. Bind key cellname in 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);}

However, the defect of this method is that the + Load method will have a certain impact on the startup time of the application. We add up to hundreds of cells, so the + Load method is not a good method. (here, we directly bind the Widget to simplify the process of external processing. The binding of Cell and Model and data related processing are completed by the Widget)

OC injection binding scheme II

Finally, our current implementation method is to directly write the bound data to Macho in the program precompiling stage. When the program enters the business line, it is written to memory, and then find the corresponding WidgetName through the Key issued by the Server. The specific 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,\};\
However, when the Widget needs to be bound, the macro defined above is introduced and the corresponding Key and WidgetName are passed in:
HOUSELIST_PRESENTER_REGIST(list_header_data, HSHeaderWidget)

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

@interface HouseListPresenterKVManager : NSObject+ (instancetype)sharedManager;- (Class)classWithProtocol:(NSString *)key;@end
. m file
#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;}@end
This method avoids the disadvantages of previous centralized binding and has no performance problems in + Load. However, after introducing Swift code mixing, we find that there is neither + Load method nor precompile mechanism in Swift How can we solve the problem of key wdiget binding and seamlessly connect with our current mechanism?

Problems and solutions of Swift and OC mixed injection binding

After trying various schemes, we finally chose because when Swift is mixed with OC, it has the characteristics of OC runtime. First, we create a Class such as BindKVCenter, but each time we create a new Widget, we add an Extension to BindKVCenter, which implements an enter method to bind the key Widget. Finally, when entering the real estate business, get the enter methods in all extensions in BindKVCenter and directly call the function to achieve the binding effect. The specific code is as follows: BindKVCenter.swift
@objc(BindKVCenter)public class BindKVCenter: NSObject {    // Class rewrite binding private class func enter() {}}
Widget1
private extension BindKVCenter {    @objc class func enter() {        HouseListPresenterKVManager.shared().bindKV(withKey: "list_header_data", value: "HSHeaderWidget")    }}
@objc(HSHeaderWidget)class HSHeaderWidget: NSObject {}
Widget2
private extension BindKVCenter {    @objc class func enter() {        HouseListPresenterKVManager.shared().bindKV(withKey: "list_foot_data", value: "HSFootWidget")    }}
@objc(HSFootWidget)class HSFootWidget: NSObject {}
Distribute the core code of the binding
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);    }

Get all the methods in BindKVCenter. Because the duplicate name methods added in the classification will not be overwritten, find all the methodList enter methods, and then call them directly through the function pointer. Binding to implement an injection method. This method can seamlessly connect with the macho binding method in OC, and avoid conflicts with the original design intention. And the loss of performance is minimized

Performance comparison and benefits

We tested the key performance indicators in the case of mixed swift and OC, and compared the rotation chart function page realized by swift with the rotation chart function page before OC. The test scheme is to load 100 times and take the average value every 10 times to obtain the data performance index. The results are as follows: FPS is about the same. With the increase of traffic, swift has obvious advantages in CPU performance consumption. In terms of memory, swift occupies more than OC. It is mainly because the current project is still a mixed programming environment. Swift needs to be compatible with OC characteristics. The amount of code swift is 38% less than OC

06

Summary

Swift is a very excellent language, which integrates the advantages and characteristics of various languages. Compared with OC, swift has a great improvement in performance, security and efficiency. Although there are many pits in the process of access, they finally break through one by one. After more than half a year's precipitation in the real estate, swift is currently used to develop the rental / commercial real estate category page, detail page and live broadcasting business. From 0 to 50% of the team's developers have swift development capabilities. In the future, we will continue to increase investment in swift and fully embrace swift.


reference:

https://stackoverflow.com/questions/24030814/swift-language-nsclassfromstring
https://stackoverflow.com/questions/27776497/include-of-non-modular-header-inside-framework-module
https://tech.meituan.com/2015/03/03/diveintocategory.html
https://swifter.tips/objc-dynamic/


About the author:

Wu pin: real estate business department - big front end Technology Department - Mobile Technology Department - Senior Engineer.

This article is from WeChat official account 58 Technology (architects_58).
In case of infringement, please contact support@oschina.cn Delete.
Article participation“ OSC source creation program ”, you who are reading are welcome to join us and share with us.

Keywords: Swift iOS Framework objective-c GDB

Added by mouli on Wed, 05 Jan 2022 13:48:34 +0200