Android API 准则

本页旨在为开发者提供指南,帮助他们了解 API 委员会在 API 审核中强制执行的一般原则。

除了在编写 API 时遵循这些准则之外,开发者还应运行 API Lint 工具,该工具会在针对 API 运行的检查中编码许多此类规则。

您可以将此文档视为相应 Lint 工具所遵循的规则指南,以及有关无法以高准确度编入该工具的规则的一般建议。

API Lint 工具

API Lint 已集成到 Metalava 静态分析工具中,并在 CI 中的验证期间自动运行。您可以使用 m checkapi 从本地平台代码库手动运行,也可以使用 ./gradlew :path:to:project:checkApi 从本地 AndroidX 代码库运行。

API 规则

Android 平台和许多 Jetpack 库是在这套指南创建之前就已存在,并且本页后面阐述的政策也在不断发展,以满足 Android 生态系统的需求。

因此,某些现有 API 可能不符合相关准则。在其他情况下,如果新 API 与现有 API 保持一致,而不是严格遵守指南,则可能会为应用开发者提供更好的用户体验。

请自行判断,如果遇到难以解决的 API 问题或需要更新的指南,请与 API 委员会联系。

API 基础知识

此类别与 Android API 的核心方面有关。

必须实现所有 API

无论 API 的受众群体(例如公开或 @SystemApi)如何,在合并或公开为 API 时,都必须实现所有 API surface。请勿合并 API 存根与稍后实现的实现。

没有实现的 API 表面存在多个问题:

  • 无法保证已露出适当或完整的表面。 在客户端测试或使用 API 之前,无法验证客户端是否具有使用相应功能所需的 API。
  • 在开发者预览版中,无法测试未实现 API。
  • 无法在 CTS 中测试没有实现的 API。

必须测试所有 API

这符合平台 CTS 要求、AndroidX 政策,以及 API 必须实现的总体理念。

测试 API 表面可基本保证 API 表面可用,并且我们已解决预期用例。仅测试是否存在是不够的;还必须测试 API 本身的行为。

添加新 API 的更改应在同一 CL 或 Gerrit 主题中包含相应的测试。

API 还应是可测试的。您应该能够回答“应用开发者将如何测试使用您的 API 的代码?”这个问题。

所有 API 都必须有文档

文档是 API 可用性的关键组成部分。虽然 API 表面的语法可能看起来很明显,但任何新客户端都不会了解 API 背后的语义、行为或上下文。

所有生成的 API 都必须符合相应准则

由工具生成的 API 必须遵循与手写代码相同的 API 准则。

不建议用于生成 API 的工具:

  • AutoValue:以各种方式违反了指南,例如,无法通过 AutoValue 的工作方式实现最终值类或最终构建器。

代码样式

此类别涉及开发者应使用的常规代码样式,尤其是在编写公共 API 时。

遵循标准编码规范,除非另有说明

Android 编码规范已在此处记录,供外部贡献者参考:

https://source.android.com/source/code-style.html

总的来说,我们倾向于遵循标准的 Java 和 Kotlin 编码规范。

方法名称中的首字母缩写词不应采用大写形式

例如:方法名称应为 runCtsTests,而非 runCTSTests

名称不应以 Impl 结尾

这会暴露实现细节,请避免这样做。

本部分介绍了有关类、接口和继承的规则。

从适当的基类继承新的公共类

继承会公开子类中可能不合适的 API 元素。例如,FrameLayout 的新公共子类看起来像 FrameLayout,外加新的行为和 API 元素。如果该继承的 API 不适合您的使用情形,请从树中更上层的类(例如 ViewGroupView)继承。

如果您想替换基类中的方法以抛出 UnsupportedOperationException,请重新考虑您使用的基类。

使用基本集合类

无论是将集合作为实参还是作为值返回,都应始终首选基类而非具体实现(例如,返回 List<Foo> 而不是 ArrayList<Foo>)。

使用可表达相应 API 限制的基类。例如,对于集合必须有序的 API,请使用 List;对于集合必须由唯一元素组成的 API,请使用 Set

在 Kotlin 中,建议使用不可变集合。如需了解详情,请参阅集合可变性

抽象类与接口

Java 8 添加了对默认接口方法的支持,这使得 API 设计者能够向接口添加方法,同时保持二进制兼容性。平台代码和所有 Jetpack 库应以 Java 8 或更高版本为目标平台。

如果默认实现是无状态的,API 设计者应该优先选择接口而不是抽象类,也就是说,默认接口方法可以实现为对其他接口方法的调用。

如果默认实现需要构造函数或内部状态,则必须使用抽象类。

在这两种情况下,API 设计者都可以选择将单个方法保留为抽象方法,以简化作为 lambda 的用法:

public interface AnimationEndCallback {
  // Always called, must be implemented.
  public void onFinished(Animation anim);
  // Optional callbacks.
  public default void onStopped(Animation anim) { }
  public default void onCanceled(Animation anim) { }
}

类名称应反映其扩展的内容

例如,扩展 Service 的类应命名为 FooService,以提高清晰度:

public class IntentHelper extends Service {}
public class IntentService extends Service {}

通用后缀

避免为实用方法集合使用 HelperUtil 等通用类名称后缀。而是将这些方法直接放在关联的类中或放在 Kotlin 扩展函数中。

如果方法桥接多个类,请为包含类指定一个有意义的名称,以说明其用途。

少数情况下,使用 Helper 后缀可能比较合适:

  • 用于组合默认行为
  • 可能涉及将现有行为委托给新类
  • 可能需要持久性状态
  • 通常涉及 View

例如,如果向后移植工具提示需要保留与 View 关联的状态,并在 View 上调用多个方法来安装向后移植,则 TooltipHelper 将是一个可接受的类名称。

请勿直接将 IDL 生成的代码公开为公共 API

将 IDL 生成的代码作为实现详情。这包括 protobuf、套接字、FlatBuffers 或任何其他非 Java、非 NDK API 接口。不过,Android 中的大多数 IDL 都是 AIDL,因此本页重点介绍 AIDL。

生成的 AIDL 类不符合 API 样式指南要求(例如,它们不能使用重载),并且 AIDL 工具并非专门设计用于保持语言 API 兼容性,因此您无法将它们嵌入到公共 API 中。

相反,请在 AIDL 接口之上添加一个公共 API 层,即使它最初只是一个浅层封装容器。

Binder 接口

如果 Binder 接口是实现细节,则可以在未来随意更改,而公共层允许保持所需的向后兼容性。例如,您可能需要向内部调用添加新实参,或者通过使用批处理或流式传输、使用共享内存等方式来优化 IPC 流量。如果您的 AIDL 接口也是公共 API,则无法执行上述任何操作。

例如,请勿直接将 FooService 作为公共 API 公开:

// BAD: Public API generated from IFooService.aidl
public class IFooService {
   public void doFoo(String foo);
}

请改为将 Binder 接口封装在管理器或其他类中:

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo);
}

public IFooManager {
   public void doFoo(String foo) {
      mFooService.doFoo(foo);
   }
}

如果稍后需要为此调用添加新实参,则内部接口可以保持最小化,并向公共 API 添加便捷的重载。随着实现不断发展,您可以使用封装层来处理其他向后兼容性问题:

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo, int flags);
}

public IFooManager {
   public void doFoo(String foo) {
      if (mAppTargetSdkLevel < 26) {
         useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
         mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
      } else {
         mFooService.doFoo(foo, 0);
      }
   }

   public void doFoo(String foo, int flags) {
      mFooService.doFoo(foo, flags);
   }
}

对于不属于 Android 平台的 Binder 接口(例如,由 Google Play 服务导出的供应用使用的服务接口),稳定、已发布且已进行版本控制的 IPC 接口的要求意味着,接口本身很难发展。不过,最好还是在它周围添加一个封装层,以符合其他 API 指南,并使您在需要时更轻松地为新版本的 IPC 接口使用相同的公共 API。

请勿在公共 API 中使用原始 Binder 对象

Binder 对象本身没有任何意义,因此不应在公共 API 中使用。一种常见的用例是将 BinderIBinder 用作令牌,因为它具有身份语义。请改用封装容器令牌类,而不是使用原始 Binder 对象。

public final class IdentifiableObject {
  public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
  /**
   * @hide
   */
  public Binder getRawValue() {...}

  /**
   * @hide
   */
  public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}

public final class IdentifiableObject {
  public IdentifiableObjectToken getToken() {...}
}

管理器类必须是最终类

管理器类应声明为 final。管理器类与系统服务进行通信,是唯一的互动点。无需进行自定义,因此将其声明为 final

不使用 CompletableFuture 或 Future

java.util.concurrent.CompletableFuture 具有较大的 API Surface,允许任意更改未来的值,并且具有容易出错的默认值。

相反,java.util.concurrent.Future 缺少非阻塞监听,因此很难与异步代码搭配使用。

平台代码由 Kotlin 和 Java 使用的低级库 API 中,最好使用完成回调、Executor 的组合,如果 API 支持取消,则使用 CancellationSignal

public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
    Executor callbackExecutor,
    android.os.OutcomeReceiver<FooResult, Throwable> callback);

如果您以 Kotlin 为目标平台,请首选 suspend 函数。

suspend fun asyncLoadFoo(): Foo

Java 特定的集成库中,您可以使用 Guava 的 ListenableFuture

public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();

不使用 Optional

虽然 Optional 在某些 API Surface 中可能具有优势,但它与现有的 Android API Surface 区域不一致。@Nullable@NonNull 可为 null 安全性提供工具辅助,而 Kotlin 在编译器级别强制执行可为 null 性协定,因此无需使用 Optional

对于可选的原语,请使用配对的 hasget 方法。如果未设置值(has 返回 false),get 方法应抛出 IllegalStateException

public boolean hasAzimuth() { ... }
public int getAzimuth() {
  if (!hasAzimuth()) {
    throw new IllegalStateException("azimuth is not set");
  }
  return azimuth;
}

为不可实例化的类使用私有构造函数

只能由 Builder 创建的类、仅包含常量或静态方法的类,或者其他无法实例化的类应至少包含一个私有构造函数,以防止使用默认的无实参构造函数进行实例化。

public final class Log {
  // Not instantiable.
  private Log() {}
}

单例

我们不建议使用单例,因为它们存在以下与测试相关的缺点:

  1. 构造由类管理,可防止使用伪对象
  2. 由于单例的静态性质,测试无法实现密封
  3. 为了解决这些问题,开发者要么必须了解单例的内部细节,要么必须围绕单例创建一个封装容器

最好采用单例模式,该模式依赖于抽象基类来解决这些问题。

单一实例

单例类使用具有 privateinternal 构造函数的抽象基类,并提供用于获取实例的静态 getInstance() 方法。getInstance() 方法在后续调用中必须返回同一对象。

getInstance() 返回的对象应该是抽象基类的私有实现。

class Singleton private constructor(...) {
  companion object {
    private val _instance: Singleton by lazy { Singleton(...) }

    fun getInstance(): Singleton {
      return _instance
    }
  }
}
abstract class SingleInstance private constructor(...) {
  companion object {
    private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
    fun getInstance(): SingleInstance {
      return _instance
    }
  }
}

单例与 singleton 的不同之处在于,开发者可以创建 SingleInstance 的虚假版本,并使用自己的依赖项注入框架来管理实现,而无需创建封装容器,或者库可以在 -testing 制品中提供自己的虚假版本。

释放资源的类应实现 AutoCloseable

通过 closereleasedestroy 或类似方法释放资源的类应实现 java.lang.AutoCloseable,以便开发者在使用 try-with-resources 代码块时自动清理这些资源。

避免在 android.* 中引入新的 View 子类

请勿在平台公共 API(即 android.* 中)中引入直接或间接从 android.view.View 继承的新类。

