京东App Swift 混编及组件化落地

发表于 3年以前  | 总阅读数:462 次

背景

自 Swift 诞生以来,逐步见证其从饱受诟病到日渐完善。在苹果的全力推动下,潜移默化地把开发支持中心从 Objective-C 转向 Swift,在业界的呼声也越演越烈。当我们相继迎来 ABI稳定、Module stability、Library evolution 等功能后,我们期盼已久的 Swift 已然到来,毅然启动了京东 App 的混编之旅。我们依然坚持稳扎稳打,前期对 Swift 技术做了诸多调研工作,具体可见《Swift环境及编译优化调研》。2020年7月京东 App 的首个混编版本上线苹果商店,完成了组件内和主工程的混编工作;近期,我们完成了对京东组件化管理工具(iBiuTool)的改造,混编组件化功能正式落地,这也标志着京东 Swift 混编基础支持建设完毕。但是,Just the beginning...

期待的Swift已经到来

2.1ABI稳定

Swift 5.0,提供 ABI 稳定,解决了 Swift runtime 的版本兼容问题。这意味着通过 Swift 5.0 及以上的编译器编译出来的二进制,就可以运行在任意 Swift 5.0 及以上的 Swift runtime 上。ABI 稳定后,Swift runtime 和标准库已经植入 macOS 10.14.4、iOS 12.2、watchOS 5.2 及以上系统中。根据苹果官方数据,截止到 2020年12月15日,四年内发布的 iPhone 设备中 iOS 13及以上占比已达 98%。

另外,ABI 稳定还带来了性能上的提升。由于 Swift runtime 已经被深入的集成在了设备的操作系统中,并且结合系统层做了许多优化,这就使得 Swift 程序具有更快的启动速度、更好的运行性能,以及更少的内存占用量。

2.2Module Stability

Swift 5.1,支持 Module Stability,解决模块间编译器版本兼容的问题。这意味着使用不同版本编译器构建的 Swift 模块可以在同一个应用程序中一起使用。即使某些三方库的 Swift 编译器版本与你所使用的不同,也不会存在编译问题。官方文档中举了一个十分恰当的例子,使用 Swift 6 构建的 framwork,可以被 Swift 6 和未来的 Swift 7 编译器正常使用。所以这个进化对于开发者来说,绝对是一件非常美好事情。

在 Swift 中有一个 .swiftmodule 文件,它是一种二进制文件,主要包含模块中的数据信息和内部编译器的数据结构。由于内部编译器的数据结构的存在,同一个模块编译的 swiftmodule 文件在不同版本的编译器中都是不一样的。这也就是为什么在某个版本编译器中编译的二进制文件,在另一个版本编译器中无法被导入使用的原因。

Module Stability 解决了这个问题,在模块稳定后,存储模块信息的文件已经替代为 swiftinterface 格式了。它是一个文本格式的文件,它包含所有 public 或者 open 的 API 以及一些隐式的代码或者 API,还包括 swiftinterface 的版本、生成此 swiftinterface 的编译器版本,以及 Swift 编译器将其作为模块导入时所需的命令行标志的子集。而且这些 API 与源代码很类似,通过源码稳定实现了模块稳定。

2.3Library Evolution

Swift 5.1, 支持 Library Evolution,解决了二进制库向下兼容的问题。在 Library Evolution 特性开启的状态下,二进制库某些场景下的 API 更新后,就会自动实现对旧版本库的兼容。Library Evolution 可以在不破坏二进制兼容性的情况下对库进行某些修改。

举例来具体说明一下这个问题。组件 B 和组件 C 都依赖了组件 A,他们的组件版本都是 v1.0。主工程的 v1.0 发布时,这三个组件需要各种构建,并集成到主工程中。如下图所示:

当主工程 v2.0 发布时,组件 A 对组件 B 在 v1.0 版本所使用的 API 进行了一些 resilient 的修改,但这些修改并没有影响到组件 C。所以,组件 B 在构建二进制库时,就需要更新依赖的组件 A 到 v2.0 版本。而组件 C 没有功能修改,则不需要更新依赖和发布新版本。然后,他们都集成到 v2.0 版本的主工程中。

如果组件 A 的 Library Evolution 在没有启用的情况下,在组件 C 中与组件 A 相关的代码就有可能在运行时产生问题、甚至崩溃。而开启 Library Evolution 后,就能够做到对旧版本的兼容。

混编的方式

