Android 开发最佳实践全指南:从 Gradle 构建到资源规范与 ProGuard 混淆的 Futurice 实战经验

原创2026-09-30 23:48:501,209 阅读
文章标签:文档教程移动开发

Android 开发最佳实践全指南:从 Gradle 构建到资源规范与 ProGuard 混淆的 Futurice 实战经验

本文基于本仓库中的法语版官方指南 translations/French/README.fr.md 编译整理,并对照仓库根目录的英文原版 README.md 进行补充印证。这是一份由 Futurice 公司 Android 开发者多年实战沉淀的"做与不做"清单,覆盖构建系统、工程结构、依赖选型、Activity/Fragment 架构、资源文件组织、模拟器与代码混淆等完整链路。读完本文,你将掌握一套可直接落地的 Android 工程化规范:如何配置 Gradle 与签名、如何组织包结构与 XML 资源、如何规避 65k 方法数与深层视图层级等经典陷阱,以及如何用 ProGuard 安全地完成发布包瘦身与混淆。

本指南源自 Futurice 公司 Android 开发者们的实战经验总结,核心宗旨是"不要重复发明轮子"(Avoid reinventing the wheel)。原文档同时提供了中文、日文、韩文、西班牙文、法文、德文等多种翻译版本(见 translations 目录),本文以法语版为骨架展开,并吸收英文原版中的最新建议作为补充。

一、Android SDK:安装位置与权限考量

把 Android SDK 安装在用户主目录(home)或其它与应用无关的独立位置,这是第一条环境层面的最佳实践。

很多 IDE 在安装时会自带 SDK,并把它放到与 IDE 相同的目录下。这会带来两个隐患:

  • 当你需要升级或重新安装 IDE 时,SDK 可能随之一并丢失,被迫重新下载;
  • 当你更换 IDE 时,SDK 与旧 IDE 深度绑定,迁移成本很高。

此外,还要避免把 SDK 放在系统级目录中——如果你的 IDE 不是以管理员(root)身份运行,放在需要管理员权限的目录里会引发持续的权限问题。正确的做法是让 SDK 独立于任何 IDE 存在,这样无论工具链如何更换,SDK 本身都能保持稳定。

二、构建系统:以 Gradle 为默认方案

项目的默认构建系统应当是 Gradle(配合 Google 的 Android Gradle 插件)。原文档明确指出:Ant 构建系统功能受限且写法繁琐("Ant est bien plus limité et aussi plus verbeux"),而 Gradle 可以轻松实现:

  • 构建应用的不同 flavors(风味变体);
  • 编写实现自定义任务的脚本;
  • 管理和下载依赖;
  • 自定义签名密钥(Store Keys);
  • 以及更多能力。

同时,Google 正在积极开发 Android Gradle 插件,其目标是成为新一代构建系统的标准。英文原版 README.md 还补充了一条关键原则:应用的构建过程必须由 Gradle 文件定义,而不是依赖 IDE 特有的配置。这样能保证不同工具之间构建结果一致,并为持续集成(CI)系统提供良好支持。

三、工程结构:从旧结构迁移到新结构

Android 工程存在两种流行的结构:旧式 Ant & Eclipse ADT 结构与新式 Gradle & Android Studio 结构。官方建议直接采用新结构;如果项目还在用旧结构,应当尽早迁移。

旧结构

ancienne-structure(旧结构)
├─ assets
├─ libs
├─ res
├─ src
│  └─ com/futurice/project
├─ AndroidManifest.xml
├─ build.gradle
├─ project.properties
└─ proguard-rules.pro

新结构

nouvelle-structure(新结构)
├─ library-foobar
├─ app
│  ├─ libs
│  ├─ src
│  │  ├─ androidTest
│  │  │  └─ java
│  │  │     └─ com/futurice/project
│  │  └─ main
│  │     ├─ java
│  │     │  └─ com/futurice/project
│  │     ├─ res
│  │     └─ AndroidManifest.xml
│  ├─ build.gradle
│  └─ proguard-rules.pro
├─ build.gradle
└─ settings.gradle