Android 的界面工具包现在是 Compose-first。平台公开的新界面功能应作为较低级别的 API 公开,这些 API 可用于在 Jetpack 库中为开发者实现 Jetpack Compose 和基于 View 的可选界面组件。以库的形式提供这些组件,可在平台功能不可用时实现向后移植。

字段

这些规则涉及类中的公共字段。

不要公开原始字段

Java 类不应直接公开字段。字段应为私有,并且只能使用公共 getter 和 setter 进行访问,无论这些字段是否为 final 字段。

极少数例外情况包括基本数据结构,在这种情况下,无需增强指定或检索字段的行为。在这种情况下,应使用标准变量命名惯例来命名字段,例如 Point.xPoint.y

Kotlin 类可以公开属性。

公开字段应标记为 final

强烈建议不要使用原始字段(请参阅不要公开原始字段)。但在极少数情况下,如果某个字段作为公共字段公开,请将该字段标记为 final

不应公开内部字段

请勿在公共 API 中引用内部字段名称。

public int mFlags;

使用 public 而不是 protected

@see 使用 public 而不是 protected

常量

以下是有关公共常量的规则。

标志常量不应与 int 或 long 值重叠

标志是指可以组合成某个并集值的位。如果不是这种情况,请勿将变量或常量命名为 flag

public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;

如需详细了解如何定义公共标志常量,请参阅 @IntDef(针对位掩码标志)

静态最终常量应使用全大写、下划线分隔的命名惯例

常量中的所有字词都应大写,多个字词之间应以 _ 分隔。例如:

public static final int fooThing = 5
public static final int FOO_THING = 5

为常量使用标准前缀

Android 中使用的许多常量都用于标准事物,例如标志、键和操作。这些常量应具有标准前缀,以便更易于识别它们。

例如,intent extra 应以 EXTRA_ 开头。intent 操作应以 ACTION_ 开头。与 Context.bindService() 搭配使用的常量应以 BIND_ 开头。

关键常量名称和范围

字符串常量值应与常量名称本身保持一致,并且通常应限定在软件包或网域范围内。例如:

public static final String FOO_THING = "foo"

既未保持命名一致性,也未适当限定范围。您应考虑:

public static final String FOO_THING = "android.fooservice.FOO_THING"

范围限定的字符串常量中 android 的前缀是为 Android 开源项目预留的。

intent 操作和 extra 以及 Bundle 条目应使用其定义所在的软件包名称进行命名空间化。

package android.foo.bar {
  public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
  public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}

使用 public 而不是 protected

@see 使用 public 而不是 protected

使用一致的前缀

相关常量应全部以相同的前缀开头。例如,对于一组要与标志值搭配使用的常量:

public static final int SOME_VALUE = 0x01;

public static final int SOME_OTHER_VALUE = 0x10;

public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;

public static final int FLAG_SOME_OTHER_VALUE = 0x10;

public static final int FLAG_SOME_THIRD_VALUE = 0x100;

@see 为常量使用标准前缀

使用一致的资源名称

公共标识符、属性和值必须使用 camelCase 命名惯例进行命名,例如 @id/accessibilityActionPageUp@attr/textAppearance,类似于 Java 中的公共字段。

在某些情况下,公开标识符或属性包含一个以英文下划线分隔的常见前缀:

  • config.xml 中的平台配置值(例如 @string/config_recentsComponentName
  • 特定于布局的视图属性,例如 attrs.xml 中的 @attr/layout_marginStart

公共主题和样式必须遵循分层 PascalCase 命名惯例,例如 @style/Theme.Material.Light.DarkActionBar@style/Widget.Material.SearchView.ActionBar,类似于 Java 中的嵌套类。

布局和可绘制资源不应作为公共 API 公开。不过,如果必须公开,则公共布局和可绘制对象必须使用下划线命名惯例进行命名,例如 layout/simple_list_item_1.xmldrawable/title_bar_tall.xml

如果常量可能会发生变化,请将其设为动态

编译器可能会内联常量值,因此保持值不变被视为 API 合约的一部分。如果 MIN_FOOMAX_FOO 常量的值将来可能会发生变化,请考虑将其改为动态方法。

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

考虑回调的向前兼容性

以旧版 API 为目标平台的应用无法识别在未来 API 版本中定义的常量。因此,传递给应用的常量应考虑应用的目标 API 版本,并将较新的常量映射到一致的值。请考虑以下场景:

假设的 SDK 来源:

// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;

具有 targetSdkVersion="22" 的假设应用:

if (result == STATUS_FAILURE) {
  // Oh no!
} else {
  // Success!
}

在这种情况下,应用是在 API 级别 22 的限制范围内设计的,并做出了(在某种程度上)合理的假设,即只有两种可能的状态。不过,如果应用收到新添加的 STATUS_FAILURE_RETRY,则会将其解读为成功。

返回常量的方法可以通过限制其输出以匹配应用的目标 API 级别,安全地处理此类情况:

private int mapResultForTargetSdk(Context context, int result) {
  int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
  if (targetSdkVersion < 26) {
    if (result == STATUS_FAILURE_ABORT) {
      return STATUS_FAILURE;
    }
    if (targetSdkVersion < 23) {
      if (result == STATUS_FAILURE_RETRY) {
        return STATUS_FAILURE;
      }
    }
  }
  return result;
}

开发者无法预测常量列表在未来是否会发生变化。如果您定义的 API 包含看起来像通配符的 UNKNOWNUNSPECIFIED 常量,开发者会认为他们在编写应用时发布的常量是详尽无遗的。如果您不愿意设置此预期,请重新考虑是否适合为您的 API 使用全能常量。

此外,库无法指定自己的 targetSdkVersion(与应用分开),并且从库代码处理 targetSdkVersion 行为变更非常复杂且容易出错。

整数或字符串常量

如果值的命名空间在您的软件包之外不可扩展,请使用整数常量和 @IntDef。如果命名空间由您软件包之外的代码共享或可以扩展,请使用字符串常量。

数据类

数据类表示一组不可变的属性,并提供一组小巧且定义明确的实用函数来与这些数据进行交互。

请勿在公共 Kotlin API 中使用 data class,因为 Kotlin 编译器无法保证生成的代码在语言 API 或二进制文件方面的兼容性。请改为手动实现所需的功能。

实例化

在 Java 中,如果数据类的属性较少,应提供构造函数;如果属性较多,应使用 Builder 模式。

在 Kotlin 中,无论属性数量是多少,数据类都应提供带有默认实参的构造函数。面向 Java 客户端时,以 Kotlin 定义的数据类可能也会受益于提供构建器。

修改和复制

如果需要修改数据,请提供具有复制构造函数 (Java) 或返回新对象的 copy() 成员函数 (Kotlin) 的 Builder 类。

在 Kotlin 中提供 copy() 函数时,实参必须与类的构造函数匹配,并且必须使用对象的当前值填充默认值:

class Typography(
  val labelMedium: TextStyle = TypographyTokens.LabelMedium,
  val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
    fun copy(
      labelMedium: TextStyle = this.labelMedium,
      labelSmall: TextStyle = this.labelSmall
    ): Typography = Typography(
      labelMedium = labelMedium,
      labelSmall = labelSmall
    )
}

其他行为

数据类应同时实现 equals()hashCode(),并且每个属性都必须在这些方法的实现中得到体现。

数据类可以实现 toString(),建议采用与 Kotlin 的数据类实现相匹配的格式,例如 User(var1=Alex, var2=42)

方法

这些规则涉及方法中的各种具体细节,包括参数、方法名称、返回类型和访问权限说明符。

时间

这些规则涵盖了如何在 API 中表达日期和时长等时间概念。

尽可能优先使用 java.time.* 类型

java.time.Durationjava.time.Instant 和许多其他 java.time.* 类型可通过 desugaring 在所有平台版本上使用,并且在 API 参数或返回值中表示时间时应优先使用这些类型。

最好仅公开接受或返回 java.time.Durationjava.time.Instant 的 API 变体,并省略具有相同使用情形的原始变体,除非 API 领域是预期使用模式中的对象分配会对性能产生严重影响的领域。

表示时长的方法应命名为“duration”

如果时间值表示所涉及的时间段,请将参数命名为“duration”,而不是“time”。

ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);

例外情况

如果时长专门应用于超时值,则“timeout”是合适的。

当指代特定时间点而非时长时,使用类型为 java.time.Instant 的“time”是合适的。

以原始类型表示时长或时间的方法应以其时间单位命名,并使用 long

接受或返回时长作为基元的方法应在方法名称后添加关联的时间单位(例如 MillisNanosSeconds),以便将未修饰的名称保留给 java.time.Duration 使用。请参阅时间

方法还应使用相应的单位和时间基准进行适当的注释:

  • @CurrentTimeMillisLong:值是一个非负时间戳,以自 1970-01-01T00:00:00Z 以来的毫秒数来衡量。
  • @CurrentTimeSecondsLong:值是一个非负时间戳,以自 1970-01-01T00:00:00Z 以来的秒数来衡量。
  • @DurationMillisLong:值是以毫秒为单位的非负时长。
  • @ElapsedRealtimeLong:值是 SystemClock.elapsedRealtime() 时间基准中的非负时间戳。
  • @UptimeMillisLong:值是SystemClock.uptimeMillis()时间基准中的非负时间戳。

原始时间形参或返回值应使用 long,而不是 int

ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);

表示时间单位的方法应优先使用非缩写形式的单位名称

public void setIntervalNs(long intervalNs);

public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);

public void setTimeoutMicros(long timeoutMicros);

为长时间实参添加注释

该平台包含多个注释,可为 long 类型的时间单位提供更强的类型化功能:

  • @CurrentTimeMillisLong:值是一个非负时间戳,以自 1970-01-01T00:00:00Z 以来的毫秒数来衡量,因此采用 System.currentTimeMillis() 时基。
  • @CurrentTimeSecondsLong:值是一个非负时间戳,以自 1970-01-01T00:00:00Z 起的秒数来衡量。
  • @DurationMillisLong:值是以毫秒为单位的非负时长。
  • @ElapsedRealtimeLong:值是 SystemClock#elapsedRealtime() 时间基准中的非负时间戳。
  • @UptimeMillisLong:值是 SystemClock#uptimeMillis() 时间基准中的非负时间戳。

度量单位

对于所有表示时间以外的计量单位的方法,请优先使用采用驼峰式命名法的 SI 单位前缀

public  long[] getFrequenciesKhz();

public  float getStreamVolumeDb();

将可选参数放在重载的末尾

如果您有具有可选参数的方法的重载,请将这些参数放在末尾,并与其他参数保持一致的顺序:

public int doFoo(boolean flag);

public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);

public int doFoo(boolean flag, int id);

为可选实参添加重载时,较简单方法的行为应与为较复杂方法提供默认实参时的行为完全相同。

推论:除非是为了添加可选实参或接受不同类型的实参(如果方法是多态的),否则不要重载方法。如果重载方法的功能从根本上不同,则为其指定新名称。

具有默认参数的方法必须使用 @JvmOverloads 进行注释(仅限 Kotlin)

具有默认参数的方法和构造函数必须使用 @JvmOverloads 进行注释,以保持二进制兼容性。

如需了解详情,请参阅官方 Kotlin-Java 互操作指南中的默认值的函数重载

class Greeting @JvmOverloads constructor(
  loudness: Int = 5
) {
  @JvmOverloads
  fun sayHello(prefix: String = "Dr.", name: String) = // ...
}

请勿移除默认形参值(仅限 Kotlin)

如果某个方法在发布时带有具有默认值的形参,那么移除该默认值会造成源代码中断。

最能区分和识别的方法参数应放在前面

如果您的方法有多个形参,请将最相关的形参放在前面。 指定标志和其他选项的参数不如描述所操作对象的参数重要。如果有完成回调,请将其放在最后。

public void openFile(int flags, String name);

public void openFileAsync(OnFileOpenedListener listener, String name, int flags);

public void setFlags(int mask, int flags);
public void openFile(String name, int flags);

