概述

前段时间58和合作搞了一次技术沙龙,我有幸作为分享嘉宾之一,以下是我本次分享的内容和沙龙所有视频

背景

2014年Apple在WWDC发布了新的语言Swift。随后一直在不断的更新迭代和优化,国内外各大公司一直在踊跃欲试,但一直都没有商用或大规模使用。直到2019年Apple发布了5.0版本,并宣布ABI稳定,2020年更是陆续SwiftUI、CareKit等Swift专属SDK,并且Apple一直在大力推广鼓励大家使用Swift。在这样的背景下,越来越多的开发者、开源项目都加快了Swift生态搭建的脚步.
另外Swift作为一门新语言,相比于Objective-C有巨大的后发优势:安全、高效、高性能等。这些特性有利于开发者提升开发效率和APP质量。在《Swift 2021 生态调研报告》中App Store免费前100中国外APP使用Swift占比91%。国内占比近50%

现状

在这样的趋势之下,58集团与2020年底启动了Swift共建项目,内部称为混天项目。目标是搭建Swift的基础组件、辅助工具及基础设施。制定集团Swift开发规范和代码检测工具以及Swift在各个业务线中的落地。
房产业务作为集团核心产业,深度参与了混天项目的研发及Swift的落地。下面的内容主要是Swift在房产业务线从0到1落地的过程中遇到的一些问题和探索。
目前公司项目都是OC语言开发的,在这样的一个快速迭代历史悠久的项目中,短期内是不可能将所有项目用Swift重写,所以我们前期采用的都是Swift和OC的混合开发。

工程架构

在接入Swift之前,我们先来了解下集团APP的工程架构以及房产的业务结构。58集团iOS团队维护了58同城、安居客、赶集网、58同镇、招财猫、车商通等十几款APP,为了降低维护的成本和开发效率,团队将基础功能进行组件化,提供多个自研基础组件以及SDK,打造APP工厂。但不同APP和业务线对底层的依赖情况各不相同。于是在此基础之上增加中间层,来解决业务垂直业务跨APP时的底层差异和APP内业务之间的共享。

房产业务结构

集团在58同城、安居客、赶集网、58同镇都有房产业务,在早期各个团队之间相对独立运行维护。随着业务的垂直化和产业化。房产发起木星计划,目标是打造成一套代码,多APP运行。来降低维护的成本和开发效率。让不同团队更加关注自己的业务,发挥自己的优势。虽然开发维护效率提高了,但同时也增加了工程的复杂度,下面就是房产目前核心业务的业务结构
房产业务架构

混编方案

目前Swift和Objective-C业内混编的方式主要有两种:

定向桥接

如果是在一个 App Target 内部混编的话,通过在宿主工程中添加桥接文件的方式进行混编,每个工程第一次创建Swift文件的时候,系统都会创建一个桥接(ProductModuleName-Bridging-Header.h)文件

Swift类需要访问OC的类时,只需要在这个桥接文件中导入需要暴露的类,即可在Swift中访问相应 OC的类和方法
OC访问Swift时在OC类中导入ProductName-Swift.h(隐藏文件),即可访问Swift中暴露给 Objective-C的类和方法
这种方式使用起来非常的简单便捷,但有两个缺陷:
1.随着Swift使用场景越来越多,导入的头文件也会变得臃肿。
2.如果工程是通过Cocoapods管理,Pod和Pod之间是不能相互调用的

Module

我们的项目工程一个壳工程和多个业务子工程组成在一起,每一个业务线子工程都有一个或多个模块连接在一起,通过Cocoapods进行管理,模块之间是有依赖关系的。我们不但需要Swift和OC之间的调用,还需要跨Pod调用,所以桥接的方式肯定是不能满足。这时候还有一种方式就是Module。将 Build Settings 中的 Defines Module 选项设置为 YES,
然后新建一个 umbrella header,再将需要暴露给Swift 调用的 OC 的头文件在这个 umbrella header 中导入
如果要想在 ObjC 调用 Swift,同样也要将 Build Settings 中的 Defines Module 选项设置为 YES,然后在要引用 Swift 代码的 ObjC 文件中导入编译器生成的头文件 #import <ProductName/ProductModuleName-Swift.h>
###Module化实践