京东 App 根本上是一个基于 Cocoapods 实现的组件化工程,总的来看需要划分为两个场景:一、主工程的混编;二、各组件内的混编。在这两个场景中,对 Swift 引入 ObjC 和 ObjC 引入 Swift又做了不同的处理。

3.1工程中 - Swift 调用 ObjC

在主工程的 Target 下,需要通过桥接头文件的方式,将 ObjC 的头文件暴露给 Swift 进行使用。

  • 创建桥接文件(-Bridging-Header.h)
  • 确保 Build Setting 中 SWIFT_OBJC_BRIDGING_HEADER 为该桥接文件的路径
  • 将需要引入到 Swift 的 ObjC 的头文件添加进去

3.2工程中 - ObjC 调用 Swift

在主工程的 Target 下,可以通过引入 Swift Module 的 ObjC Interface Header的方式,在 ObjC 中使用 Swift。由于 Swift Module的缘故,所以引入一个文件,便可以使用该模块下的所有 Swift 文件。需要注意的是,这个头文件的命名默认是"ProjectName-Swift.h",如果工程名中有一些 nonalphanumeric 字符,则会被替换为下划线。

  • 确保 Build Setting 中 SWIFT_OBJC_INTERFACE_HEADER_NAME 的配置正确
  • 在 ObjC 中引入该模块的 Swift 头文件,#import "XXX-Swift.h"
  • 若在 ObjC的 .h 中引入,则可以通过向前声明的方式,@class XXX

3.3组件内 - Swift 调用 ObjC

在同一个 .framework 或者 .a 中实现 Swift 调用 ObjC,通过 Bridging-Header 的方式是无法解决的。如果你尝试使用 Bridging-Header 的方式,并且通过 .podspec 对 Bridging-Header 进行配置写入。只会有短暂性的编译成功,最终将会报错:

经过官方文档中的近一步查证,发现在同一个 framework 中的 Swift 想要引入 ObjC,需要将该 ObjC 文件导入到其 umbrella-header 文件中。这样 Swift 模块就可以对 umbrella-header 中向外暴露的类进行调用了。另外,官方文档中还提到 DEFINES_MODULE 要配置为 YES,这样整个组件就可以作为一个模块被外部导入使用了。

3.4组件内 - ObjC 调用 Swift

在同一个 .framework 或者 .a 中实现 ObjC 调用 Swift,依然需要通过引入 Swift Module 的 ObjC Interface Header。

  • 确保 Build Setting 中 SWIFT_OBJC_INTERFACE_HEADER_NAME 的配置正确
  • 在 ObjC 中引入该模块的 Swift 头文件,.framework 中为 #import <XXX/XXX-Swift.h>,.a 中为 #import "XXX-Swift.h"。
  • 若在 ObjC的 .h 中引入,则可以通过向前声明的方式,@class XXX

组件内混编

4.1组件内混编实施方案

参照上文中梳理的大体方案,便可以对京东App组件进行混编实施。京东组件通过自己的工具进行组件管理的,由于历史原因,某些方面的功能还不能完全支持,比如 modulemap、module stability、new build setting。所以这一些问题需要绕过,这些问题文章后面会针对说明。具体的实施步骤如下:

  • 组件内添加 Swift 文件,且不需要创建桥接文件
  • podspec 中 source_files 配置中添加 swift 项
  • ObjC 调用 Swift

o 在苹果官方文档中,推荐配置 DEFINES_MODULE = YES,并通过 #import <XXX/XXX-Swift.h> 的方式导入 Swift Module。但在京东组件中动态库是以 .framework 形式存在的,需要以 #import <XXX/XXX-Swift.h> 的方式导入 Swift Module;而静态库是以 .a 形式存在的,需要以 #import "XXX-Swift.h" 的方式导入。

o 值得注意的是,要确保你的 Swift 类为 Public 或 Open 的访问权限,否则在 Swift Module 之外的 ObjC 文件中是无论如何都不能调用的。对于 ObjC 中想要使用属性和函数,需要标记 @objc,它会告诉编译器该属性或者函数能够应用于 Objective-C 代码中。而且标有 @objc 特性的类必须继承自 ObjC 的类。

  • Swift 调用 ObjC

o Swift 模块想要调用 ObjC 就需要将 ObjC 的头文件暴露在 umbrella-header 中。这样就需要在 podspec 中将 public_header_files 配置中添加要暴露的 ObjC 头文件后,供 Swift 进行调用。

4.2组件内混编通信方案

按照上述方案实施后,组件内的通信归为 ObjC 调用 Swift 和 Swift 调用 ObjC 两个方面。具体通信方式如下图所示:

组件间混编

5.1Swift 调用 ObjC

Swift 调用 ObjC API 前,首先需要通过 import module 语法找到对应的模。Module机制是在2013年加入了Xcode中,目的是为了提升编译速度,解决C、C++中#include机制的一些遗留问题。具体是如何解决的,可以看下这两篇文章:关于objective-cmodules和autolinking(1)、Clang官方文档(2)。

我们需要解决的问题是如何让编译器找到Module,通过查看 Clang 官方的文档,我们发现:

  • 如果要支持Module,必须提供一个module.modulemap文件,用来声明模块与头文件之间的映射关系
  • 针对 framework,Clang 会通过指定路径查找命名为module.modulemap的文件:.framework/Modules/module.modulemap

module.modulemap文件中的内容大致如下,主要是用来声明模块与头文件之间的映射关系,支持 import module 方式调用。

framework module STStaticBasicStableModule {  
     umbrella header "STStaticBasicStableModule-umbrella.h"  
     export *  
     module * {  export * }  
 }  
 module STStaticBasicStableModule.Swift {  
     header "STStaticBasicStableModule-Swift.h"  
     requires objc  
 }

找到模块,并且知道模块有哪些头文件后,就可以访问组件提供的类、方法了。

5.2Swift 调用 Swift

我们知道 ObjC 代码之间调用 API 是通过头文件的形式,但 Swift 是没有头文件的,它使用一个二进制格式的文件(.swiftmodule)来代替头文件,这个文件中包含了 Swift 模块的所有 API、inlinable function bodies。

编译器会去哪找 swiftmodule 文件呢?我们在 swift 源码中找到了一些蛛丝马迹:

// SerializedModuleLoader.cpp  
 void SerializedModuleLoaderBase::collectVisibleTopLevelModuleNamesImpl(  
     SmallVec torImpl<Identifier> &names, StringRef extension) const {  
   // ...  
   forEachModuleSearchPath(Ctx, [&](StringRef searchPath, SearchPathKind Kind,  
                                    bool isSystem) {  
     switch (Kind) {  
     // ...  
     case SearchPathKind::Framework: {  
       // 源码中的注释及相关代码说明了 swiftmodule 在 framework 中的查找机制  
       // Look for:  
       // $PATH/{name}.framework/Modules/{name}.swiftmodule/{arch}.{extension}  
       forEachDirectoryEntryPath(searchPath, [&](StringRef path) {  
         // ...  
       });  
       return None;  
     }  
     }  
     llvm_unreachable("covered switch");  
   });  
 }

.swiftmodule是一个序列化后的二进制文件,从文件名SerializedModuleLoader.cpp可以猜测这个是负责加载.swiftmodule文件的。另外上述代码中也说明了针对 framework,Swift 编译器如何查找.swiftmodule。

同时为了保证组件支持 x86、arm 架构下编译,我们还需要将不同架构编译生成的 swiftmodule 文件手动合并到最终的 framework 中。

5.3ObjC 调用 Swift

编译器会通过我们编写的 Swift 代码生成xxx-Swift.h,这样 ObjC 就可以通过这个头文件访问 Swift 的 API 了。我们可以通过两种 import 方式调用 Swift API:

  • @import STStaticBasicStableModule
  • #import "STStaticBasicStableModule-Swift.h"

还记得上面 modulemap 中的STStaticBasicStableModule.Swift吧,@import STStaticBasicStableModule在找到模块后,会通过 modulemap 文件中声明找到STStaticBasicStableModule-Swift.h:

 module STStaticBasicStableModule.Swift {  
     header "STStaticBasicStableModule-Swift.h"  
     requires objc  
 } 

xxx-Swift.h也需要支持多架构,我们需要把不同架构下生成的xxx-Swift.h内容合并到一个文件中,最终合并的xxx-Swift.h,去掉部分代码,大致结构是这个样子:

#ifndef TARGET_OS_SIMULATOR  
 #include <TargetConditionals.h>  
 #endif  

 #if TARGET_OS_SIMULATOR  

     // Release-iphonesimulator/Swift Compatibility Header/XXX-Swift.h  
     #if 0  
     #elif defined(__x86_64__) && __x86_64__  
     // __x86_64__  
     #elif defined(__i386__) && __i386__  
     // __i386__  
     #endif  

 #else  

     // Release-iphoneos/Swift Compatibility Header/XXX-Swift.h  
     #if 0  
     #elif defined(__arm64__) && __arm64__  
     // __arm64__  
     #elif defined(__ARM_ARCH_7A__) && __ARM_ARCH_7A__  
     // __ARM_ARCH_7A__  
     #endif  

 #endif // TARGET_OS_SIMULATOR 

