Android 价值十亿美元的错误

阅读数:1827 2019 年 11 月 6 日 18:36

Android价值十亿美元的错误

Android价值十亿美元的错误

这是一篇关于 Android 价值十几亿美元级错误的文章,包含那些被假设的错误和没有说出来的错误,本文还讨论了不要用糟糕的文档误导新开发人员的重要性。

十亿美元的错误的故事

你有没有听过价值数十亿美元的错误的故事? 下面就是一个很好的例子:

我把它称为我的价值十几亿美元的错误。这是关于 1965 年 null 引用的发明。那时,我正在用面向对象语言 (ALGOL W) 设计第一个全面的引用类型系统,我的目标是确保所有引用的使用都是绝对安全的,由编译器自动执行检查。但是我无法抗拒放入 null 引用的诱惑,因为它太容易实现了。这导致了无数的错误、漏洞和系统崩溃,在过去的 40 年里可能造成了十亿美元的麻烦和损失。

2009 年,Tony Hoare 在伦敦 QCon
https://en.wikipedia.org/wiki/Tony_Hoare

Tony Hoare 是一位编程英雄

如果你和我一样,当你第一次听到这句话的时候,你的反应是:“哇哦,我也犯了很多错误,但通常不会导致那么多钱的损失!”

最近我对此有了更深入的思考,现在我认为 Tony Hoare 是一个伟大的编程英雄!这不仅是因为他在这个 10 亿美元的错误之外所做的一切令人印象深刻的工作。

Android价值十亿美元的错误

我认为,因为他公开承认了它的“错误”,这也使他变得更伟大了!

你认为他是唯一 一个犯了 10 亿美元级错误的程序员吗? 仔细思考一下,IT 行业规模庞大,Facebook、谷歌、亚马逊、苹果、微软的市值在 5000 亿到 1 万亿美元之间。任何使其估值缩水 0.2% 的编程错误都可以被定义为数十亿美元的错误。

Tony Hoare 被称为“犯下数十亿美元错误的人”的真正原因是,他明确而公开地将自己的决定描述为一个错误,并通过这样做发出了一个明确的信号,即事情必须改变。

这样做我的朋友们会为软件行业带来巨大的利益,这就是为什么 Kotlin 和其他编程语言在它们的类型系统中构建了 null-safety 。它们仍然有 null,这本身不是问题,但它集成在类型系统中,以确保所有引用都是绝对安全的,由编译器自动执行检查。

Tony Hoare 是一个真正的好人,一个不以自我为中心的程序员,他敢于为一个错误承担责任,让我们充分认知错误的严重性,我们都应该感谢他。

回到 Android 世界,事情有点不同。在深入研究问题之前,我们将从一个简单的示例开始。

Android 匈牙利标记法

在 Android 刚开始的 9 年里,世界上大多数 Android 的代码都经受着无意义的变量匈牙利标记法的困扰。

它的缺点是对 Android Studio 中简单的代码高亮显示规则没有任何好处,并且明显的缺点是使所有东西的可读性降低。

当你在 2019 年之前提出这个问题时,你通常会得到以下两个答案:

  • 这就是现状,所以是好的。
  • Android 团队的要求,如果您在 Android 开源项目中贡献代码,就必须遵循这个约定。

但实际上

  • 第一个答案是错误的。我们之所以这样讲是因为自从匈牙利记数法被废除后,再没有什么人想要使用它了。
  • 第二个答案更糟糕,它属于“不偏不倚”的类别。这种说法基本上是说其他人都错了。我们明显可以反问:为什么? 因为每个人都在学习 Android 文档和示例,关于这个约定到处都是。这正是在开始时没有完善的一致性约定工作,而这恰巧导致了一个有害的规则。

是什么扼杀了 2019 年 5 月的匈牙利标记? 不是对错误的改正,而是因为要引入 Kotlin。为什么我们要等这么久?

Android 的十亿美元的错误

其实这涉及到很多方面:最大的错误是关于延至今日的 Android 编程的教授方式,它对编程实践有很大的破坏力,早期决策的短视是这一切混乱的根源。 我们应该认识到这个错误,并向每个人发出警告,让他们停止走这条路。但首先,我需要处理一些反馈,我得到的反馈是,将 Android 所做的事情贴上“错误”的标签太苛刻了。Android 不是我们这个时代最大的成功之一吗?