两种结构最核心的区别在于:新结构显式地区分了 source sets(源码集,如 main、androidTest),这是 Gradle 的重要概念。借助源码集,你可以轻松扩展出 payant(付费版)与 gratuit(免费版)等不同的源码目录,让同一套代码工程同时承载多个产品变体。

同时,新结构中的顶层 app 目录可以清晰地把你的主应用与其它被引用的库(例如 library-foobar)区分开来;settings.gradle 记录这些库的引用,之后在 app/build.gradle 中即可直接引用它们。英文原版 README.md 补充强调:尽管 Gradle 对工程结构提供了很大的灵活性,但除非有充分理由,否则应当接受其默认结构,这能简化构建脚本。

四、Gradle 配置实战

4.1 总体结构

总体配置遵循 Google 官方的 "Gradle for Android" 指南。英文原版 README.md 额外建议将 minSdkVersion 设为 21(Android 5.0),理由是:部分 Material Design 特性仅在 API 21 及以上可用,且从 API 21 起不再需要 multidex 支持库。当然,设定最低版本前应参考 Android 版本使用情况统计,并注意这些是全球统计值,针对特定区域/人群市场时可能不同。

4.2 小任务:用 Gradle 任务替代外部脚本

与其为构建流程编写 shell、Python、Perl 等外部脚本,不如把这些逻辑写成 Gradle 任务。这样构建流程完全由 Gradle 文件定义,不依赖外部环境,参考 Gradle 官方文档即可掌握任务编写方法。

4.3 签名密码:永远放进 gradle.properties

发布(Release)构建需要在 build.gradle 中定义 signingConfigs。以下写法必须避免——明文密码会进入版本控制系统(如 Git),造成严重的安全隐患:

// 不要这样做!密码会出现在版本控制系统中
signingConfigs {
    release {
        storeFile file("myapp.keystore")
        storePassword "password123"
        keyAlias "thekey"
        keyPassword "password789"
    }
}

正确做法:创建一个不应加入版本控制的 gradle.properties 文件:

KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789

Gradle 会自动导入这个文件,因此在 build.gradle 中可以这样引用,并加上健壮的异常处理:

signingConfigs {
    release {
        try {
            storeFile file("myapp.keystore")
            storePassword KEYSTORE_PASSWORD
            keyAlias "thekey"
            keyPassword KEY_PASSWORD
        }
        catch (ex) {
            throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
        }
    }
}

当密码变量缺失时,构建会立即抛出 InvalidUserDataException 并给出明确提示,而不是在发布阶段才暴露问题。gradle.properties 应加入 .gitignore,确保敏感信息永不入库。

4.4 优先使用 Maven 依赖解析,而非导入 jar 文件

如果显式把 .jar 文件放进项目,它们会被"冻结"在某个特定版本(例如 2.1.1);后续下载新版本、维护升级都是沉重且繁琐的工作,而 Maven 依赖解析已经优雅地解决了这个问题。例如:

dependencies {
    implementation 'com.squareup.okhttp:okhttp:2.2.0'
    implementation 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
}

4.5 避免 Maven 动态依赖版本

不要使用 2.1.+ 这类动态版本号:它可能导致构建不稳定,或在不同构建之间产生难以追踪的细微行为差异。使用 2.1.1 这样的静态版本号,才能创造更稳定、可预测、可复现的开发环境。

英文原版 README.md 还提供了两条值得借鉴的补充实践:

  • 为 debug 构建使用独立的包名:通过 applicationIdSuffix 给 debug 构建追加 .debug 后缀,并给 versionName 追加 -DEBUG 后缀,这样 debug 与 release 的 APK 可以同时安装在同一台设备上,对发布后的线上问题排查尤其有价值;同时可以为不同构建类型放置不同图标(默认结构下 debug 图标放在 app/src/debug/res,release 图标放在 app/src/release/res);
  • 共享 debug 签名 keystore:debug 密钥文件可以安全地放入代码仓库,便于团队成员在同一设备上测试时免于反复卸载/重装,也简化了 Facebook 等要求注册单一 keystore 哈希的 SDK 接入流程。

