Android 的十亿美元错误 十亿美元错误引言 Android 的十亿美元错误 💣 LeakCanary 🐤 Android 的 Burrito 设计模式是怎么诞生的?它的文档和示例至今仍然很糟糕 GenAI LIVE!| 2025 年 6 月 4 日

2025-06-09

Android 的十亿美元错误

十亿美元的错误引言

Android 的十亿美元错误

LeakCanary🐤

Android Burrito 设计模式是如何诞生的?

文档和示例今天仍然很糟糕

GenAI LIVE! | 2025年6月4日

这篇文章讨论了价值数十亿美元的错误,包括那些被认为是错误的、那些没有说出来的错误,以及不要用糟糕的文档误导新开发人员的重要性

十亿美元的错误引言

你听说过那句“十亿美元错误”的名言吗?这句名言很棒:

我称之为“十亿美元级的错误”。它指的是1965年空引用的发明。当时,我正在设计面向对象语言(ALGOL W)中第一个全面的引用类型系统。我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但我无法抗拒引入空引用的诱惑,仅仅因为它太容易实现了。这导致了无数的错误、漏洞和系统崩溃,在过去的四十年里,这些错误和崩溃可能造成了数十亿美元的损失。Tony
Hoare 在 2009 年伦敦 QCon 大会上发表讲话
https://en.wikipedia.org/wiki/Tony_Hoare

Tony Hoare 是一位编程英雄🦸

如果您像我一样,那么当您第一次听到这句话时的反应是:“哇。我也犯了很多错误,但通常不会花费太多钱!”。

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

托尼·霍尔

不,我的主张是,他公开承认它的“错误”也是很伟大的!

你以为他是唯一一个犯下十亿美元错误的程序员吗?再想想。IT行业规模庞大。Facebook、谷歌、亚马逊、苹果、微软的市值都在5000亿美元到1万亿美元之间。任何导致估值下跌0.2%的编程错误,都算作十亿美元的错误。

不,托尼·霍尔被称为“十亿美元错误的人”的真正原因是,他清楚而公开地将自己的决定描述为一个错误,并通过这样做发出了一个明确的信号:事情必须改变

朋友们,这对软件行业大有裨益,这也是 Kotlin 和其他编程语言在其类型系统中内置空安全的原因。它们仍然保留空null安全,这本身并没有问题,但它集成在类型系统中,以确保所有引用都绝对安全,并由编译器自动执行检查

托尼·霍尔 (Tony Hoare) 是真正的好人,他是一位没有自我的程序员,他承担了错误的责任,以便我们能够认识到它,我们都应该感谢他。

回到 Android 的世界,情况有些不同。在深入探讨之前,我们先从一个简单的例子来说明问题。

sAndroid 匈牙利标记法

在 Android 诞生后的前 9 年里,世界上大多数 sAndroid mCodebases 都受到匈牙利命名法 (Hungarian mNotation) 中毫无意义的 mVariant 的困扰。

它的缺点是与 Android Studio 中现有的简单代码高亮规则相比没有任何好处,而且明显的缺点是使所有内容的可读性降低。

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

  • 这是现状,所以是好的。
  • 我们 Android 团队刚才说过,如果你在 Android 开源项目中贡献代码,就必须遵循这个惯例。

但实际上

  • 第一个答案是错的。我们知道这一点,是因为自从匈牙利命名法消亡以来,并没有人强烈要求恢复它。
  • 第二个答案更糟糕,它属于“甚至不算错”的范畴。其本质上是在说其他人都错了。那么显而易见的问题是:为什么?因为每个人都在学习 Android 文档和示例,而这个约定无处不在。这正是你为了创建一个约定而应该付出的艰苦而持续的努力。它恰好是一个有害的约定。

是什么在 2019 年 5 月扼杀了 mHungarian 表示法?不是因为承认错误,而是因为引入了 Kotlin。为什么我们要等这么久?

Android 的十亿美元错误