背景

通过上面了解到目前我们的工程架构已经是通过Cocoapods进行组件/模块化管理.每个模块就是一个Module,而定向桥接的方式是不能跨Module通信,所以我们适合Module的方式进行混编,那如果进行混编呢?

环境搭建

  • 开启Module选项

为了Pod库之间能够引用暴露的Swift接口, 第一步需要让被访问的库开启module, 需要在Swift所在的Pod文件夹下的podspec中的xcconfig下,添加’DEFINES_MODULE’ => ‘YES’

  • 添加依赖

调用方需要在自己的podspec里添加moudule依赖,s.dependency ‘被调用方的pod库’

  • 使用方式

配置好上述的依赖配置后,就可以调用开启Module的pod库了。不管是Swift文件中还是OC文件中都可以通过@import方式引用即可,同时Components组件也可以跨Pod调用WBLOCO中的OC的方法以及对外暴露的Swift接口,当然,暴露Swift接口想要暴露在OC环境下,需要用@objc声明,同时接口要声明成public

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

工程变化

业务库开启moudule后工程目录的变化及注意事项
WBLOCO开启module后,额外生成WBLOCO.modulemap和WBLOCO-umbrella.h两个文件

.默认情况下WBLOCO中的umbrella.h里会导出所有OC的头文件,为解决此问题,可通过在podspec的private_header_files中添加屏蔽头文件的导出

Components Pod添加WBLOCO依赖后工程文件的变化

Swift类型对外暴露注意事项

Swift接口想要暴露在OC环境中,无论是在当前Pod还是跨Pod暴露, 首先Swift的class想要定义成public, 同时对外暴露的接口需要用@objc声明,接口也要定义成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)
    }

踩坑案例

环境配置好,在接入Swift混编开发是时候遇到各种各样的问题坑,以下是房产在接入Swift时遇到的一些相对通用问题
####重复定义问题
创建Swift文件之后,工程直接编译失败,错误如下:

根据提示排查发现是因为工程代码中协议名称或者Block名称有重复,在纯OC中只要两个文件没有相互引用,编译器检测相对没有那么严格,所以编译不会报错,但接入Swift之后,编译器检测更加严格,编译就失败了,那解决的方式独立抽离一份,删除重复定义的或者修改相应的协议名和Block名称之后,再次编译成功
####LLDB调试问题
我们在混编开发过程中,但我们在在控制台po调试的时候,发现变量名看不到,提示类似错误信息如下:

(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"

这种错误的原因都是跨Pod引用OC头文件引入不规范导致,我们需要把所有.h文件中跨Pod导入的文件都需要改成全路径的方式引入例如:
Components的Pod中引入WBLOCO的Pod类
修改前

#import "WBLOCO.h"

修改后

#import <WBLOCO/WBLOCO.h>

这样就能调试,但代码中有很多这种不规范的写法,我们可以通过脚本替换,把项目中所以不规范的全部统一修改

Swift与OC混编时反射问题及原理探究

Swift与OC混编时反射问题背景

在日常开发中,我们经常会用到反射这个机制。iOS开发中系统也给我们提供了相应的API,我们可以通过这些API执行将字符串转为Class、SEL等操作。由于OC语言的动态性,这些操作都是发生在运行时的。

// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
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);

通过这些方法,我们可以在运行时通过字符串创建相应的实例,并动态选择调用相应的方法

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

房产主要的核心页面都是流式布局,业务的特点是子业务相似度高,更新频率快,要有一定的动态性和灵活性。
基于以上的原因我们设计的方案客户端内置Cell,每个Cell绑定一个Key,通过Server下发数据Key,反射出对应的Cell,从而展示不同的内容和控制显示的内容的顺序。
但最近我们接入Swift与OC进行混编之后,就遇到了NSClassFromString反射的问题。

Swift与OC混编时反射初探

我们先来创建Swift类TestClass,当前的Module名称为HouseTest,下面看NSClassFromString这两种获取方式打印输出的结果