五、开发工具:IDE 与编辑器选择

你可以使用任何编辑器,前提是它必须兼容工程的 Gradle 结构与构建系统。工具选择是个人自由,但责任在自己——务必选择能配合工程结构和构建系统的工具。

当前最受推荐的 IDE 是 Android Studio:它由 Google 开发、与 Gradle 深度集成、默认使用新工程结构、已经稳定,并且是专门为 Android 开发设计的。英文原版补充道,Android Studio 还内置了大量监控与分析工具,并且 Google 在持续更新它。

如果你仍在使用 Eclipse ADT,需要知道它默认采用旧工程结构和 Ant 构建,因此必须自行配置,改走 Gradle 与命令行 adb 的路线。若 Gradle 集成在 Eclipse 中无法正常工作,可选方案是只用命令行构建,或者干脆迁移到 Android Studio——后者是更好的选择,因为 ADT 插件已经停止维护(deprecated)。

无论选择什么工具,请确保:使用新工程结构、使用 Gradle、并且不要把编辑器特有的配置文件(例如 Ant 的 build.xml)提交到版本控制系统。变更构建配置时记得同步更新 build.gradle。最后一条职场建议:善待团队中的其他开发者,不要强迫他们更换开发工具或偏好。

六、第三方库选型

6.1 JSON 解析:Jackson 与 Gson

Jackson 是一个能够在 Java 对象与 JSON 之间互相转换的库。它与 Gson 功能相似且同样流行,但 Futurice 团队认为 Jackson 更强大,因为它支持多种 JSON 处理方式:

  • 流式处理(streaming);
  • 内存树模型(tree model);
  • 传统的 JSON-POJO 数据绑定(data binding)。

不过 Jackson 的体积比 Gson 更大,在需要规避 65k 方法数限制的场景下,可能反而要选择更小的 Gson。其它可选方案还包括 Json-smart 与 Boon JSON。英文原版 README.md 还提到了 Square 出品的 Moshi——它建立在 Gson 的开发经验之上,并且与 Kotlin 集成良好。

6.2 网络、缓存与图片加载

做后端请求有多种经过实战验证的成熟方案,不要自己编写 HTTP 客户端:

  • Volley:Google 出品的网络库,还附带图片加载与缓存的辅助工具;
  • Retrofit:如果选择它,建议配合 Picasso 做图片加载与缓存、OkHttp 做高性能 HTTP 请求。Retrofit、Picasso、OkHttp 三件套出自同一家公司(Square),彼此互补、兼容性问题少;OkHttp 也可以与 Volley 结合使用。

英文原版 README.md 的推荐更聚焦:以 OkHttp 为基础做高效 HTTP 请求,用 Retrofit 提供类型安全(typesafe)的 API 层,图片加载除 Picasso 外也可考虑 Glide(支持 GIF 动画、圆形图片,性能声称更好,但方法数更多)。

6.3 RxJava:响应式编程的引入节奏

RxJava 是用于响应式编程(Reactive Programming)的库,本质上是处理异步事件。这是一个强大但有学习曲线的范式,官方建议在用它构建整个应用架构之前保持谨慎。Futurice 团队的部分项目已用 RxJava 实现,如果你需要帮助,可以联系文档中列出的团队成员(Timo Tuominen、Olli Salonen、Andre Medeiros 等),团队还撰写过多篇相关技术博客。

如果此前没有 Rx 经验,建议的引入路线是:

  1. 先只把它用在后端 API 响应的处理上;
  2. 或者先用于简单的 UI 事件,例如按钮点击、搜索框输入;
  3. 只有当你的 Rx 技能足够自信、并决定把它应用于整体架构时,才全面推进——但务必为所有难懂的部分编写 Javadoc。请记住:一个不熟悉 RxJava 的后来维护者会非常吃力,你的职责是尽量帮助他们理解代码与 Rx 范式。