我们有很多内容要讨论:迄今为止,Android 编程教学方式的巨大错误,它在实践中造成的损害,造成混乱的根源——早期的短视决策,以及认识到错误的好处,以此提醒大家不要再走这条路。但首先,我需要回应一些反馈,我收到的反馈是,将 Android 的某些行为定性为“错误”过于苛刻。Android 难道不是我们这个时代最大的成功之一吗?

定义“错误”一词

Android 显然取得了巨大的商业成功,我并非持相反观点。Android 和 iPhone 已经成功在智能手机领域形成了双重垄断,因此接下来的局面或许也并非战术上的“失误”。无论如何,我们都必须使用 Android 团队提供的任何工具。

我也认为从用户的角度来看,Android 是一款不错的操作系统。你可以更喜欢 iOS,我对此也挺满意,但这并不意味着 Android 不好。

Context本文中,错误的具体含义是误导开发人员走上一条会给他们带来痛苦和折磨的道路。

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

如果你想了解 Android 的缺陷,#androiddev Reddit 社区整理了一份非常有用的清单,列出了他们认为 Android 的缺陷。但在这里,我将重点讨论一个有趣的基本错误。

Android Burrito 设计模式 🌯

Android 的一个遗憾是,官方的 Android 示例遵循了 Israel Ferrer Camacho 所说的Android Burrito 设计模式:将所有内容包装成 墨西哥卷饼🌯可以完成所有事情的and GodActivity/or 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()
}
}
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 官方文档和示例至今仍在做的事情。

如果遵循 Android Burrito 设计模式,会出现什么问题?

崩溃

Activity是一种特殊的 Context,充满了随时可能爆炸的“地雷”。最明显的问题是,由于其复杂的生命周期,你的 ContextActivity可能随时被系统终止。使用生命Context周期更简单的 Context 会更安全,例如Application

内存泄漏

Activity是一个与整个用户界面绑定的昂贵对象。很容易陷入持有 Activity 对象的陷阱。随之而来的是内存泄漏。事实上,这是一个非常常见的陷阱,即使在 Android SDK 本身的类中,你也会看到这个错误,无论是在一些糟糕的三星分支中,还是在 Android 开源项目本身中。这是一个非常常见的问题,以至于 Square 的优秀员工投入了大量的时间和精力来自动检测这些问题。

GitHub 徽标 方形/ leakcanary

Android 的内存泄漏检测库。

LeakCanary🐤

Android 的内存泄漏检测库。

🙏 如果您喜欢 LeakCanary,您可以通过为此存储库加星标 ⭐ 来表示支持。

执照

Copyright 2015 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.



即时遗留代码

Legacy code遗留代码通常被用作一个模糊的术语,意为“难以理解,以至于你不敢修改的代码”。Michael Feathers 的经典著作《有效处理遗留代码》对此有一个更精确、更具操作性的定义:任何未被单元测试自动覆盖的代码都属于遗留代码。

任何遵循 Android Burrito 设计模式的代码都会立即被视为遗留代码。

我一直想知道为什么 Android 官方文档如此重视仪器测试。

根据我的经验,这些都很难编写,从根本上来说很慢 - 它们必须在 Android 设备上运行 - 最糟糕的是,当它们失败时,它们通常不会告诉你什么。

我反其道而行之,编写了大量简单、快速、专注的 JVM 测试,结果却好得多。事实上,Google 测试团队有一篇精彩的文章,解释了为什么端到端测试看似好主意,却在实践中失败了:

拒绝更多的端到端测试

好的想法在实践中往往会失败,而在测试领域,一个普遍存在的好想法在实践中往往会失败,那就是围绕端到端测试构建的测试策略。

[请阅读全文,非常好]

Google 测试博客:拒绝更多端到端测试

因此,对 Android 进行仪表化测试并不是一个好主意。

但老实说,如果你将逻辑放入 Android 组件中,那么你所能做的几乎就这些了。

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

回想起来,Android Burrito 设计模式显然是错误的,这让我很好奇:它从何而来,又是如何存活至今的?

Android Burrito 设计模式是如何诞生的?

一些Context📚

以下Context是 Android SDK 1.0 的两个最基本的构建块:

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

