Android价值十亿美元的错误

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

十亿美元的错误的故事

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

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

Tony Hoare是一位编程英雄

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

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

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

你认为他是唯一 一个犯了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的工程师投入了很多时间和精力以实现自动检测这些问题。

点击查看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)

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/OzGN1geQ20ol5bNMWhr5
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券