Android 开发实践经验

良好的习惯,可达到事半功倍的效果,所以,从今天开始保持好习惯!



一、引言

遵循必要的准则,避免重复发明轮子(Avoid reinventing the wheel by following these guidelines.)!

二、工程配置

2.1 选择合适的IDE

无论使用什么编辑器,一定要构建一个良好的工程结构。 编辑器每个人都有自己的选择,让编辑器根据工程结构和构建系统运作。当下首推 Android Studio,因为它是由谷歌开发,很好地支持 Gradle,包含很多有用的检测和分析工具,默认使用最新的工程结构,它就是为 Android 开发定制的。不再推荐使用 Eclipse 和 ADT 开发,谷歌在 2015 年年末结束了对 ADT 的支持,并呼吁开发者尽快迁移到 Android Studio。

无论使用何种开发工具,避免将编辑器配置文件(比如 Android Studio 的 iml 文件)加入到版本控制,因为这些文件通常包含与本地机器有关的配置,可能会影响你的同事。

善待其他开发者,不强制改变他们的开发工具和偏好。

2.2 Android项目工程

目前有两种Android项目结构:老的 Ant & Eclipse ADT 工程结构,和新的 Gradle & Android Studio 工程结构。

而作为Android开发者,基本上都转到了新的I结构,Android Studio提供很好操作的导入流程。

老的结构:

old-structure
├─ assets
├─ libs
├─ res
├─ src
│ └─ com/cchip/project
├─ AndroidManifest.xml
├─ build.gradle
├─ project.properties
└─ proguard-rules.pro

新的结构

new-structure
├─ library-tool
├─ app
│ ├─ libs
│ ├─ src
│ │ ├─ androidTest
│ │ │ └─ java
│ │ │ └─ com/cchip/project
│ │ └─ main
│ │ ├─ java
│ │ │ └─ com/cchip/project
│ │ ├─ res
│ │ └─ AndroidManifest.xml
│ ├─ build.gradle
│ └─ proguard-rules.pro
├─ build.gradle
└─ settings.gradle

主要的区别在于

  • 新的结构明确的分开了'source sets' (main,androidTest),这是 Gradle 的一个理念
  • 项目引用第三方项目库时(例如,library-tool),拥有一个顶级包名 APP 从第三方库项目区分你的应用程序是非常有用的
  • settings.gradle 不断引用这些库项目,其中 app/build.gradle 可以引用

2.3 Java 包结构

Android 应用程序在架构上大致是 Java 中的 Model-View-Controller 结构。在 Android 中 Fragment 和 Activity 通常上是控制器类,换句话说,他们是用户接口的部分,同样也是Views 视图的部分。正是因为如此,才很难严格的将 Fragments (或者 Activities) 严格的划分成 控制器 Controlloers 还是视图 Views。

遵循建议:

  • Activities 则可以放在顶级目录下,如果规划有2到3个以上的 Activity,那么还是新建一个 activities 包吧
  • 若有多个 fragments 的话,把它们放在自己单独的 fragments 包中
  • 自定义视图、通知、导航视图,widgets 等等可放入一个 views 包中
  • 一些繁杂的数据处理类,比如说"DateUtils",放在 utils 包下面
  • 与后端交互负责网络处理类,放在 network 包下面。
  • 适配器 Adapter 是在数据和视图之间。可以将 adapters 包放在 views 包里面。也可单独出来

总而言之,以最接近用户而不是最接近后端去安排他们。

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

2.4 把密码和敏感数据放在 gradle.properties

密码 在做版本 release 时 APP 的 build.gradle 你需要定义 signingConfigs,此时你应该避免以下内容:

不要这么做,这使得密码会出现在版本控制中

signingConfigs {
    release {
        // DON'T DO THIS!!
        storeFile file("cchip.keystore")
        storePassword "cchip123"
        keyAlias "cchip"
        keyPassword "cchip123"
    }
}

而是,建立一个不加入版本控制系统的 gradle.properties 文件。

KEYSTORE_PASSWORD=cchip123
KEY_PASSWORD=cchip123

那个文件是 gradle 自动引入的,你可以在 buld.gradle 文件中使用,例如:

signingConfigs {
    release {
        try {
            storeFile file("cchip.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.")
        }
    }
}

2.5 使用 Maven 依赖方案代替使用导入 jar 包方案

如果在项目中明确使用某些 jar 文件,那么它们可能成为固定的版本,如 2.1.1. 下载 jar 包更新他们是很繁琐的,这个问题 Maven 很好的解决了,这在 Android Gradle 构建中也是推荐的方法。你可以指定版本的一个范围,如 2.1.+,然后Maven会自动升级到制定的最新版本,例如:

dependencies {
    compile 'com.squareup.okhttp:okhttp3:3.8.0'
}