英文原版还建议配套使用 RxAndroid(Android 线程调度支持)与 RxBinding(从现有 Android 组件便捷地创建 Observable)。

6.4 Retrolambda:在旧 JDK 上使用 Lambda 语法

Retrolambda 是用于在 Android 及其它 JDK8 之前平台上使用 Lambda 表达式语法的 Java 库。它能保持代码简洁易读,尤其适合配合 RxJava 这类函数式风格编程。

使用步骤:

  1. 安装 JDK8,并在 Android Studio 项目中将其设为默认 SDK;
  2. 配置环境变量 JAVA8_HOME 与 JAVA7_HOME;
  3. 在项目的 build.gradle 中添加插件依赖:
dependencies {
    classpath 'me.tatarka:gradle-retrolambda:2.4.1'
}
  1. 在每个模块的 build.gradle 中启用插件并配置:
apply plugin: 'retrolambda'

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

retrolambda {
    jdk System.getenv("JAVA8_HOME")
    oldJdk System.getenv("JAVA7_HOME")
    javaVersion JavaVersion.VERSION_1_7
}

Android Studio 本身也为 Java 8 lambda 提供了代码辅助支持。如果你是 lambda 新手,记住两条起步技巧:

  • 任何只有一个方法的接口都是"lambda 友好"的,可以被折叠成最紧凑的语法;
  • 如果对参数等细节拿不准,先写一个普通的匿名内部类,然后让 Android Studio 帮你把它折叠成 lambda。

需要说明的是:英文原版 README.md 已指出,从 Android Studio 3.0 起,Retrolambda 不再是必需品(官方已原生支持部分 Java 8 特性)。本文保留 Retrolambda 章节,因为它仍是老项目中的常见实践,且上述配置方式至今仍有参考价值。

6.5 警惕 65k 方法数限制,慎用 Guava

Android 应用打包成 Dex 文件后,存在一个硬性上限:65536 个被引用的方法。一旦超过这个限制,编译会直接报致命错误。因此:

  • 尽量少引入第三方库;
  • 使用 dex-method-counts 工具统计各库的方法数,判断哪组库可以组合使用而不超限;
  • 尤其避免使用 Guava——它包含超过 1.3 万个方法。

英文原版 README.md 进一步提醒:Google Play Services 本身也是一个庞大的方法数来源("play-services-is-a-monolith"),做依赖裁剪时值得专门关注。从 API 21 起 multidex 支持库不再是必需品,这也是前文建议 minSdkVersion: 21 的动机之一。

七、Activity 与 Fragment:架构取舍

无论是社区还是 Futurice 内部,对于如何用 Activity 与 Fragment 组织 Android 架构都没有统一共识。Square 甚至推出了一个主要基于 View 构建架构的库(mortar),从而绕过 Fragment,但这并未被社区广泛推荐。

由于 Android API 的历史原因,可以粗略地把 Fragment 理解为屏幕上的 UI 片段(通常与界面相关),而把 Activity 理解为控制器——它特别重要地承担了生命周期管理与状态管理职责。不过角色经常会有变化:Activity 也可能承担 UI 角色(例如在屏幕间转场),Fragment 也可能被单独用作控制器(无 UI 的 Fragment)。官方建议:谨慎决策、充分了解每种方案的利弊(纯 Fragment、纯 Activity、纯 View 三种路线都有各自的缺点)。以下是几条需要保持警惕的建议:

  • 避免滥用嵌套 Fragment:过度嵌套可能触发 "套娃 bug"(matryoshka problem)。仅在确实有意义时使用嵌套,例如在横向滑动的 ViewPager 中放置 Fragment,或者在你完全清楚后果的情况下;
  • 避免在 Activity 中放过多代码:尽量让 Activity 保持轻量,只作为容器,主要用于生命周期和与 Android 系统交互的重要 API。优先使用"单 Fragment Activity"而非裸 Activity——把 UI 代码放进 Activity 对应的 Fragment 中。这样当你要把它放进带标签页的布局,或放进平板的双 Fragment 屏幕时,代码可以直接复用。除非你做了充分决策,否则避免存在没有对应 Fragment 的 Activity;
  • 不要滥用 Intent 等系统 API 做应用内部通信:这可能引发 Bug 或卡顿。例如已有实践证明,如果应用在手机刚开机后立即被打开,用 Intent 在应用内部包之间通信可能造成长达数秒的卡顿。