活动即情境

这是 Android 1.0 中发生的致命错误

package android.app;

import android.content.Context;

class Activity extends Context { }
Enter fullscreen mode Exit fullscreen mode

但首先要讲一点理论。

继承优于组合

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

  • 继承:房屋就是建筑
  • 作文:一所房子有一个房间

优先使用组合而不是继承是一个众所周知的设计原则,在以下有影响力的书籍中都有阐述:

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

片段!是上下文🤔

如果你看一下androidx.app.Fragment(Android SDK 中另一个与 Activity 非常相似但后来引入的构建块),你会发现它并没有扩展 Context。相反,Fragment本身就有一个Context。

那么,为什么 Android 团队会改变主意呢?尽管是悄无声息的。

Android 中的一切都需要 Context

你可以也应该避免使用 Burrito 设计模式。但你无法逃避的是,在 Android 中,你基本上需要一个 Context 来完成所有事情:

class SomeThirdPartyClass {
    fun doStuff(contex: Context) = TODO()
}
Enter fullscreen mode Exit fullscreen mode

但即使是这个平庸的SomeThirdPartyClass阶层也是一座随时可能爆炸的地雷。

Activity本身就是一个Context,因此很容易将其this@Activity作为参数传递给doStuff()。但这样做是错误的,你无法确定 ContextSomeThirdPartyClass做的是否正确,或者你做的是否正确。这样会导致崩溃、内存泄漏以及不可测试性。

文档和示例今天仍然很糟糕

我想指出的是,我所谈论的不仅仅是一个历史性的短视决定。

2014年,我当时还是一个年轻、缺乏经验的Android开发者,团队里也都是些年轻、缺乏经验的Android开发者。我们当时试图学习这些事情的运作方式,并使用Android文档和示例作为蓝图。现在回想起来,这是一个可怕的错误。最终,我们陷入了难以理解、难以测试、甚至更难以修改的困境。这并不是因为我们没有遵循“Android最佳实践”,而是因为我们遵循了!

如今,尽管 Android 在许多领域都取得了进展,但官方文档和示例的很大一部分仍然写得很糟糕。这继续误导着新一代缺乏经验的开发者。正如 Bob 大叔所说,由于 IT 行业规模每五年翻一番,大多数开发者都是新手。

我知道,对某些学派来说,所有这些都是公平的。“那些错误很愚蠢,我是个真正的程序员,不会犯这种错误。你又没法阻止愚蠢的人继续犯愚蠢的错误,不是吗?”

但我秉持“以人为本”的设计理念,所以在我看来,一个程序员犯错是程序员的错,但如果十多年来,成千上万的程序员犯了同样的错误,那就是设计师没做好。理想情况下,做正确的事情应该很容易,而搬起石头砸自己的脚应该很难。

所以现在是时候明确声明 Burrito 的 Activity 和 Fragments 是不可接受的了。修复文档和示例也早就该这么做了。

“犯了错误”😔

我理解,虽然这些错误在今天看来令人痛心,但它们是在特定的历史背景下犯下的。Android项目必须发布一些东西,否则就会变得无关紧要,毕竟当时的智能手机功能远不如今天强大,所以当时的领域完全不同。

JavaScript 的故事也一样。它的设计在短短十天内就匆匆完成,然后就随Netscape Navigator 1.0一起发布,剩下的就成为历史了。

并非没有解决方案可以弥补这类历史性错误。聪明人一旦深刻地意识到问题所在,通常就能迅速找到解决方案。而这正是 Tony Hoare 坦诚直率的伟大之处:它能立即让人们意识到这里存在一个亟待解决的问题。这正是当今 Android 世界所缺乏的。Android 官方文档至今仍在用精彩的 Android Burrito 设计模式来敷衍了事。

请允许我引用 Tony Hoare 的话来结束我的演讲:

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

鏂囩珷鏉ユ簮锛�https://dev.to/jmfayard/android-s-billion-dollar-mistake-327b
PREV
GitHub Actions:YAML 编程荒原中的新希望
NEXT
CRUD 中没有 U