2.6 避免使用一些类库 65k method limit(一个 Android 程序中最多能执行65536个方法)

当心 Dex 方法数限制,同时避免使用过多的类库 Android apps,当打包成一个 Dex 文件时,有一个 65535 个应用方法强硬限制。当你突破 65k 限制之后你会看到一个致命错误。因此,使用一个正常范围的类库文件,同时使用 dex-method-counts 工具来决定哪些类库可以再 65k 限制之下使用。

2.7 使用 Volley 或 OkHttp 库代替自己写网络请求

网络请求,缓存,图片 执行请求后端服务器,有几种交互的解决方案,应该考虑实现你自己的网络客户端。使用 Volley 或 Retrofit。Volley 同时提供图片缓存类。如果你选择使用Retrofit,那么考虑使用 Picasso 来加载图片和缓存,同时使用 OkHttp 作为高效的网络请求。Retrofit,Picasso 和 OkHttp 都是同一家公司开发(注:是由Square 公司开发),所以它们能很好的在一起运行。

2.8 RxJava

RxJava 是函数式反应性的一个类库,换句话说,能处理异步的事件。这是一个强大的和有前途的模式,同时也可能会造成混淆,因为它是如此的不同。我们建议在使用这个库架构整个应用程序之前要谨慎考虑。

如若你之前没有使用过 Rx 的经历,从 API 响应开始应用它。另外,从简单的 UI 事件处理开始运用,如单击事件或在搜索栏输入事件。若对你的 Rx 技术有信心,同时想要将它应用到你的整体架构中,那么请在复杂的部分写好 Javadocs 文档。

三、UI界面

3.1 Activities and Fragments

Fragments 应该作为实现 UI 界面默认选择。可以重复使用 Fragments 用户接口来组合成你的应用。强烈推荐使用 Fragments 而不是 Activity 来呈现 UI 界面,理由如下:
提供多窗格布局解决方案 Fragments 的引入主要将手机应用延伸到平板电脑,所以在平板电脑上你可能有 A、B 两个窗格,但是在手机应用上 A、B 可能分别充满整个屏幕。如果你的应用在最初就使用了 Fragments,那么以后将你的应用适配到其他不同尺寸屏幕就会非常简单。

屏幕间数据通信 从一个 Activity 发送复杂数据(例如Java对象)到另外一个 Activity,Android 的 API 并没有提供合适的方法。不过使用 Fragment,你可以使用一个 Activity 实例作为这个 Activity 子 Fragments 的通信通道。即使这样比 Activity 与 Activity 间的通信好,你也想考虑使用 Event Bus 架构,使用如 Otto 或者 Greenrobot EventBus 作为更简洁的实现。

如果你希望避免添加另外一个类库 RxJava 同样可以实现一个 Event Bus。Fragments 一般通用的不止有 UI,可以有一个没有界面的 Fragment 作为 Activity 提供后台工作。进一步可以使用这个特性来创建一个 Fragment 包含改变其它 Fragment 的逻辑而不是把这个逻辑放在 Activity 中。甚至 ActionBar 都可以使用内部 Fragment 来管理 你可以选择使用一个没有UI界面的 Fragment 来专门管理 ActionBar,或者你可以选择使用在每个 Fragment 中添加它自己的 Action 来作为父 Activity 的 ActionBar。

很不幸,我们不建议广泛的使用嵌套的 Fragments,因为有时会引起 Matryoshka(碎片化) bugs。我们只有当它有意义(例如,在水平滑动的 ViewPager 在像屏幕一样 Fragment中)或者他的确是一个明智的选择的时候才广泛的使用 Fragment。

在一个架构级别,你的APP应该有一个顶级的 Activity 来包含绝大部分业务相关的 Fragment。你也可能还有一些辅助的 Activity ,这些辅助的 Activity 与主 Activity 通信很简单限制在这两种方法:

  • Intent.setData()
  • Intent.setAction()

四、资源文件 Resources

4.1 Layout 布局是 XMLs 代码

命名 遵循 前缀 表明类型的习惯,形如 type_foo_bar.xml。例如:fragment_contact_details.xml, view_primary_button.xml, activity_main.xml。
组织布局文件 如果你不确定如何排版一个布局文件,遵循以下规则可能会有帮助:

  • 每一个属性一行,缩进 4 个空格
  • android:id 总是作为第一个属性
  • android:layou_xxx 属性在上边
  • style 属性在底部
  • 关闭标签/>单独起一行,有助于调整和添加新的属性
  • 考虑使用 Designtime attributes 设计时布局属性,Android Studio 已经提供支持,而不是硬编码 android:text
<?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_xxx 属性应该在 layout XML 中定义,同时其它属性 android:**** 应放在 style XML 中。此规则也有例外,不过大体工作的很好。这个思想整体是保持 layout 属性(positioning, margin, sizing) 和 content 属性在布局文件中,同时将所有的外观细节属性(colors, padding, font)放在 style 文件中。