import Cocoa
@objc public class TestClass: NSObject {
}

OC代码中通过反射获取这个TestClass这个类对象第一种方式输出的cls是为空的,第二种方式是在类名前期需求拼接上Module名称我们看到拿到cls实例

但这种方案我们实际在项目中使用时有2个明显的缺陷
1.Swift类我们必须知道Module名,而OC是没有Module名,我们需要判断是Swift还是OC的类来做特殊处理。每次新增一个Swift类,就得判断,代码很不优雅
2.在多pod下,如果类被移动到另外一个pod,那么这个Class就找不到了,编译也不会报错
我们工程中核心页面都是流式布局,根据Server下发的数据通过反射拿到对应的Cell Class实现动态化布局,混编情况下上面的方案差异处理较大,不能满足我们的需求
我们最终使用了另外一种方案:在Swift的类中@ojbc后面加上自定义的类名,代码如下:

@objc(TestClass)
public class TestClass: NSObject {
}

加上@objc(TestClass)之后,OC中反射时获取Swift的Class我们就不需要关心Module名,底层处理方式和OC一样,这样就抹平了底层的差异性

Class cls =  NSClassFromString(@"TestClass");

我们验证一下

这们看到这里我们TestClass,通过这种方式就解决了上面那种方式的两个缺陷。
那么通过上面的问题思考:为什么Swift类在反射的时候需要加上Module名,@objc(TestClass)底层干了什么?

Swift与OC混编时反射打破砂锅问到底

因OC 源码不是开源的,没办法直接看源码,先下个符号断点,看下汇编代码(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

通过上面的汇编代码,查看关键信息,最后我们看到调用了objc_lookUpClass 通过上面的汇编模拟一下伪代码

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]);
}

验证结果

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

通过伪代码我们发现这里并没有看出异常,加不加@objc(TestClass)都一样,那肯定是后面流程有问题。那只能调试源码(当前为objc-781)
我们跟踪源码的调用流程:objc_lookUpClass -> look_up_class -> getClassExceptSomeSwift

最后我们看到是从 NXMapGet(gdb_objc_realized_classes, name, cls)获取的。gdb_objc_realized_classes保存的是从Mach-O中加载的全部的类,难道是Swift 写的类没有被加载进来? 那我们去看看加入的时候是怎么加进去的,我们找到程序启动类插入到gdb_objc_realized_classes 方法

我们看到这里加个Log打印一下

到TestClass这个类并不是我们看到的样子,变成了 _TtC6KCObjc9TestClass所以我们在调用NSStringFromClass(“TestClass”)时,传入的key是TestClass,maptable中存入的key是_TtC6KCObjc9TestClass
。所以返回空了。 那为什NSClassFromString(“ModuleName.ClassName”)就可以了呢? 我们跟踪一下流程

走到这里result 还是为空,往下执行 copySwiftV1MangledName


到这里处理完之后结果
那为什么加上@objc(ClassName)就可以了,验证一下

这里就变成了真实的类名所以直接就能拿到对应Class的地址

我们在看一下加上 @objc(TestClass)和@objc编译之后的Swift桥接件和没加区别 @objc(TestClass)

没加@objc(TestClass)

这里我们看到一个 className是TestClass,一个是_TtC6KCObjc9TestClass
####@objc后续
从这里我们也能够看到Swift中不同Pod中可以有相同的Class。通过ModuleName进行区分 第一个Pod

import Foundation
class TestClass: NSObject {
    var name = "我是One Pod"
}

第二个Pod

import Foundation
class TestClass: NSObject {
    var name = "我是Tow Pod"
}

两个Module名不同,类名相同,最后的拼接完之后是不相同的,所以能够正常编译。如果我们给这两个类都加上 @objc(TestClass)呢? 我可以看到编译直接失败

这里也说明Swift中不能的Module可以同名原因
###Swift与OC注入绑定问题与优化
####背景
前面的文章也说到房产主要的核心页面都是流式布局,我们设计的方案客户端内置Cell,每个Cell绑定一个Key,通过Server下发数据Key,反射出对应的Cell。那么Cell和Key是如何进行绑定的?
在纯OC时代,绑定Key-Class方式有很多种,最开始采用的也是比较简单和直接的方式,在进入房产业务时,通过NSDictionary进行绑定Key-CellName和Key-Model,绑定方式如下:

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"];