八、Java 包结构设计

Android 应用的 Java 架构可以近似看作 MVC(Model-View-Controller)。在 Android 中,Fragment 和 Activity 既是控制器,同时又是 UI 的组成部分,因此也是"视图"。这种双重身份使得把它们单纯归类为控制器或视图都很困难。

基于此,原文档给出了一套按层(layer)划分的包结构建议:

  • Fragment(以及 Activity)保留在 fragments 包中;
  • Activity 可以留在顶层包内;如果 Activity 数量超过 2~3 个,就建一个 activities 包;
  • models 包:存放由 API 响应填充的 POJO 对象(配合 JSON 解析器使用);
  • views 包:存放所有自定义视图、通知、ActionBar、Widget 等;Adapter 介于数据与视图之间,但因为它需要经由 getView() 导出视图,可以放进 views 包下的 adapters 子包;
  • managers 包:存放作用域为整个应用、贴近 Android 系统的控制器;
  • utils 包:存放 DateUtils 之类的杂项数据处理类;
  • network 包:存放与后端交互的类。

按"离后端最近 → 离用户最近"排序,整体结构如下:

com.futurice.project
├─ network
├─ models
├─ managers
├─ utils
├─ fragments
└─ views
   ├─ adapters
   ├─ actionbar
   ├─ widgets
   └─ notifications

英文原版 README.md 已经演进为基于功能(feature based)的包结构建议,理由是它带来更清晰的功能依赖边界、更好的封装性、更易理解的组件归属、更低的误改风险、更简单的导航、更易移除功能,以及更平滑地过渡到模块化构建(更好的构建时间与 Instant Apps 支持)。而按"如何构建"(把相关 Activity、Fragment、Adapter 分别放不同包)的方式组织,容易导致代码库碎片化、实现灵活性下降。两套方案各有适用场景,老项目可参考法语版的分层方案,新项目建议采用英文原版的功能化方案。

九、资源文件组织规范

9.1 命名规则

遵循类型前缀约定 type_foo_bar.xml,例如:

  • fragment_contact_details.xml
  • view_primary_button.xml
  • activity_main.xml

9.2 Layout XML 的组织约定

如果不确定如何组织布局 XML,以下约定很有帮助:

  • 每个属性单独一行,用空格缩进(英文原版建议 4 空格);
  • android:id 永远是第一个属性;
  • android:layout_**** 系列属性放在顶部(紧随 android:id);
  • style 属性放在最底部;
  • 结束标签 /> 单独占一行,便于后续添加和排序属性;
  • 与其硬编码 android:text,不如使用 Android Studio 提供的 Design-time(设计期)属性。

标准示例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="@string/name"
        style="@style/FancyText"
        />

    <include layout="@layout/reusable_part" />

</LinearLayout>

通用规则:android:layout_**** 属性应放在布局 XML 中,其余 android:**** 属性放进样式 XML。也就是说,布局文件只保留布局(位置、边距、尺寸)与内容属性,而所有外观细节(颜色、内边距、字体)都放进样式文件。

例外情况:

  • android:id 显然必须在布局文件里;
  • LinearLayout 的 android:orientation 通常放在布局文件里更合理;
  • android:text 定义的是内容,应放在布局文件里;
  • 偶尔用一个通用样式统一设置 android:layout_width 和 android:layout_height 也是合理的,但默认情况下它们应当出现在布局文件中。

9.3 使用 Styles 消除重复属性

几乎所有项目都需要正确使用样式(Styles),因为视图外观重复非常常见。至少应为应用中大部分文本内容定义一个通用样式,例如:

<style name="ContentText">
    <item name="android:textSize">@dimen/font_normal</item>
    <item name="android:textColor">@color/basic_black</item>
</style>