例外有以下这些:

  • android:id 明显应该在 layout 文件中
  • layout 文件中 android:orientation 对于一个 LinearLayout 布局通常更有意义
  • android:text 由于是定义内容,应该放在 layout 文件中
  • 有时候将 android:layout_width 和 android:layout_height 属性放到一个 style 中作为一个通用的风格中更有意义,但是默认情况下这些应该放到 layout 文件中

4.2 在 layoutout XMLs 布局时使用 styles 文件来避免使用重复的属性

使用 styles 几乎每个项目都需要适当的使用 style 文件,因为对于一个视图来说有一个重复的外观是很常见的。在应用中对于大多数文本内容,最起码你应该有一个通用的 style 文件,例如:

<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:**** 的属性放到一个通用的 style 中。

4.3 使用多个 style 文件来避免单一的一个大 style 文件

将一个大的 style 文件分割成多个文件,可以有多个 styles.xml 文件。Android SDK 支持其它文件,styles 这个文件名称并没有作用,起作用的是在文件里 xml 的<style>标签。因此你可以有多个 style 文件 styles.xml,style_home.xml,style_item_details.xml,styles_forms.xml。不用于资源文件路径需要为系统构建起的有意义,在 res/values 目录下的文件可以任意命名。

4.4 保持 colors.xml 简短 DRY(不要重复自己),只是定义调色板

colors.xml 是一个调色板 在你的 colors.xml 文件中应该只是映射颜色的名称一个 RGBA 值,而没有其它的。不要使用它为不同的按钮来定义 RGBA 值。

不要这样做:

<resources>
    <color name="button_foreground">#FFFFFF</color>
    <color name="button_background">#2A91BD</color>
</resources>

使用这种格式,你会非常容易的开始重复定义 RGBA 值,这使如果需要改变基本色变的很复杂。同时,这些定义是跟一些环境关联起来的,如 button 或者 comment,应该放到一个按钮风格中,而不是在 color.xml 文件中。

可以这么做:

<resources>
    <!-- grayscale -->
    <color name="white">#FFFFFF</color>

    <!-- basic colors -->
    <color name="blue">#2A91BD</color>
</resources>

向应用设计者那里要这个调色板,名称不需要跟 "green", "blue", 等等相同。"brand_primary", "brand_secondary", "brand_negative" 这样的名字也是完全可以接受的。像这样规范的颜色很容易修改或重构,会使应用一共使用了多少种不同的颜色变得非常清晰。

通常一个具有审美价值的 UI 来说,减少使用颜色的种类是非常重要的。

4.5 总是使用 dimens.xml DRY(不要重复自己),定义通用常数

像对待 colors.xml 一样对待 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>

布局时在写 margins 和 paddings 时,你应该使用 spacing_xxx 尺寸格式来布局,而不是像对待 String 字符串一样直接写值。

这样写会非常有感觉,会使组织和改变风格或布局是非常容易。

4.6 不要做一个深层次的 ViewGroup

避免深层次的视图结构 有时候为了摆放一个视图,你可能尝试添加另一个 LinearLayout。你可能使用这种方法解决:

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

    <RelativeLayout
        ...
        >

        <LinearLayout
            ...
            >

            <LinearLayout
                ...
                >

                <LinearLayout
                    ...
                    >
                </LinearLayout>

            </LinearLayout>

        </LinearLayout>

    </RelativeLayout>

</LinearLayout>

即使你没有非常明确的在一个 layout 布局文件中这样使用,如果你在 Java 文件中从一个 view inflate 到其他 views 当中,也是可能会发生的。

可能会导致一系列的问题。你可能会遇到性能问题,因为处理起需要处理一个复杂的 UI 树结构。还可能会导致以下更严重的问题 StackOverflowError.因此尽量保持你的视图 tree:学习如何使用 RelativeLayout,如何 optimize 你的布局 和如何使用 <merge> 标签。

五、测试框架

Android SDK 的测试框架还处于初级阶段,特别是关于 UI 测试方面。Android Gradle 目前实现了一个叫 connectedAndroidTest 的测试,它使用一个 JUnit 为 Android 提供的扩展插件 extension of JUnit with helpers for Android.可以跑你生成的 JUnit 测试,只当做单元测试时使用 Robolectric ,views 不用它是一个最求提供"不连接设备的"为了加速开发的测试,非常时候做 models 和 view models 的单元测试。

然而,使用 Robolectric 测试时不精确的,也不完全对 UI 测试。当你对有关动画的 UI 元素、对话框等,测试时会有问题,这主要是因为你是在 “在黑暗中工作”(在没有可控的界面情况下测试)Robotium 使写 UI 测试非常简单。对于 UI 测试你不需 Robotium 跑与设备连接的测试。但它可能会对你有益,是因为它有许多来帮助类的获得和分析视图,控制屏幕。