定义“错误”这个词

Android 显然是一个巨大的商业成功,我并不是说要反对这个。Android 和 iPhone 成功地在智能手机领域形成了双重垄断,因此即便有什么肯定也不是战术上的“错误”。我们必须使用 Android 团队提供的任何工具。

我也认为从用户的角度来看 Android 是一个很好的操作系统。你可以更多地喜欢 iOS,我对此也没什么意见,但这并不会让 Android 变差。

在本文的上下文中,错误意味着在一条会给开发人员带来痛苦的道路上误导他们。

我也不是说这是 Android SDK 中唯一的大错误,也不一定是 Android SDK 中最重要的错误。

如果你想了解 Android 的缺点,#androiddev Reddit 社区整理了一个非常有用的列表,列出了 Android 的缺点。但这里我要关注一个有趣的基础性错误。

Android 墨西哥卷设计模式

关于 Android 一件令人悲伤的事是,官方 Android 样例都采用以色列费雷尔卡马乔称为 Android 墨西哥卷的设计模式:将一切包装成一个 GodActivity 或 GodFragment,然后一切基于此完成。

官方的 camera-samples 就是一个很好的例子。不幸的是,我不能在这里展示它,因为它比我的文章长的多,但看看他的结构就可以了:

复制代码
public inline fun needsRefactoring(): Nothing = throw NotImplementedError("""
This does too much and needs to be refactored.
Don't put any kind of logic in the Activities and Fragments.
""".trimIndent())
class Camera2BasicFragment : Fragment(), View.OnClickListener,
ActivityCompat.OnRequestPermissionsResultCallback {
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) = needsRefactoring()
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) = needsRefactoring()
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) = needsRefactoring()
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) = needsRefactoring()
}
private lateinit var cameraId: String
private lateinit var textureView: AutoFitTextureView
private var captureSession: CameraCaptureSession? = null
private var cameraDevice: CameraDevice? = null
private lateinit var previewSize: Size
private val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(cameraDevice: CameraDevice) = needsRefactoring()
override fun onDisconnected(cameraDevice: CameraDevice) = needsRefactoring()
override fun onError(cameraDevice: CameraDevice, error: Int) = needsRefactoring()
}
private var backgroundThread: HandlerThread? = null
private var backgroundHandler: Handler? = null
private var imageReader: ImageReader? = null
private lateinit var file: File
private val onImageAvailableListener: ImageReader.OnImageAvailableListener = needsRefactoring()
private lateinit var previewRequestBuilder: CaptureRequest.Builder
private lateinit var previewRequest: CaptureRequest
private var state = STATE_PREVIEW
private val cameraOpenCloseLock = Semaphore(1)
private var flashSupported = false
private var sensorOrientation = 0
private val captureCallback = object : CameraCaptureSession.CaptureCallback() {
private fun process(result: CaptureResult): Unit = needsRefactoring()
private fun capturePicture(result: CaptureResult): Unit = needsRefactoring()
override fun onCaptureProgressed(session: CameraCaptureSession, request: CaptureRequest, partialResult: CaptureResult) = needsRefactoring()
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) = needsRefactoring()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = needsRefactoring()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = needsRefactoring()
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
needsRefactoring()
}
override fun onResume() {
super.onResume()
needsRefactoring()
}
override fun onPause() {
super.onPause()
needsRefactoring()
}
private fun requestCameraPermission(): Unit = needsRefactoring()
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Unit = needsRefactoring()
private fun setUpCameraOutputs(width: Int, height: Int): Unit = needsRefactoring()
private fun areDimensionsSwapped(displayRotation: Int): Boolean = needsRefactoring()
private fun openCamera(width: Int, height: Int): Unit = needsRefactoring()
private fun closeCamera(): Unit = needsRefactoring()
private fun startBackgroundThread(): Unit = needsRefactoring()
private fun stopBackgroundThread(): Unit = needsRefactoring()
private fun createCameraPreviewSession(): Unit = needsRefactoring()
private fun configureTransform(viewWidth: Int, viewHeight: Int): Unit = needsRefactoring()
private fun lockFocus(): Unit = needsRefactoring()
private fun runPrecaptureSequence(): Unit = needsRefactoring()
private fun captureStillPicture(): Unit = needsRefactoring()
private fun unlockFocus(): Unit = needsRefactoring()
override fun onClick(view: View): Unit = needsRefactoring()
private fun setAutoFlash(requestBuilder: CaptureRequest.Builder): Unit = needsRefactoring()
companion object {
init {
needsRefactoring()
}
private val ORIENTATIONS = SparseIntArray()
private val FRAGMENT_DIALOG = "dialog"
private val TAG = "Camera2BasicFragment"
private val STATE_PREVIEW = 0
private val STATE_WAITING_LOCK = 1
private val STATE_WAITING_PRECAPTURE = 2
private val STATE_WAITING_NON_PRECAPTURE = 3
private val STATE_PICTURE_TAKEN = 4
private val MAX_PREVIEW_WIDTH = 1920
private val MAX_PREVIEW_HEIGHT = 1080
@JvmStatic
private fun chooseOptimalSize(
choices: Array<Size>, textureViewWidth: Int, textureViewHeight: Int,
maxWidth: Int, maxHeight: Int, aspectRatio: Size): Size = needsRefactoring()
@JvmStatic
fun newInstance(): Camera2BasicFragment = needsRefactoring()
}
}