public void openFileAsync(String name, int flags, OnFileOpenedListener listener);

public void setFlags(int flags, int mask);

另请参阅:在重载中将可选参数放在末尾

开拓者

建议使用 Builder 模式创建复杂的 Java 对象,并且该模式通常在以下 Android 场景中使用:

  • 生成的对象的属性应该是不可变的
  • 有大量必需的属性,例如许多构造函数实参
  • 在构建时,属性之间存在复杂的关系,例如需要验证步骤。请注意,这种复杂程度通常表明 API 的易用性存在问题。

考虑是否需要构建器。如果构建器用于以下用途,则在 API 表面中非常有用:

  • 仅配置一组可能很大的可选创建参数中的一部分
  • 配置许多不同的可选或必需创建参数,有时这些参数的类型相似或匹配,否则调用方网站可能会难以阅读或容易出错
  • 配置对象的增量创建,其中多个不同的配置代码可能会各自对构建器进行调用,作为实现细节
  • 允许通过在未来的 API 版本中添加其他可选的创建参数来扩展类型

如果您有一个类型,其必需参数不超过 3 个,且没有可选参数,那么您几乎总是可以跳过构建器,直接使用普通构造函数。

Kotlin 来源的类应优先选择带有默认实参的 @JvmOverloads 注释构造函数,而不是构建器,但也可以选择通过在前面概述的案例中提供构建器来提高 Java 客户端的可用性。

class Tone @JvmOverloads constructor(
  val duration: Long = 1000,
  val frequency: Int = 2600,
  val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
  class Builder {
    // ...
  }
}

构建器类必须返回构建器

构建器类必须通过从每个方法(build() 除外)返回构建器对象(例如 this)来启用方法链。其他构建的对象应作为实参传递,不要返回其他对象的 build。例如:

public static class Builder {
  public void setDuration(long);
  public void setFrequency(int);
  public DtmfConfigBuilder addDtmfConfig();
  public Tone build();
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}

在基构建器类必须支持扩展的极少数情况下,请使用泛型返回类型:

public abstract class Builder<T extends Builder<T>> {
  abstract T setValue(int);
}

public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
  T setValue(int);
  T setTypeSpecificValue(long);
}

必须通过构造函数创建构建器类

为了通过 Android API 表面保持一致的 build 器创建,所有 build 器必须通过构造函数创建,而不是通过静态创建器方法创建。对于基于 Kotlin 的 API,即使 Kotlin 用户应通过工厂方法/DSL 样式的创建机制隐式依赖构建器,Builder 也必须是公开的。库不得使用 @PublishedApi internal 有选择地向 Kotlin 客户端隐藏 Builder 类构造函数。

public class Tone {
  public static Builder builder();
  public static class Builder {
  }
}
public class Tone {
  public static class Builder {
    public Builder();
  }
}

构建器构造函数的所有实参都必须是必需实参(例如 @NonNull)

可选,例如 @Nullable,实参应移至 setter 方法。 如果未指定任何必需实参,构建器构造函数应抛出 NullPointerException(考虑使用 Objects.requireNonNull)。

构建器类应是其构建类型的最终静态内部类

为了在软件包内实现逻辑组织,构建器类通常应作为其构建类型的最终内部类公开,例如 Tone.Builder 而不是 ToneBuilder

构建器可能包含一个构造函数,用于从现有实例创建新实例

构建器可以包含复制构造函数,以从现有构建器或已构建的对象创建新的构建器实例。它们不应提供从现有构建器或构建对象创建构建器实例的替代方法。

public class Tone {
  public static class Builder {
    public Builder clone();
  }

  public Builder toBuilder();
}
public class Tone {
  public static class Builder {
    public Builder(Builder original);
    public Builder(Tone original);
  }
}
如果 build 具有复制构造函数,则 build setter 应采用 @Nullable 实参

如果可能从现有实例创建构建器的新实例,则重置至关重要。如果没有可用的复制构造函数,则构建器可以具有 @Nullable@NonNullable 实参。

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
构建器 setter 可以针对可选属性采用 @Nullable 实参

使用可为 null 的值作为二度输入通常更简单,尤其是在 Kotlin 中,它使用默认实参而不是构建器和重载。

此外,@Nullable setter 将与 getter 相匹配,对于可选属性,getter 必须为 @Nullable

Value createValue(@Nullable OptionalValue optionalValue) {
  Value.Builder builder = new Value.Builder();
  if (optionalValue != null) {
    builder.setOptionalValue(optionalValue);
  }
  return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
  return new Value.Builder()
    .setOptionalValue(optionalValue);
    .build();
}

// Or in other cases:

Value createValue() {
  return new Value.Builder()
    .setOptionalValue(condition ? new OptionalValue() : null);
    .build();
}

Kotlin 中的常见用法:

fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .apply { optionalValue?.let { setOptionalValue(it) } }
    .build()
fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .setOptionalValue(optionalValue)
    .build()

默认值(如果未调用 setter)以及 null 的含义必须在 setter 和 getter 中正确记录。

/**
 * ...
 *
 * <p>Defaults to {@code null}, which means the optional value won't be used.
 */

可以为可变属性提供 build 设置器,其中设置器可在构建的类中使用

如果您的类具有可变属性并且需要 Builder 类,请先问自己,您的类是否应该实际具有可变属性。

接下来,如果您确定需要可变属性,请确定以下哪种情形更适合您的预期使用情形:

  1. 构建的对象应可立即使用,因此应为所有相关属性(无论可变还是不可变)提供 setter。

    map.put(key, new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .setUsefulMutableProperty(usefulValue)
        .build());
    
  2. 在构建的对象变得有用之前,可能需要进行一些额外的调用,因此不应为可变属性提供 setter

    Value v = new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .build();
    v.setUsefulMutableProperty(usefulValue)
    Result r = v.performSomeAction();
    Key k = callSomeMethod(r);
    map.put(k, v);
    

请勿将这两种情况混为一谈。

Value v = new Value.Builder(requiredValue)
    .setImmutableProperty(immutableValue)
    .setUsefulMutableProperty(usefulValue)
    .build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);

构建器不应具有 getter

getter 应位于已构建的对象上,而不是构建器上。

构建器 setter 必须在构建的类上具有相应的 getter

public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }

  public long getDuration();
  public int getFrequency();
  public @NonNull List<DtmfConfig> getDtmfConfigs();
}

构建器方法命名

构建器方法名称应使用 setFoo()addFoo()clearFoo() 样式。

构建器类应声明 build() 方法

构建器类应声明一个 build() 方法,该方法返回所构建对象的实例。

构建器的 build() 方法必须返回 @NonNull 对象

构建器的 build() 方法应返回所构建对象的非 null 实例。如果由于参数无效而无法创建对象,则可以将验证推迟到 build 方法,并应抛出 IllegalStateException

不公开内部锁

公共 API 中的方法不应使用 synchronized 关键字。此关键字会导致您的对象或类被用作锁,并且由于它会暴露给其他代码,因此如果您的类之外的其他代码开始将其用于锁定目的,您可能会遇到意想不到的副作用。

请改为针对内部私有对象执行任何所需的锁定。

public synchronized void doThing() { ... }
private final Object mThingLock = new Object();

public void doThing() {
  synchronized (mThingLock) {
    ...
  }
}

访问器样式的方法应遵循 Kotlin 属性指南

从 Kotlin 源代码中查看时,使用 getsetis 前缀的访问器样式方法也将作为 Kotlin 属性提供。 例如,在 Java 中定义的 int getField() 在 Kotlin 中以属性 val field: Int 的形式提供。

出于此原因,并且为了满足开发者对访问器方法行为的普遍预期,使用访问器方法前缀的方法应与 Java 字段的行为类似。在以下情况下,请避免使用访问器样式的前缀:

  • 该方法具有副作用 - 建议使用更具描述性的方法名称
  • 该方法涉及计算量大的工作 - 请优先使用 compute
  • 该方法涉及阻塞或其他长时间运行的工作来返回值,例如 IPC 或其他 I/O -- 首选 fetch
  • 该方法会阻塞线程,直到可以返回值为止 - 建议使用 await
  • 该方法每次调用都会返回一个新的对象实例 - 建议使用 create
  • 该方法可能无法成功返回值 - 请优先使用 request

请注意,即使只执行一次计算量大的工作并将值缓存以供后续调用使用,仍算作执行计算量大的工作。Jank 不会分摊到各个帧。

为布尔值访问器方法使用 is 前缀

这是 Java 中布尔值方法和字段的标准命名惯例。一般来说,布尔值方法和变量名称应写成可通过返回值回答的问题。

Java 布尔值访问器方法应遵循 set/is 命名方案,字段应优先使用 is,如下所示:

// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();

// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();

final boolean isAvailable;

为 Java 访问器方法使用 set/is 或为 Java 字段使用 is 将允许它们从 Kotlin 用作属性:

obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return

属性和访问器方法通常应使用肯定式命名,例如 Enabled 而不是 Disabled。使用否定术语会颠倒 truefalse 的含义,并使行为推演变得更加困难。

// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);

如果布尔值描述的是属性的包含或所有权,您可以使用 has 而不是 is;不过,这适用于 Kotlin 属性语法:

// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();

以下是一些可能更适合的替代前缀,包括 canshould

// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();

// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();

用于切换行为或功能的方法可以使用 is 前缀和 Enabled 后缀:

// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()

同样,指示对其他行为或功能的依赖性的方法可以使用 is 前缀和 SupportedRequired 后缀:

// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()

一般来说,方法名称应写成问题,返回值是问题的答案。

Kotlin 属性方法

对于类属性 var foo: Foo,Kotlin 将使用以下一致的规则生成 get/set 方法:为 getter 添加 get 前缀并将第一个字符大写,为 setter 添加 set 前缀并将第一个字符大写。相应属性声明将分别生成名为 public Foo getFoo()public void setFoo(Foo foo) 的方法。

如果属性的类型为 Boolean,则在名称生成方面适用另一条规则:如果属性名称以 is 开头,则不会为 getter 方法名称添加 get 前缀,而是将属性名称本身用作 getter。因此,为了遵循命名准则,最好使用 is 前缀来命名 Boolean 属性

var isVisible: Boolean

如果您的属性属于上述例外情况之一,并且以相应的前缀开头,请在属性上使用 @get:JvmName 注释来手动指定相应的名称:

@get:JvmName("hasTransientState")
var hasTransientState: Boolean

@get:JvmName("canRecord")
var canRecord: Boolean

@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean

位掩码访问器

如需查看有关定义位掩码标志的 API 指南,请参阅使用 @IntDef 表示位掩码标志

设置器

应提供两种 setter 方法:一种接受完整的位串并覆盖所有现有标志,另一种接受自定义位掩码以实现更高的灵活性。

/**
 * Sets the state of all scroll indicators.
 * <p>
 * See {@link #setScrollIndicators(int, int)} for usage information.
 *
 * @param indicators a bitmask of indicators that should be enabled, or
 *                   {@code 0} to disable all indicators
 * @see #setScrollIndicators(int, int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators);

/**
 * Sets the state of the scroll indicators specified by the mask. To change
 * all scroll indicators at once, see {@link #setScrollIndicators(int)}.
 * <p>
 * When a scroll indicator is enabled, it will be displayed if the view
 * can scroll in the direction of the indicator.
 * <p>
 * Multiple indicator types may be enabled or disabled by passing the
 * logical OR of the specified types. If multiple types are specified, they
 * will all be set to the same enabled state.
 * <p>
 * For example, to enable the top scroll indicator:
 * {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
 * <p>
 * To disable the top scroll indicator:
 * {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
 *
 * @param indicators a bitmask of values to set; may be a single flag,
 *                   the logical OR of multiple flags, or 0 to clear
 * @param mask a bitmask indicating which indicator flags to modify
 * @see #setScrollIndicators(int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);

getter

应提供一个 getter 来获取完整的位掩码。

/**
 * Returns a bitmask representing the enabled scroll indicators.
 * <p>
 * For example, if the top and left scroll indicators are enabled and all
 * other indicators are disabled, the return value will be
 * {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
 * <p>
 * To check whether the bottom scroll indicator is enabled, use the value
 * of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
 *
 * @return a bitmask representing the enabled scroll indicators
 */
