一套代码两端运行不靠谱?是时候放弃 C++ 跨 Android、iOS 端开发!

Dropbox分享了使用C++进行iOS和Android跨平台开发的经验,指出隐形成本超过预期,最终选择回归原生语言Swift和Kotlin。文章详细分析了自定义框架、开发环境、平台差异及人才培训等方面的额外开销。

640?wx_fmt=gif

「Write once,run anywhere!」想必是很多开发者以及企业梦寐以求的愿望,但是在分析跨平台中的种种成本之后,我们不禁发问,这种策略真的靠谱吗?

近日,云存储公司 Dropbox 就此发文剖析了这一现状,其表示一直以来,他们均使用了 C++ 语言编写跨 Android、iOS 端的代码,但是经过了 6 年的实践之后,他们发现代码共享相关的隐形成本太高,还不如直接动手写两套代码,最终,其放弃了原来的开发模式,转而使用每个平台的原生语言(如 Swift 和 Kotlin)。接下来,我们将详细了解其中的缘由以及在跨开发过程中所消耗的成本。

 

640?wx_fmt=jpeg

作者 | Eyal Guthmann

译者 | 弯月,责编 | 屠敏

出品 | 优快云(ID:优快云news)

以下为译文:

 

Dropbox一直有一个共享移动设备上的iOS和Android代码的策略:使用C++语言编写。

这种策略背后的想法很简单:利用C++编写一次代码就够了,否则就需要用Java和Objective C写两次。我们于2013年开始采用这种C++的策略,当时我们的移动工程团队规模相对较小,而且需要支持快速增长的移动发展蓝图。我们需要找到一种方法,让这个小团队快速发布大量iOS和Android代码。

直到最近,我们才决定完全摒弃这种策略,转而使用每个平台的原生语言(主要是Swift和Kotlin,我们刚开始做移动开发的时候这些语言还没有出现)。做这个决定的原因关系到代码共享相关的隐形成本。我将在本文中分享我们公司在有效共享代码方面得到的经验教训。其实,所有这些都源自同一个基本问题:

如果以非标准的方式编写代码,我们就需要承担额外的开销(如果采用广泛使用的平台就没有这种担心)。结果却发现,这种额外的开销超过了写两次代码的代价。

在深入介绍我们遭遇的所有不同的额外开销之前,我想先澄清一下,实际上我们之前的目标(即大多数代码都用C++开发)从未实现。采用C++引发的额外开销导致我们无法完全朝着这个方向迈进。

请注意,多年以来谷歌和Facebook等大公司一直在开发可扩展的代码共享解决方案。然而,迄今为止这些解决方案的采用范围依然很有限。虽然你可以利用React Native或Flutter等第三方代码共享解决方案,来避免本文描述的部分额外开销,但仍有一些开销避无可避,至少在其中一项技术发展成熟之前我们依然需要承担这些额外的开销。例如,Airbnb也因为本文中描述的许多原因,而不再使用React Native。

我们可以将我们遭遇的额外开销大致分为如下四类:


640?wx_fmt=png

自定义框架和库的开销

 

采用C++时最显而易见的开销就是构建框架和库。而这又包含两个方面:

1. 框架:通过与主机环境的交互构建完整的移动应用的框架。

例如:

  • Djinni,一种用于生成跨语言类型声明和接口绑定的工具。

 

  • 负责在后台运行任务与主线程(用平台原生语言执行的简单任务)的框架。

 

2. 库:替换我们本可以在平台原生语言中使用的语言默认开源标准的库。

例如:

  • json11,实现JSON(de)序列化。

 

  • nn,C++的非可空指针。

 

如果我们坚持使用平台原生语言,那么这些代码都没有必要,而且我们为开源项目贡献的代码也可以让更多使用平台原生语言的开发人员受益。虽然我们可以利用开源的C++代码库,但C++开发社区的开源文化并不如移动开发社区那般强大(特别是移动开发社区中没有C++移动社区)。