应用到 TextView:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/price"
    style="@style/ContentText"
    />

按钮通常也需要类似处理。但不要止步于此——更进一步,把一组相关的、重复出现的 android:**** 属性整体收敛到通用样式中。

9.4 拆分大型样式文件

你完全不必只有一个 styles.xml。Android SDK 原生支持多个样式文件,styles 这个名字本身没有任何魔法,真正重要的是文件内部的 <style> 标签。因此可以这样拆分:

  • styles.xml
  • styles_home.xml
  • styles_item_details.xml
  • styles_forms.xml

与资源目录名不同(目录名对构建系统有特定含义),res/values 目录下的文件名可以是任意的。

9.5 colors.xml:只做调色板

colors.xml 中除了"颜色名 → RGBA 值"的映射之外,不应该有任何别的东西。不要用它去定义各种按钮类型。

不要这样做——带上下文的命名(如 button、comment)难以复用,改一个基础色要牵动多处,而且这些"上下文"本身应该体现在按钮样式中,而非颜色文件里:

<resources>
    <color name="button_foreground">#FFFFFF</color>
    <color name="button_background">#2A91BD</color>
    <color name="comment_background_inactive">#5F5F5F</color>
    <color name="comment_background_active">#939393</color>
    <color name="comment_foreground">#FFFFFF</color>
    <color name="comment_foreground_important">#FF9D2F</color>
    ...
    <color name="comment_shadow">#323232</color>
</resources>

应该这样做——只维护一份干净的调色板:

<resources>

    <!-- grayscale 灰度色 -->
    <color name="white"     >#FFFFFF</color>
    <color name="gray_light">#DBDBDB</color>
    <color name="gray"      >#939393</color>
    <color name="gray_dark" >#5F5F5F</color>
    <color name="black"     >#323232</color>

    <!-- basic colors 基础色 -->
    <color name="green">#27D34D</color>
    <color name="blue">#2A91BD</color>
    <color name="orange">#FF9D2F</color>
    <color name="red">#FF432F</color>

</resources>

这份调色板可以直接向设计师索取。颜色名不一定是 "green"、"blue" 这种字面颜色名,像 brand_primary、brand_secondary、brand_negative 这样的语义化命名也完全可以接受。把颜色这样组织之后:改动颜色很容易,应用实际使用了多少种颜色一目了然——通常一个好看的界面应当尽量减少颜色种类。

英文原版 README.md 进一步给出了三层职责分离的进阶做法:colors.xml 只定义调色板 → 可选的中间层颜色资源文件把调色板映射成语义名(如 @color/white 映射为 button_foreground)→ styles.xml 中的样式引用语义色。这样多组相关样式共享相似颜色与用途时,重构更安全、样式定义更稳定,代价是需要多维护一套颜色映射。

9.6 dimens.xml:像 colors.xml 一样保持 DRY

同样地,应该为应用中典型的间距与字号定义一套"维度调色板"。一个良好的 dimens.xml 示例:

<resources>

    <!-- font sizes 字号 -->
    <dimen name="font_larger">22sp</dimen>
    <dimen name="font_large">18sp</dimen>
    <dimen name="font_normal">15sp</dimen>
    <dimen name="font_small">12sp</dimen>

    <!-- typical spacing between two views 视图间典型间距 -->
    <dimen name="spacing_huge">40dp</dimen>
    <dimen name="spacing_large">24dp</dimen>
    <dimen name="spacing_normal">14dp</dimen>
    <dimen name="spacing_small">10dp</dimen>
    <dimen name="spacing_tiny">4dp</dimen>

    <!-- typical sizes of views 视图典型尺寸 -->
    <dimen name="button_height_tall">60dp</dimen>
    <dimen name="button_height_normal">40dp</dimen>
    <dimen name="button_height_short">32dp</dimen>

</resources>

在布局、边距(margin)与内边距(padding)中,应当使用 spacing_**** 这类维度常量,而不是硬编码数值——就像字符串资源那样对待维度。这会给应用带来一致的视觉感受,同时让样式与布局的组织和修改都更加容易。