到这里看全部信息 android / camera-samples / Camera2BasicFragment.kt

Activity 中有一个新活动事件后前一个事件都会被挤到后台。直到现在,官方 Android 文档和示例都是这样做的。

如果你遵循安卓的墨西哥卷设计模式,会出现什么问题呢?

崩溃

Activity 是一种特殊的环境,充满了随时可能爆炸的地雷。最明显的问题是,由于这个复杂的生命周期,您的 Activity 可能在任何时候被系统终止。使用具有简单生命周期 (如 Application) 的上下文要安全得多。

内存泄漏

Activity 是绑定到整个用户界面的高消费对象。依附于 Activity 对象很容易产生很多麻烦。随之而来的是内存泄漏。实际上,这是一个非常常见的陷阱,甚至在 Android SDK 本身的类中也会看到这个错误,不管是在一些糟糕的三星 fork 中,还是在 Android 开源项目本身中。这是一个如此常见的问题,以至于 square 的工程师投入了很多时间和精力以实现自动检测这些问题。

Android价值十亿美元的错误
点击查看 GitHub

大量遗留代码

遗留代码经常被用作一个模糊的术语,意思是“代码非常难以理解,以至于您害怕更改它”。Michael Feathers 的经典著作《有效地使用遗留代码》有一个更精确且具有操作性的定义:任何没有被单元测试自动覆盖的代码都可以被定义为遗留代码。

任何遵循 Android Burrito 设计模式的代码都可能立即成为遗留代码。

我一直想知道为什么官方的 Android 文档如此强调仪表化测试。

以我的经验,这些很难写,从根本上来说很慢——它们必须在 Android 设备上运行——最糟糕的是,当它们失败时,我们能从中获得的错误信息很少。

我采用了完全相反的方案,写了很多简单、快速、有侧重点的 JVM 测试用例,结果要好得多。事实上,谷歌的测试团队有一篇精彩的文章,解释了为什么端到端测试是一个好的方案却在实践中失败了:

不要再用端到端测试了
好的想法常常在实践中失败,在测试的世界中,一个公认的好测试方案也常常会在实践中失败,这就是建立在端到端测试基础上的测试策略。
[请阅读整篇文章,非常好]
谷歌测试博客:不要再用端到端测试了

所以仪表化的 Android 测试不是一个好方案。

但老实说,如果你把你的逻辑放到 Android 组件里,你能做的就只有这些了。

检验墨西哥卷的唯一方法就是品尝。

回想起来,Android 墨西哥卷的设计模式明显是错误的,这让我很好奇:它从何而来,又是如何存活到今天的?

安卓墨西哥卷的设计模式是如何形成的?

一些 Context 组件

给你一些 Context 组件,这是 Android SDK 1.0 的两个最基本的构件:

  • android.content.Context 提供对有关应用程序环境的所有全局信息的访问。它允许访问特定于应用程序的资源和类,以及应用程序级操作的向上调用,如启动活动、广播和接收意图等。
  • android.app.Activity 为一个应用程序提供了一个 main() 函数,但是添加了很多移动操作系统需要的功能,最重要的是一个复杂的 Activity 生命周期