请注意,这些成本在C++中特别高(与Python或C#等其他非原生语言相比),因为它缺少单一全面的标准库。尽管如此,C / C++是谷歌和苹果唯一支持的带编译器的语言,因此使用其他语言会面临更多其他需要处理的问题。

 

640?wx_fmt=png

自定义开发环境的开销

 

移动生态系统有许多可以提高开发效率的工具。移动的IDE非常丰富,谷歌和苹果为提高各自平台上的开发体验投入了大量资源。由于我们没有采用默认的平台原生语言,所以无法享受这些好处。最值得注意的是,平台原生语言的调试体验通常都远胜于在平台默认的IDE中调试C++代码的体验。

有一个令我特别难忘的例子,有一次引发后台线程框架死锁的某个bug导致我们的应用频频崩溃。即使在简单的标准技术栈中,你也很难确定这类的bug。因为这个问题需要调试在C ++和Java之间反复切换的多线程代码,所以我们花费了数周时间才修复!

除了丧失了一大批工具之外,我们还需要花时间构建支持C++代码共享的工具。最重要的是,我们需要一个自定义的构建系统,其中包含打包C ++代码以及Java和Objective-C的库,还需要生成Xcodebuild和Gradle都能理解的构建目标。这个系统占用了我们的大量资源,因为它需要不断的更新,才能支持两个构建系统的变动。

 

640?wx_fmt=png

解决平台之间差异的开销

 

虽说iOS和Android应用都是“移动应用”,而且二者通常都具备相同的特性和功能,但平台本身确实存在一些影响实现的差异。例如,应用在每个平台上执行后台任务的方式是不同的。即便我们采用了这种跨平台式策略,虽然刚开始还有点相似,可随着时间的推移差异也逐步拉大(例如,与相机相册的交互等)。

因此,实际上“只需编写一次代码就可以在不同平台上开箱即用”的说法只是一个传说。最终为了将代码集成到不同的平台上,你依然需要花费大量时间编写特定于平台的代码(而且有时你需要改动C++层的代码!)。

因此,只需编写一次代码只是一个永远无法兑现的美梦,这种做法并没有太大益处。

 

640?wx_fmt=png

培训、招聘和留住开发人员的开销

 

最后,虽然这不是最重要的因素,但培训和/或雇用开发人员在我们自定义的技术栈上工作也有一定的成本。当初采用这种移动策略时,我们拥有一批经验丰富的C++开发人员。该小组负责启用了这个C++项目,并为Dropbox培训其他移动开发人员。

过了一段时间后,这些开发人员逐步跳转到了其他团队和其他公司。留下的工程师没有足够的经验挑起技术领导的大梁,而且招聘具备C++经验且对移动开发感兴趣的高级工程师也越来越难。

最终,我们深陷缺乏维护C++代码库的关键专业知识的困境。重获这种专业知识的唯一方法是投资如下其中一个方向:

招聘具备这种特定技术力的工程师(我们尝试了一年多,却无果而终)。

培训内部的移动(或C++)工程师,然而如果你没有具备这些技术力的人员来做培训,这种方法也行不通。此外,早在核心小组的人员发生变动之前,大多数移动工程师就对学习C++失去了感兴趣,因此找不到培训对象也是一个大问题。

在招聘问题之上,维持我们的技术栈面临人员去留的问题,因为移动开发人员根本不想掺和C++的项目。这导致许多有才华的移动工程师都离开了这个项目,最后自定义技术栈的维护工作陷入了举步维艰。总的来说,移动开发社区的发展日新月异,新技术和模式频频出现,且被迅速采用。优秀的开发人员都希望尝试最新的技术。

在拥有标准技术栈的成熟产品环境中,跟上最新技术和产品的步伐是一项很大的挑战。有时,为了保持稳定,你不得不牺牲采用新技术的速度。如果你将自己封闭到某个自定义技术栈中,远离移动生态系统的花花世界,那么这种挑战的难度会急剧上升。

 

640?wx_fmt=png

总结

 

虽然“只需编写一次代码”的说法听起来性价比很高,但实际的额外开销会导致这种做法得不偿失(并让你大失所望)。最终,我们放弃利用C++(或任何非标准的方式)共享移动代码,转而使用平台原生的语言编写代码。

另外,我们也希望我们的工程师享受快乐的工作时光,并为回馈社区做贡献。这也是我们决定向行业标准看齐的原因。

 

640?wx_fmt=png

网友说

 

对于这样一套代码两端运行的成本的计算与使用,网友也纷纷发表了自己的看法:

评论1:

我不明白像Dropbox这种规模的公司明明完全可以采用平台原生语言,为什么还要利用C++开发移动应用。

话说回来,我们公司有一个非常老的ERP系统,我们需要开发一些移动应用。当时我们的团队中只有7个人,却有很多项目需要维护,再加上考虑到共享代码的成本以及我们对C#的熟悉程度后,我们选择了Xamarin。这绝对是一个折衷方案,但也不失为一种务实的选择。

如果你有能力雇佣Java或Obj-C的开发人员,而且要求他们承担起移动开发的工作,那么就应该考虑使用平台的原生语言。

但是,如果你是一家小公司,且拥有Java和C#的开发,同时Obj-C和C#开发人员的招聘有难度,那么就可以考虑Xamarin或其他跨平台共享代码。

总结起来就是,根据自身的实际情况而定。

评论2:

经过多年的反复验证后,我也得出了相同的结论。在两个移动平台之间共享任何定制的业务逻辑(模型、控制器等)并不明智。如果你有一些非常棘手的低级算法或库,或者你需要考虑到速度、数据库、加密,丰富的图形等,那么可以尝试利用C ++或其他语言共享模块。除此之外,即便是谷歌和苹果尝试利用CRUD应用来实现高效的代码共享,最后也无果而终。

对此,你怎么看?

原文:https://blogs.dropbox.com/tech/2019/08/the-not-so-hidden-cost-of-sharing-code-between-ios-and-android/

本文为优快云翻译,转载请注明来源出处。

【END】

Python工程师的薪资,了解一下

https://edu.youkuaiyun.com/topic/python115?utm_source=csdn_bw

640?wx_fmt=jpeg

 

 热 文 推 荐 

李彦宏内部信曝光;三大运营商否认 4G 降速;ThinkPHP 6.0 RC4 版本发布 | 极客头条

工程师与项目经理之间差距何在?

每秒 10 万并发的 BI 系统如何频繁发生 Young GC?

漫画高手、小说家、滑板专家……解锁程序员的另一面!

每天超50亿推广流量、3亿商品展现,阿里妈妈的推荐技术有多牛?

手把手教你如何用Python模拟登录淘宝

做了中台就不会死吗?每年至少40%开发资源是被浪费的!

17大主链均狂跌国内项目却看涨, 本周不可不知的7组榜单 | 数据周榜

行!这下 优快云 玩大了!粉丝:太良心

640?wx_fmt=gif点击阅读原文,输入关键词,即可搜索您想要的 优快云 文章。

 

 

 

 

 

640?wx_fmt=png

你点的每个“在看”,我都认真当成了喜欢

<think> 根据用户的问题,用户希望了解Android底部导航栏的实现方式,并找出最可靠和最常用的实现方式。 尽管用户提供了几个引用,但这些引用似乎与底部导航栏关系大。引用[1]讨论的是状态栏,引用[2]是工具库集合,引用[3]提到了Fragment在屏幕旋转时的处理。 因此,我们应忽略这些引用,专注于用户的实际问题。 根据之前的回答,我们提到过两种实现方式: 1. 使用uni-app框架(平台方案) 2. 单Activity多Fragment架构结合Navigation组件(原生方案) 但是,用户现在希望列出多种实现方式,并指出最可靠、最常用的方式。 常见的Android底部导航栏实现方式包括: 1. 使用TabLayout和ViewPager(或ViewPager2)组合实现滑动切换的底部导航。 2. 使用RadioGroup和多个RadioButton自定义底部导航栏,通过FragmentTransaction切换Fragment。 3. 使用BottomNavigationView组件(属于Material Design组件库),结合Navigation组件或者Fragment管理。 4. 单Activity多Fragment架构中使用Navigation组件(Google官方推荐)。 5. 平台方案:如uni-app、Flutter等框架提供的底部导航组件。 其中,最可靠且最常用的原生Android实现方式是使用BottomNavigationView配合Navigation组件(或FragmentManager)实现单Activity多Fragment架构。 理由: - BottomNavigationView是Material Design组件的一部分,专门为底部导航设计,样式规范,易于使用。 - 结合Navigation组件可以方便地管理Fragment的导航栈,处理返回栈问题,且与Android Studio的可视化工具集成。 - Google官方推荐,社区支持好,文档丰富。 下面我们详细列出几种方式: 1. **使用TabLayout和ViewPager2(适合可滑动的标签页,也可放在底部作为导航)** - 通常TabLayout放在顶部,但也可以放在底部。结合ViewPager2可以实现左右滑动切换页面。 - 但是,这种方式通常用于顶部导航,底部导航较少使用,因为底部导航通常需要左右滑动切换(符合Material Design指南)。 2. **自定义RadioGroup和FragmentTransaction** - 在布局中使用RadioGroup包含多个RadioButton,每个RadioButton代表一个底部导航项。 - 在Activity中监听RadioGroup的选择事件,通过FragmentTransaction动态替换Fragment。 - 这种方式灵活,但需要手动管理Fragment的切换和状态保存,代码量相对较多。 3. **使用BottomNavigationView(Material Design组件)** - 在布局中添加BottomNavigationView,并配置菜单资源(定义每个导航项)。 - 在Activity中设置OnNavigationItemSelectedListener,根据选中的菜单项切换Fragment。 - 这种方式简单,符合Material Design规范,但需要自己处理Fragment的切换(可以使用FragmentContainerView配合Navigation组件简化)。 4. **使用BottomNavigationView结合Navigation组件(推荐)** - 这是Google官方推荐的方式。在Activity的布局中放置一个FragmentContainerView作为Fragment的容器。 - 使用Navigation组件创建导航图(nav_graph),其中定义每个Fragment以及它们之间的关系(注意:底部导航的各个目的地通常是平级的)。 - 将BottomNavigationView与NavController绑定,使用setupWithNavController方法,这样点击底部导航项时,NavController会自动导航到对应的目的地(Fragment)。 - 这种方式自动处理了Fragment的切换和返回栈,且与Android Jetpack组件集成,是目前最常用的方式。 5. **平台方案(如uni-app、Flutter、React Native等)** - uni-app:使用uni-tabbar组件,通过配置文件设置底部导航,编写对应页面(Vue组件)即可。 - Flutter:使用CupertinoTabBar或BottomNavigationBar组件。 最常用且最可靠的方式(对于原生Android开发)是上述的第4种:BottomNavigationView结合Navigation组件。 下面我们给出一个简单的实现示例(原生Android,使用Kotlin): 步骤: 1. 在build.gradle中添加依赖: ``` implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'com.google.android.material:material:1.7.0' ``` 2. 在res/menu目录下创建底部导航菜单(如bottom_nav_menu.xml): ```xml <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/homeFragment" android:icon="@drawable/ic_home" android:title="Home"/> <item android:id="@+id/dashboardFragment" android:icon="@drawable/ic_dashboard" android:title="Dashboard"/> <item android:id="@+id/notificationsFragment" android:icon="@drawable/ic_notifications" android:title="Notifications"/> </menu> ``` 3. 在Activity的布局中(如activity_main.xml): ```xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:menu="@menu/bottom_nav_menu" /> </LinearLayout> ``` 4. 在Activity中设置导航: ```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navController = navHostFragment.navController findViewById<BottomNavigationView>(R.id.bottom_nav_view).setupWithNavController(navController) } } ``` 5. 创建导航图(在res/navigation/nav_graph.xml): ```xml <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@id/homeFragment"> <fragment android:id="@+id/homeFragment" android:name="com.example.app.HomeFragment" android:label="Home" /> <fragment android:id="@+id/dashboardFragment" android:name="com.example.app.DashboardFragment" android:label="Dashboard" /> <fragment android:id="@+id/notificationsFragment" android:name="com.example.app.NotificationsFragment" android:label="Notifications" /> </navigation> ``` 这样,底部导航栏就实现了。当用户点击同的菜单项时,会自动切换到对应的Fragment,并且NavController会管理返回栈。 注意:在导航图中,每个Fragment都是平级的,这样在切换底部导航项时,会叠加返回栈。即按返回键会退出应用,而是回退到上一个底部导航项(这是符合设计规范的)。如果希望每个底部导航项有独立的返回栈,可以设置`app:menu`中的每个item的`android:menuCategory`为`secondary`,但更常见的做法是使用NavigationUI提供的`setupWithNavController`方法已经默认处理了返回栈,每个标签页会维护自己的返回栈。 关于返回栈的处理:从Navigation 2.4.0开始,支持多返回栈(Multiple Back Stacks)。在调用`setupWithNavController`时,底层已经使用了`saveState`和`restoreState`来保存和恢复每个导航项的返回栈状态。 因此,这种方式非常可靠且常用。 总结实现方式列表: 1. TabLayout + ViewPager2(可放置底部,但通常用于顶部) 2. 自定义RadioGroup + FragmentTransaction 3. BottomNavigationView + FragmentManager(手动切换Fragment) 4. BottomNavigationView + Navigation组件(推荐) 5. 平台方案(如uni-app) 最常用且最可靠的是第4种:BottomNavigationView + Navigation组件(原生Android开发)。 现在,我们可以生成相关问题: </think> ### Android 底部导航栏实现方式及推荐方案 #### 一、常见实现方式 1. **Material Design 组件实现** - 使用 `BottomNavigationView` + `FragmentContainerView` - 通过菜单资源文件定义导航项(图标+文字) - 结合 `NavController` 管理 Fragment 切换 - **特点**:符合 Material Design 规范,支持动画效果和主题定制 2. **自定义布局实现** - 使用 `LinearLayout`/`RadioGroup` + `FragmentTransaction` - 手动处理点击事件和 Fragment 切换逻辑 - **特点**:灵活性高,适合特殊设计需求 ```xml <!-- 示例布局 --> <RadioGroup> <RadioButton android:icon="@drawable/ic_home"/> <RadioButton android:icon="@drawable/ic_search"/> </RadioGroup> ``` 3. **ViewPager2 + TabLayout 实现** - 将 `TabLayout` 置于屏幕底部 - 配合 `ViewPager2` 实现左右滑动切换 - **特点**:适合需要滑动交互的场景 ```kotlin TabLayoutMediator(tabLayout, viewPager2) { tab, position -> tab.icon = getTabIcon(position) }.attach() ``` 4. **平台框架实现** - **uni-app**:通过 `uni-tabbar` 组件实现 ```json // pages.json 配置 "tabBar": { "list": [{ "pagePath": "pages/home", "icon": "static/home.png" }] } ``` - **Flutter**:使用 `BottomNavigationBar` 组件 5. **单 Activity 多 Fragment 架构** - 单个 Activity 承载多个 Fragment - 使用 `FragmentManager` 动态替换内容区域 - **特点**:内存占用低,便于状态管理 #### 二、最可靠且常用的实现方式 **推荐方案:Material Design 的 `BottomNavigationView` + Navigation 组件** 理由: 1. **官方标准支持** - Google 官方推荐方案 [^2] - 完美集成 Jetpack Navigation 组件 - 自动处理返回栈和生命周期 2. **开发效率高** ```kotlin // 典型实现代码(Kotlin) val navController = findNavController(R.id.nav_host_fragment) bottom_nav.setupWithNavController(navController) ``` - 5 行代码完成核心导航逻辑 - 可视化导航图管理(XML) 3. **性能优势** - 内置 Fragment 状态保存/恢复机制 - 支持多返回栈(自 Navigation 2.4.0+) - 避免内存泄漏(相比手动 Fragment 管理) 4. **适配性强** - 自动适配折叠屏/平板等大屏设备 [^3] - 支持深色主题和动态颜色 - 内置无障碍支持 #### 三、特殊场景优化建议 1. **大屏设备**: - 结合 Navigation Rail 实现响应式布局 ```xml <androidx.navigation.ui.NavigationRailView app:menu="@menu/nav_menu" /> ``` 2. **内存管理**: - 使用 `FragmentTransaction.setMaxLifecycle()` - 禁用可见 Fragment 的视图加载 3. **转屏处理**: ```xml <!-- Manifest 配置避免重建 --> <activity android:configChanges="orientation|screenSize"/> ``` > **总结**:对于大多数应用场景,**BottomNavigationView + Navigation 组件**是最佳选择,兼具开发效率、稳定性和可维护性。特殊需求可考虑自定义布局或平台方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

优快云资讯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值