9.7 strings.xml:命名空间式命名

字符串的 key 应当采用类似命名空间的形式,并且不要害怕为两三个 key 复用同一个值——因为命名空间是消除歧义、提供上下文所必需的。

不推荐:

<string name="network_error">Erreur de réseau</string>
<string name="call_failed">L'appel a échoué</string>
<string name="map_failed">Le chargement de la carte a échoué</string>

推荐:

<string name="error.message.network">Erreur de réseau</string>
<string name="error.message.call">L'appel a échoué</string>
<string name="error.message.map">Le chargement de la carte a échoué</string>

另一个硬性规则:不要把字符串值写成全大写,遵循正常的文本大小写惯例(例如首字母大写)。如果确实需要全大写显示,请在 TextView 上使用 textAllCaps 属性来完成:

不推荐:

<string name="error.message.call">L'APPEL A ECHOUE</string>

推荐:

<string name="error.message.call">L'appel a échoué</string>

(英文原版 README.md 使用下划线风格 error_message_network,两种分隔风格均可,关键是保持命名空间式的语义层级。)

9.8 避免过深的视图层级

有时你会忍不住为了某个排列效果再套一层 LinearLayout。这种嵌套可能不断蔓延:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <RelativeLayout
        ...
        >

        <LinearLayout
            ...
            >

            <LinearLayout
                ...
                >

                <LinearLayout
                    ...
                    >
                </LinearLayout>

            </LinearLayout>

        </LinearLayout>

    </RelativeLayout>

</LinearLayout>

即使你没有在布局文件中显式看到这种结构,当你在 Java 代码里把视图 inflate 进其它视图时,同样可能发生。

深层视图层级会带来两类问题:一是性能问题——处理器需要遍历一棵复杂的视图树;二是更严重的 StackOverflowError(视图过多引发的栈溢出)。

因此,请尽量让视图层级保持扁平:学会使用 RelativeLayout(英文原版已进一步推荐功能更强的 ConstraintLayout)、学习官方 "optimize your layouts" 布局优化指南,以及合理使用 <merge> 标签来合并冗余层级。

9.9 WebView 使用注意事项

当必须展示网页时(例如新闻文章),避免在客户端做 HTML 清洗处理,而应要求后端开发者直接提供一份"纯净"版本的 HTML 页面。

WebView 还容易引发内存泄漏:当它持有 Activity 的引用、而不是绑定 ApplicationContext 时,会导致泄漏。此外,不要用 WebView 展示简单的文本或按钮——这类需求请使用平台原生控件 TextView 与 Button。

十、测试与模拟器

10.1 测试框架

原文档摘要中给出的组合是:Robolectric 做单元测试、Robotium 做 UI(图形界面)测试。这是文档撰写时期的主流方案。

需要说明的是,英文原版 README.md 已经更新了推荐:单元测试使用 JUnit(纯 JVM 上的无 Android 依赖测试);UI 测试使用 Espresso;断言库推荐 AssertJ-Android(AssertJ 的扩展,覆盖 Android Support、Google Play Services、Appcompat 等组件的断言)。英文原版同时明确不建议再用 Robolectric——因为它通过 mock Android 平台实现"脱离设备"测试,测试结果不准确也不完整;更好的组合是 JVM 单元测试 + 真机/设备上的集成测试。AssertJ-Android 风格的断言示例:

// Example assertion using AssertJ-Android
assertThat(layout).isVisible()
    .isVertical()
    .hasChildCount(5);

(注:法语版文档对应的测试章节内容较少,以上为对照英文原版 README.md 的补充。)

10.2 模拟器:Genymotion

如果你以 Android 开发为职业,原文档建议购买 Genymotion 模拟器的许可证。其优势包括:

  • 比 AVD 虚拟机刷新率更高、运行更流畅;
  • 内置演示工具,可模拟不同类型的网络连接、模拟 GPS 位置等;
  • 非常适合做已连接设备(connected)测试;
  • 支持大量(虽非全部)不同机型,购买许可证的成本远低于购置同等数量的真机。

