Swift在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%
现 状
目前公司项目都是OC语言开发的,在这样的一个快速迭代历史悠久的项目中,短期内是不可能将所有项目用Swift重写,所以我们前期采用的都是Swift和OC的混合开发。
工程架构
房产业务结构
集团在58同城、安居客、赶集网、58同镇都有房产业务,在早期各个团队之间相对独立运行维护。随着业务的垂直化和产业化。房产发起木星计划,目标是打造成一套代码,多APP运行。来降低维护的成本和开发效率。让不同团队更加关注自己的业务,发挥自己的优势。虽然开发维护效率提高了,但同时也增加了工程的复杂度,下面就是房产目前核心业务的业务结构:
混编方案
定向桥接
OC访问Swift时在OC类中导入ProductName-Swift.h(隐藏文件),即可访问Swift中暴露给 Objective-C的类和方法
这种方式使用起来非常的简单便捷,但有两个缺陷:
-
随着Swift使用场景越来越多,导入的头文件也会变得臃肿。 -
如果工程是通过Cocoapods管理,Pod和Pod之间是不能相互调用的
Module
如果要想在 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
工程变化
WBLOCO开启module后,额外生成WBLOCO.modulemap和WBLOCO-umbrella.h两个文件
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)
}
踩坑案例
重复定义问题
LLDB调试问题
(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"
Components的Pod中引入WBLOCO的Pod类
修改前:
#import "WBLOCO.h"
#import <WBLOCO/WBLOCO.h>
这样就能调试,但代码中有很多这种不规范的写法,我们可以通过脚本替换,把项目中所以不规范的全部统一修改。
Swift与OC混编时反射问题及原理探究
Swift与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];
房产主要的核心页面都是流式布局,业务的特点是子业务相似度高,更新频率快,要有一定的动态性和灵活性。
但最近我们接入Swift与OC进行混编之后,就遇到了NSClassFromString反射的问题。
Swift与OC混编时反射初探
import Cocoa
@objc public class TestClass: NSObject {
}
1.Swift类我们必须知道Module名,而OC是没有Module名,我们需要判断是Swift还是OC的类来做特殊处理。每次新增一个Swift类,就得判断,代码很不优雅。
2.在多pod下,如果类被移动到另外一个pod,那么这个Class就找不到了,编译也不会报错。
我们工程中核心页面都是流式布局,根据Server下发的数据通过反射拿到对应的Cell Class实现动态化布局,混编情况下上面的方案差异处理较大,不能满足我们的需求。
我们最终使用了另外一种方案:在Swift的类中@ojbc后面加上自定义的类名,代码如下:
@objc(TestClass)
public class TestClass: NSObject {
}
Class cls = NSClassFromString(@"TestClass");
那么通过上面的问题思考:为什么Swift类在反射的时候需要加上Module名,@objc(TestClass)底层干了什么?
Swift与OC混编时反射打破砂锅问到底
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"
}
import Foundation
class TestClass: NSObject {
var name = "我是Tow Pod"
}
两个Module名不同,类名相同,最后的拼接完之后是不相同的,所以能够正常编译。如果我们给这两个类都加上 @objc(TestClass)呢? 我可以看到编译直接失败。
Swift与OC注入绑定问题与优化
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,\
};\
HOUSELIST_PRESENTER_REGIST(list_header_data, HSHeaderWidget)
进入房产业务时,读取Macho中DATA段之前存储的数据,并保存到内存中,代码如下:.h文件
@interface HouseListPresenterKVManager : NSObject
+ (instancetype)sharedManager;
- (Class)classWithProtocol:(NSString *)key;
@end
#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
Swift与OC混编注入绑定问题及解决方案
@objc(BindKVCenter)
public class BindKVCenter: NSObject {
// 分类重写绑定
private class func enter() {
}
}
private extension BindKVCenter {
@objc class func enter() {
HouseListPresenterKVManager.shared().bindKV(withKey: "list_header_data", value: "HSHeaderWidget")
}
}
@objc(HSHeaderWidget)
class HSHeaderWidget: NSObject {
}
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%
总 结
参考文献:
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/
作者简介:
吴品:房产事业部-大前端技术部-移动技术部-资深工程师。
本文分享自微信公众号 - 58技术(architects_58)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
当MySQL执行XA事务时遭遇崩溃,且看华为云如何保障数据一致性
摘要:当前MySQL所有版本不支持分布式事务的崩溃恢复安全,这严重影响了分布式事务的高可用保障。 华为云数据库内核高级技术专家,拥有十多年MySQL内核研发经验,目前在华为云数据库团队研发华为云数据库(RDS for MySQL和GaussDB(for MySQL))内核特性和服务化特性,修复华为云数据库现网问题;曾在官方MySQL团队研发MySQL内核特性和修复MySQL内核问题九年多,尤其擅长MySQL Replication。 注:本文如没有特殊说明,MySQL指社区版MySQL;binlog指MySQL server日志;redo Log指MySQL InnoDB日志 MySQL replication实时同步主库上执行的事务到备库,并且支持一般事务的崩溃恢复安全,这为一般事务的高可用提供了坚实的保障。如果没有此高可用保障,主库崩溃(不能正常恢复场景)后,数据库服务轻则中断几十分钟甚至几小时,重则丢失用户数据。 但是当前MySQL所有版本不支持分布式事务的崩溃恢复安全,这严重影响了分布式事务的高可用保障。华为云数据库(包括RDS (for MySQL) 和GaussDB (fo...
- 下一篇
今天你的静态变量和静态代码块执行了吗?
摘要:今天你的静态变量和静态代码块执行了吗? 本文分享自华为云社区《【java】静态变量和静态代码块那些事》,作者: 大金(内蒙的)。 今日题目: 今天你的静态变量和静态代码块执行了吗? 话不多说,开始今天的题目讲解吧。 先介绍个常识: 静态成员属性的初始化早于静态代码块; 静态代码块是指的类的初始化操作,初始化早于对象的创建; 类静态域的只会初始化一次。 题目一:输出啥? class Father{ public static int m = 33; static{ System.out.println("父类被初始化"); } } class Child extends Father{ static{ System.out.println("子类被初始化"); } } class StaticTest{ public static void main(String[] args){ System.out.println(Child.m); System.out.println(new Child()); } } 答案: 父类被初始化 33 子...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS6,CentOS7官方镜像安装Oracle11G