5.4Module stability & Library evolution

文章开篇说过,它们是 Swift 5.1 新增的2个关于二进制稳定的特性,可以支持发布和共享 framework。只有当你的库要独立于客户端进行构建的情况下,才需要开启 Build Libraries for Distribution 选项,而且 Module Stability 和 Library Evolution 就会同时生效。通常我们在开发二进制库时,最好尽早打开此开关,以提供任何二进制兼容性的保证。

如果不支持这2个特性,可能会出现的问题:

  • User1 使用 Xcode 11.2 发布了基础组件,User2 依赖了这个组件,并使用 Xcode 11.7 编译,结果:编译报错Module compiled with Swift 5.1.2 cannot be imported by the Swift 5.2.4compiler
  • User1 给结构体中新增了一个属性,然后发布了新版本组件,依赖该组件的下游较多,User1 需要周知所有依赖方依赖最新版本重新编译,否则可能会引发运行时崩溃

开启这2个特性的方式:

  • Xcode 中设置 build setting 中的 BUILD_LIBRARY_FOR_DISTRIBUTION 为 YES
  • 需要支持 New Build System。

5.5组件间混编通信方案

按照上述方案实施后,组件间的通信归为 ObjC 调用 Swift 和 Swift 调用 ObjC,以及 Swift 调用 Swift 三个方面。具体通信方式如下图所示:

京东主工程混编支持方案

6.1静态编译的问题

由于我们之前是纯 ObjC 的开发环境,所以即使实现了组件内混编,京东组件化的主工程(或者组件的Example工程)也并不能成功编译。原因在于它们还不支持 Swift 混编环境,在编译时可能会报类似错误:

或者

上述的错误信息说明,编译器不能自动链接到 Swift 相关的一些静态库和动态库。而这些资源是存在于 Xcode Toolchains 下的,在本地的路径为:

  • /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos
  • /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/iphoneos

接下来将这些资源路径配置在工程中,一般地需要在Build Settings -> Library Search Paths 中添加:

  • "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
  • "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"

6.2动态库加载的问题

当配置好可链接资源的路径之后,就可以成功编译了。但在启动时,动态库加载的问题将会引起程序的崩溃。诸如以下错误:

如果你的设备是 iOS 12.2 及以上,可能报错如下:

在 Build Settings -> Runpath Search Paths 中首行添加配置 /usr/lib/swift 配置,特别要注意的是只能在首行配置才能解决问题。

如果你的设备是 iOS 12.2 以下,可能报错如下:

iOS 12.2 以下,将 Build Settings -> Always Embed Swift Standard Libraries 设置为 YES。

这两个问题都是在应用启动后,动态库加载时,发生的崩溃。那为什么要以 iOS 12.2 为分水岭呢?这就是从 iOS 12.2 Swift 已经实现 ABI 稳定了。所以上述两处的解决方案缺一不可,因为它们分别针对 ABI 稳定前后的系统版本。这些配置完成后,京东组件的主工程(或者组件的Example工程)就已经完全支持 Swift 混编环境了。另外,还有一种更加便捷方案也可以达到同样的效果。

6.3一键配置混编环境

除了 Xcode Build Settings 配置的方式,还可以通过在工程中新建一个 Swift 文件(文件中无需添加任何代码),通过这种方式 Xcode 会自动完成部分环境配置,能够解决静态编译问题和部分设备的动态库加载问题。另外,还需要处理 iOS 12.2 以下 Swift 动态库加载的问题。将 Always Embed Swift Standard Libraries 设置为 YES。

假如通过 Xcode Build Settings 配置的方式,从 Xcode 11 beta 4 开始,就需要在工程配置中添加 swift-5.0 的新配置项了。但如果通过新建 Swift 文件的方式,就不必更新配置,它的优势在于机动性好。

京东是一个标准的组件化应用,主工程中无代码实现,所以不需要实现混编代码,仅需要使其支持 Swift 开发环境即可。因此 Xcode Build Settings 配置的方式能够满足需求,无需多余文件,在工程的简洁性上更好。

通信方案总结

最后,对京东 App 中涵盖的混编通信方式做个汇总。以组件内、组件间,以及主工程的混编形式为基础,将整体的混编通信方式汇总如下:

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/FfqQfFXcwORf4jujftOWPA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录