缺点也很明确:Genymotion 模拟器不包含全部 Google 服务(如 Google Play Store、Google Maps);而且如果需要测试三星手机特有的 API,仍然必须购买真实设备。英文原版 README.md 的立场有所演进:认为 Android SDK 模拟器(尤其 x86 版本)性能近年提升明显,已足以应付大多数日常开发场景,但真机验证的价值不可忽视——不必测试所有机型,应聚焦于市场份额大、与你的应用最相关的设备。

十一、ProGuard 配置与发布构建

11.1 ProGuard 基础配置

ProGuard 通常用于缩减 Android 应用体积并对打包后的代码进行混淆。是否启用取决于项目配置,通常会在构建 release APK 时启用:

buildTypes {
    debug {
        minifyEnabled false
    }
    release {
        signingConfig signingConfigs.release
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

要确定哪些代码需要保留、哪些可以删除或混淆,你必须为代码指定一个或多个入口点(entry points)。Android 框架自带一份默认配置,位于 SDK_HOME/tools/proguard/proguard-android.txt;使用上述配置时,项目自定义规则文件 my-project/app/proguard-rules.pro 会自动追加到默认配置之后。

11.2 启动崩溃排查:usage.txt 与 mapping.txt

一个非常常见的问题是:即使构建命令(例如 assembleRelease)成功且没有任何警告,应用启动时仍崩溃,抛出 ClassNotFoundException、NoSuchFieldException 或类似异常。这通常意味着两种情况之一:

  1. ProGuard 删除了它认为不需要的类、枚举、方法、字段或注解;
  2. ProGuard 混淆(重命名)了类、枚举或字段名,但它正通过原名被间接使用——例如通过 Java 反射。

排查方法:

  • 检查 app/build/outputs/proguard/release/usage.txt,确认目标对象是否被删除;
  • 检查 app/build/outputs/proguard/release/mapping.txt,确认目标对象是否被混淆(重命名)。

11.3 keep 与 keepnames 规则

为了阻止 ProGuard 删除运行所必需的类或类成员,在 ProGuard 配置中添加 keep 选项:

-keep class com.futurice.project.MyClass { *; }

为了阻止 ProGuard 混淆类或类成员,添加 keepnames 选项:

-keepnames class com.futurice.project.MyClass { *; }

更多示例可查阅 ProGuard 官方手册的 examples 章节。

11.4 尽早构建 Release 并妥善保存 mapping.txt

两条重要的发布实践:

  • 在开发早期就做一次 release 构建并测试,验证 ProGuard 规则是否正确保留了重要依赖。每引入一个新库或升级依赖,都应该做一次 release 构建并在真机上测试 APK。不要等到应用做到 "1.0" 版本才做 release 构建——那时你可能收获一堆不愉快的意外,而修复时间已经所剩无几;
  • 为每次发布给用户的 release 保存一份 mapping.txt。保留每个 release 构建的 mapping 文件,当用户上报包含混淆栈轨迹的 Bug 时,你才能通过反混淆定位问题。

11.5 DexGuard

如果需要更强大的优化与混淆工具,可以考虑 DexGuard——由 ProGuard 同一团队开发的商业软件。它特别擅长混淆发布代码,而且还能把 Dex 文件拆分成多个文件,从而绕过 65k 方法数限制。

十二、致谢与许可证

本指南内容由 Antti Lammi、Joni Karppinen、Peter Tackage、Timo Tuominen、Vera Izrailit、Vihtori Mäntylä、Mark Voit、Andre Medeiros、Paul Houghton 以及 Futurice 其他 Android 开发者共同贡献。

本仓库采用 Creative Commons Attribution 4.0 International(CC BY 4.0) 许可证,版权归 Futurice Oy 所有(详见仓库根目录的 LICENSE 文件)。如果你还想了解同一团队在 iOS、Windows Phone 等平台上的最佳实践,可在仓库首页 README.md 中查看相关指引;各语言版本见 translations 目录。

登录后查看全文
android-best-practices