Activity 就是一个 Context

在 Android 1.0 有一个致命的错误

复制代码
package android.app;
import android.content.Context;
class Activity extends Context { }

首先讲一点理论知识。
继承和组合

在你的面向对象编程课程中,你可能记得对象之间有两种非常不同的关系:

  • 继承:房子是建筑的一种
  • 组合:一所房子有一个房间

比起继承,组合更受大家的喜欢,它也是一个众所周知的设计原则,在一些有影响力的书中也提到过。

Android 只是另一种 SDK(软件开发工具包),但可能有一个原因,原则不适用这里? 我知道事实并非如此,因为……

Fragment 不是上下文

如果你看看来自 Android SDK 的另一个构建块 androidx.app.Fragment ,它与 Activity 非常相似,但在后面才被引入,你应该注意到它并没有扩展 Context。但 Fragment 具有上下文。

那么,为什么 Android 团队改变了主意,尽管并没有大肆宣扬?

在 Android 中一切都需要上下文

你可以也应该避免墨西哥卷的设计模式。但你不能逃避的是,在 Android 中,你需要一个上下文来做基本上所有的事情:

复制代码
class SomeThirdPartyClass {
fun doStuff(contex: Context) = TODO()
}

但即使是这个比较传统的 SomeThirdPartyClass 类也是一个随时可能爆炸的地雷。
Activity 是一个 Context,所以很容易将 this@Activity 作为参数传递给 doStuff()。但是这样做是错误的,你不能确定某个 SomeThirdPartyClass 正在做正确的事情,或者你正在做正确的事情。崩溃、内存泄漏和不可测试性将接踵而至。

现在的文档和示例仍然很差

我想指出的是,我说的不仅仅是一个历史性的短视决策。

2014 年,我还是一名年轻且经验不足的 Android 开发人员,周围也是一群年轻且经验不足的 Android 开发人员。我们试图了解 Android 是如何工作的,并使用 Android 文档和示例作为指导。回想起来,这是一个可怕的错误。我们最终得到了一个难以理解、难以测试、甚至难以修改的痛苦的烂摊子。不是因为我们没有遵循“Android 最佳实践”,而是因为我们恰恰遵循了!

快进到今天,虽然在许多领域都取得了进展,但 Android 官方文档和示例的很大一部分仍然编写得很糟糕。它继续误导新一代缺乏经验的开发人员。正如 Bob 叔叔会告诉你的那样,大多数开发人员都是新手,因为 IT 行业的规模每五年就会扩大一倍。

我知道,对于某一学派来说,所有这些都是公平的游戏。“这些错误是愚蠢的,我是一个真正的程序员,不会上当。但你总不能阻止蠢人变蠢吧?”

但是我来自为人类设计的思想学派,所以在我看来,当一个程序员犯了一个错误,那是程序员的错,但是当超过十年,成千上万的程序员犯了同样的错误,那就是设计师没有做好工作。理想情况下,做正确的事情应该是容易的,且不容易搬起石头砸自己的脚。

所以,现在是时候明确地指出,墨西哥卷的 Activity 和 Fragment 是不可接受的。修复文档和示例也已经是早就该做的事情了。

错误已经犯下

我确实理解,尽管这些错误在今天令人痛苦,但它们是在特定的历史背景下犯下的。Android 项目不得不做出一些改变或者变得无关紧要,这是一个不同的领域,那时智能手机的功能还没有今天这么强大。

这和 JavaScript 是一样的。它的设计在短短十天内就完成了,然后在 Netscape Navigator 1.0 中发布,其中很多设计都成为历史了。

这并不是说没有解决办法可以解决这类历史错误。聪明人一旦痛苦地意识到问题所在,通常会很快找到解决方案。这正是托尼•霍尔 (Tony Hoare) 无私诚实的伟大之处:它立刻让人们意识到,这里有一个问题需要解决。这正是当今 Android 世界所缺乏的。直到现在,官方的 Android 文档仍然继续使用 Android 墨西哥卷设计模式。

请允许我引用 Tony Hoare 的话作为结尾:

这导致了无数的错误、漏洞和系统崩溃,在过去的十年中已经造成了价值数十亿美元的麻烦和损失。

原文链接
Android’s billion-dollar mistake(s)

评论

发布