@ScrollIndicators
public int getScrollIndicators();

使用 public 而不是 protected

在公共 API 中,始终首选 public 而不是 protected。从长远来看,受保护的访问权限最终会带来麻烦,因为实现者必须进行替换,才能在默认情况下外部访问权限同样有效的情况下提供公共访问器。

请注意,protected 可见性并不会阻止开发者调用 API,只会让调用变得稍微麻烦一些。

实现 equals() 和 hashCode() 中的一个或同时实现这两个方法

如果您替换其中一个,则必须替换另一个。

为数据类实现 toString()

建议数据类替换 toString(),以帮助开发者调试代码。

记录输出是用于程序行为还是调试

决定是否要让程序行为依赖于您的实现。 例如,UUID.toString()File.toString() 会记录其特定格式,以供程序使用。如果您仅公开用于调试的信息(例如 Intent),则表示从超类继承文档。

请勿包含额外信息

toString() 中提供的所有信息也应通过该对象的公共 API 提供。否则,您就是在鼓励开发者解析并依赖您的 toString() 输出,这会妨碍未来的更改。一种好的做法是仅使用对象的公共 API 来实现 toString()

不鼓励依赖调试输出

虽然无法阻止开发者依赖调试输出,但通过在对象的 toString() 输出中包含 System.identityHashCode,可以大大降低两个不同对象具有相同 toString() 输出的可能性。

@Override
public String toString() {
  return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}

这可以有效地阻止开发者在对象上编写 assertThat(a.toString()).isEqualTo(b.toString()) 等测试断言。

在返回新创建的对象时使用 createFoo

对于将创建返回值(例如通过构造新对象)的方法,请使用前缀 create,而不是 getnew

如果方法将创建要返回的对象,请在方法名称中明确说明这一点。

public FooThing getFooThing() {
  return new FooThing();
}
public FooThing createFooThing() {
  return new FooThing();
}

接受 File 对象的方法也应接受流

Android 上的数据存储位置并不总是磁盘上的文件。例如,跨用户边界传递的内容表示为 content:// Uri。为了能够处理各种数据源,接受 File 对象的 API 也应接受 InputStreamOutputStream 或同时接受这两者。

public void setDataSource(File file)
public void setDataSource(InputStream stream)

获取并返回原始基元,而不是 boxed 版本

如果您需要传达缺失值或 null 值,请考虑使用 -1Integer.MAX_VALUEInteger.MIN_VALUE

public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)

避免使用原始类型的等效类可以避免这些类的内存开销、对值的方法访问,更重要的是,可以避免在原始类型和对象类型之间进行转换时出现的自动装箱。避免这些行为可以节省内存和临时分配,从而避免昂贵且更频繁的垃圾回收。

使用注释来明确有效的形参和返回值

添加了开发者注释,以帮助明确各种情况下的允许值。这样一来,当开发者提供错误的值时(例如,当框架需要一组特定的常量值时,传递任意 int),工具可以更轻松地帮助开发者。在适当的时候使用以下任意注解:

是否可为 null

Java API 需要显式可为 null 性注解,但可为 null 性概念是 Kotlin 语言的一部分,因此 Kotlin API 中绝不应使用可为 null 性注解。

@Nullable:表示给定的返回值、形参或字段可以为 null:

@Nullable
public String getName()

public void setName(@Nullable String name)

@NonNull:表示给定的返回值、形参或字段不能为 null。将内容标记为 @Nullable 在 Android 中相对较新,因此 Android 的大多数 API 方法都没有得到一致的记录。因此,我们有“未知、@Nullable@NonNull”这三种状态,这也是 @NonNull 属于 API 指南的原因:

@NonNull
public String getName()

public void setName(@NonNull String name)

对于 Android 平台文档,除非在参数文档的其他位置明确使用“null”,否则注释方法参数会自动生成“此值可能为 null”形式的文档。

现有的“并非真正可为 null”的方法:如果 API 中没有声明 @Nullable 注释的现有方法在特定明显的情况下(例如 findViewById())可以返回 null,则可以为该方法添加 @Nullable 注释。对于不想进行 null 检查的开发者,应添加会抛出 IllegalArgumentException 的配套 @NotNull requireFoo() 方法。

接口方法:实现接口方法时,新 API 应添加适当的注释,例如 Parcelable.writeToParcel()(即,实现类中的相应方法应为 writeToParcel(@NonNull Parcel, int),而非 writeToParcel(Parcel, int));缺少注释的现有 API 无需“修复”。

null 性强制执行

在 Java 中,建议使用 Objects.requireNonNull()@NonNull 参数执行输入验证,并在参数为 null 时抛出 NullPointerException此操作会在 Kotlin 中自动执行。

资源

资源标识符:表示特定资源 ID 的整数形参应使用相应的资源类型定义进行注释。除了涵盖所有类型的 @AnyRes 之外,每种类型的资源都有对应的注释,例如 @StringRes@ColorRes@AnimRes。例如:

public void setTitle(@StringRes int resId)

针对常量集的 @IntDef

魔数常量:旨在接收由公共常量表示的有限组可能值的 Stringint 参数应使用 @StringDef@IntDef 进行适当的注释。借助这些注释,您可以创建新的注释,该注释可像 typedef 一样用于允许的参数。例如:

/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
  NAVIGATION_MODE_STANDARD,
  NAVIGATION_MODE_LIST,
  NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}

public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);

建议方法检查带注释的形参的有效性,如果形参不属于 @IntDef,则抛出 IllegalArgumentException

针对位掩码标志的 @IntDef

注释还可以指定常量是标志,并且可以与 & 和 I 结合使用:

/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
  FLAG_USE_LOGO,
  FLAG_SHOW_HOME,
  FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

用于字符串常量集的 @StringDef

还有 @StringDef 注释,它与上一部分中的 @IntDef 完全一样,但适用于 String 常量。您可以包含多个“前缀”值,这些值用于自动为所有值生成文档。

用于 SDK 常量的 @SdkConstant

@SdkConstant 当公共字段为以下 SdkConstant 值之一时,请使用此注释:ACTIVITY_INTENT_ACTIONBROADCAST_INTENT_ACTIONSERVICE_ACTIONINTENT_CATEGORYFEATURE

@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";

为替换提供兼容的可为 null 性

为了实现 API 兼容性,替换的可为 null 性应与父级的当前可为 null 性兼容。下表显示了兼容性预期。简而言之,替换项的限制应不低于其替换的元素的限制。

类型 家长 儿童
返回类型 未添加注释 未添加注释或非 null
返回类型 是否可为 null? 可为 null 或不可为 null
返回类型 Nonnull Nonnull
有趣实参 未添加注释 未添加注释或可为 null
有趣实参 是否可为 null? 是否可为 null?
有趣实参 Nonnull 可为 null 或不可为 null

尽可能使用不可为 null 的实参(例如 @NonNull)

如果方法被重载,最好让所有实参都不为 null。

public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }

此规则也适用于重载的属性 setter。主要实参应为非 null,并且清除相应属性应通过单独的方法来实现。这样可以防止“无意义”的调用,即开发者必须设置尾随参数,即使这些参数不是必需的。

public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)

// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()

最好为容器使用不可为 null 的(例如 @NonNull)返回类型

对于 BundleCollection 等容器类型,返回一个空容器(如果适用,则返回不可变容器)。如果使用 null 来区分容器的可用性,请考虑提供单独的布尔值方法。

@NonNull
public Bundle getExtras() { ... }

get 和 set 对的可为 null 性注释必须一致

单个逻辑属性的 get 和 set 方法对在可为 null 性注释方面应始终保持一致。如果不遵循此指南,Kotlin 的属性语法将失效,因此向现有属性方法添加不一致的可为 null 性注释会造成 Kotlin 用户的源代码中断。

@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }

失败或错误情况下的返回值

所有 API 都应允许应用对错误做出反应。返回 false-1null 或其他“出了点问题”的通用值,无法让开发者充分了解失败情况,从而无法设定用户预期或准确跟踪其应用在实际使用中的可靠性。设计 API 时,请想象您正在构建应用。如果您遇到错误,API 是否会提供足够的信息来向用户显示错误或做出适当的反应?

  1. 在异常消息中包含详细信息是可以的(也建议这样做),但开发者不应必须解析该消息才能妥善处理错误。详细的错误代码或其他信息应作为方法公开。
  2. 请确保您选择的错误处理选项可让您灵活地在日后引入新的错误类型。对于 @IntDef,这意味着包含 OTHERUNKNOWN 值 - 返回新代码时,您可以检查调用方的 targetSdkVersion,以避免返回应用不知道的错误代码。对于异常,请使用您的异常实现的通用超类,以便处理该类型的任何代码也能捕获并处理子类型。
  3. 开发者应很难或无法意外忽略错误 - 如果您的错误是通过返回值传达的,请使用 @CheckResult 注释您的方法。

如果因开发者做错事(例如忽略输入参数的限制或未能检查可观测状态)而达到失败或错误条件,最好抛出 ? extends RuntimeException

如果 setter 或操作(例如 perform)方法可能会因异步更新的状态或开发者无法控制的条件而失败,则这些方法可能会返回整数状态代码。

状态代码应在包含类中定义为 public static final 字段,以 ERROR_ 为前缀,并在 @hide @IntDef 注释中进行枚举。

方法名称应始终以动词开头,而不是以主语开头

方法名称应始终以动词(例如 getcreatereload 等)开头,而不是以您要操作的对象开头。

public void tableReload() {
  mTable.reload();
}
public void reloadTable() {
  mTable.reload();
}

首选将集合 类型用作返回类型或参数类型,而不是数组

与数组相比,泛型类型化集合接口具有多项优势,包括围绕唯一性和排序的更强大的 API 协定、对泛型的支持以及许多方便开发者使用的便捷方法。

基元的例外情况

如果元素是基本类型,优先使用数组,以避免自动装箱的开销。请参阅采用并返回原始基元,而不是封装版本

对性能敏感的代码的例外情况

在某些情况下,如果 API 用于对性能敏感的代码(例如图形或其他测量/布局/绘制 API),则可以使用数组而不是集合,以减少分配和内存抖动。

Kotlin 的例外情况

Kotlin 数组是不变的,并且 Kotlin 语言提供了大量与数组相关的实用程序 API,因此对于旨在从 Kotlin 访问的 Kotlin API,数组与 ListCollection 相当。

优先选择 @NonNull 集合

始终首选集合对象的 @NonNull。返回空集合时,请使用适当的 Collections.empty 方法返回低成本、类型正确且不可变的集合对象。

在支持类型注释的情况下,始终首选 @NonNull 作为集合元素。

如果使用数组而非集合,您也应首选 @NonNull(请参阅上一个条目)。如果您担心对象分配问题,可以创建一个常量并将其传递出去 - 毕竟,空数组是不可变的。示例:

private static final int[] EMPTY_USER_IDS = new int[0];

@NonNull
public int[] getUserIds() {
  int [] userIds = mService.getUserIds();
  return userIds != null ? userIds : EMPTY_USER_IDS;
}

集合可变性

Kotlin API 默认应首选集合的只读(而非 Mutable)返回类型,除非 API 契约明确要求使用可变返回类型。

不过,Java API 默认应首选可变返回类型,因为 Java API 的 Android 平台实现尚未提供不可变集合的便捷实现。此规则的例外情况是 Collections.empty 返回类型,该类型是不可变的。如果客户端可能会有意或无意地利用可变性来破坏 API 的预期使用模式,Java API 应强烈考虑返回集合的浅层副本。