六、混淆配置

ProGuard 是一个在 Android 项目中广泛使用的压缩和混淆打包的源码的工具。你是否使用 ProGuard 取决你项目的配置,当你构建一个 release 版本的 apk 时,通常你应该配置 gradle 文件。

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

为了决定哪些代码应该被保留,哪些代码应该被混淆,你不得不指定一个或多个实体类在你的代码中。这些实体应该是指定的类包含 main 方法,applets,midlets,activities,等等。

Android framework 使用一个默认的配置文件,可以在 SDK_HOME/tools/proguard/proguard-android.txt 目录下找到。自定义的工程指定的 project-specific 混淆规则,如在 my-project/app/proguard-rules.pro 中定义,会被添加到默认的配置中。

关于 ProGuard 一个普遍的问题,是看应用程序是否崩溃并报 ClassNotFoundException 或者 NoSuchFieldException 或类似的异常,即使编译是没有警告并运行成功。

这意味着以下两种可能:

  • ProGuard 已经移除了类,枚举,方法,成员变量或注解,考虑是否是必要的。
  • ProGuard 混淆了类,枚举,成员变量的名称,但是这些名字又被拿原始名称使用了,比如通过 Java 的反射。

检查 app/build/outputs/proguard/release/usage.txt 文件看有问题的对象是否被移除了。检查 app/build/outputs/proguard/release/mapping.txt 文件看有问题的对象是否被混淆了。

以防 ProGuard 剥离 需要的类和类成员,添加一个 keep 选项在你的 proguard 配置文件中:

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

防止 ProGuard 混淆 一些类和成员,添加 keepnames:

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

在构建项目之初,发布一个版本 来检查 ProGuard 规则是否正确的保持了重要的部分。同时无论何时你添加了新的类库,做一个发布版本,同时 apk 在设备上跑起来测试一下。不要等到你的 app 要发布 "1.0"版本了才做版本发布,那时候你可能会碰到好多意想不到的异常,需要一些时间去修复他们。

每次发布新版本都要写 mapping.txt。每发布一个版本,如果用户遇到一个 bug,同时提交了一个混淆过的堆栈跟踪。通过保留 mapping.txt文件,来确定你可以调试的问题。

DexGuard 如果你需要核心工具来优化,和专门混淆的发布代码,考虑使用 DexGuard,一个商业软件,ProGuard 也是有他们团队开发的。它会很容易将 Dex 文件分割,来解决 65K 个方法限制问题。

七、数据存储

7.1 SharedPreferences

如果你只是需要持久化存储简单的标记位,并且你的应用运行在单一进程,那么 SharedPreferences 可能就满足了你的需求。它是一个非常好的选择。

这里有两个使你可能不使用 SharedPreferences 的原因:

  • 性能问题:你的很多数据结构负责的数据需要存储。
  • 多线程访问数据:你有多个控件或者运行在各自线程上的远程的服务需要同步数据。

7.2 ContentProviders

如果 SharedPreferences 不足以满足你的需求,那么你可以使用平台标准的 ContentProviders,它不仅快速,并且线程安全。

使用 ContentProviders 的唯一问题是建立他们需要大量的模板代码,并且少有高质量的教程。如果可以,我们可以通过使用第三方库 Schematic,极大降低了冗余操作,去生成ContentProviders。

你可能仍然需要亲自写一些解析代码去从 Sqlite 读取数据对象,或者进行相反的操作。如果可以序列化数据对象,例如通过 Gson,只持久化存储最终是字符串。通过这种方式虽然会降低性能,但是从另一个角度来讲,你不需要为每一个数据结构声明表结构。

7.3 使用 ORM

我们通常不推荐使用对象关系映射第三方库除非你有非常复杂的数据结构,并且你确定你真的需要它。他们通常比较复杂,并且需要时间去学习。如果你决定了在你的应用中使用 ORM,你应该注意它是否是线程安全的,而对于目前大多数 ORM 解决方案都是非线程安全的。

7.4 使用Stetho

Stetho 是一个 Facebook 开源的 Android 调试工具,它是 Chrome Developer Tools 的扩展。通过它可以检测应用的网络情况。它也允许你可以检测应用的数据库,shared preferences。但是,你应该确保 Stetho 只有在 Debug 状态下得以开启,而不是在正式发布版本中。

八、使用 LeakCanary

LeakCanary 是可以在应用运行中检测,定位内存泄露的 Java 库。使用它应是你开发应用过程中的一部分。只需要记得它在你的正式版本中你是不需要配置的。

九、致谢

感谢 Futurice 开发者分享他们的 Android 开发经验。


Refer