但经过一段时间的迭代,我们发现这种方式有一些弊端,每次新增Cell都需要来这里修改代码,而且多业务线同时开发的时候不容易管理和维护,且容易代码冲突,违反了设计原则中的开闭原则。所以我们后期在重构的时候想到解决这个问题。
####OC注入绑定方案一
首先我们想到的方案是注入的方式,在每个类的+Load方法中绑定Key-CellName,代码如下:

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

但这种方式的缺陷就是+Load方法会对应用程序的启动时长有一定的影响,我们加起来有上百个Cell,所以+Load方法也不是一个很好的方式。(在这里我们直接绑定的是Widget,简化外部处理的流程,Cell、Model的绑定和数据相关的处理由Widget来完成)
####OC注入绑定方案二
最后我们现在实现的方式是在程序预编译阶段,直接把绑定的数据写到Macho中,程序在进入业务线时,写入到内存中,然后通过Server下发的Key找到对应的WidgetName,具体代码如下:

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,\
};\

但Widget需要绑定时,引入上面定义好的宏,传入对应的Key和WidgetName:

HOUSELIST_PRESENTER_REGIST(list_header_data, HSHeaderWidget)

进入房产业务时,读取Macho中DATA段之前存储的数据,并保存到内存中,代码如下:
.h文件

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

.m文件

#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

这种方式即规避了之前集中绑定的弊端,也没有+Load中的性能问题,但我们引入Swift代码混编之后,发现Swift中既没有+Load方法,也没有预编译这样的机制.那我们如何解决Key-Wdiget绑定的问题并且还能和我们现在的机制无缝衔接?

Swift与OC混编注入绑定问题及解决方案

在尝试各种方案之后,最终我们选择的是因为Swift与OC混编时,具有OC运行时的特性,首先创建一个BindKVCenter这样的Class,但每次创建新的Widget时,我们就给BindKVCenter添加一个Extension,Extension中实现一个enter的方法来绑定Key-Widget。最后进入房产业务时,拿到BindKVCenter中所有Extension中的enter方法,直接进行函数调用,达到绑定的效果,具体代码如下:
BindKVCenter.swift

@objc(BindKVCenter)
public class BindKVCenter: NSObject {
    // 分类重写绑定
    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 {
}

分发绑定的核心代码

 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);
    }

拿到BindKVCenter中的所有方法,分类中因为添加的重名方法不会覆盖,找到methodList所有enter方法,再通过函数指针直接调用。进行绑定,从而实现一种注入的方式。这种方式即能够和OC中macho绑定的方式无缝衔接,还能避免和之前设计初衷冲突。并且对性能的损耗达到最小
###性能对比及收益
我们测试了下Swift和OC混编情况下的关键性能指标,通过Swift实现的轮播图功能的页面和之前OC之前的轮播图功能页面进行对比。测试方案是加载100次每10次取平均值所得到数据性能指标,得到的结果是:FPS不差上下,CPU性能消耗随着业务量的增加Swift有明细的优势,内存方面Swift比OC占用更高,主要是目前项目工程还是混编环境,Swift需要兼容OC的特性。代码量Swift相比于OC减少38%。

总结

Swift是一门非常优秀的语言,融合了各种语言的优点和特性,相比于OC在性能、安全、效率等方面都有很大的提升。虽然在接入的过程中虽然有很多的坑,但最终逐个突破。在房产经过半年多时间的沉淀,目前租房/商业地产大类页、详情页以及直播业务均有使用Swift开发。团队从0到50%开发人员具备Swift开发能力。未来我们会在Swift方向继续加大投入,全面拥抱Swift

参考文献

stackoverflow.com/questions/2…
stackoverflow.com/questions/2…
tech.meituan.com/2015/03/03/…
swifter.tips/objc-dynami…

本次沙龙的回放,有兴趣的同学可以观看/post/702290…