@Nullable
public PermissionInfo[] getGrantedPermissions() {
  return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
  if (mPermissions == null) {
    return Collections.emptySet();
  }
  return new ArraySet<>(mPermissions);
}

显式可变返回类型

返回集合的 API 最好不要在返回后修改返回的集合对象。如果返回的集合必须以某种方式更改或重复使用(例如,可变数据集的改编视图),则必须明确记录内容可以更改的时间的精确行为,或者遵循已建立的 API 命名惯例。

/**
 * Returns a view of this object as a list of [Item]s.
 */
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)

Kotlin .asFoo() 约定如下所述,允许在原始集合发生更改时更改 .asList() 返回的集合。

返回的数据类型对象的可变性

与返回集合的 API 类似,返回数据类型对象的 API 最好不要在返回后修改所返回对象的属性。

val tempResult = DataContainer()

fun add(other: DataContainer): DataContainer {
  tempResult.innerValue = innerValue + other.innerValue
  return tempResult
}
fun add(other: DataContainer): DataContainer {
  return DataContainer(innerValue + other.innerValue)
}

少数情况下,一些对性能敏感的代码可能会受益于对象池化或重用。请勿自行编写对象池数据结构,也请勿在公共 API 中公开重复使用的对象。无论哪种情况,都应非常谨慎地管理并发访问。

使用 vararg 形参类型

建议 Kotlin 和 Java API 在以下情况下使用 vararg:开发者很可能会在调用方创建一个数组,其唯一目的是传递多个相同类型的相关参数。

public void setFeatures(Feature[] features) { ... }

// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }

// Developer code
setFeatures(Features.A, Features.B, Features.C);

防御性复制

vararg 参数的 Java 和 Kotlin 实现都会编译为相同的数组支持字节码,因此可以从 Java 代码中调用具有可变数组的参数。强烈建议 API 设计者在数组形参将持久保存到字段或匿名内部类的情况下,创建该形参的防御性浅层副本。

public void setValues(SomeObject... values) {
   this.values = Arrays.copyOf(values, values.length);
}

请注意,创建防御性副本并不能防止在初始方法调用和创建副本之间发生并发修改,也不能防止数组中包含的对象发生突变。

通过集合类型形参或返回类型提供正确的语义

List<Foo> 是默认选项,但您可以考虑使用其他类型来提供更多含义:

  • 如果您的 API 不关心元素的顺序,并且不允许重复元素或重复元素没有意义,请使用 Set<Foo>

  • 如果您的 API 不关心顺序并允许重复,则为 Collection<Foo>,

Kotlin 转换函数

Kotlin 经常使用 .toFoo().asFoo() 从现有对象获取不同类型的对象,其中 Foo 是转换的返回类型的名称。这与熟悉的 JDK Object.toString() 保持一致。Kotlin 更进一步,将其用于 25.toFloat() 等原始类型转换。

名为 .toFoo().asFoo() 的转化之间的区别非常重要:

创建新的独立对象时,请使用 .toFoo()

.toString() 类似,“to”转换会返回一个新的独立对象。如果稍后修改原始对象,新对象不会反映这些更改。 同样,如果稍后修改对象,对象不会反映这些更改。

fun Foo.toBundle(): Bundle = Bundle().apply {
    putInt(FOO_VALUE_KEY, value)
}

创建依赖性封装容器、装饰对象或转换时,请使用 .asFoo()

在 Kotlin 中,转换是使用 as 关键字执行的。它反映了界面的变化,但不是身份的变化。在扩展函数中用作前缀时,.asFoo() 会修饰接收者。原始接收器对象中的突变将反映在 asFoo() 返回的对象中。新 Foo 对象中的突变可能会反映在原始对象中。

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
    collect {
        emit(it)
    }
}

转换函数应编写为扩展函数

在接收者和结果类定义之外编写转换函数可减少类型之间的耦合。理想的转换只需要对原始对象进行公共 API 访问。此示例证明,开发者也可以为自己偏好的类型编写类似的转换。

抛出适当的特定异常

方法不得抛出 java.lang.Exceptionjava.lang.Throwable 等泛型异常,而必须使用适当的特定异常(例如 java.lang.NullPointerException),以便开发者能够处理异常,而不会过于宽泛。

与直接提供给公开调用方法的实参无关的错误应抛出 java.lang.IllegalStateException,而不是 java.lang.IllegalArgumentExceptionjava.lang.NullPointerException

监听器和回调

以下是与用于监听器和回调机制的类和方法相关的规则。

回调类名称应采用单数形式

不过,应使用 MyObjectCallback 代替 MyObjectCallbacks

回调方法名称应采用 on 格式

onFooEvent 表示正在发生 FooEvent,回调应采取相应行动。

过去时态与现在时态应描述时间行为

与事件相关的回调方法应命名为指示事件是已经发生还是正在发生。

例如,如果该方法是在执行点击操作后调用的:

public void onClicked()

不过,如果该方法负责执行点击操作,则:

public boolean onClick()

回调注册

当可以向对象添加或从中移除监听器或回调时,关联的方法应命名为 add 和 remove register 和 unregister。与类或同一软件包中的其他类所用的现有惯例保持一致。如果不存在此类先例,则首选添加和移除。

涉及注册或取消注册回调的方法应指定回调类型的完整名称。

public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);

避免为回调使用 getter

请勿添加 getFooCallback() 方法。在开发者可能希望将现有回调与其自己的替换项链接在一起的情况下,这是一种诱人的应急方案,但它很脆弱,并且使得组件开发者难以推断当前状态。例如,

  • 开发者 A 致电 setFooCallback(a)
  • 开发者 B 调用 setFooCallback(new B(getFooCallback()))
  • 开发者 A 希望移除其回调 a,但在不知道 B 的类型的情况下,无法移除该回调,并且 B 已构建为允许对其封装的回调进行此类修改。

接受执行程序来控制回调分派

注册没有明确线程处理预期的回调(几乎在界面工具包之外的任何位置)时,强烈建议在注册时包含 Executor 参数,以便开发者指定将调用回调的线程。

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

作为我们通常的有关可选形参的指南的例外情况,即使 Executor 不是形参列表中的最后一个实参,也可以提供省略它的重载。如果未提供 Executor,则应使用 Looper.getMainLooper() 在主线程上调用回调,并且应在关联的重载方法中记录这一点。

/**
 * ...
 * Note that the callback will be executed on the main thread using
 * {@link Looper.getMainLooper()}. To specify the execution thread, use
 * {@link registerFooCallback(Executor, FooCallback)}.
 * ...
 */
public void registerFooCallback(
    @NonNull FooCallback callback)

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

Executor 实现注意事项:请注意,以下是一个有效的执行器!

public class SynchronousExecutor implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

这意味着,在实现采用此形式的 API 时,应用进程端的传入 binder 对象实现必须在调用应用在应用提供的 Executor 上提供的回调之前调用 Binder.clearCallingIdentity()。这样一来,任何使用 binder 身份(例如 Binder.getCallingUid())进行权限检查的应用代码都会正确地将运行的代码归因于应用,而不是调用到应用中的系统进程。如果您的 API 的用户想要获取调用者的 UID 或 PID 信息,那么这应该是 API 表面的一部分,而不是基于他们提供的 Executor 运行位置的隐式部分。

您的 API 支持指定 Executor。在对性能要求较高的应用中,可能需要立即运行代码或与 API 的反馈同步运行代码。接受 Executor 允许这样做。 防御性地创建额外的 HandlerThread 或与 trampoline 类似的组件会破坏这种理想的用例。

如果应用要在其自己的进程中的某个位置运行代价高昂的代码,请允许它们这样做。应用开发者为克服您的限制而找到的解决方法在长期来看将更难支持。

单个回调的例外情况:当所报告事件的性质要求仅支持单个回调实例时,请使用以下样式:

public void setFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

public void clearFooCallback()

使用 Executor 而不是 Handler

过去,Android 的 Handler 用作将回调执行重定向到特定 Looper 线程的标准。此标准已更改为首选 Executor,因为大多数应用开发者会管理自己的线程池,从而使主线程或界面线程成为应用可用的唯一 Looper 线程。使用 Executor 可让开发者获得所需的控制权,以便重复使用其现有的/首选的执行上下文。

kotlinx.coroutines 或 RxJava 等现代并发库提供了自己的调度机制,可在需要时执行自己的调度,因此提供使用直接执行器(例如 Runnable::run)的功能非常重要,这样可以避免双重线程跳转带来的延迟。例如,一个跃点用于使用 Handler 发布到 Looper 线程,然后是来自应用并发框架的另一个跃点。

此准则的例外情况很少。常见的例外情况申诉包括:

我必须使用 Looper,因为我需要使用 Looperepoll活动。 此例外请求已获批准,因为在这种情况下无法实现 Executor 的优势。

我不希望应用代码阻止我的线程发布事件。对于在应用进程中运行的代码,此例外请求通常不会获准。如果应用错误地设置了此值,只会影响自身,而不会影响整体系统运行状况。正确使用并发或使用通用并发框架的应用不应受到额外的延迟时间惩罚。

Handler 在本地与同一类中的其他类似 API 保持一致。 此例外请求会根据具体情况获得批准。首选添加基于 Executor 的重载,将 Handler 实现迁移为使用新的 Executor 实现。(myHandler::post 是有效的 Executor!)根据类的大小、现有 Handler 方法的数量以及开发者需要同时使用现有 Handler 方法和新方法的可能性,可以授予例外权限来添加新的基于 Handler 的方法。

注册中的对称性

如果可以添加或注册某项内容,也应该可以移除/取消注册该内容。方法

registerThing(Thing)

应具有匹配的

unregisterThing(Thing)

提供请求标识符

如果开发者可以合理地重复使用回调,请提供一个标识符对象,将回调与请求相关联。

class RequestParameters {
  public int getId() { ... }
}

class RequestExecutor {
  public void executeRequest(
    RequestParameters parameters,
    Consumer<RequestParameters> onRequestCompletedListener) { ... }
}

多方法回调对象

多方法回调应首选 interface,并在添加到之前发布的接口时使用 default 方法。之前,由于 Java 7 中缺少 default 方法,此指南建议使用 abstract class

public interface MostlyOptionalCallback {
  void onImportantAction();
  default void onOptionalInformation() {
    // Empty stub, this method is optional.
  }
}

在对非阻塞函数调用进行建模时使用 android.os.OutcomeReceiver

OutcomeReceiver<R,E> 在成功时报告结果值 R,否则报告 E : Throwable - 与普通方法调用相同。在将返回结果或抛出异常的阻塞方法转换为非阻塞异步方法时,请使用 OutcomeReceiver 作为回调类型:

interface FooType {
  // Before:
  public FooResult requestFoo(FooRequest request);

  // After:
  public void requestFooAsync(FooRequest request, Executor executor,
      OutcomeReceiver<FooResult, Throwable> callback);
}

以这种方式转换的异步方法始终返回 voidrequestFoo 将返回的任何结果都会改为通过在提供的 executor 上调用 requestFooAsynccallback 参数的 OutcomeReceiver.onResult 来报告。requestFoo 会抛出的任何异常都会以相同的方式报告给 OutcomeReceiver.onError 方法。

使用 OutcomeReceiver 报告异步方法结果还可为使用 androidx.core:core-ktx 中的 Continuation.asOutcomeReceiver 扩展函数的异步方法提供 Kotlin suspend fun 封装容器:

suspend fun FooType.requestFoo(request: FooRequest): FooResult =
  suspendCancellableCoroutine { continuation ->
    requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
  }

借助此类扩展,Kotlin 客户端可以像调用普通函数一样方便地调用非阻塞异步方法,而不会阻塞调用线程。这些平台 API 的 1 对 1 扩展程序在与标准版本兼容性检查和注意事项相结合时,可能会作为 Jetpack 中的 androidx.core:core-ktx 制品提供。如需了解详情、取消注意事项和示例,请参阅 asOutcomeReceiver 的文档。

如果异步方法不符合以下语义:在工作完成时返回结果或抛出异常,则不应使用 OutcomeReceiver 作为回调类型。请改为考虑以下部分中列出的其他选项之一。

优先使用功能接口,而不是创建新的单一抽象方法 (SAM) 类型

API 级别 24 添加了 java.util.function.*参考文档)类型,这些类型提供通用的 SAM 接口(例如 Consumer<T>),适合用作回调 lambda。在许多情况下,创建新的 SAM 接口在类型安全或传达意图方面几乎没有价值,同时还会不必要地扩大 Android API Surface 区域。

请考虑使用以下通用接口,而不是创建新接口:

SAM 参数的放置位置

SAM 参数应放在最后,以便从 Kotlin 中以惯用方式使用,即使该方法被重载为具有其他参数也是如此。

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

文档

以下是关于 API 公共文档 (Javadoc) 的规则。

所有公共 API 都必须有文档

所有公共 API 都必须有足够的文档来解释开发者如何使用该 API。假设开发者使用自动补全功能或在浏览 API 参考文档时找到了该方法,并且从相邻的 API 表面(例如,同一类)获得了最少量的上下文。

方法

方法参数和返回值必须分别使用 @param@return 文档注释进行记录。将 Javadoc 正文的格式设置为好像前面有“此方法...”一样。

如果某个方法不接受任何参数,没有特殊注意事项,并且返回的内容与方法名称所说的内容一致,您可以省略 @return 并编写类似于以下内容的文档:

/**
 * Returns the priority of the thread.
 */
@IntRange(from = 1, to = 10)
public int getPriority() { ... }

文档应链接到其他文档,以获取相关的常量、方法和其他元素。使用 Javadoc 标记(例如 @see{@link foo}),而不仅仅是纯文本字词。

对于以下来源示例:

public static final int FOO = 0;
public static final int BAR = 1;

请勿使用纯文本或代码字体:

/**
 * Sets value to one of FOO or <code>BAR</code>.
 *
 * @param value the value being set, one of FOO or BAR
 */
public void setValue(int value) { ... }

请改用以下链接:

/**
 * Sets value to one of {@link #FOO} or {@link #BAR}.
 *
 * @param value the value being set
 */
public void setValue(@ValueType int value) { ... }

请注意,在参数上使用 IntDef 注释(例如 @ValueType)会自动生成指定允许类型的文档。如需详细了解 IntDef,请参阅有关注释的指南。

添加 Javadoc 时运行 update-api 或 docs 目标

添加 @link@see 标记时,此规则尤为重要,请确保输出符合预期。Javadoc 中的错误输出通常是由于链接无效造成的。update-apidocs Make 目标会执行此检查,但如果您仅更改 Javadoc,而不需要运行 update-api 目标,docs 目标可能会更快。

使用 {@code foo} 区分 Java 值

使用 {@code...} 封装 truefalsenull 等 Java 值,以便将它们与文档文本区分开来。

在 Kotlin 源代码中编写文档时,您可以像在 Markdown 中一样使用反引号将代码括起来。

@param 和 @return 摘要应为单个句子片段

形参和返回值摘要应以小写字符开头,并且只能包含一个句子片段。如果您有超出单个句子的其他信息,请将其移至方法 Javadoc 正文中:

/**
 * @param e The element to be appended to the list. This must not be
 *       null. If the list contains no entries, this element will
 *       be added at the beginning.
 * @return This method returns true on success.
 */

应更改为:

/**
 * @param e element to be appended to this list, must be non-{@code null}
 * @return {@code true} on success, {@code false} otherwise
 */

文档注释需要说明

记录了注释 @hide@removed 为何从公共 API 中隐藏。 添加了有关如何替换带有 @deprecated 注释的 API 元素的说明。

使用 @throws 记录异常

如果某个方法抛出受检异常(例如 IOException),请使用 @throws 记录该异常。对于旨在供 Java 客户端使用的 Kotlin 源 API,请使用 @Throws 注释函数。

如果某个方法抛出指示可预防错误的未检查异常(例如 IllegalArgumentExceptionIllegalStateException),请记录该异常并说明抛出该异常的原因。抛出的异常还应指明抛出原因。

某些未检查的异常情况被认为是隐式的,不需要记录,例如 NullPointerExceptionIllegalArgumentException,其中实参与将 API 协定嵌入到方法签名中的 @IntDef 或类似注释不匹配:

/**
 * ...
 * @throws IOException If it cannot find the schema for {@code toVersion}
 * @throws IllegalStateException If the schema validation fails
 */
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
    boolean validateDroppedTables, Migration... migrations) throws IOException {
  // ...
  if (!dbPath.exists()) {
    throw new IllegalStateException("Cannot find the database file for " + name
        + ". Before calling runMigrations, you must first create the database "
        + "using createDatabase.");
  }
  // ...

或者,在 Kotlin 中:

/**
 * ...
 * @throws IOException If something goes wrong reading the file, such as a bad
 *                     database header or missing permissions
 */
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
  // ...
  val read = input.read(buffer)
    if (read != 4) {
      throw IOException("Bad database header, unable to read 4 bytes at " +
          "offset 60")
    }
  }
  // ...

如果该方法调用可能会抛出异常的异步代码,请考虑开发者如何发现并响应此类异常。通常,这涉及将异常转发到回调,并记录接收异常的方法上抛出的异常。除非异步异常确实是从带注释的方法重新抛出的,否则不应使用 @throws 对其进行记录。

在文档的第一句话末尾添加句点

Doclava 工具会以简单的方式解析文档,一旦看到句点 (.) 后跟空格,就会结束概要文档(类文档顶部快速说明中使用的第一句话)。这会导致两个问题:

  • 如果短文档未以句点结尾,并且该成员继承了会被该工具提取的文档,那么概要也会提取这些继承的文档。例如,请参阅 R.attr 文档中的 actionBarTabStyle,其中包含添加到概要中的维度说明。
  • 出于同样的原因,请避免在第一句中使用“例如”,因为 Doclava 会在“g.”之后结束概要文档。例如,请参阅 View.java 中的 TEXT_ALIGNMENT_CENTER。 请注意,Metalava 会在句点后插入不间断空格来自动更正此错误;不过,最好一开始就不要犯这个错误。

将文档格式设置为以 HTML 呈现

Javadoc 以 HTML 格式呈现,因此请相应地设置这些文档的格式:

  • 换行应使用显式 <p> 标记。请勿添加结束 </p> 标记。

  • 请勿使用 ASCII 渲染列表或表格。

  • 列表应分别使用 <ul><ol> 来表示无序列表和有序列表。 每个文本项都应以 <li> 标记开头,但不需要添加结束 </li> 标记。最后一个商品后必须有结束标记 </ul></ol>

  • 表格应使用 <table><tr>(用于行)、<th>(用于标题)和 <td>(用于单元格)。所有表格标记都需要匹配的结束标记。您可以在任何标记上使用 class="deprecated" 来表示弃用。

  • 如需创建内嵌代码字体,请使用 {@code foo}

  • 如需创建代码块,请使用 <pre>

  • 浏览器会解析 <pre> 块内的所有文本,因此请谨慎使用方括号 <>。您可以使用 &lt;&gt; HTML 实体对其进行转义。

  • 或者,如果您将有问题的部分用 {@code foo} 括起来,也可以在代码段中保留原始方括号 <>。例如:

    <pre>{@code <manifest>}</pre>
    

遵循 API 参考样式指南

为了确保类摘要、方法说明、参数说明和其他项的样式一致,请遵循官方 Java 语言指南中的建议,详见如何为 Javadoc 工具编写文档注释

Android 框架专用规则

这些规则涉及特定于 API 和内置于 Android 框架中的行为的 API、模式和数据结构(例如 BundleParcelable)。

Intent 构建器应使用 create*Intent() 模式

Intent 的创建者应使用名为 createFooIntent() 的方法。

使用 Bundle 而不是创建新的通用数据结构

避免创建新的通用数据结构来表示任意键到类型化值的映射。请考虑改用 Bundle

这种情况通常发生在编写用作非平台应用和服务之间通信渠道的平台 API 时,其中平台不读取通过该渠道发送的数据,并且 API 合约可能部分定义在平台外部(例如,在 Jetpack 库中)。

如果平台确实读取了数据,请避免使用 Bundle,而应使用强类型数据类。

Parcelable 实现必须具有公共 CREATOR 字段

Parcelable 膨胀通过 CREATOR(而非原始构造函数)公开。如果某个类实现了 Parcelable,则其 CREATOR 字段也必须是公共 API,并且采用 Parcel 实参的类构造函数必须是私有的。

使用 CharSequence 作为界面字符串

在界面中显示字符串时,请使用 CharSequence 以允许使用 Spannable 实例。

如果只是一个键或一些其他用户看不到的标签或值,则使用 String 即可。

避免使用枚举

在所有平台 API 中,必须使用 IntDef 而不是枚举,并且在非捆绑的库 API 中应强烈考虑使用 IntDef。仅当您确定不会添加新值时,才使用枚举。

IntDef的优势:

  • 启用随时间推移添加值的功能
    • 如果 Kotlin when 语句因平台中添加了枚举值而不再详尽,则可能会在运行时失败。
  • 运行时不使用任何类或对象,仅使用基元
    • 虽然 R8 或缩小化可以避免未捆绑的库 API 产生此开销,但此优化无法影响平台 API 类。

枚举的优势

  • Java、Kotlin 的惯用语言功能
  • 启用详尽的 switch、when 语句使用情况
    • 注意 - 值不得随时间变化,请参阅上一个列表
  • 范围明确且易于发现的命名
  • 启用编译时验证
    • 例如,Kotlin 中返回值的 when 语句
  • 是一个可实现接口、具有静态辅助函数、公开成员或扩展方法以及公开字段的正常运行的类。

遵循 Android 软件包分层层次结构

android.* 软件包层次结构具有隐式排序,其中较低级别的软件包不能依赖于较高级别的软件包。

避免提及 Google、其他公司及其产品

Android 平台是一个开源项目,旨在保持供应商中立。该 API 应该是通用的,并且系统集成商或具有必要权限的应用都可以同样轻松地使用它。

Parcelable 实现应为最终实现

平台定义的 Parcelable 类始终从 framework.jar 加载,因此应用尝试替换 Parcelable 实现是无效的。

如果发送应用扩展了 Parcelable,接收应用将没有发送方的自定义实现来解封装。关于向后兼容性的注意事项:如果您的类在过去不是 final 类,但没有公开可用的构造函数,您仍然可以将其标记为 final

调用系统进程的方法应将 RemoteException 重新抛出为 RuntimeException

RemoteException 通常由内部 AIDL 抛出,表示系统进程已终止,或者应用尝试发送的数据过多。在这两种情况下,公共 API 都应重新抛出 RuntimeException,以防止应用持久保留安全或政策决策。

如果您知道 Binder 调用的另一端是系统进程,那么此样板代码是最佳实践:

try {
    ...
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}

针对 API 更改抛出特定异常

公共 API 行为可能会因 API 级别而异,并导致应用崩溃(例如,为了强制执行新的安全政策)。

当 API 需要针对之前有效的请求抛出异常时,请抛出新的特定异常,而不是通用异常。例如,ExportedFlagRequired 而不是 SecurityException(并且 ExportedFlagRequired 可以扩展 SecurityException)。

这将有助于应用开发者和工具检测 API 行为变化。

实现复制构造函数,而不是克隆

强烈建议不要使用 Java clone() 方法,因为 Object 类缺少 API 合约,并且扩展使用 clone() 的类存在固有的困难。请改为使用采用相同类型对象的复制构造函数。

/**
 * Constructs a shallow copy of {@code other}.
 */
public Foo(Foo other)

依赖于构建器进行构建的类应考虑添加构建器复制构造函数,以允许对副本进行修改。

public class Foo {
    public static final class Builder {
        /**
         * Constructs a Foo builder using data from {@code other}.
         */
        public Builder(Foo other)

使用 ParcelFileDescriptor 而不是 FileDescriptor

java.io.FileDescriptor 对象的所有权定义不明确,可能会导致难以发现的 use-after-close bug。API 应返回或接受 ParcelFileDescriptor 实例。旧版代码可以使用 dup()getFileDescriptor() 在 PFD 和 FD 之间进行转换(如果需要)。

避免使用奇数大小的数值

避免直接使用 shortbyte 值,因为它们通常会限制您未来可能如何发展 API。

避免使用 BitSet

java.util.BitSet 非常适合实现,但不适合公共 API。它可变,需要为高频方法调用分配内存,并且不提供每个位所代表的语义含义。

对于高性能场景,请使用 @IntDefintlong。对于低性能场景,请考虑使用 Set<EnumType>。对于原始二进制数据,请使用 byte[]

首选 android.net.Uri

android.net.Uri 是 Android API 中 URI 的首选封装。

避免使用 java.net.URI,因为它在解析 URI 时过于严格;切勿使用 java.net.URL,因为其等值定义严重损坏。

隐藏标记为 @IntDef、@LongDef 或 @StringDef 的注释

标记为 @IntDef@LongDef@StringDef 的注释表示可传递给 API 的一组有效常量。不过,当它们本身作为 API 导出时,编译器会内联常量,并且只有(现在无用的)值保留在注释的 API 桩(对于平台)或 JAR(对于库)中。

因此,这些注释的使用必须在平台中标记为 @hide 文档注释,或在库中标记为 @RestrictTo.Scope.LIBRARY) 代码注释。在上述两种情况下,都必须将它们标记为 @Retention(RetentionPolicy.SOURCE),以防止它们出现在 API 桩或 JAR 中。

@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

在构建平台 SDK 和库 AAR 时,某个工具会提取注释,并将其与已编译的源代码分开打包。Android Studio 会读取这种捆绑格式并强制执行类型定义。

不添加新的设置提供程序键

不公开来自 Settings.GlobalSettings.SystemSettings.Secure 的新密钥。

相反,请在相关类(通常是“管理器”类)中添加适当的 getter 和 setter Java API。添加监听器机制或广播,以便根据需要通知客户端更改。

与 getter/setter 相比,SettingsProvider 设置存在以下问题:

  • 无类型安全。
  • 没有提供默认值的统一方式。
  • 没有适当的方式来自定义权限。
    • 例如,无法使用自定义权限保护您的设置。
  • 无法正确添加自定义逻辑。
    • 例如,无法根据设置 B 的值来更改设置 A 的值。

示例:Settings.Secure.LOCATION_MODE 已存在很长时间,但位置信息团队已弃用它,转而使用合适的 Java API LocationManager.isLocationEnabled()MODE_CHANGED_ACTION 广播,这为团队带来了更大的灵活性,并且 API 的语义现在也清晰得多。

不扩展 Activity 和 AsyncTask

AsyncTask 是实现细节。而是公开一个监听器,或者在 androidx 中公开一个 ListenableFuture API。

Activity 子类无法组合。延长功能的使用时间会导致该功能与其他需要用户执行相同操作的功能不兼容。相反,应使用 LifecycleObserver 等工具来依赖组合。

使用 Context 的 getUser()

绑定到 Context 的类(例如从 Context.getSystemService() 返回的任何内容)应使用绑定到 Context 的用户,而不是公开以特定用户为目标的成员。

class FooManager {
  Context mContext;

  void fooBar() {
    mIFooBar.fooBarForUser(mContext.getUser());
  }
}
class FooManager {
  Context mContext;

  Foobar getFoobar() {
    // Bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBarForUser(Process.myUserHandle());
  }

  Foobar getFoobar() {
    // Also bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBar();
  }

  Foobar getFoobarForUser(UserHandle user) {
    mIFooBar.fooBarForUser(user);
  }
}

例外情况:如果方法接受不代表单个用户的值(例如 UserHandle.ALL),则可以接受用户实参。

使用 UserHandle 而不是纯整数

建议使用 UserHandle,以提供类型安全并避免将用户 ID 与 UID 混淆。

Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);

在不可避免的情况下,表示用户 ID 的 int 必须使用 @UserIdInt 进行注释。

Foobar getFoobarForUser(@UserIdInt int user);

优先选择监听器或回调,而不是广播 intent

广播 intent 非常强大,但它们会导致可能对系统运行状况产生负面影响的紧急行为,因此应谨慎添加新的广播 intent。

以下是一些具体问题,导致我们不鼓励引入新的广播 intent:

  • 发送不带 FLAG_RECEIVER_REGISTERED_ONLY 标志的广播时,它们会强制启动任何尚未运行的应用。虽然这有时是预期结果,但可能会导致数十个应用同时启动,对系统运行状况产生负面影响。我们建议使用 JobScheduler 等替代策略,以便在满足各种前提条件时更好地进行协调。

  • 发送广播时,几乎无法过滤或调整传递给应用的内容。这使得我们难以或无法响应未来的隐私问题,也无法根据接收应用的目标 SDK 引入行为变更。

  • 由于广播队列是共享资源,因此可能会过载,导致您的活动无法及时交付。我们发现,实际应用中的多个广播队列的端到端延迟时间为 10 分钟或更长。

出于这些原因,我们建议新功能考虑使用监听器或回调或其他设施(例如 JobScheduler),而不是广播 intent。

在广播 intent 仍然是理想设计的情况下,应考虑以下一些最佳实践:

  • 如果可能,请使用 Intent.FLAG_RECEIVER_REGISTERED_ONLY 将广播限制为仅发送给已在运行的应用。例如,ACTION_SCREEN_ON 使用此设计来避免唤醒应用。
  • 如果可能,请使用 Intent.setPackage()Intent.setComponent() 将广播定位到感兴趣的特定应用。例如,ACTION_MEDIA_BUTTON 使用此设计来专注于当前应用处理播放控件。
  • 如果可以,请将广播定义为 <protected-broadcast>,以防止恶意应用冒充操作系统。

系统绑定开发者服务中的 intent

旨在由开发者扩展并由系统绑定的服务(例如 NotificationListenerService 等抽象服务)可能会响应来自系统的 Intent 操作。此类服务应满足以下条件:

  1. 在包含服务的完全限定类名的类上定义 SERVICE_INTERFACE 字符串常量。此常量必须使用 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 进行注释。
  2. 关于开发者必须向其 AndroidManifest.xml 添加 <intent-filter> 才能从平台接收 intent 的类的文档。
  3. 强烈建议添加系统级权限,以防止恶意应用向开发者服务发送 Intent

Kotlin-Java 互操作性

如需查看完整的指南列表,请参阅官方 Android Kotlin-Java 互操作指南。我们已将部分准则复制到本指南中,以提高其可发现性。

API 可见性

某些 Kotlin API(例如 suspend fun)不适合 Java 开发者使用;不过,请勿尝试使用 @JvmSynthetic 控制特定于语言的可见性,因为这会对调试器中 API 的呈现方式产生副作用,从而使调试更加困难。

如需获取具体指导,请参阅 Kotlin-Java 互操作指南异步指南

伴生对象

Kotlin 使用 companion object 来公开静态成员。在某些情况下,这些内容会从 Java 中的内部类(名为 Companion)中显示,而不是从包含类中显示。Companion 类可能会在 API 文本文件中显示为空类,这是预期行为。

为了最大限度地提高与 Java 的兼容性,请使用 @JvmField 注释伴生对象的非常量字段,并使用 @JvmStatic 注释公共函数,以便直接在包含类中公开它们。

companion object {
  @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
  @JvmStatic fun fromPointF(pointf: PointF) {
    /* ... */
  }
}

Android 平台 API 的演变

本部分介绍了有关您可以对现有 Android API 进行哪些类型的更改的政策,以及您应如何实现这些更改,以最大限度地提高与现有应用和代码库的兼容性。

二进制文件重大变更

避免在最终确定的公共 API Surface 中出现二进制兼容性中断性变更。运行 make update-api 时,此类更改通常会引发错误,但可能存在 Metalava 的 API 检查未捕获的极端情况。如有疑问,请参阅 Eclipse 基金会的不断发展的基于 Java 的 API 指南,详细了解哪些类型的 API 更改在 Java 中是兼容的。隐藏(例如系统)API 中的二进制重大变更应遵循弃用/替换周期。

源代码重大变更

我们不鼓励进行源代码级重大更改,即使这些更改不是二进制级重大更改。一个与二进制文件兼容但会破坏源代码的变更示例是向现有类添加泛型,这与二进制文件兼容,但可能会因继承或不明确的引用而引入编译错误。运行 make update-api 时,源代码破坏性变更不会引发错误,因此您必须注意了解对现有 API 签名所做的变更会产生哪些影响。

在某些情况下,为了改善开发者体验或提高代码正确性,必须进行源代码破坏性变更。例如,向 Java 源代码添加可为 null 性注解可提高与 Kotlin 代码的互操作性并降低出错的可能性,但通常需要对源代码进行更改(有时是重大更改)。

对私有 API 的更改

您可以随时更改带有 @TestApi 注释的 API。

您必须保留使用 @SystemApi 注释的 API 三年。您必须按照以下时间表移除或重构系统 API:

  • API y - 已添加
  • API y+1 - 弃用
    • 使用 @Deprecated 标记代码。
    • 添加替换项,并使用 @deprecated 文档注释在已弃用代码的 Javadoc 中添加指向替换项的链接。
    • 在开发周期内,针对内部用户提交 bug,告知他们相应 API 即将被弃用。这有助于验证替换 API 是否足够。
  • API y+2 - 软移除
    • 使用 @removed 标记代码。
    • 可选,针对以发布版本中的当前 SDK 级别为目标平台的应用,抛出异常或不执行任何操作。
  • API y+3 - 硬移除
    • 从源代码树中彻底移除代码。

废弃

我们将弃用视为 API 变更,并且它可能会在主要(例如字母)版本中发生。在废弃 API 时,请同时使用 @Deprecated 源代码注释和 @deprecated <summary> 文档注释。您的摘要必须包含迁移策略。此策略可能会链接到替代 API 或说明为何不应使用该 API:

/**
 * Simple version of ...
 *
 * @deprecated Use the {@link androidx.fragment.app.DialogFragment}
 *             class with {@link androidx.fragment.app.FragmentManager}
 *             instead.
 */
@Deprecated
public final void showDialog(int id)

您还必须弃用在 XML 中定义并在 Java 中公开的 API,包括在 android.R 类中公开的属性和可设置样式的属性,并提供摘要:

<!-- Attribute whether the accessibility service ...
     {@deprecated Not used by the framework}
 -->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />

何时弃用 API

弃用最适合用于阻止在新代码中采用 API。

我们还要求您在 API @removed 之前将其标记为 @deprecated,但这并不能为开发者提供足够的动力来迁移他们已在使用的 API。

在废弃 API 之前,请考虑对开发者的影响。弃用 API 的影响包括:

  • javac 在编译期间发出警告。
    • 弃用警告无法全局抑制或基准化,因此使用 -Werror 的开发者需要单独修复或抑制每个已弃用 API 的用法,然后才能更新其编译 SDK 版本。
    • 无法禁止针对已弃用类的导入操作显示弃用警告。因此,开发者需要内嵌已弃用类的每次完全限定类名,然后才能更新其编译 SDK 版本。
  • 有关 d.android.com 的文档显示了弃用通知。
  • Android Studio 等 IDE 会在 API 使用位置显示警告。
  • IDE 可能会降低 API 在自动补全中的排名或隐藏该 API。

因此,弃用某个 API 可能会让最关心代码健康状况(使用 -Werror)的开发者不愿意采用新的 SDK。如果开发者不关心现有代码中的警告,则很可能会完全忽略弃用。

如果某个 SDK 引入了大量弃用项,这两种情况都会变得更糟。

因此,我们建议仅在以下情况下弃用 API:

  • 我们计划在未来的版本中@remove该 API。
  • API 使用会导致错误或未定义的行为,如果不破坏兼容性,我们就无法修复。

当您弃用某个 API 并将其替换为新 API 时,我们强烈建议向 Jetpack 库(例如 androidx.core)添加相应的兼容性 API,以简化对旧设备和新设备的支持。

我们建议弃用在当前和未来版本中可正常运行的 API:

/**
 * ...
 * @deprecated Use {@link #doThing(int, Bundle)} instead.
 */
@Deprecated
public void doThing(int action) {
  ...
}

public void doThing(int action, @Nullable Bundle extras) {
  ...
}

在 API 无法再保持其已记录的行为时,弃用是合适的做法:

/**
 * ...
 * @deprecated No longer displayed in the status bar as of API 21.
 */
@Deprecated
public RemoteViews tickerView;

已弃用 API 的变更

必须保持已弃用 API 的行为。这意味着测试实现必须保持不变,并且在您弃用 API 后,测试必须继续通过。如果 API 没有测试,您应该添加测试。

请勿在未来的版本中扩展已弃用的 API Surface。您可以向现有的已弃用 API 添加 lint 正确性注释(例如 @Nullable),但不应添加新的 API。

请勿将新 API 添加为已弃用。如果在预发布周期内添加了任何 API,随后又弃用了这些 API(因此这些 API 最初会以弃用状态进入公共 API 表面),您必须在最终确定 API 之前移除这些 API。

软移除

软移除是一项会破坏源代码的变更,除非 API 委员会明确批准,否则您应避免在公共 API 中使用此变更。 对于系统 API,您必须在软移除之前,先在主要版本中弃用该 API。移除所有对 API 的文档引用,并在软移除 API 时使用 @removed <summary> 文档注释。您的摘要必须包含移除原因,并且可以包含迁移策略,如弃用中所述。

软移除的 API 的行为可以保持不变,但更重要的是必须保留,这样现有调用方在调用该 API 时就不会崩溃。在某些情况下,这可能意味着保留行为。

必须保持测试覆盖率,但可能需要更改测试内容以适应行为变化。测试仍必须验证现有调用方在运行时不会崩溃。您可以保持软移除 API 的行为不变,但更重要的是,您必须保留该行为,以免现有调用方在调用 API 时崩溃。在某些情况下,这可能意味着保留行为。

必须保持测试覆盖率,但测试的内容可能需要更改以适应行为变化。测试仍必须验证现有调用方在运行时不会崩溃。

从技术层面来说,我们使用 @remove Javadoc 注释从 SDK 桩 JAR 和编译时 classpath 中移除了该 API,但它仍然存在于运行时 classpath 中,与 @hide API 类似:

/**
 * Ringer volume. This is ...
 *
 * @removed Not functional since API 2.
 */
public static final String VOLUME_RING = ...

从应用开发者的角度来看,该 API 不再显示在自动补全中,并且当 compileSdk 等于或高于移除该 API 的 SDK 时,引用该 API 的源代码将无法编译;不过,源代码仍可针对较早的 SDK 成功编译,并且引用该 API 的二进制文件仍可正常运行。

某些类别的 API 不得软移除。您不得软移除某些类别的 API。

抽象方法

不得软移除开发者可能会扩展的类中的抽象方法。这样做会导致开发者无法在所有 SDK 级别成功扩展该类。

在极少数情况下,如果开发者从未不会能够扩展某个类,您仍然可以软移除抽象方法。

硬移除

硬移除是一项会破坏二进制文件的变更,绝不应在公共 API 中发生。

不建议使用的注释

我们使用 @Discouraged 注释来表明在大多数(超过 95%)情况下,我们不建议使用某个 API。不建议使用的 API 与已弃用的 API 的不同之处在于,前者存在一个狭窄的关键用例,因此无法弃用。将 API 标记为不推荐使用时,您必须提供说明和替代解决方案:

@Discouraged(message = "Use of this function is discouraged because resource
                        reflection makes it harder to perform build
                        optimizations and compile-time verification of code. It
                        is much more efficient to retrieve resources by
                        identifier (such as `R.foo.bar`) than by name (such as
                        `getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
    return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

不应添加不建议使用的新 API。

现有 API 行为的变化

在某些情况下,您可能需要更改现有 API 的实现行为。例如,在 Android 7.0 中,我们改进了 DropBoxManager,以便在开发者尝试发布过大的事件(无法通过 Binder 发送)时清晰地传达相关信息。

不过,为避免给现有应用造成问题,我们强烈建议为旧版应用保留安全行为。过去,我们一直根据应用的 ApplicationInfo.targetSdkVersion 来控制这些行为变更,但最近我们已迁移到要求使用应用兼容性框架。以下示例展示了如何使用此新框架来实现行为变更:

import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;

public class MyClass {
  @ChangeId
  // This means the change will be enabled for target SDK R and higher.
  @EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
  // Use a bug number as the value, provide extra detail in the bug.
  // FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
  static final long FOO_NOW_DOES_X = 123456789L;

  public void doFoo() {
    if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
      // do the new thing
    } else {
      // do the old thing
    }
  }
}

借助此应用兼容性框架设计,开发者可以在预览版和 Beta 版发布期间暂时停用特定行为变更,以便调试应用,而无需同时适应数十项行为变更。

向前兼容性

向前兼容性是一种设计特征,可让系统接受旨在用于其更高版本的输入。对于 API 设计,您必须特别注意初始设计以及未来的更改,因为开发者希望编写一次代码、测试一次代码,然后让代码在任何地方都能顺利运行。

以下原因会导致 Android 中最常见的前向兼容性问题:

  • 向之前被假定为完整的集合(例如 @IntDefenum)添加新常量(例如,其中 switch 具有会抛出异常的 default)。
  • 添加对未直接在 API 表面中捕获的功能的支持(例如,支持在 XML 中分配 ColorStateList 类型资源,而之前仅支持 <color> 资源)。
  • 放宽对运行时检查的限制,例如移除较低版本中存在的 requireNotNull() 检查。

在所有这些情况下,开发者只有在运行时才会发现问题。更糟糕的是,他们可能会通过现场旧设备的崩溃报告发现此问题。

此外,这些情况在技术上都是有效的 API 变更。它们不会破坏二进制或源代码兼容性,并且 API lint 不会捕获任何这些问题。

因此,API 设计人员在修改现有类时必须格外注意。提出以下问题:“此变更是否会导致仅针对最新版本的平台编写和测试的代码在较低版本上失败?”

XML 架构

如果 XML 架构充当组件之间的稳定接口,则必须明确指定该架构,并且必须以向后兼容的方式改进该架构,与其他 Android API 类似。例如,XML 元素和属性的结构必须保持不变,这与其他 Android API 表面上的方法和变量的维护方式类似。

XML 弃用

如果您想弃用某个 XML 元素或属性,可以添加 xs:annotation 标记,但必须遵循典型的 @SystemApi 演变生命周期,继续支持所有现有的 XML 文件。

<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

必须保留元素类型

架构支持 sequence 元素、choice 元素和 all 元素作为 complexType 元素的子元素。不过,这些子元素的子元素数量和顺序各不相同,因此修改现有类型会造成不兼容的变更。

如果您想修改现有类型,最佳实践是弃用旧类型并引入新类型来替换它。

<!-- Original "sequence" value -->
<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

<!-- New "choice" value -->
<xs:element name="fooChoice">
    <xs:complexType>
        <xs:choice>
            <xs:element name="name" type="xs:string"/>
        </xs:choice>
    </xs:complexType>
</xs:element>

Mainline 专用模式

Mainline 是一项旨在允许单独更新 Android 操作系统的子系统(“Mainline 模块”)的项目,而不是更新整个系统映像。

Mainline 模块必须从核心平台“解绑”,这意味着每个模块与外界的所有互动都必须使用正式(公共或系统)API 来完成。

Mainline 模块应遵循某些设计模式。本部分将介绍这些功能。

<Module>FrameworkInitializer 模式

如果主线模块需要公开 @SystemService 类(例如 JobScheduler),请使用以下格式:

  • 从模块中公开 <YourModule>FrameworkInitializer 类。此类需要位于 $BOOTCLASSPATH 中。示例: StatsFrameworkInitializer

  • 使用 @SystemApi(client = MODULE_LIBRARIES) 标记。

  • 向其添加 public static void registerServiceWrappers() 方法。

  • 当服务管理器类需要引用 Context 时,使用 SystemServiceRegistry.registerContextAwareService() 注册该类。

  • 当服务管理器类不需要对 Context 的引用时,使用 SystemServiceRegistry.registerStaticService() 注册该类。

  • SystemServiceRegistry 的静态初始化程序中调用 registerServiceWrappers() 方法。

<Module>ServiceManager 模式

通常,为了注册系统服务 binder 对象或获取对它们的引用,人们会使用 ServiceManager,但主线模块无法使用它,因为它处于隐藏状态。此类之所以被隐藏,是因为 Mainline 模块不应注册或引用静态平台或其他模块公开的系统服务 binder 对象。

Mainline 模块可以使用以下模式,以便注册和获取模块内部实现的 binder 服务的引用。

  • 按照 TelephonyServiceManager 的设计创建 <YourModule>ServiceManager

  • 将该类公开为 @SystemApi。如果您只需要从 $BOOTCLASSPATH 类或系统服务器类访问它,可以使用 @SystemApi(client = MODULE_LIBRARIES);否则,可以使用 @SystemApi(client = PRIVILEGED_APPS)

  • 此类包含:

    • 一个隐藏的构造函数,因此只有静态平台代码才能实例化它。
    • 返回特定名称的 ServiceRegisterer 实例的公共 getter 方法。如果您有一个 binder 对象,则需要一个 getter 方法。如果您有两个,则需要两个 getter。
    • ActivityThread.initializeMainlineModules() 中,实例化此类,并将其传递给模块公开的静态方法。通常,您会在 FrameworkInitializer 类中添加一个静态 @SystemApi(client = MODULE_LIBRARIES) API 来获取该对象。

此模式会阻止其他 Mainline 模块访问这些 API,因为其他模块无法获取 <YourModule>ServiceManager 的实例,即使 get()register() API 对它们可见也是如此。

以下是电话服务获取电话服务引用的方式:代码搜索链接

如果您在原生代码中实现服务 binder 对象,则可以使用AServiceManager 原生 API。这些 API 对应于 ServiceManager Java API,但原生 API 直接向主线模块公开。请勿使用它们来注册或引用不归您的模块所有的 binder 对象。如果您从原生代码公开 binder 对象,则 <YourModule>ServiceManager.ServiceRegisterer 不需要 register() 方法。

Mainline 模块中的权限定义

包含 APK 的 Mainline 模块可以在其 APK AndroidManifest.xml 中定义(自定义)权限,就像常规 APK 一样。

如果定义的权限仅在模块内部使用,则其权限名称应以 APK 软件包名称为前缀,例如:

<permission
    android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
    android:protectionLevel="signature" />

如果定义的权限将作为可更新的平台 API 的一部分提供给其他应用,则其权限名称应以“android.permission.”为前缀。(与任何静态平台权限一样)加上模块软件包名称,以表明它是来自模块的平台 API,同时避免任何命名冲突,例如:

<permission
    android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
    android:label="@string/active_calories_burned_read_content_description"
    android:protectionLevel="dangerous"
    android:permissionGroup="android.permission-group.HEALTH" />

然后,模块可以在其 API surface 中将此权限名称公开为 API 常量,例如 HealthPermissions.READ_ACTIVE_CALORIES_BURNED